#===============================================================================================
# Key frame based animator framework
# v1.0 (countd360)
# v1.1 (countd360)
# - fix an optimize bug
# v1.1.2 (countd360)
# - fix ClipKeyFrame load issue.
# v1.1.3 (countd360)
# - Remove utf-8 dependence.
# v1.2 (countd360)
# - Export clips into scene, so clips can be save with PNG file.
# - Auto and manual load in-scene clips.
# - Stop update clip aftet last keyframe.
# v1.3 (countd360)
# - A new clip optimization UI
# - Clips sorted by priority first and then by clip's name
# - Status list of actor/prop/system can be sorted/translated/colored by setting in vnactor.ini
# - Adjust of curve data applied automatically, no 'update' button click needed.
# v1.3.1 (Joan)
# - includes Joan fix for VideoExport plugin
# v1.4 (countd360)
# - fix anime speed save bug
# - fix seek bar time display bug
# - work with vnactor v3.9 
# v1.5 (countd360)
# - update to new event_reg_listener/event_unreg_listener function
# - fix copy/parse keyframe when playing bug
# v1.5.1 (countd360)
# - fix AIPE support bug
# v1.6 (countd360)
# - support auto run when scene loaded/imported
# - merge clips into one "-keyframeclips" folder when imported
# - optimized setting export to scene
# v1.6.1 (countd360)
# - fix error message on load ext file fail.
# v1.7 (Keitaro)
# - support for PNG data save/load
# 1.8 (Keitaro)
# - support for quaternion types - they interpolate using Euler angles (correct FK rotation etc.)
# - in kfa_import - preprocessing for loading (a,a,a,a) in some cases (fk_set_quat) to Quaternion.
# 1.9 (countd360)
# - renew clip editor UI
# 1.9.1 (Keitaro)
# - in kfa_import - preprocessing for loading (a,a,a,a) in some cases (fk_set_quat, ik_set_quat) to Quaternion.
# 1.10 (Keitaro)
# - in kfa_import test run - fk_set and ik_set are processed to be converted to Quaternions
# 1.11
# - to dataPNG_load_checkversion changed
# - kfa_import - changed fk_set process to correct for props
# 2.0
# - nothing changed, just update to release for 30.0
# 2.0.1 (Countd360)
# - bug fix for clip ui v2.
# 2.1 (Countd360)
# - Support KKS
# 2.2 (Countd360)
# - Fix a data linked bug when create clip from copy
# 2.3 (Countd360)
# - support for vnanime2, handle vnanime2 data, and some ui
# - use py3.x print() function instead of py2.7 format
# 2.4 (Countd360)
# - support shortcuts for vnanime clip2 editor
# - support copy and import for clip2 in manager's create menu
# - adjust export/reset menu in manager
# 2.5 (Countd360)
# - enable changing color in vnanime.ini
# - enable value editor in clip v2
# 2.6 (Countd360)
# - clip PNG data merge when import scene
# - enable clip v2 re-assign when copy
# - clip v1 add seekNormalized()
# 2.6.1? (Keitaro)
# - kfa_getPlayingClips and kfa_setPlayingClips save clip speed too
# 2.6.2 (Countd360)
# - adapt to new customwindow skin
# 2.7 (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 vnactor import cam_act_funcs, sys_act_funcs, char_act_funcs, prop_act_funcs, export_sys_status
from vnframe import act, paramInterpolater, register_actor_prop_by_tag, scriptHelperGUIToSceen, scriptHelperGUIMessage, scriptHelperGUIClose, script2string, scriptCopy, VNEnableGarbageCollect, VNGetGarbageSizeMB, VNDoGarbageCollectNow

save_png_version = "23.0"
save_png_version2 = "34.0"

# system functions
def init_keyframe_anime(game):
    """:type game:vngameengine.VNNeoController"""
    if not check_keyframe_anime(game):
        # clean up hook
        game.event_unreg_listener("update", update_keyframe_anime, "vnanime_update")
        # register update function
        game.event_reg_listener("update", update_keyframe_anime, "vnanime_update")
    # new clips
    game.gdata.kfaManagedClips = {}
    game.gdata.kfaSortedClips = []
    # load setting
    load_keyframe_anime_setting(game)
    game.gdata.kfaIsPlaying = False
    # delete manager temp data
    if hasattr(game.scenedata, "kfamSelectedClipIndex"):
        del game.scenedata.kfamSelectedClipIndex

    # try import from scene
    data = None
    data2 = None
    if game.isSceneDataSaveSupported:
        data,version = game.dataPNG_load_checkversion("vnge_kfa",["23.0"])
        data2,version2 = game.dataPNG_load_checkversion("vnge_kfa2",["34.0"])

    if data != None and data != "[]" or data2 != None and data2 != "[]": # prefer PNG data over folder data
        register_actor_prop_by_tag(game)
        if data != None and data != "[]":
            loadStringToClips(game,data)
        if data2 != None and data2 != "[]":
            from vnanime2 import clipImportFromString2
            clipImportFromString2(game, data2)
    else:
        importAllClipsFromScene(game)
    
def clear_keyframe_anime(game):
    game.event_unreg_listener("update", update_keyframe_anime, "vnanime_update")
    if check_keyframe_anime(game):
        game.gdata.kfaManagedClips = None
        game.gdata.kfaSortedClips = None
        
def check_keyframe_anime(game):
    return hasattr(game.gdata, "kfaManagedClips") and game.gdata.kfaManagedClips != None

def update_keyframe_anime(game, evid, param):
    anyClipPlaying = False
    # update clips
    for clip in game.gdata.kfaSortedClips:
        clip.update(game)
        anyClipPlaying |= clip.isPlaying
    # GC work
    if game.gdata.kfaSetting.suspendGC:
        if not game.gdata.kfaIsPlaying and anyClipPlaying:
            #print("Some clip started, suspend GC")
            VNEnableGarbageCollect(False)
        if game.gdata.kfaIsPlaying and not anyClipPlaying:
            #print("All clip stopped, revive GC")
            VNEnableGarbageCollect(True)
        if game.gdata.kfaSetting.maxGCSize > 0 and anyClipPlaying:
            if VNGetGarbageSizeMB() > game.gdata.kfaSetting.maxGCSize:
                VNDoGarbageCollectNow()
                # check GC size after collect
                if VNGetGarbageSizeMB() > game.gdata.kfaSetting.maxGCSize:
                    game.gdata.kfaSetting.suspendGC = False
                    game.logger_log_message("Suspend GC on playing disabled because of GC limit setting is too low! Stop all clip and try a bigger value.", "VNAnime: ")

    # update overview playing status
    game.gdata.kfaIsPlaying = anyClipPlaying

def merge_imported_clips(game):
    clipbases = HSNeoOCIFolder.find_all_startswith("-keyframeclips:")
    if len(clipbases) <= 1:
        # no need to merge
        return
    # run register
    register_actor_prop_by_tag(game)
    # build base info
    baseFld = clipbases[0]
    clipids = []
    for clpFld in baseFld.treeNodeObject.child:
        cspt = clpFld.textName.split(":")
        if len(cspt) != 3 or cspt[0] != "-clip":
            continue
        cid = cspt[1]
        clipids.append(cid)
    # merge imported info
    for i in range(1, len(clipbases)):
        impBaseFld = clipbases[i]
        for clpFld in impBaseFld.treeNodeObject.child:
            cspt = clpFld.textName.split(":")
            if len(cspt) != 3 or cspt[0] != "-clip":
                continue
            cid = cspt[1]
            try:
                if cid in clipids:
                    # need rename
                    while cid in clipids:
                        cid += "_import"
                    clpScp = eval(clpFld.child[0].textName)
                    tempClp = KeyFrameClip(game, clpScp)
                    tempClp.name = cid
                    clpSetting = tempClp.exportSettingForScene()
                    print("VNAnime Info: import and renamed clip:%s"%cid)
                else:
                    # just move
                    clpSetting = clpFld.child[0].textName
                    print("VNAnime Info: import clip:%s"%cid)
                clpFld = HSNeoOCIFolder.add("-clip:%s:0"%cid)
                clpFld.set_parent(baseFld)
                clpData = HSNeoOCIFolder.add(clpSetting)
                clpData.set_parent(clpFld)
            except Exception as e:
                print("Error in import %s: %s"%(cspt[1], str(e)))
        # remove imported clip base
        impBaseFld.delete()

def merge_imported_clips_png(game):
    print("VNAnime merge imported PNG's clips to current scene...")
    if not check_keyframe_anime(game):
        print("  current scene has no clip loaded, no need to merge")
        return

    # load from new scene
    data1 = None
    data2 = None
    if game.isSceneDataSaveSupported:
        data1, version1 = game.dataPNG_load_checkversion("vnge_kfa",["23.0"])
        data2, version2 = game.dataPNG_load_checkversion("vnge_kfa2",["34.0"])
    else:
        print("  PNG extend save not supported, nothing to merge")
        return

    # save clip data in old scene
    from vnframe import loadFromJsonString, dumpToJsonString
    exportSetting1 = "["
    exportSetting2 = []
    for clip in game.gdata.kfaSortedClips:
        if clip.version == 1:
            exportSetting1 += clip.exportSettingForScene() + ",\n"
        if clip.version == 2:
            exportSetting2.append(clip.exportSetting())

    # merge clips v1
    if data1 != None and data1 != "[]":
        try:
            import ast
            newsettings = ast.literal_eval(data1)
            newComers = [p["setting"]["name"] for p in newsettings]

            exportSetting1 += data1[1:]
            print("  Import %d clip v1 data from PNG: %s"%(len(newComers), str(tuple(newComers))))
        except:
            print("  Try import clip v1 data from PNG...FAILED")
            traceback.print_exc()
            print("")
            exportSetting1 += "]"
    else:
        print("  No clip v1 data found in PNG...")
        exportSetting1 += "]"

    # update v1 data to scene
    game.dataPNG_save("vnge_kfa", exportSetting1, save_png_version)
    
    # merge clips v2
    if data2 != None and data2 != "[]":
        try:
            # append new clip v2
            newsettings = loadFromJsonString(data2)
            newComers = [p["name"] for p in newsettings]
            exportSetting2.extend(newsettings)

            # TODO: Solve conflict

            print("  Import %d clip v2 data from PNG: %s"%(len(newComers), str(tuple(newComers))))
        except:
            print("  Try import clip v2 data from PNG...FAILED")
            traceback.print_exc()
            print("")
    else:
        print("  No clip v2 data found in PNG...")
        pass

    # update v2 data to scene
    game.dataPNG_save("vnge_kfa2", dumpToJsonString(exportSetting2), save_png_version2)


def start_autorun_clips(game):
    for clip in game.gdata.kfaSortedClips:
        if clip.autorun:
            clip.play()

def load_keyframe_anime_setting(game):
    import ConfigParser, os
    # Init setting
    gd = game.gdata
    gd.kfaSetting = GData()

    # shortcut helpers
    gd.kfaSetting.frameCount = -1
    gd.kfaSetting.suspendShortcut = False
    gd.kfaSetting.shortcutInput = None
    gd.kfaSetting.schelp_nextframe_info = [-1, 0, 0]
    gd.kfaSetting.schelp_prevframe_info = [-1, 0, 0]
    gd.kfaSetting.schelp_register = None
    gd.kfaSetting.schelp_delete = None
    gd.kfaSetting.schelp_copyKeyframe = None
    gd.kfaSetting.schelp_pasteKeyframe = None
    def startStopHelper():
        clip = game.scenedata.kfamOnModifyClip
        if clip.isPlaying:
            clip.pause()
        else:
            clip.play()
    def nextFrameHelper():
        ctime = time.clock()
        clip = game.scenedata.kfamOnModifyClip
        if gd.kfaSetting.schelp_nextframe_info[0] != gd.kfaSetting.frameCount - 1:
            # key down
            #print("next frame shortcut keydown")
            clip.gotoNextFrame()
            gd.kfaSetting.schelp_nextframe_info[1] = ctime + 0.75
            gd.kfaSetting.schelp_nextframe_info[2] = clip.curFrame
        else:
            # key hold
            #print("next frame shortcut keyhold")
            if ctime > gd.kfaSetting.schelp_nextframe_info[1]:
                dFrame = int(gd.kfaSetting.schelp_nextframe_info[2] + (ctime - gd.kfaSetting.schelp_nextframe_info[1]) * clip.frameRate)
                clip.seek(dFrame)
        gd.kfaSetting.schelp_nextframe_info[0] = gd.kfaSetting.frameCount
    def prevFrameHelper():
        ctime = time.clock()
        clip = game.scenedata.kfamOnModifyClip
        if gd.kfaSetting.schelp_prevframe_info[0] != gd.kfaSetting.frameCount - 1:
            # key down
            #print("next frame shortcut keydown")
            clip.gotoPrevFrame()
            gd.kfaSetting.schelp_prevframe_info[1] = ctime + 0.75
            gd.kfaSetting.schelp_prevframe_info[2] = clip.curFrame
        else:
            # key hold
            #print("next frame shortcut keyhold")
            if ctime > gd.kfaSetting.schelp_prevframe_info[1]:
                dFrame = int(gd.kfaSetting.schelp_prevframe_info[2] - (ctime - gd.kfaSetting.schelp_prevframe_info[1]) * clip.frameRate)
                clip.seek(dFrame)
        gd.kfaSetting.schelp_prevframe_info[0] = gd.kfaSetting.frameCount
    def nextSecKfHelper():
        clip = game.scenedata.kfamOnModifyClip
        clip.gotoNextSecond()
    def prevSecKfHelper():
        clip = game.scenedata.kfamOnModifyClip
        clip.gotoPrevSecond()
    def gotoFirstFrameHelper():
        clip = game.scenedata.kfamOnModifyClip
        clip.gotoFirstFrame()
    def gotoLastFrameHelper():
        clip = game.scenedata.kfamOnModifyClip
        clip.gotoLastFrame()
    def registerHelper():
        if gd.kfaSetting.schelp_register:
            gd.kfaSetting.schelp_register()
    def deleteHelper():
        if gd.kfaSetting.schelp_delete:
            gd.kfaSetting.schelp_delete()
    def copyKeyframeHelper():
        if gd.kfaSetting.schelp_copyKeyframe:
            gd.kfaSetting.schelp_copyKeyframe()
    def pasteKeyframeHelper():
        if gd.kfaSetting.schelp_pasteKeyframe:
            gd.kfaSetting.schelp_pasteKeyframe()

    # General
    gd.kfaSetting.useLegacyEditor = False
    gd.kfaSetting.hideUnusedProperties = False
    gd.kfaSetting.disableStudioKeys = False
    gd.kfaSetting.disableCameraKeys = False
    gd.kfaSetting.videoExportMode = False
    gd.kfaSetting.suspendGC = False
    gd.kfaSetting.maxGCSize = 2000
    # GUI
    gd.kfaSetting.clipEditorWindowRect = Rect(game.windowRect)
    gd.kfaSetting.targetPanelWidth = 150
    gd.kfaSetting.propertyNameWidth = 100
    # Keyframe color setting
    from libkfguictrl import KeyframeViewer, unity_adjust_color
    gd.kfaSetting.keyframeColors = []
    for key in sorted(KeyframeViewer.DEFAULT_COLOR.keys()):
        color = KeyframeViewer.DEFAULT_COLOR[key]
        color = unity_adjust_color(color)
        gd.kfaSetting.keyframeColors.append([key, color])
    gd.kfaSetting.keyframeColors.append(["Name Text Sub Group", Color(0, 0.333, 1)])
    gd.kfaSetting.keyframeColors.append(["Name Text Sub Normal", Color(0, 0, 0)])
    # curve color setting
    from libkfguictrl import CurveEditor
    gd.kfaSetting.curveColors = []
    for key in sorted(CurveEditor.DEFAULT_COLOR.keys()):
        color = CurveEditor.DEFAULT_COLOR[key]
        color = unity_adjust_color(color)
        gd.kfaSetting.curveColors.append([key, color])
    # Shortcuts
    gd.kfaSetting.shortcuts = {
        "Start/Stop": [startStopHelper, None, False],
        "Prev Frame": [prevFrameHelper, None, True],
        "Next Frame": [nextFrameHelper, None, True],
        "Prev Second": [prevSecKfHelper, None, False],
        "Next Second": [nextSecKfHelper, None, False],
        "Goto Start": [gotoFirstFrameHelper, None, False],
        "Goto End": [gotoLastFrameHelper, None, False],
        "Register": [registerHelper, None, False],
        "Delete": [deleteHelper, None, False],
        "Copy": [copyKeyframeHelper, None, False],
        "Paste": [pasteKeyframeHelper, None, False],
    }

    # Load
    try:
        config = ConfigParser.SafeConfigParser()
        config.read(os.path.splitext(__file__)[0] + '.ini')
        if "General" in config.sections():
            for k, v in config.items("General"):
                if k == "uselegacyeditor":
                    gd.kfaSetting.useLegacyEditor = bool(eval(v))
                if k == "hideunusedproperites":
                    gd.kfaSetting.hideUnusedProperties = bool(eval(v))
                if k == "defaulthelpid":
                    gd.kfaSetting.defaultHelpId = v.strip()
                if k == "disablestudiokeys":
                    gd.kfaSetting.disableStudioKeys = bool(eval(v))
                if k == "disablecamerakeys":
                    gd.kfaSetting.disableCameraKeys = bool(eval(v))
                if k == "videoexportmode":
                    gd.kfaSetting.videoExportMode = bool(eval(v))
                if k == "suspendgc":
                    gd.kfaSetting.suspendGC = bool(eval(v))
                if k == "maxgcsize":
                    gd.kfaSetting.maxGCSize = int(v)
        if "GUI" in config.sections():
            for k, v in config.items("GUI"):
                if k == "guix":
                    gd.kfaSetting.clipEditorWindowRect.x = int(v)
                if k == "guiy":
                    gd.kfaSetting.clipEditorWindowRect.y = int(v)
                if k == "guiwidth":
                    gd.kfaSetting.clipEditorWindowRect.width = int(v)
                if k == "guiheight":
                    gd.kfaSetting.clipEditorWindowRect.height = int(v)
                if k == "targetpanelwidth":
                    gd.kfaSetting.targetPanelWidth = int(v)
                if k == "propertynamewidth":
                    gd.kfaSetting.propertyNameWidth = int(v)
            # auto fix if out of screen
            if gd.kfaSetting.clipEditorWindowRect.x < 0:
                gd.kfaSetting.clipEditorWindowRect.x = 0
            if gd.kfaSetting.clipEditorWindowRect.x > Screen.width - gd.kfaSetting.clipEditorWindowRect.width:
                gd.kfaSetting.clipEditorWindowRect.x = Screen.width - gd.kfaSetting.clipEditorWindowRect.width
            if gd.kfaSetting.clipEditorWindowRect.y < 0:
                gd.kfaSetting.clipEditorWindowRect.y = 0
            if gd.kfaSetting.clipEditorWindowRect.y > Screen.height - gd.kfaSetting.clipEditorWindowRect.height:
                gd.kfaSetting.clipEditorWindowRect.y = Screen.height - gd.kfaSetting.clipEditorWindowRect.height
        if "Shortcuts" in config.sections():
            from vngameengine import parseKeyCode
            for k, v in config.items("Shortcuts"):
                for ktitle in gd.kfaSetting.shortcuts.keys():
                    if k == ktitle.lower():
                        gd.kfaSetting.shortcuts[ktitle][1] = parseKeyCode(v)
                        break
        if "KeyframeViewColor" in config.sections():
            for k, v in config.items("KeyframeViewColor"):
                for kcPair in gd.kfaSetting.keyframeColors:
                    if k == kcPair[0].lower():
                        r, g, b = v.split(",")
                        kcPair[1] = Color(float(r), float(g), float(b))
                        break
        if "CurveEditorColor" in config.sections():
            for k, v in config.items("CurveEditorColor"):
                for kcPair in gd.kfaSetting.curveColors:
                    if k == kcPair[0].lower():
                        r, g, b = v.split(",")
                        kcPair[1] = Color(float(r), float(g), float(b))
                        break
    except Exception as e:
        print("Fail to load config from ini file: " + str(e))

def save_keyframe_anime_setting(game):
    import os, codecs
    setting = game.gdata.kfaSetting

    # build contents
    content = "; VNAnime setting\n"
    # general
    content += "[General]\n"
    content += "UseLegacyEditor" + " = " + str(setting.useLegacyEditor) + "\n"
    content += "HideUnusedProperites" + " = " + str(setting.hideUnusedProperties) + "\n"
    if hasattr(setting, "defaultHelpId"):
        content += "DefaultHelpId" + " = " + setting.defaultHelpId + "\n"
    content += "DisableStudioKeys" + " = " + str(setting.disableStudioKeys) + "\n"
    content += "DisableCameraKeys" + " = " + str(setting.disableCameraKeys) + "\n"
    content += "VideoExportMode" + " = " + str(setting.videoExportMode) + "\n"
    content += "SuspendGC" + " = " + str(setting.suspendGC) + "\n"
    content += "MaxGCSize" + " = " + str(setting.maxGCSize) + "\n"
    content += "\n"
    # GUI
    content += "[GUI]\n"
    content += "GuiX" + " = " + str(int(setting.clipEditorWindowRect.x)) + "\n"
    content += "GuiY" + " = " + str(int(setting.clipEditorWindowRect.y)) + "\n"
    content += "GuiWidth" + " = " + str(int(setting.clipEditorWindowRect.width)) + "\n"
    content += "GuiHeight" + " = " + str(int(setting.clipEditorWindowRect.height)) + "\n"
    content += "TargetPanelWidth" + " = " + str(int(setting.targetPanelWidth)) + "\n"
    content += "propertyNameWidth" + " = " + str(int(setting.propertyNameWidth)) + "\n"
    content += "\n"
    # Shortcuts
    content += "[Shortcuts]\n"
    for keytitle, keyset in setting.shortcuts.items():
        if keyset[1] == None:
            content += keytitle + " = \n"
        else:
            content += keytitle + " = " + keyset[1][0] + "\n"
    content += "\n"
    # Keyframe color
    content += "[KeyframeViewColor]\n"
    for (key, color) in setting.keyframeColors:
        content += key + " = " + "%.3f, %.3f, %.3f"%(color.r, color.g, color.b) + "\n"
    content += "\n"
    # Curve color
    content += "[CurveEditorColor]\n"
    for (key, color) in setting.curveColors:
        content += key + " = " + "%.3f, %.3f, %.3f"%(color.r, color.g, color.b) + "\n"
    content += "\n"

    # save file
    cfpath = os.path.splitext(__file__)[0] + '.ini'
    cfile = codecs.open(cfpath, "w", "utf-8")
    cfile.write(content)
    cfile.close()


# clip control functions
def kfa_register(game, clip):
    try:
        game.gdata.kfaManagedClips[clip.name] = clip
        game.gdata.kfaSortedClips = game.gdata.kfaManagedClips.values()
        game.gdata.kfaSortedClips.sort(key=KeyFrameClip.sortByPriority)
    except Exception as e:
        traceback.print_exc()
        print("Fail to register clip:" + str(e))

def process_set_quat(x):
    if x == None:
        return None

    elif isinstance(x, tuple) or isinstance(x, list):
        updParam = []
        for i in range(len(x)):
            subParam = process_set_quat(x[i])
            updParam.append(subParam)

        if isinstance(x, tuple):
            return tuple(updParam)
        else:
            return updParam

    elif isinstance(x, dict):
        updParam = {}
        for ikey in x.Keys:
            #if ikey is str:

            # not additional quats parameters
            # if ikey == 'fk_set_quat':
            #     subParam = fk_set_quat_to_quats(x[ikey])
            # elif ikey == 'ik_set_quat':
            #     subParam = ik_set_quat_to_quats(x[ikey])

            # new subprocessing transfer to Vector3
            if ikey == 'fk_set':
                subParam = fk_set_to_quats(x[ikey])
            elif ikey == 'ik_set':
                subParam = ik_set_to_quats(x[ikey])

            else:
                subParam = process_set_quat(x[ikey])
            # else:
            #     subParam = process_set_quat(x[ikey])
            updParam[ikey] = subParam
        return updParam
    else:
        return x

# def fk_set_quat_to_quats(x):
#     if isinstance(x,list):
#         updParam = []
#         for i in range(len(x)):
#             subParam = Quaternion(x[i][0],x[i][1],x[i][2],x[i][3])
#             updParam.append(subParam)
#         return updParam
#     else:
#         updParam = {}
#         for i in x.Keys:
#             subParam = Quaternion(x[i][0],x[i][1],x[i][2],x[i][3])
#             updParam[i] = subParam
#         #print(x)
#         #print(updParam)
#         return updParam


    return x

def fk_set_to_quats(x):
    if len(x) == 0:
        return x

    import unity_util
    ctrl, alt, shift = unity_util.metakey_state()

    if isinstance(x,list) or isinstance(x, tuple):
        updParam = []
        for i in range(len(x)):
            if len(x[i]) == 4:
                subParam = Quaternion(x[i][0],x[i][1],x[i][2],x[i][3])
            else:
                if alt:
                    subParam = Quaternion.Euler(x[i][0],x[i][1],x[i][2])
                else:
                    subParam = Vector3(x[i][0],x[i][1],x[i][2])
            updParam.append(subParam)

        if isinstance(x, tuple):
            return tuple(updParam)

        return updParam
    else:
        updParam = {}
        for i in x.Keys:
            if len(x[i]) == 4:
                subParam = Quaternion(x[i][0],x[i][1],x[i][2],x[i][3])
            else:
                if alt:
                    subParam = Quaternion.Euler(x[i][0],x[i][1],x[i][2])
                else:
                    subParam = Vector3(x[i][0],x[i][1],x[i][2])
            updParam[i] = subParam
        #print(x)
        #print(updParam)
        return updParam


    return x

# def ik_set_quat_to_quats(x):
#
#     updParam = {}
#     for i in x.Keys:
#         if len(x[i]) == 1:
#             updParam[i] = [Vector3(x[i][0][0],x[i][0][1],x[i][0][2])]
#         else:
#             updParam[i] = [Vector3(x[i][0][0],x[i][0][1],x[i][0][2]), Quaternion(x[i][1][0],x[i][1][1],x[i][1][2],x[i][1][3])]
#
#     return updParam

def ik_set_to_quats(x):
    if len(x) == 0:
        return x

    import unity_util
    ctrl, alt, shift = unity_util.metakey_state()

    updParam = {}
    for i in x.Keys:
        if len(x[i]) == 1:
            updParam[i] = (Vector3(x[i][0][0],x[i][0][1],x[i][0][2]),)
        else:
            resq = None
            if len(x[i][1]) == 4:
                resq = Quaternion(x[i][1][0],x[i][1][1],x[i][1][2],x[i][1][3])
            else:
                if alt:
                    resq = Quaternion.Euler(x[i][1][0],x[i][1][1],x[i][1][2])
                else:
                    resq = Vector3(x[i][1][0],x[i][1][1],x[i][1][2])
            updParam[i] = (Vector3(x[i][0][0],x[i][0][1],x[i][0][2]), resq)

    return updParam




def kfa_import(game, clipScript):
    try:
        # preprocessing for "_set_quat"
        import unity_util
        ctrl, alt, shift = unity_util.metakey_state()
        if alt:
            import vngameengine
            vngameengine.vnge_game.logger_log_message("trying to convert clip animation to new 30.0+ format...")


        clipScript = process_set_quat(clipScript)
        #print(clipScript)

        newClip = KeyFrameClip(game, clipScript)
        game.gdata.kfaManagedClips[newClip.name] = newClip
        game.gdata.kfaSortedClips = game.gdata.kfaManagedClips.values()
        game.gdata.kfaSortedClips.sort(key=KeyFrameClip.sortByPriority)
    except Exception as e:
        traceback.print_exc()
        print("Fail to import clip:" + str(e))

def kfa_remove(game, clipName):
    try:
        clip = game.gdata.kfaManagedClips.pop(clipName)
        game.gdata.kfaSortedClips = game.gdata.kfaManagedClips.values()
        game.gdata.kfaSortedClips.sort(key=KeyFrameClip.sortByPriority)
    except Exception as e:
        traceback.print_exc()
        print("Fail to remove clip:" + str(e))

def kfa_rename(game, oldName, newName):
    try:
        clip = game.gdata.kfaManagedClips.pop(oldName)
        game.gdata.kfaManagedClips[newName] = clip
        clip.name = newName
    except Exception as e:
        traceback.print_exc()
        print("Fail to rename clip <%s> to <%s>: %s"%(oldName, newName, str(e)))

def kfa_resort(game, clipName, priority):
    try:
        clip = game.gdata.kfaManagedClips[clipName]
        clip.priority = priority
        game.gdata.kfaSortedClips.sort(key=KeyFrameClip.sortByPriority)
    except Exception as e:
        traceback.print_exc()
        print("Fail to resort clip <%s>: %s"%(clipName, str(e)))

def kfa_retype(game, clipName, animeType):
    try:
        clip = game.gdata.kfaManagedClips[clipName]
        #
        clip.animeType = animeType
    except Exception as e:
        traceback.print_exc()
        print("Fail to resort clip <%s>: %s"%(clipName, str(e)))
    
def kfa_play(game, clipName, forceLoop=None):
    try:
        game.gdata.kfaManagedClips[clipName].play(forceLoop)
    except Exception as e:
        traceback.print_exc()
        print("Fail to play clip <%s>: %s"%(clipName, str(e)))

def kfa_playAll(game):
    for clip in game.gdata.kfaSortedClips:
        clip.play()
        
def kfa_pause(game, clipName):
    try:
        game.gdata.kfaManagedClips[clipName].pause()
    except Exception as e:
        traceback.print_exc()
        print("Fail to pause clip <%s>: %s"%(clipName, str(e)))
        
def kfa_pauseAll(game):
    for clip in game.gdata.kfaSortedClips:
        clip.pause()
    
def kfa_stop(game, clipName):
    try:
        game.gdata.kfaManagedClips[clipName].stop()
    except Exception as e:
        traceback.print_exc()
        print("Fail to stop clip <%s>: %s"%(clipName, str(e)))
        
def kfa_stopAll(game):
    for clip in game.gdata.kfaSortedClips:
        clip.stop()
        
def kfa_seek(game, clipName, frameNo):
    try:
        game.gdata.kfaManagedClips[clipName].seek(frameNo)
    except Exception as e:
        traceback.print_exc()
        print("Fail to seek clip <%s> to %d: %s"%(clipName, frameNo, str(e)))

def kfa_rewindAll(game):
    for clip in game.gdata.kfaSortedClips:
        clip.seek(0)

def kfa_getPlayingClips(game):
    playing = []
    for clip in game.gdata.kfaSortedClips:
        if clip.isPlaying:
            playing.append((clip.name, "%.2f"%clip.speed))

    return tuple(playing)

def kfa_setPlayingClips(game, clipnames):
    for clip in game.gdata.kfaSortedClips:
        # old version code

        # if clip.name in clipnames:
        #     if not clip.isPlaying:
        #         clip.play()
        # else:
        #     if clip.isPlaying:
        #         clip.stop()

        # new version code

        processed = False
        for x in clipnames:
            if x[0] == clip.name:
                clip.speed = float(x[1])
                #clip.loop = float(x[2])
                if not clip.isPlaying:
                    clip.play()
                processed = True
                break

        if not processed:
            if clip.isPlaying:
                clip.stop()

# math work
def cbi_s(p1, p2, t):
    a = 1 - 3*p2 + 3*p1
    b = 3*(p2 - 2*p1)
    c = 3*(p1)
    return a*t*t*t + b*t*t + c*t

def root3(x):
    if x < 0:
        y = math.pow(-x, 0.3333333333333333333)
        return -y
    else:
        y = math.pow(x, 0.3333333333333333333)
        return y
    
def cbi_ts(p1, p2, y):
    a = 1 - 3*p2 + 3*p1
    b = 3*(p2 - 2*p1)
    c = 3*(p1)
    d = - y
    #y = a*t*t*t + b*t*t + c*t + d
    #print(("a=", a, "b=", b, "c=", c, "d=", d))
    if a == 0:
        if b == 0:
            #print("linear function")
            return y
        else:
            #print("square function")
            D = c*c - 4*b*d
            if D < 0:
                print("FAIL to solve square: D < 0!? %f %f %f"%(b, c, d))
                return 0
            else:
                X1 = (-c + math.sqrt(D))/(2*b)
                X2 = (-c - math.sqrt(D))/(2*b)
                if X1 >= 0 and X1 <= 1:
                    return X1
                elif X2 >= 0 and X2 <= 1:
                    return X2
                else:
                    print("FAIL to solve square: %f %f"%(X1, X2))
                    return X2
    A = b*b - 3*a*c
    B = b*c - 9*a*d
    C = c*c - 3*b*d
    D = B*B - 4*A*C
    #print("A=", A, "B=", B, "C=", C, "D=", D)
    if A == 0 and B == 0:
        X = -b/(3*a)
        #print("ptn1:", X)
        return X
    if D > 0:
        Y1 = A*b + 3*a*(-B + math.sqrt(D))/2
        Y2 = A*b + 3*a*(-B - math.sqrt(D))/2
        X = (-b - (root3(Y1) + root3(Y2)))/(3*a)
        #print("ptn2:", X)
        return X
    if D == 0:
        K = B/A
        X1 = -b/a + K
        X2 = X3 = -K/2
        #print("ptn3:",X1,X2,X3)
        if X1 >= 0 and X1 <= 1:
            return X1
        elif X2 >= 0 and X2 <= 1:
            return X2
        elif X3 >= 0 and X3 <= 1:
            return X3
        else:
            print("FAIL to solve ptn3: (%f, %f, %f)"%(X1,X2,X3))
            return X3
    if D < 0:
        T = (2*A*b - 3*a*B)/(2*math.sqrt(A*A*A))
        theta = math.acos(T)/3
        X1 = (-b - 2*math.sqrt(A)*math.cos(theta))/(3*a)
        X2 = (-b + math.sqrt(A)*(math.cos(theta) + 1.732*math.sin(theta)))/(3*a)
        X3 = (-b + math.sqrt(A)*(math.cos(theta) - 1.732*math.sin(theta)))/(3*a)
        #print("ptn4:",X1,X2,X3)
        if X1 >= 0 and X1 <= 1:
            return X1
        elif X2 >= 0 and X2 <= 1:
            return X2
        elif X3 >= 0 and X3 <= 1:
            return X3
        else:
            print("FAIL to solve ptn4: (%f, %f, %f)"%(X1,X2,X3))
            return X3
            
def cubicBezierInterpolate(p1x, p2x, p1y, p2y, x):
    t = cbi_ts(p1x, p2x, x)
    y = cbi_s(p1y, p2y, t)
    return y
    
    
# support classes
class KeyFrame():
    def __init__(self):
        self.frameNo = 0
        self.targetID = ""
        
    def applyFrame(self, game):
        raise Exception("not implemented")
        
    def createInterFrameTo(self, toFrame, newFrameNo):
        raise Exception("not implemented")
        
    def dumpContent(self):
        raise Exception("not implemented")
        
    def importContent(self, content):
        raise Exception("not implemented")
        
    def copyContent(self, fromKey):
        raise Exception("not implemented")
    
    def copy(self):
        raise Exception("not implemented")

    def optimizeWith(self, fromFrame, toFrame, effectToFrame):
        raise Exception("not implemented")
    
    @staticmethod
    def sortByFrameNo(keyframe):
        return keyframe.frameNo
        
    @staticmethod
    def alignDataType(targetData, refData):
        if isinstance(targetData, dict) and isinstance(refData, dict):
            for key in targetData.keys():
                if refData.has_key(key):
                    targetData[key] = KeyFrame.alignDataType(targetData[key], refData[key])
                else:
                    #print("alignDataType warning: key '%s' in targetData not found in reference data"%str(key))
                    pass
            return targetData
        if isinstance(targetData, list) and isinstance(refData, list):
            if len(targetData) != len(refData):
                print("alignDataType warning: targetData list size(%d) not march reference list size(%d)"%(len(targetData), len(refData)))
            for i in range(len(targetData)):
                if i < len(refData):
                    targetData[i] = KeyFrame.alignDataType(targetData[i], refData[i])
            return targetData
        if isinstance(targetData, tuple) and isinstance(refData, tuple):
            if len(targetData) != len(refData):
                print("alignDataType warning: targetData list size(%d) not march reference list size(%d)"%(len(targetData), len(refData)))
            targetData = list(targetData)
            for i in range(len(targetData)):
                if i < len(refData):
                    targetData[i] = KeyFrame.alignDataType(targetData[i], refData[i])
            return tuple(targetData)
        if isinstance(targetData, tuple) and len(targetData) == 3 and isinstance(refData, Vector3):
            return Vector3(targetData[0], targetData[1], targetData[2])
        if isinstance(targetData, tuple) and len(targetData) == 2 and isinstance(refData, Vector2):
            return Vector2(targetData[0], targetData[1])
        if isinstance(targetData, tuple) and len(targetData) == 4 and isinstance(refData, Color):
            return Color(targetData[0], targetData[1], targetData[2], targetData[3])
        return targetData        
        
class CameraKeyFrame(KeyFrame):
    def __init__(self, frameNo, curve):
        # common
        self.frameNo = frameNo
        self.targetID = "cam"
        
        # save current camera
        from Studio import Studio
        studio = Studio.Instance
        c = studio.cameraCtrl
        cdata = c.cameraData
        self.pos = cdata.pos
        self.distance = cdata.distance
        self.rotate = cdata.rotate
        self.fieldOfView = c.fieldOfView
        
        # default interpolate curve, format [x1, x2, y1, y2]
        self.curve = curve
        
    def applyFrame(self, game):
        from Studio import Studio
        studio = Studio.Instance
        c = studio.cameraCtrl
        cdata = c.cameraData
        cdata.pos = self.pos
        cdata.distance = self.distance
        cdata.rotate = self.rotate
        c.fieldOfView = self.fieldOfView
        
    def createInterFrameTo(self, toFrame, newFrameNo):
        if self.frameNo + 1 == toFrame.frameNo:
            # do not interpolate between two adjacent key frame
            return self
        # get interpolate progress
        progress = float(newFrameNo - self.frameNo) / float(toFrame.frameNo - self.frameNo)
        
        # cubic Bezier interpolate
        pcv = cubicBezierInterpolate(toFrame.curve[0], toFrame.curve[1], toFrame.curve[2], toFrame.curve[3], progress)

        # interpolate and create new frame
        nf = CameraKeyFrame(newFrameNo, None)
        nf.pos = Vector3.Lerp(self.pos, toFrame.pos, pcv)
        nf.distance = Vector3.Lerp(self.distance, toFrame.distance, pcv)
        nf.rotate = Vector3.Lerp(self.rotate, toFrame.rotate, pcv)
        nf.fieldOfView = self.fieldOfView + (toFrame.fieldOfView - self.fieldOfView) * pcv
        #print("interpolate cam frame[%f]: pos=(%.2f, %.2f, %.2f), rot=(%.1f, %.1f, %.1f), dis=%.2f, fov=%.1f"%(nf.frameNo, nf.pos.x, nf.pos.y, nf.pos.z, nf.rotate.x, nf.rotate.y, nf.rotate.z, nf.distance.z, nf.fieldOfView))
        return nf
        
    def dumpContent(self):
        dumpDict = {"pos": self.pos, "distance": self.distance, "rotate": self.rotate, "fieldOfView": self.fieldOfView,
                    "curve": self.curve,
                   }
        return script2string(dumpDict)
        
    def importContent(self, content):
        self.pos = Vector3(content["pos"][0], content["pos"][1], content["pos"][2])
        self.distance = Vector3(content["distance"][0], content["distance"][1], content["distance"][2])
        self.rotate = Vector3(content["rotate"][0], content["rotate"][1], content["rotate"][2])
        self.fieldOfView = content["fieldOfView"]
        self.curve = content["curve"]
        
    def copyContent(self, fromKey):
        self.pos = scriptCopy(fromKey.pos)
        self.distance = scriptCopy(fromKey.distance)
        self.rotate = scriptCopy(fromKey.rotate)
        self.fieldOfView = scriptCopy(fromKey.fieldOfView)
        self.curve = scriptCopy(fromKey.curve)
        
    def copy(self):
        nf = CameraKeyFrame(self.frameNo, None)
        nf.copyContent(self)
        return nf

    def optimizeWith(self, fromFrame, toFrame, effectToFrame):
        pass    # can not optimize camera
        
    @property
    def status(self):
        return {"camera": (self.pos, self.distance, self.rotate, self.fieldOfView) }

class ActorKeyFrame(KeyFrame):
    def __init__(self, frameNo, actorId, status, curve):
        # common
        self.frameNo = frameNo
        self.targetID = actorId
        
        # save actor status
        self.status = status
        
        # default interpolate curve, format [x1, x2, y1, y2]
        self.curve = curve
        
    def applyFrame(self, game):
        #print("ActorKeyFrame: apply frame %d"%self.frameNo)
        curStatus = game.scenedata.actors[self.targetID].export_full_status()
        dfs = {}
        for key in self.status.keys():
            if not key in curStatus.keys() or curStatus[key] != self.status[key]:
                dfs[key] = self.status[key]
        act(game, {self.targetID : dfs})
        
    def createInterFrameTo(self, toFrame, newFrameNo):
        #print("ActorKeyFrame: create frame %f"%newFrameNo)
        if self.frameNo + 1 == toFrame.frameNo:
            # do not interpolate between two adjacent key frame
            return self
        # get interpolate progress
        progress = float(newFrameNo - self.frameNo) / float(toFrame.frameNo - self.frameNo)
        
        # cubic Bezier interpolate
        cbiProgress = cubicBezierInterpolate(toFrame.curve[0], toFrame.curve[1], toFrame.curve[2], toFrame.curve[3], progress)

        dfs = {}
        for ak in self.status.keys():
            if not char_act_funcs[ak][1] or not ak in toFrame.status.keys() or toFrame.status[ak] == self.status[ak]:
                dfs[ak] = self.status[ak]
            else:
                dfs[ak] = paramInterpolater(self.status[ak], toFrame.status[ak], cbiProgress)
        nf = ActorKeyFrame(newFrameNo, self.targetID, dfs, None)
        return nf
        
    def dumpContent(self):
        dumpDict = {'status': self.status, 'curve': self.curve}
        return script2string(dumpDict)
        
    def importContent(self, content):
        self.status = content["status"]
        self.curve = content["curve"]
        
    def copyContent(self, fromKey):
        self.status = scriptCopy(fromKey.status)
        self.curve = scriptCopy(fromKey.curve)

    def copy(self):
        nf = ActorKeyFrame(self.frameNo, self.targetID, None, None)
        nf.copyContent(self)
        return nf

    def optimizeWith(self, fromFrame, toFrame, effectToFrame):
        for st in self.status.keys():
            if (not toFrame.status.has_key(st) or self.status[st] == toFrame.status[st]) and (fromFrame == None or not fromFrame.status.has_key(st) or self.status[st] == fromFrame.status[st]):
                # remove from self
                self.status.pop(st)
            else:   # special care for ik_set and fk_set
                if (st == "ik_set" or st == "fk_set") and isinstance(self.status[st], dict):
                    # delete duplicated bone info
                    for bi in self.status[st].keys():
                        if (not toFrame.status.has_key(st) or not toFrame.status[st].has_key(bi) or self.status[st][bi] == toFrame.status[st][bi]) and (fromFrame == None or not fromFrame.status.has_key(st) or not fromFrame.status[st].has_key(bi) or self.status[st][bi] == fromFrame.status[st][bi]):
                            self.status[st].pop(bi)
        if effectToFrame:
            for st in toFrame.status.keys():
                if not self.status.has_key(st) or self.status[st] == toFrame.status[st]:
                    toFrame.status.pop(st)
                else:   # special care for ik_set and fk_set
                    if (st == "ik_set" or st == "fk_set") and isinstance(toFrame.status[st], dict):
                        # delete duplicated bone info
                        for bi in toFrame.status[st].keys():
                            if not self.status[st].has_key(bi) or self.status[st][bi] == toFrame.status[st][bi]:
                                toFrame.status[st].pop(bi)

class PropKeyFrame(KeyFrame):
    def __init__(self, frameNo, propId, propStatus, curve):
        # common
        self.frameNo = frameNo
        self.targetID = propId
        
        # save actor status
        self.status = propStatus
        
        # default interpolate curve, format [x1, x2, y1, y2], not necessary for non-key-frame
        self.curve = curve
        
    def applyFrame(self, game):
        #print("PropKeyFrame: apply frame %d"%self.frameNo)
        curStatus = game.scenedata.props[self.targetID].export_full_status()
        dfs = {}
        for key in self.status.keys():
            if not key in curStatus.keys() or curStatus[key] != self.status[key]:
                dfs[key] = self.status[key]
        act(game, {self.targetID : dfs})
        
    def createInterFrameTo(self, toFrame, newFrameNo):
        #print("PropKeyFrame: create frame %f"%newFrameNo)
        if self.frameNo + 1 == toFrame.frameNo:
            # do not interpolate between two adjacent key frame
            return self
        # get interpolate progress
        progress = float(newFrameNo - self.frameNo) / float(toFrame.frameNo - self.frameNo)

        # cubic Bezier interpolate
        cbiProgress = cubicBezierInterpolate(toFrame.curve[0], toFrame.curve[1], toFrame.curve[2], toFrame.curve[3], progress)
        
        dfs = {}
        for ak in self.status.keys():
            if not prop_act_funcs[ak][1] or not ak in toFrame.status.keys() or toFrame.status[ak] == self.status[ak]:
                dfs[ak] = self.status[ak]
            else:
                dfs[ak] = paramInterpolater(self.status[ak], toFrame.status[ak], cbiProgress)
        nf = PropKeyFrame(newFrameNo, self.targetID, dfs, None)
        return nf
        
    def dumpContent(self):
        dumpDict = {'status': self.status, 'curve': self.curve}
        return script2string(dumpDict)
        
    def importContent(self, content):
        self.status = content["status"]
        self.curve = content["curve"]
        
    def copyContent(self, fromKey):
        self.status = scriptCopy(fromKey.status)
        self.curve = scriptCopy(fromKey.curve)

    def copy(self):
        nf = PropKeyFrame(self.frameNo, self.targetID, None, None)
        nf.copyContent(self)
        return nf

    def optimizeWith(self, fromFrame, toFrame, effectToFrame):
        for st in self.status.keys():
            if (not toFrame.status.has_key(st) or self.status[st] == toFrame.status[st]) and (fromFrame == None or not fromFrame.status.has_key(st) or self.status[st] == fromFrame.status[st]):
                # remove from self
                self.status.pop(st)
        if effectToFrame:
            for st in toFrame.status.keys():
                if not self.status.has_key(st) or self.status[st] == toFrame.status[st]:
                    toFrame.status.pop(st)

class SystemKeyFrame(KeyFrame):
    def __init__(self, frameNo, sysStatus, curve):
        # common
        self.frameNo = frameNo
        self.targetID = "sys"
        
        # save actor status
        self.status = sysStatus
        
        # default interpolate curve, format [x1, x2, y1, y2], not necessary for non-key-frame
        self.curve = curve

    def applyFrame(self, game):
        #print("SystemKeyFrame: apply frame %d"%self.frameNo)
        curStatus = export_sys_status(game)
        dfs = {}
        for key in self.status.keys():
            if not key in curStatus.keys() or curStatus[key] != self.status[key]:
                dfs[key] = self.status[key]
        act(game, {self.targetID : dfs})
        
    def createInterFrameTo(self, toFrame, newFrameNo):
        #print("SystemKeyFrame: create frame %f"%newFrameNo)
        if self.frameNo + 1 == toFrame.frameNo:
            # do not interpolate between two adjacent key frame
            return self
        # get interpolate progress
        progress = float(newFrameNo - self.frameNo) / float(toFrame.frameNo - self.frameNo)

        # cubic Bezier interpolate
        cbiProgress = cubicBezierInterpolate(toFrame.curve[0], toFrame.curve[1], toFrame.curve[2], toFrame.curve[3], progress)
        
        dfs = {}
        for ak in self.status.keys():
            if not sys_act_funcs[ak][1] or not ak in toFrame.status.keys() or toFrame.status[ak] == self.status[ak]:
                dfs[ak] = self.status[ak]
            else:
                dfs[ak] = paramInterpolater(self.status[ak], toFrame.status[ak], cbiProgress)
        nf = SystemKeyFrame(newFrameNo, dfs, None)
        return nf
        
    def dumpContent(self):
        dumpDict = {'status': self.status, 'curve': self.curve}
        return script2string(dumpDict)
        
    def importContent(self, content):
        self.status = content["status"]
        self.curve = content["curve"]
        
    def copyContent(self, fromKey):
        self.status = scriptCopy(fromKey.status)
        self.curve = scriptCopy(fromKey.curve)

    def copy(self):
        nf = SystemKeyFrame(self.frameNo, None, None)
        nf.copyContent(self)
        return nf

    def optimizeWith(self, fromFrame, toFrame, effectToFrame):
        for st in self.status.keys():
            if (not toFrame.status.has_key(st) or self.status[st] == toFrame.status[st]) and (fromFrame == None or not fromFrame.status.has_key(st) or self.status[st] == fromFrame.status[st]):
                # remove from self
                self.status.pop(st)
        if effectToFrame:
            for st in toFrame.status.keys():
                if not self.status.has_key(st) or self.status[st] == toFrame.status[st]:
                    toFrame.status.pop(st)

class ClipKeyFrame(KeyFrame):
    def __init__(self, frameNo, clipId, clipStatus):
        # common
        self.frameNo = frameNo
        self.targetID = clipId
        
        # save clip status
        self.status = clipStatus
                
    def applyFrame(self, game):
        #print("ClipKeyFrame: apply frame %d"%self.frameNo)
        if game.gdata.kfaManagedClips.has_key(self.targetID):
            game.gdata.kfaManagedClips[self.targetID].import_status(self.status)
        
    def createInterFrameTo(self, toFrame, newFrameNo):
        #print("ClipKeyFrame: create frame %f"%newFrameNo)
        if self.frameNo + 1 == toFrame.frameNo:
            # do not interpolate between two adjacent key frame
            return self
        if int(newFrameNo) == self.frameNo:
            return self
        else:
            return None
        
    def dumpContent(self):
        dumpDict = {'status': self.status}
        return script2string(dumpDict)
        
    def importContent(self, content):
        self.status = content["status"]
        
    def copyContent(self, fromKey):
        self.status = scriptCopy(fromKey.status)

    def copy(self):
        nf = ClipKeyFrame(self.frameNo, self.targetID, None)
        nf.copyContent(self)
        return nf

    def optimizeWith(self, fromFrame, toFrame, effectToFrame):
        pass    # do not optimize clip keyframe

class KeyFrameClip():
    def __init__(self, game, clipScript):
        # init runtime
        self.game = game
        self.isPlaying = False
        self.curFrame = 0
        self.clipboard = None
        # setting
        self.name = ""
        self.frameLength = 300
        self.frameRate = 30
        self.priority = 100
        self.animeType = "absolute"
        self.loop = -1
        self.speed = 1
        # key frames
        self.keyframes = {}
        # new setting
        self.autorun = False    # from ver1.6
        self.version = 1
        self.syncActorAnime = False # not supported yet
        # import settings & keyframe or an empty one
        if clipScript:
            self.importSetting(clipScript)

    @property
    def lastFrameNo(self):  # the last frame no of all keyframes
        lfn = 0
        for kfs in self.keyframes.values():
            if len(kfs) == 0:
                continue
            if kfs[-1].frameNo > lfn:
                lfn = kfs[-1].frameNo
        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

    def copy(self, newName):
        newClip = KeyFrameClip(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
        # key frames
        newClip.keyframes = {}
        for actId in self.keyframes:
            newClip.keyframes[actId] = []
            for orgKf in self.keyframes[actId]:
                newClip.keyframes[actId].append(orgKf.copy())
        # return
        return newClip

    def importSetting(self, clipScript, tgtId=None):
        if tgtId:
            if tgtId == "cam":
                self.keyframes["cam"] = []
                for frameNo in clipScript.keys():
                    frm = CameraKeyFrame(frameNo, None)
                    frm.importContent(clipScript[frameNo])
                    self.keyframes["cam"].append(frm)
                if len(self.keyframes["cam"]):
                    self.keyframes["cam"].sort(key=KeyFrame.sortByFrameNo)
                else:
                    self.keyframes.pop("cam")
            elif tgtId == "sys":
                refScript = export_sys_status(self.game)
                self.keyframes["sys"] = []
                for frameNo in clipScript.keys():
                    frm = SystemKeyFrame(frameNo, None, None)
                    frm.importContent(clipScript[frameNo])
                    frm.status = KeyFrame.alignDataType(frm.status, refScript)
                    self.keyframes["sys"].append(frm)
                if len(self.keyframes["sys"]):
                    self.keyframes["sys"].sort(key=KeyFrame.sortByFrameNo)
                else:
                    self.keyframes.pop("sys")
            elif self.game.scenedata.actors.has_key(tgtId):
                refScript = self.game.scenedata.actors[tgtId].export_full_status()
                self.keyframes[tgtId] = []
                for frameNo in clipScript.keys():
                    frm = ActorKeyFrame(frameNo, tgtId, None, None)
                    frm.importContent(clipScript[frameNo])
                    frm.status = KeyFrame.alignDataType(frm.status, refScript)
                    self.keyframes[tgtId].append(frm)
                if len(self.keyframes[tgtId]):
                    self.keyframes[tgtId].sort(key=KeyFrame.sortByFrameNo)
                else:
                    self.keyframes.pop(tgtId)
            elif self.game.scenedata.props.has_key(tgtId):
                refScript = self.game.scenedata.props[tgtId].export_full_status()
                self.keyframes[tgtId] = []
                for frameNo in clipScript.keys():
                    frm = PropKeyFrame(frameNo, tgtId, None, None)
                    frm.importContent(clipScript[frameNo])
                    frm.status = KeyFrame.alignDataType(frm.status, refScript)
                    self.keyframes[tgtId].append(frm)
                if len(self.keyframes[tgtId]):
                    self.keyframes[tgtId].sort(key=KeyFrame.sortByFrameNo)
                else:
                    self.keyframes.pop(tgtId)
            elif self.game.gdata.kfaManagedClips.has_key(tgtId):
                self.keyframes[tgtId] = []
                for frameNo in clipScript.keys():
                    frm = ClipKeyFrame(frameNo, tgtId, None)
                    frm.importContent(clipScript[frameNo])
                    self.keyframes[tgtId].append(frm)
                if len(self.keyframes[tgtId]):
                    self.keyframes[tgtId].sort(key=KeyFrame.sortByFrameNo)
                else:
                    self.keyframes.pop(tgtId)
            else:
                raise Exception("Unknown target ID <%s>"%tgtId)
        else:
            # setting
            self.name = clipScript["setting"]["name"]
            self.frameLength = clipScript["setting"]["frameLength"]
            self.frameRate = clipScript["setting"]["frameRate"]
            self.priority = clipScript["setting"]["priority"]
            self.animeType = clipScript["setting"]["animeType"]
            self.loop = clipScript["setting"]["loop"]
            self.speed = clipScript["setting"]["speed"]
            # from v1.6
            if "autorun" in clipScript["setting"]:
                self.autorun = clipScript["setting"]["autorun"]
            
            # key frames
            self.keyframes = {}
            # actors
            if clipScript.has_key("actors"):
                for actkey in clipScript["actors"].keys():
                    if not self.game.scenedata.actors.has_key(actkey):
                        continue
                    refScript = self.game.scenedata.actors[actkey].export_full_status()
                    self.keyframes[actkey] = []
                    for frameNo in clipScript["actors"][actkey].keys():
                        frm = ActorKeyFrame(frameNo, actkey, None, None)
                        frm.importContent(clipScript["actors"][actkey][frameNo])
                        frm.status = KeyFrame.alignDataType(frm.status, refScript)
                        self.keyframes[actkey].append(frm)
                    if len(self.keyframes[actkey]):
                        self.keyframes[actkey].sort(key=KeyFrame.sortByFrameNo)
                    else:
                        self.keyframes.pop(actkey)
            if clipScript.has_key("props"):
                for prokey in clipScript["props"].keys():
                    if not self.game.scenedata.props.has_key(prokey):
                        continue
                    refScript = self.game.scenedata.props[prokey].export_full_status()
                    self.keyframes[prokey] = []
                    for frameNo in clipScript["props"][prokey].keys():
                        frm = PropKeyFrame(frameNo, prokey, None, None)
                        frm.importContent(clipScript["props"][prokey][frameNo])
                        frm.status = KeyFrame.alignDataType(frm.status, refScript)
                        self.keyframes[prokey].append(frm)
                    if len(self.keyframes[prokey]):
                        self.keyframes[prokey].sort(key=KeyFrame.sortByFrameNo)
                    else:
                        self.keyframes.pop(prokey)
            if clipScript.has_key("clips"):
                for clpkey in clipScript["clips"].keys():
                    self.keyframes[clpkey] = []
                    for frameNo in clipScript["clips"][clpkey].keys():
                        frm = ClipKeyFrame(frameNo, clpkey, None)
                        frm.importContent(clipScript["clips"][clpkey][frameNo])
                        self.keyframes[clpkey].append(frm)
                    if len(self.keyframes[clpkey]):
                        self.keyframes[clpkey].sort(key=KeyFrame.sortByFrameNo)
                    else:
                        self.keyframes.pop(clpkey)
            if clipScript.has_key("cam"):
                self.keyframes["cam"] = []
                for frameNo in clipScript["cam"].keys():
                    frm = CameraKeyFrame(frameNo, None)
                    frm.importContent(clipScript["cam"][frameNo])
                    self.keyframes["cam"].append(frm)
                if len(self.keyframes["cam"]):
                    self.keyframes["cam"].sort(key=KeyFrame.sortByFrameNo)
                else:
                    self.keyframes.pop("cam")
            if clipScript.has_key("sys"):
                refScript = export_sys_status(self.game)
                self.keyframes["sys"] = []
                for frameNo in clipScript["sys"].keys():
                    frm = SystemKeyFrame(frameNo, None, None)
                    frm.importContent(clipScript["sys"][frameNo])
                    frm.status = KeyFrame.alignDataType(frm.status, refScript)
                    self.keyframes["sys"].append(frm)
                if len(self.keyframes["sys"]):
                    self.keyframes["sys"].sort(key=KeyFrame.sortByFrameNo)
                else:
                    self.keyframes.pop("sys")

    def exportSetting(self, tgtId=None):
        if tgtId:
            output = "{\n"
            for frm in self.keyframes[tgtId]:
                output += "    %5d : %s,\n"%(frm.frameNo, frm.dumpContent())
            output += "}"
        else:
            output = "{\n"
            # save setting
            output += "    'setting': {\n"
            output += "        'name' : '%s',\n"%self.name
            output += "        'frameLength': %d,\n"%self.frameLength
            output += "        'frameRate': %d,\n"%self.frameRate
            output += "        'priority': %d,\n"%self.priority
            output += "        'animeType': '%s',\n"%self.animeType
            output += "        'loop': %d,\n"%self.loop
            output += "        'speed': %.2f,\n"%self.speed
            output += "        'autorun': %s,\n"%self.autorun
            output += "    },\n"
            # save actors
            actKeys = [tgt for tgt in self.keyframes.Keys if self.game.scenedata.actors.has_key(tgt)]
            if len(actKeys) > 0:
                output += "    'actors': {\n"
                for tgt in actKeys:
                    output += "        '%s': {\n"%tgt
                    for frm in self.keyframes[tgt]:
                        output += "        %5d : %s,\n"%(frm.frameNo, frm.dumpContent())
                    output += "        },\n"
                output += "    },\n"
            # save props
            proKeys = [tgt for tgt in self.keyframes.Keys if self.game.scenedata.props.has_key(tgt)]
            if len(proKeys) > 0:
                output += "    'props': {\n"
                for tgt in proKeys:
                    output += "        '%s': {\n"%tgt
                    for frm in self.keyframes[tgt]:
                        output += "        %5d : %s,\n"%(frm.frameNo, frm.dumpContent())
                    output += "        },\n"
                output += "    },\n"
            # save clips
            clpKeys = [tgt for tgt in self.keyframes.Keys if self.game.gdata.kfaManagedClips.has_key(tgt)]
            if len(clpKeys) > 0:
                output += "    'clips': {\n"
                for tgt in clpKeys:
                    output += "        '%s': {\n"%tgt
                    for frm in self.keyframes[tgt]:
                        output += "        %5d : %s,\n"%(frm.frameNo, frm.dumpContent())
                    output += "        },\n"
                output += "    },\n"
            # save camera
            if self.keyframes.has_key("cam"):
                output += "    'cam': {\n"
                for frm in self.keyframes["cam"]:
                    output += "        %5d : %s,\n"%(frm.frameNo, frm.dumpContent())
                output += "    },\n"
            # save system
            if self.keyframes.has_key("sys"):
                output += "    'sys': {\n"
                for frm in self.keyframes["sys"]:
                    output += "        %5d : %s,\n"%(frm.frameNo, frm.dumpContent())
                output += "    },\n"
            output += "}\n"
        # export
        return output

    def exportSettingForScene(self, tgtId=None):
        if tgtId:
            output = "{"
            for frm in self.keyframes[tgtId]:
                output += "%5d:%s,"%(frm.frameNo, frm.dumpContent())
            output += "}"
        else:
            output = "{"
            # save setting
            output += "'setting':{"
            output += "'name':'%s',"%self.name
            output += "'frameLength':%d,"%self.frameLength
            output += "'frameRate':%d,"%self.frameRate
            output += "'priority':%d,"%self.priority
            output += "'animeType':'%s',"%self.animeType
            output += "'loop':%d,"%self.loop
            output += "'speed':%.2f,"%self.speed
            output += "'autorun':%s,"%self.autorun
            output += "},"
            # save actors
            actKeys = [tgt for tgt in self.keyframes.Keys if self.game.scenedata.actors.has_key(tgt)]
            if len(actKeys) > 0:
                output += "'actors':{"
                for tgt in actKeys:
                    output += "'%s':{"%tgt
                    for frm in self.keyframes[tgt]:
                        output += "%5d:%s,"%(frm.frameNo, frm.dumpContent())
                    output += "},"
                output += "},"
            # save props
            proKeys = [tgt for tgt in self.keyframes.Keys if self.game.scenedata.props.has_key(tgt)]
            if len(proKeys) > 0:
                output += "'props':{"
                for tgt in proKeys:
                    output += "'%s':{"%tgt
                    for frm in self.keyframes[tgt]:
                        output += "%5d:%s,"%(frm.frameNo, frm.dumpContent())
                    output += "},"
                output += "},"
            # save clips
            clpKeys = [tgt for tgt in self.keyframes.Keys if self.game.gdata.kfaManagedClips.has_key(tgt)]
            if len(clpKeys) > 0:
                output += "'clips':{"
                for tgt in clpKeys:
                    output += "'%s':{"%tgt
                    for frm in self.keyframes[tgt]:
                        output += "%5d:%s,"%(frm.frameNo, frm.dumpContent())
                    output += "},"
                output += "},"
            # save camera
            if self.keyframes.has_key("cam"):
                output += "'cam':{"
                for frm in self.keyframes["cam"]:
                    output += "%5d:%s,"%(frm.frameNo, frm.dumpContent())
                output += "},"
            # save system
            if self.keyframes.has_key("sys"):
                output += "'sys':{"
                for frm in self.keyframes["sys"]:
                    output += "%5d:%s,"%(frm.frameNo, frm.dumpContent())
                output += "},"
            output += "}"
        # export
        return output        

    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

    def optimizeKeyFrames(self, includeFirst):
        for frms in self.keyframes.values():
            if includeFirst:
                tgtIdx = range(0, len(frms) - 1)
            else:
                tgtIdx = range(1, len(frms) - 1)
            for idx in tgtIdx:
                fromFrame = None if idx == 0 else frms[idx-1]
                frms[idx].optimizeWith(fromFrame, frms[idx+1], idx == tgtIdx[-1])

    def clearUnchangedStatus(self, tgtId=None):
        def doClrUchSt(tgtKeyframes):
            print("clear unchanged status for", tgtKeyframes[0].targetID)
            for st in tgtKeyframes[0].status.keys():
                unchanged = True
                for i in range(1, len(tgtKeyframes)):
                    if not tgtKeyframes[i].status.has_key(st) or tgtKeyframes[0].status[st] != tgtKeyframes[i].status[st]:
                        unchanged = False
                        break
                if unchanged:
                    print(" delete status:" + str(st))
                    for kf in tgtKeyframes:
                        kf.status.pop(st)
        # clear target clip or every clip
        if tgtId == None:
            for tgtId in self.keyframes.keys():
                if len(self.keyframes[tgtId]) > 1:
                    tgtkfs = self.keyframes[tgtId]
                    if isinstance(tgtkfs[0], ActorKeyFrame) or isinstance(tgtkfs[0], PropKeyFrame) or isinstance(tgtkfs[0], SystemKeyFrame):
                        doClrUchSt(tgtkfs)
        else:
            if self.keyframes.has_key(tgtId) and len(self.keyframes[tgtId]) > 1:
                doClrUchSt(self.keyframes[tgtId])

    def checkUnchangedStatus(self, tgtId):
        # check
        if not self.keyframes.has_key(tgtId):
            return None     # unknown ID
        tgtkfs = self.keyframes[tgtId]
        if len(tgtkfs) == 0:
            return None     # no data at all
        if len(tgtkfs) == 1:
            return ()       # not enough data for check
        if isinstance(tgtkfs[0], ActorKeyFrame) or isinstance(tgtkfs[0], PropKeyFrame) or isinstance(tgtkfs[0], SystemKeyFrame):
            ucList = []
            for st in tgtkfs[0].status.keys():
                unchanged = True
                for i in range(1, len(tgtkfs)):
                    if not tgtkfs[i].status.has_key(st) or tgtkfs[0].status[st] != tgtkfs[i].status[st]:
                        unchanged = False
                        break
                if unchanged:
                    ucList.append(st)
            return tuple(ucList)    # list of unchanged status
        else:
            return None     # camera and clip keyframe exclude

    def clearSpecifiedStatus(self, tgtId, delList, keep1st):
        # delete status in list
        tgtkfs = self.keyframes[tgtId]
        for i in range(1 if keep1st else 0, len(tgtkfs)):
            for st in delList:
                tgtkfs[i].status.pop(st)

    # play control
    def play(self, forceLoop=None):
        if forceLoop != None:
            self.playLoop = forceLoop
        else:
            self.playLoop = self.loop
        self.isPlaying = True
        if self.syncActorAnime:
            self.syncActor(False)
        
    def pause(self):
        self.isPlaying = False
        if self.syncActorAnime:
            self.syncActor(False)
        
    def stop(self):
        self.isPlaying = False
        self.curFrame = 0
        if self.syncActorAnime:
            self.syncActor(False)
        
    def seek(self, frameNo, apply=True):
        self.curFrame = frameNo if frameNo <= self.frameLength else frameNo % self.frameLength
        if apply:
            self.applyFrames()
            if self.syncActorAnime:
                self.syncActor(True)
        
    def seekNormalized(self, ntime, apply=True):
        self.curFrame = ntime * self.frameLength
        if apply:
            self.applyFrames()
            if self.syncActorAnime:
                self.syncActor(True)

    def update(self, game):
        from UnityEngine import Time
        if self.isPlaying:
            deltaTime = Time.deltaTime
            self.curFrame += deltaTime * self.frameRate * self.speed
            if self.curFrame > self.frameLength:
                if self.playLoop != 0:
                    self.curFrame -= self.frameLength
                    if self.playLoop > 0: self.playLoop -= 1
                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 applyFrames(self):
        for tgtId in self.keyframes.keys():
            frame = self.getCurFrame(tgtId)
            if frame:
                frame.applyFrame(self.game)

    def getCurFrames(self):
        keyframes = {}
        for tgtId in self.keyframes.keys():
            keyframes[tgtId] = self.getCurFrame(tgtId)
        return keyframes

    def getCurFrame(self, tgtId):
        tgtKeyFrames = self.keyframes[tgtId]
        tgtKeys = [i.frameNo for i in tgtKeyFrames]
        if tgtKeys == None or len(tgtKeys) == 0:
            return None
        if self.curFrame < tgtKeys[0]:
            return None
        if self.curFrame >= tgtKeys[-1]+1:
            return None
        if self.curFrame >= tgtKeys[-1]:
            return tgtKeyFrames[-1]
        #if self.curFrame > self.cameraKeyFrames[self.fsIndex].frameNo and self.curFrame < self.cameraKeyFrames[self.fsIndex+1].frameNo:
        #    return self.createInterFrame()
        for i in range(len(tgtKeys)):
            if self.curFrame == tgtKeys[i]:
                #self.fsIndex = i
                return tgtKeyFrames[i]
            elif self.curFrame < tgtKeys[i+1]:
                #self.fsIndex = i
                return tgtKeyFrames[i].createInterFrameTo(tgtKeyFrames[i+1], self.curFrame)
    
    def syncActor(self, seek):
        actKeys = [tgt for tgt in self.keyframes.Keys if self.game.scenedata.actors.has_key(tgt)]
        for actId in actKeys:
            actor = self.game.scenedata.actors[actId]
            if self.isPlaying:
                actor.set_anime_speed(1)
                #print("sync Actor %s's anime speed to %f"%(actId, 1))
            else:
                actor.set_anime_speed(0)
                #print("sync Actor %s's anime speed to %f"%(actId, 0))
            if seek:
                if self.curFrame == 0:
                    actor.restart_anime()
                    #print("syncActor restart anime")
                else:
                    ani = actor.get_animate()
                    nt = float(self.curFrame) / self.frameLength
                    actor.animate(ani[0], ani[1], ani[2], nt, 1)
                    #print("syncActor force animate update: %f"%(nt))

    # edit function
    def addTarget(self, id):
        self.keyframes[id] = []

    def updateKeyFrame(self, keyFrame):
        # update keyframes dictionary
        if not self.keyframes.has_key(keyFrame.targetID):
            self.keyframes[keyFrame.targetID] = []
        # check if existed
        existKeys = tuple(i.frameNo for i in self.keyframes[keyFrame.targetID])
        exist = existKeys.count(keyFrame.frameNo)
        if exist == 0:
            # append new keyframe
            self.keyframes[keyFrame.targetID].append(keyFrame)
            self.keyframes[keyFrame.targetID].sort(key=KeyFrame.sortByFrameNo)
        elif exist == 1:
            # replace old keyframe
            idx = existKeys.index(keyFrame.frameNo)
            self.keyframes[keyFrame.targetID][idx] = keyFrame
        else:
            raise Exception("Duplicated keyframes at %d for id:<%s>"%(keyFrame.frameNo, keyFrame.targetID))
        
    def shiftKeyFrame(self, tgtId, shift, includeFollower):
        print("Shift key frame for %s at %d by %d, includeFollower = %s"%(tgtId, self.curFrame, shift, includeFollower))
        # check
        if shift == 0:
            return
        # shift frames
        for kf in self.keyframes[tgtId]:
            if kf.frameNo == self.curFrame or (includeFollower and kf.frameNo > self.curFrame):
                kf.frameNo += shift
        
    def deleteKeyFrame(self, tgtId):
        print("delete a key frame for %s at %d"%(tgtId, self.curFrame))
        existKeys = tuple(i.frameNo for i in self.keyframes[tgtId])
        idx = existKeys.index(self.curFrame)
        self.keyframes[tgtId].pop(idx)
        if len(self.keyframes[tgtId]) == 0:
            self.keyframes.pop(tgtId)
        
    def copyKeyFrame(self, tgtId):
        print("copy a key frame for %s at %d to clipboard"%(tgtId, self.curFrame))
        existKeys = tuple(i.frameNo for i in self.keyframes[tgtId])
        idx = existKeys.index(self.curFrame)
        self.clipboard = self.keyframes[tgtId][idx].copy()

    def canPasteKeyFrame(self, tgtId):
        if self.clipboard == None:
            return False
        if self.clipboard.targetID == tgtId:
            # same tgtId
            return True
        elif self.game.scenedata.actors.has_key(self.clipboard.targetID) and self.game.scenedata.actors.has_key(tgtId):
            # actor -> actor
            if type(self.game.scenedata.actors[self.clipboard.targetID].objctrl) == type(self.game.scenedata.actors[tgtId].objctrl):
                return True
            else:
                return False
        elif self.game.scenedata.props.has_key(self.clipboard.targetID) and self.game.scenedata.props.has_key(tgtId):
            # prop -> prop
            if type(self.game.scenedata.props[self.clipboard.targetID].objctrl) == type(self.game.scenedata.props[tgtId].objctrl):
                return True
            else:
                return False
        else:
            return False
        
    def pasteKeyFrame(self, tgtId):
        if tgtId == "cam":
            nf = CameraKeyFrame(self.curFrame, None)
            nf.copyContent(self.clipboard)
        elif tgtId == "sys":
            nf = SystemKeyFrame(self.curFrame, None, None)
            nf.copyContent(self.clipboard)
        elif tgtId in self.game.scenedata.actors:
            nf = ActorKeyFrame(self.curFrame, tgtId, None, None)
            nf.copyContent(self.clipboard)
        elif tgtId in self.game.scenedata.props:
            nf = PropKeyFrame(self.curFrame, tgtId, None, None)
            nf.copyContent(self.clipboard)
        elif tgtId in self.game.gdata.kfaManagedClips:
            nf = ClipKeyFrame(self.curFrame, tgtId, None)
            nf.copyContent(self.clipboard)
        else:
            raise Exception("Unexpected target id '%s' to paste key frame!"%tgtId)
        self.updateKeyFrame(nf)

    @staticmethod
    def sortByPriority(keyframeClip):
        return (keyframeClip.priority, keyframeClip.name)

# script wrapper function
# not supported, just for anime/non-anime check
clip_act_funcs = {
    'play': ("clip_play", False),
    'frame': ("clip_seek", False),
    'loop': ("clip_loop", False),
    'speed': ("clip_speed", True),
}

# GUI function
def kfam_GUI(sh):
    game = sh.game
    if not hasattr(game.scenedata, "kfaGuiFunction"):
        changeGuiScreen(sh, kfam_GUI_manager)
    try:
        game.scenedata.kfaGuiFunction(sh)
    except Exception as e:
        print("\n*** VNAnime GUI Failed ***")
        traceback.print_exc()
        changeGuiScreen(sh, kfam_GUI_manager)
        scriptHelperGUIMessage("Oh NO! VNAnime GUI failed!<color=#00ffff>%s</color>"%str(e))

def changeGuiScreen(sh, newGuiScreen, targetClip=None):
    from vnanime2 import kfam_GUI_modify_clip_setting2
    game = sh.game
    sd = game.scenedata
    if not hasattr(sd, "kfaGuiFunction"):
        sd.kfaGuiFunction = None
    if newGuiScreen == kfam_GUI_manager:
        sd.kfamOnModifyClip = None
        game.windowName = "Key Frame Animation Clip Manager"
    elif newGuiScreen == kfam_GUI_create_clip:
        sd.kfamOnModifyClip = targetClip
        sd.kfamCreateType = 0
        sd.kfamCreateName = ""
        sd.kfamCreateFile = ""
        sd.kfamCreateVer = 2
        sd.kfamImportClipName = True
        game.windowName = "Create new key frame animation clip"
    elif newGuiScreen == kfam_GUI_modify_clip:
        if sd.kfaGuiFunction == kfam_GUI_modify_clip_min:
            # restore
            game.windowRect = Rect(game.windowRect.x, game.windowRect.bottom - game.gdata.kfaSetting.clipEditorWindowRect.height, game.windowRect.width, game.gdata.kfaSetting.clipEditorWindowRect.height)
            game.gdata.kfaSetting.clipEditorWindowRect = game.windowRect
        elif sd.kfaGuiFunction == kfam_GUI_modify_clip_add:
            pass
        elif sd.kfaGuiFunction == kfam_GUI_modify_clip_setting:
            pass
        elif sd.kfaGuiFunction == kfam_GUI_modify_clip_setting2:
            pass
        else:
            # start editor
            sd.kfamOnModifyClip = targetClip
            sd.kfamModifyTabIndex = -1
            game.windowName = "Editing clip <color=#00ffff>v%d</color>: <b><color=#ff0000>%s</color></b>"%(targetClip.version, targetClip.name)
            # Load setting
            load_keyframe_anime_setting(game)
    elif newGuiScreen == kfam_GUI_modify_clip_min:
        # mininize
        game.windowRect = Rect(game.windowRect.x, game.windowRect.bottom - 65, game.windowRect.width, 65)
    elif newGuiScreen == kfam_GUI_modify_clip_add:
        pass
    elif newGuiScreen == kfam_GUI_modify_clip_setting:
        pass
    elif newGuiScreen == kfam_GUI_modify_clip_setting2:
        pass
    else:
        raise Exception("Unknown KeyFrameAnime GUI Function")
    # change
    sd.kfaGuiFunction = newGuiScreen

def outputAllClipsToScene(game):
    # change to new fashion if supported
    if game.isSceneDataSaveSupported:
        scene_saved(game)
        scriptHelperGUIMessage("All keyframe clips in keyframe clip manager were saved in png extend data. <color=#ff0000>Save your scene to save your work!</color>")
        return

    # old fashion, no more support!
    # shortcuts
    sClips = game.gdata.kfaSortedClips
    # delete "-keyframeclips:" folder if existed
    flds = game.scene_get_all_folders()
    for fld in flds:
        if fld.name.startswith("-keyframeclips:"):
            fld.delete()
            break
    # create "-keyframeclips:" folder
    clpBaseFld = HSNeoOCIFolder.add("-keyframeclips:v1")
    # build output for all managered clips
    for clip in sClips:
        if clip.version != 1:
            continue    # this method only for v1 now
        clpFld = HSNeoOCIFolder.add("-clip:%s:0"%clip.name)
        clpFld.set_parent(clpBaseFld)
        output = clip.exportSettingForScene()
        clpData = HSNeoOCIFolder.add(output)
        clpData.set_parent(clpFld)
    scriptHelperGUIMessage("All keyframe clips in keyframe clip manager were export into <color=#ffff00>-keyframeclips:v1</color> folder. <color=#ff0000>Save your scene to save your work!</color>")

def importAllClipsFromScene(game, baseFld=None):
    # this is old fashion, load data from workspace folder!

    # try to found base folder if not setted
    if baseFld == None:
        flds = game.scene_get_all_folders()
        for fld in flds:
            if fld.name.startswith("-keyframeclips:"):
                baseFld = fld
                break
    if baseFld == None:
        return
    # update regist tag
    register_actor_prop_by_tag(game)
    # load all clips
    for clpFld in baseFld.treeNodeObject.child:
        clpFldTxt = clpFld.textName
        if clpFldTxt.startswith("-clip:"):
            try:
                clpScp = eval(clpFld.child[0].textName)
                kfa_import(game, clpScp)
            except Exception as e:
                print("Error in load %s: %s"%(clpFldTxt, str(e)))

def outputAllClipsToExtFile(sh):
    # shortcuts
    sClips = sh.game.gdata.kfaSortedClips
    # build output for all managered clips
    output = ""
    for clip in sClips:
        setting = clip.exportSetting()
        if isinstance(setting, dict):
            from vnframe import dumpToJsonString
            setting = dumpToJsonString(setting, True)
        output += setting
        output += ",\n"
    # dump or save to python
    if sh.asTemplate and sh.asEnable:
        msg = sh.as_rebuild_clips(output)
        scriptHelperGUIMessage(msg, ("OK",))
    else:
        if saveStringToFile("dumppython.txt", output, append=True):
            scriptHelperGUIMessage("All keyframe clips in keyframe clip manager were dumpped into <color=#ffff00>dumppython.txt</color>.")

def selectTaggedActorOrProp(game):
    try:
        sel = HSNeoOCI.create_from_selected()
    except:
        return (None, None)
    if isinstance(sel, HSNeoOCIChar):
        for actorId in game.scenedata.actors.keys():
            if game.scenedata.actors[actorId].objctrl == sel.objctrl:
                return (actorId, game.scenedata.actors[actorId])
        return (None, sel.as_actor)
    if isinstance(sel, HSNeoOCIProp):
        for propId in game.scenedata.props.keys():
            if game.scenedata.props[propId].objctrl == sel.objctrl:
                return (propId, game.scenedata.props[propId])
        return (None, sel.as_prop)
    return (None, None)

def drawCurve(texture, curveData):
    from System import Array
    from UnityEngine import Color, Color32
    w = texture.width
    h = texture.height
    #print("Update curve:", curveData)
    # build canvas
    canvasCols = []
    for y in range(h):
        for x in range(w):
            if x == 0 or y == 0 or x == 99 or y == 99:
                canvasCols.append(Color.white)
            else:
                canvasCols.append(Color(0, 0, 0, 0))
    # funcs
    def point(x, y, color):
        if x < 0: x = 0
        if x > w-1: x = w-1
        if y < 0: y = 0
        if y > h-1: y = h-1
        idx = int(y*w+x)
        canvasCols[idx] = color
    def handle(x, y, color):
        point(x-1, y-1, color)
        point(x, y-1, color)
        point(x+1, y-1, color)
        point(x-1, y, color)
        point(x, y, color)
        point(x+1, y, color)
        point(x-1, y+1, color)
        point(x, y+1, color)
        point(x+1, y+1, color)
    # draw cuvre
    for x in range(1, w-1):
        y = cubicBezierInterpolate(curveData[0], curveData[1], curveData[2], curveData[3], float(x)/w)
        y = round(y*h)
        point(x, y, Color.yellow)
    # draw handle
    h1x = round(w*curveData[0])
    h2x = round(w*curveData[1])
    h1y = round(h*curveData[2])
    h2y = round(h*curveData[3])
    handle(h1x, h1y, Color.cyan)
    handle(h2x, h2y, Color.magenta)
    # done
    texture.SetPixels(Array[Color](canvasCols))
    texture.Apply()

def saveStringToFile(filename, outputString, append=False, backup=True):
    import shutil
    import codecs
    fp = None
    try:
        if not append and backup and path.isfile(filename):
            bckfile = filename + ".bak"
            shutil.move(filename, bckfile)
        fp = codecs.open(filename, 'a+' if append else 'w', 'utf-8')
        fp.write(outputString)
        fp.close()
        return True
    except Exception as e:
        if fp != None: fp.close()
        msg = "Save file '%s' failed: '%s'"%(filename, str(e))
        scriptHelperGUIMessage(msg)
        return False

def loadScriptFromFile(filename):
    import codecs
    fp = None
    try:
        fp = codecs.open(filename, "r", 'utf-8')
        fileContent = fp.read()
        fp.close()
        fileScript = eval(fileContent)
        return fileScript
    except Exception as e:
        if fp != None: fp.close()
        msg = "Load file '%s' failed: '%s'"%(filename, str(e))
        raise Exception(msg)

def translateAndSortStatusKeys(orgStatusKeys, colorTag = True):
    from vnactor import get_ini_translation
    forSort = []
    stDict = {}
    for st in orgStatusKeys:
        tst = get_ini_translation(st)
        try:
            tsts = tst.split(":")
            snum = int(tsts[0])
            stxt = tsts[1]
            scolor = "ffffff" if len(tsts) < 3 else tsts[2]
        except:
            snum = 99999
            stxt = st
            scolor = "ffffff"
        forSort.append((snum, stxt, st))
        stDict[st] = "<color=#%s>%s</color>"%(scolor, stxt) if colorTag else stxt
    forSort.sort(key=lambda x:(x[0], x[1]))
    oslist = [i[2] for i in forSort]
    return oslist, stDict

def startFileBrower(sh, startPath=None, extFilter=None, openSave="open", title=None, onOk=None, onCancel=None):
    from UnityEngine import Application
    game = sh.game
    # list all drivers
    game.scenedata.fb_drivers = []
    for drv in range(65, 91):
        vol = chr(drv) + ':\\'
        if path.isdir(vol):
            game.scenedata.fb_drivers.append(vol)
    # check start path
    if startPath != None and path.exists(startPath):
        game.scenedata.fb_cur_input = path.realpath(startPath)
    elif not hasattr(game.scenedata, "fb_cur_input"):
        game.scenedata.fb_cur_input = path.realpath(path.join(Application.dataPath, '..'))
    # setup
    game.scenedata.fb_ext_filter = None if extFilter == None else (extFilter if isinstance(extFilter, list) or isinstance(extFilter, tuple) else [extFilter])
    game.scenedata.fb_open_save = openSave
    game.scenedata.fb_title = "Select a file to %s:"%openSave if title == None else title
    game.scenedata.fb_scroll_pos = Vector2.zero    
    game.scenedata.fb_selected_file = None
    game.scenedata.fb_on_ok = onOk
    game.scenedata.fb_on_cancel = onCancel
    # goto file browser
    game.scenedata.fb_previous_gui_function = sh.game.scenedata.kfaGuiFunction
    game.scenedata.kfaGuiFunction = util_GUI_file_browser

def util_GUI_file_browser(sh):
    from UnityEngine import GUI, GUILayout, GUIStyle, GUIContent, GUIUtility, Screen, Rect, Vector3, Input, KeyCode, TextAnchor
    fullw = sh.game.windowRect.width-30
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    fbStyle = GUIStyle("button")
    fbStyle.alignment = TextAnchor.MiddleLeft
    
    # shortcuts
    game = sh.game
    
    # path info
    from os import listdir
    if path.isfile(game.scenedata.fb_cur_input):
        game.scenedata.fb_cur_input = path.realpath(game.scenedata.fb_cur_input)
        curPath = path.split(game.scenedata.fb_cur_input)[0]        
    elif path.isdir(game.scenedata.fb_cur_input):
        if not game.scenedata.fb_cur_input.endswith("\\"):
            game.scenedata.fb_cur_input += "\\"
        curPath = game.scenedata.fb_cur_input
    else:
        curPath = path.split(game.scenedata.fb_cur_input)[0]
        if not path.isdir(curPath):
            curPath = game.pygamepath
            game.scenedata.fb_cur_input = curPath
    #print("info: fb_cur_input:", game.scenedata.fb_cur_input, "curPath:", curPath)
    if game.scenedata.fb_cur_input == "":
        subfiles = game.scenedata.fb_drivers
    else:
        subfiles = listdir(curPath)
    
    # title
    GUILayout.BeginHorizontal()
    GUILayout.Label(game.scenedata.fb_title, GUILayout.Width(fullw))
    GUILayout.EndHorizontal()

    # file indicator
    GUILayout.BeginHorizontal()
    if GUILayout.Button("^", GUILayout.Width(20)):
        if curPath in game.scenedata.fb_drivers or curPath == "":
            game.scenedata.fb_cur_input = ""
        else:
            game.scenedata.fb_cur_input = path.realpath(path.join(curPath, '..'))
    if game.scenedata.fb_open_save == "save":
        game.scenedata.fb_cur_input = GUILayout.TextField(game.scenedata.fb_cur_input, GUILayout.Width(fullw - 22))
    else:
        GUILayout.TextField(game.scenedata.fb_cur_input, GUILayout.Width(fullw - 22))
    GUILayout.EndHorizontal()
    
    # file/folder selector
    GUILayout.BeginHorizontal()
    game.scenedata.fb_scroll_pos = GUILayout.BeginScrollView(game.scenedata.fb_scroll_pos, GUILayout.Width(fullw), GUILayout.Height(150))
    for f in subfiles:
        if not path.isfile(path.join(curPath, f)):
            if GUILayout.Button("<color=#6edb00>[" + f + "]</color>", fbStyle):
                game.scenedata.fb_cur_input = path.realpath(path.join(curPath, f))
    for f in subfiles:
        if not path.isfile(path.join(curPath, f)):
            continue
        if game.scenedata.fb_ext_filter != None:
            filter = [flt.lower() for flt in game.scenedata.fb_ext_filter]
            fext = path.splitext(f)[1].lower()
            if not fext in filter:
                continue
        if GUILayout.Button(f, fbStyle):
            game.scenedata.fb_cur_input = path.realpath(path.join(curPath, f))
    GUILayout.EndScrollView()
    GUILayout.EndHorizontal()
    
    # Control buttons
    btnW = fullw / 2 - 4
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("OK", btnstyle, GUILayout.Width(btnW)):
        game.scenedata.fb_selected_file = game.scenedata.fb_cur_input
        if game.scenedata.fb_open_save == "save" and game.scenedata.fb_ext_filter and len(game.scenedata.fb_ext_filter) == 1 and not game.scenedata.fb_selected_file.lower().endswith(game.scenedata.fb_ext_filter[0].lower()):
            game.scenedata.fb_selected_file += game.scenedata.fb_ext_filter[0]
        game.scenedata.kfaGuiFunction = game.scenedata.fb_previous_gui_function
        if game.scenedata.fb_on_ok != None:
            game.scenedata.fb_on_ok(game, game.scenedata.fb_selected_file)
    if GUILayout.Button("Cancel", btnstyle, GUILayout.Width(btnW)):
        game.scenedata.fb_selected_file = None
        game.scenedata.kfaGuiFunction = game.scenedata.fb_previous_gui_function
        if game.scenedata.fb_on_cancel != None:
            game.scenedata.fb_on_cancel(game)
    GUILayout.EndHorizontal()

def startManualOptimize(sh):
    game = sh.game
    sd = game.scenedata
    clip = sd.kfamOnModifyClip
    # list up keyframe tgt
    sd.mo_target_ids = {}
    sd.mo_target_sts = {}
    sd.mo_keep1st = {}
    #sd.mo_cleanIKFK = {}
    for kfId in clip.keyframes.keys():
        st = clip.checkUnchangedStatus(kfId)
        if st != None and len(st) != 0:
            sd.mo_target_ids[kfId] = True
            sd.mo_keep1st[kfId] = False
            #sd.mo_cleanIKFK[kfId] = False
            stdict = {}
            for s in st:
                stdict[s] = False
            sd.mo_target_sts[kfId] = stdict
    sd.mo_target_sclpos = Vector2.zero
    sd.mo_status_sclpos = Vector2.zero
    sd.mo_cur_tid = None if len(sd.mo_target_ids) == 0 else sd.mo_target_ids.keys()[0]
    # goto optimize GUI
    game.scenedata.fb_previous_gui_function = sh.game.scenedata.kfaGuiFunction
    game.scenedata.kfaGuiFunction = util_GUI_manual_optimize

def util_GUI_manual_optimize(sh):
    from UnityEngine import GUI, GUILayout, GUIStyle
    from System import String, Array
    fullw = sh.game.windowRect.width - 30
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14

    # shortcuts
    game = sh.game
    sd = game.scenedata
    clip = sd.kfamOnModifyClip

    if sd.mo_cur_tid != None:
        GUILayout.BeginHorizontal()
        GUILayout.Label("Select and clear unchanged status to optimzie clip:")
        GUILayout.EndHorizontal()

        GUILayout.BeginHorizontal()
        # left panel: target list
        GUILayout.BeginVertical(GUILayout.Width(150))
        sd.mo_target_sclpos = GUILayout.BeginScrollView(sd.mo_target_sclpos, GUILayout.Width(150), GUILayout.Height(170))
        for tid in sd.mo_target_ids.keys():
            GUILayout.BeginHorizontal()
            sd.mo_target_ids[tid] = GUILayout.Toggle(sd.mo_target_ids[tid], " ", GUILayout.Width(15))
            if GUILayout.Button(tid if tid != sd.mo_cur_tid else "<color=#00ff00>" + tid + "</color>"):
                sd.mo_cur_tid = tid
            GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
        GUILayout.EndVertical()

        # mid panel: status list
        GUILayout.Space(10)
        sd.mo_status_sclpos = GUILayout.BeginScrollView(sd.mo_status_sclpos, GUILayout.Width(150), GUILayout.Height(170))
        sortIds, idDict = translateAndSortStatusKeys(sd.mo_target_sts[sd.mo_cur_tid].keys())
        for sid in sortIds:
            sd.mo_target_sts[sd.mo_cur_tid][sid] = GUILayout.Toggle(sd.mo_target_sts[sd.mo_cur_tid][sid], idDict[sid])
        GUILayout.EndScrollView()

        # right panel: infos
        GUILayout.Space(10)
        GUILayout.BeginVertical(GUILayout.Width(fullw - 20 - 300))
        if sd.mo_cur_tid in sd.actors:
            cName = "<color=#ff0000>%s</color>"%sd.actors[sd.mo_cur_tid].text_name
        elif sd.mo_cur_tid in sd.props:
            cName = "<color=#00ff00>%s</color>"%sd.props[sd.mo_cur_tid].text_name
        elif sd.mo_cur_tid == "sys":
            cName = "<color=#ffff00>System</color>"
        else:
            cName = "<color=#ff0000>Unknown!?</color>"
        if sd.mo_target_ids[sd.mo_cur_tid]:
            GUILayout.Label("<color=#00ff00>%d</color> unchanged status detected for %s, <color=#ff0000>%d</color> status are selected to delete."%(len(sd.mo_target_sts[sd.mo_cur_tid]), cName, sd.mo_target_sts[sd.mo_cur_tid].values().count(True)))
            GUILayout.BeginHorizontal()
            if GUILayout.Button("All", GUILayout.Width((fullw - 20 - 300)/2-2)):
                for sid in sd.mo_target_sts[sd.mo_cur_tid]:
                    sd.mo_target_sts[sd.mo_cur_tid][sid] = True
            if GUILayout.Button("None", GUILayout.Width((fullw - 20 - 300)/2-2)):
                for sid in sd.mo_target_sts[sd.mo_cur_tid]:
                    sd.mo_target_sts[sd.mo_cur_tid][sid] = False
            GUILayout.EndHorizontal()
            sd.mo_keep1st[sd.mo_cur_tid] = GUILayout.Toggle(sd.mo_keep1st[sd.mo_cur_tid], "Keep first keyframe")
            #if clip.keyframes[sd.mo_cur_tid][0].status.has_key("ik_set") or clip.keyframes[sd.mo_cur_tid][0].status.has_key("fk_set"):
            #    sd.mo_cleanIKFK[sd.mo_cur_tid] = GUILayout.Toggle(sd.mo_cleanIKFK[sd.mo_cur_tid], "Clean unchanged IK/FK nodes")
        else:
            GUILayout.Label("%s excluded from clean-up optimization."%(cName))
        GUILayout.EndVertical()
        GUILayout.EndHorizontal()

        # tail button
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if GUILayout.Button("<color=#ff6666>Do cleanup</color>", btnstyle, GUILayout.Height(24), GUILayout.Width(fullw/2-4)):
            for tid in sd.mo_target_ids.keys():
                if sd.mo_target_ids[tid]:
                    delList = []
                    for st in sd.mo_target_sts[tid].keys():
                        if sd.mo_target_sts[tid][st]:
                            delList.append(st)
                    print("For %s delete:"%tid + str(delList))
                    clip.clearSpecifiedStatus(tid, delList, sd.mo_keep1st[tid])
            game.scenedata.kfaGuiFunction = game.scenedata.fb_previous_gui_function
        if GUILayout.Button("Cancel", btnstyle, GUILayout.Height(24), GUILayout.Width(fullw/2-4)):
            game.scenedata.kfaGuiFunction = game.scenedata.fb_previous_gui_function
        GUILayout.EndHorizontal()

    else:
        # nothing to optimize
        GUILayout.BeginHorizontal()
        GUILayout.Label("Nothing to optimzie!")
        GUILayout.EndHorizontal()

        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if GUILayout.Button("Cancel", btnstyle, GUILayout.Height(24), GUILayout.Width(fullw)):
            game.scenedata.kfaGuiFunction = game.scenedata.fb_previous_gui_function
        GUILayout.EndHorizontal()

def kfam_GUI_manager(sh):
    from UnityEngine import GUI, GUILayout, GUIStyle
    from System import String, Array
    fullw = sh.game.windowRect.width - 30
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    
    # shortcuts
    game = sh.game
    sd = game.scenedata
    mClips = game.gdata.kfaManagedClips
    sClips = game.gdata.kfaSortedClips

    # auto load from scene
    clpBaseFld = None
    flds = sh.game.scene_get_all_folders()
    for fld in flds:
        if fld.name.startswith("-keyframeclips:"):
            clpBaseFld = fld
            break
    if clpBaseFld and len(mClips) == 0:
        scriptHelperGUIMessage("Auto load clips from scene...")
        importAllClipsFromScene(game, clpBaseFld)
    
    # setting for clips
    if not hasattr(sd, "kfamSelectedClipIndex"):
        sd.kfamSelectedClipIndex = -1
        sd.kfamScrollPos = Vector2.zero
        sd.kfamTempName = ""
        sd.kfamCheckStatus = {}
    clipNameList = []
    for clip in sClips:
        if clip.isPlaying:
            n = "<color=#00ff00>" + clip.name + "</color>"
        else:
            n = "<color=#ffffff>" + clip.name + "</color>"
        if not sd.kfamCheckStatus.has_key(clip.name):
            sd.kfamCheckStatus[clip.name] = False
        if sd.kfamCheckStatus[clip.name]:
            n = "<b>" + n + "</b>"
        if clip.animeType == "relative":
            n = "<i>" + n + "</i>"
        clipNameList.append(n)
    #clipNameList.reverse()
    for clipName in sd.kfamCheckStatus.keys():
        if not mClips.has_key(clipName):
            sd.kfamCheckStatus.pop(clipName)
    checkedCount = sd.kfamCheckStatus.values().count(True)
    
    # clip manager UI
    GUILayout.BeginHorizontal()
    # left panel
    GUILayout.BeginVertical(GUILayout.Width(100))
    sd.kfamScrollPos = GUILayout.BeginScrollView(sd.kfamScrollPos, GUILayout.Width(100-4), GUILayout.Height(200))
    for i in range(len(clipNameList)):
        ocolor = GUI.color
        if sd.kfamSelectedClipIndex == i:
            GUI.color = Color.cyan
        if GUILayout.Button(clipNameList[i]):
            sd.kfamSelectedClipIndex = i
            sd.kfamTempName = sClips[i].name
            sd.kfamTempPriority = "%d"%sClips[i].priority
            sd.kfamTempType = sClips[i].animeType
            sd.kfamTempLength = "%d"%sClips[i].frameLength
            sd.kfamTempFPS = "%d"%sClips[i].frameRate
            sd.kfamTempSpeed = "%.2f"%sClips[i].speed
            sd.kfamTempLoop = "%d"%sClips[i].loop
            sd.kfamTempAutorun = sClips[i].autorun
        GUI.color = ocolor
    """
    selectedClip = GUILayout.SelectionGrid(sd.kfamSelectedClipIndex, Array[String](tuple(clipNameList)), 1)
    if selectedClip != sd.kfamSelectedClipIndex:
        sd.kfamSelectedClipIndex = selectedClip
        sd.kfamTempName = sClips[selectedClip].name
        sd.kfamTempPriority = "%d"%sClips[selectedClip].priority
        sd.kfamTempType = sClips[selectedClip].animeType
        sd.kfamTempLength = "%d"%sClips[selectedClip].frameLength
        sd.kfamTempFPS = "%d"%sClips[selectedClip].frameRate
        sd.kfamTempSpeed = "%.2f"%sClips[selectedClip].speed
        sd.kfamTempLoop = "%d"%sClips[selectedClip].loop
        sd.kfamTempAutorun = sClips[selectedClip].autorun
    """
    GUILayout.EndScrollView()
    GUILayout.EndVertical()
    # right panel
    if sd.kfamSelectedClipIndex != -1:
        clip = sClips[sd.kfamSelectedClipIndex]
    else:
        clip = None
    GUILayout.BeginVertical(GUILayout.Width(fullw-100))
    if clip:
        # setting
        GUILayout.BeginHorizontal()
        GUILayout.Label("Clip v%d <<color=#00ffff>%s</color>> settings:"%(clip.version, clip.name), GUILayout.Width(200))
        sd.kfamCheckStatus[clip.name] = GUILayout.Toggle(sd.kfamCheckStatus[clip.name], "<color=#ff0000><b>Marked</b></color>" if sd.kfamCheckStatus[clip.name] else "Mark", GUILayout.Width(80))
        settingModified = sd.kfamTempName != clip.name or sd.kfamTempPriority != "%d"%clip.priority or sd.kfamTempType != clip.animeType or sd.kfamTempLength != "%d"%clip.frameLength or sd.kfamTempFPS != "%d"%clip.frameRate or sd.kfamTempSpeed != "%.2f"%clip.speed or sd.kfamTempLoop != "%d"%clip.loop or sd.kfamTempAutorun != clip.autorun
        if settingModified and GUILayout.Button("<color=#ff0000>Apply</color>"):
            try:
                oldName = clip.name
                name = sd.kfamTempName.strip()
                if len(name) == 0:
                    raise Exception("Clip name can not be empty!")
                if name != clip.name and mClips.has_key(name):
                    raise Exception("Clip name dupulicated!")
                priority = int(sd.kfamTempPriority)
                frameLength = int(sd.kfamTempLength)
                if frameLength < clip.lastFrameNo:
                    raise Exception("Clip's length must be equal or greater than last key frame No. #%d!"%clip.lastFrameNo)
                frameRate = int(sd.kfamTempFPS)
                speed = float(sd.kfamTempSpeed)
                loop = int(sd.kfamTempLoop)
                if name != clip.name:
                    kfa_rename(game, oldName, name)
                if priority != clip.priority:
                    kfa_resort(game, name, priority)
                    sd.kfamSelectedClipIndex = -1
                if sd.kfamTempType != clip.animeType:
                    kfa_retype(game, name, sd.kfamTempType)
                clip.frameLength = frameLength
                clip.frameRate = frameRate
                clip.speed = speed
                clip.loop = loop
                clip.autorun = sd.kfamTempAutorun
            except Exception as e:
                scriptHelperGUIMessage("Apply setting failed: " + str(e))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("Name:", GUILayout.Width(50))
        sd.kfamTempName = GUILayout.TextField(sd.kfamTempName, GUILayout.Width(80))
        GUILayout.Space(5)
        GUILayout.Label("Priority:", GUILayout.Width(50))
        sd.kfamTempPriority = GUILayout.TextField(sd.kfamTempPriority, GUILayout.Width(35))
        GUILayout.Space(5)
        sd.kfamTempAutorun = GUILayout.Toggle(sd.kfamTempAutorun, "Auto run on load")
        #GUILayout.Label("Type:", GUILayout.Width(40))
        #if GUILayout.Button(sd.kfamTempType if sd.kfamTempType == "absolute" else "<i>" + sd.kfamTempType + "</i>"):
        #    if sd.kfamTempType == "absolute":
        #        sd.kfamTempType = "relative"
        #    else:
        #        sd.kfamTempType = "absolute"
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("Length:", GUILayout.Width(50))
        sd.kfamTempLength = GUILayout.TextField(sd.kfamTempLength, GUILayout.Width(50))
        GUILayout.Space(5)
        GUILayout.Label("FPS:", GUILayout.Width(35))
        sd.kfamTempFPS = GUILayout.TextField(sd.kfamTempFPS, GUILayout.Width(25))
        GUILayout.Space(5)
        GUILayout.Label("Speed:", GUILayout.Width(50))
        sd.kfamTempSpeed = GUILayout.TextField(sd.kfamTempSpeed, GUILayout.Width(35))
        GUILayout.Space(5)
        GUILayout.Label("Loop:", GUILayout.Width(35))
        sd.kfamTempLoop = GUILayout.TextField(sd.kfamTempLoop, GUILayout.Width(35))
        GUILayout.EndHorizontal()
        # play control
        GUILayout.BeginHorizontal()
        newframe = GUILayout.HorizontalSlider(clip.curFrame, 0, clip.frameLength, GUILayout.Width(fullw-100))
        if not clip.isPlaying and int(newframe) != int(clip.curFrame):
            clip.seek(int(newframe))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        if GUILayout.Button("||" if clip.isPlaying else u'\u25b6', GUILayout.Width(23)):
            if clip.isPlaying:
                clip.pause()
            else:
                clip.play()
        if GUILayout.Button(u'\u25a0', GUILayout.Width(23)):
            clip.stop()
        if GUILayout.Button("|<", GUILayout.Width(23)):
            clip.seek(0)
        if GUILayout.Button("<<", GUILayout.Width(23)):
            newframe = 0 if clip.curFrame <  clip.frameRate else clip.curFrame -  clip.frameRate
            clip.seek(newframe)
        if GUILayout.Button("<", GUILayout.Width(23)):
            newframe = 0 if clip.curFrame < 1 else clip.curFrame - 1
            clip.seek(newframe)
        GUILayout.Label("%d/%d %.1f/%.1fs"%(clip.curFrame, clip.frameLength, clip.curFrame/clip.frameRate, clip.frameLength/clip.frameRate))
        if GUILayout.Button(">", GUILayout.Width(23)):
            newframe = clip.frameLength if clip.curFrame + 1 >= clip.frameLength else clip.curFrame + 1
            clip.seek(newframe)
        if GUILayout.Button(">>", GUILayout.Width(23)):
            newframe = clip.frameLength if clip.curFrame + clip.frameRate >= clip.frameLength else clip.curFrame +  clip.frameRate
            clip.seek(newframe)
        if GUILayout.Button(">|", GUILayout.Width(23)):
            clip.seek(clip.frameLength)
        GUILayout.EndHorizontal()
    else:
        GUILayout.BeginHorizontal()
        GUILayout.Label("Select a clip to edit")
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("")
        GUILayout.EndHorizontal()
    # all play control
    GUILayout.BeginHorizontal()
    GUILayout.Label("Sync play control for:")
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label("All Clips:")
    if GUILayout.Button(u'\u25b6', GUILayout.Width(25)):
        kfa_playAll(game)
    if GUILayout.Button("||", GUILayout.Width(25)):
        kfa_pauseAll(game)
    if GUILayout.Button(u'\u25a0', GUILayout.Width(25)):
        kfa_stopAll(game)
    if GUILayout.Button("|<", GUILayout.Width(25)):
        kfa_rewindAll(game)
    GUILayout.Label("", GUILayout.Width(10))
    GUILayout.Label("Marked:")
    if GUILayout.Button(u'\u25b6', GUILayout.Width(25)):
        for clipName in sd.kfamCheckStatus.keys():
            if sd.kfamCheckStatus[clipName]:
                kfa_play(game, clipName)
    if GUILayout.Button("||", GUILayout.Width(25)):
        for clipName in sd.kfamCheckStatus.keys():
            if sd.kfamCheckStatus[clipName]:
                kfa_pause(game, clipName)
    if GUILayout.Button(u'\u25a0', GUILayout.Width(25)):
        for clipName in sd.kfamCheckStatus.keys():
            if sd.kfamCheckStatus[clipName]:
                kfa_stop(game, clipName)
    if GUILayout.Button("|<", GUILayout.Width(25)):
        for clipName in sd.kfamCheckStatus.keys():
            if sd.kfamCheckStatus[clipName]:
                kfa_seek(game, clipName, 0)
    GUILayout.EndHorizontal()
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Create", btnstyle, GUILayout.Width(70)):
        changeGuiScreen(sh, kfam_GUI_create_clip, clip)
    if clip and GUILayout.Button("Modify" if clip else "", btnstyle, GUILayout.Width(70)):
        changeGuiScreen(sh, kfam_GUI_modify_clip, clip)
    if clip and GUILayout.Button("Delete" if clip else "", btnstyle, GUILayout.Width(70)):
        def onDeleteClip():
            kfa_remove(game, clip.name)
            sd.kfamSelectedClipIndex = -1
        scriptHelperGUIMessage("Are you sure to delete the clip <%s>?"%clip.name, (("Yes, Delete it!", onDeleteClip), "Cancel"))
    if checkedCount > 1 and GUILayout.Button("Merge", btnstyle, GUILayout.Width(70)):
        scriptHelperGUIMessage("Not supported yet :)")
    if clpBaseFld and GUILayout.Button("Reload", btnstyle, GUILayout.Width(70)):
        scriptHelperGUIMessage("Are you sure to reload all the clips from the scene?", (("Yes, Reload them!", importAllClipsFromScene, game), "Cancel"))
    GUILayout.EndHorizontal()
    GUILayout.EndVertical() # right panel
    GUILayout.EndHorizontal() # all ui

    # Control buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Script Builder", btnstyle, GUILayout.Width(100), GUILayout.Height(24)):
        scriptHelperGUIToSceen(0)
    if GUILayout.Button("Anime Buffer", btnstyle, GUILayout.Width(100), GUILayout.Height(24)):
        scriptHelperGUIToSceen(1)
    if GUILayout.Button("Export/Reset" if game.isSceneDataSaveSupported else "<color=#ff6666ff>Export/Reset</color>", btnstyle, GUILayout.Width(100), GUILayout.Height(24)):
        if game.isSceneDataSaveSupported:
            saveSceneInfo = "* <color=#00ff00>To Scene</color>: Save clips data into scene. This operation is <color=#ff6666ff>not necessory</color> because when you save PNG scene file clips data will be auto-saved, still you can cache current data manually to reduce the risk of data lost."
        else:
            saveSceneInfo = "* <color=#ff6666>To Scene</color>: Save clips data into scene and then save your PNG scene file, or your work will lost! VNAnime clip v2 not supported! Please update ExtSave plugin to support auto-save!"
        scriptHelperGUIMessage(saveSceneInfo + "\n* <color=#00ff00>To File</color>: Export clip data into py file or dump file. This is for python user or debugger, exported file <color=#ff6666ff>CANNOT</color> be imported directly!\n* <color=#00ff00>Reset</color>: Reset vnanime module and reload all clips from saved data, <color=#ff6666ff>unsaved changes lost!</color>", (("To Scene", outputAllClipsToScene, sh.game), ("To File", outputAllClipsToExtFile, sh), ("Reset", init_keyframe_anime, sh.game), "Cancel"))
    if GUILayout.Button("Scene Helper", btnstyle, GUILayout.Width(100), GUILayout.Height(24)):
        scriptHelperGUIToSceen(3)
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(50), GUILayout.Height(24)):
        scriptHelperGUIClose()
    GUILayout.EndHorizontal()

def kfam_GUI_create_clip(sh):
    from UnityEngine import GUI, GUILayout, GUIStyle
    from System import String, Array
    from vnframe import verifyTagName, addTagToObject
    fullw = sh.game.windowRect.width - 30
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    
    # shortcuts
    game = sh.game
    sd = game.scenedata
    clip = sd.kfamOnModifyClip
    mClips = game.gdata.kfaManagedClips
    sClips = game.gdata.kfaSortedClips

    def getOciFromId(tid):
        a = game.scenef_get_actor(tid)
        if a != None:
            return a
        a = game.scenef_get_prop(tid)
        if a != None:
            return a
        return None
    def checkReAssign(toci):
        #print("Try check selected object against %s:%s"%(toci.text_name, type(toci.objctrl)))
        try:
            if len(game.studio.treeNodeCtrl.selectNodes) != 1:
                raise Exception("Please select just ONE object in the workspace")
            tno = game.studio.treeNodeCtrl.selectNodes[0]
            roci = HSNeoOCI.create_from_treenode(tno)
            if roci == None:
                raise Exception("Can not create oci object from selected node: %s"%tno.textName)
            if toci.objctrl == roci.objctrl:
                raise Exception("Select a different object to RE-assign keyframes to")
            if type(toci.objctrl) != type(roci.objctrl):
                raise Exception("Re-assign target must be the same type of original one: %s"%str(type(toci.objctrl))[7:-2])
            rid = game.scenef_query_id(roci)
            if rid == None:
                if isinstance(roci, HSNeoOCIChar):
                    rid_t = "act%d"
                    rid_n = len(game.scenef_get_all_actors())
                else:
                    rid_t = "prp%d"
                    rid_n = len(game.scenef_get_all_props())
                excId = [cInfo[3] for cInfo in sd.kfamContentsInfo if cInfo[2] != toci and cInfo[3] != None]
                while verifyTagName(rid_t%rid_n) != None or rid_t%rid_n in excId:
                    rid_n += 1
                return rid_t%rid_n, roci, False
            else:
                return rid, roci, True
        except Exception as e:
            game.logger_log_message(str(e), "Re-assign error: ")
        return None, None, False

    # type
    GUILayout.BeginHorizontal()
    createTypes = ["Empty Clip", "Copy <%s>"%clip.name if clip else "<color=#555555>Copy</color>", "Import File"]
    createTypes[sd.kfamCreateType] = "<color=#00ffff>" + createTypes[sd.kfamCreateType] + "</color>"
    GUILayout.Label("Create:", GUILayout.Width(80))
    sel = GUILayout.SelectionGrid(sd.kfamCreateType, Array[String](createTypes), 3)
    if sel != sd.kfamCreateType:
        if clip == None and sel == 1:
            scriptHelperGUIMessage("Select a clip in manager then click the <create> button to create a copy.")
        else:
            sd.kfamCreateType = sel
            sd.kfamContentsSclPos = Vector2.zero
            sd.kfamContentsInfo = []
            if sel == 1 and clip.version == 2:  # contents selection only for clip v2
                for k in clip.keyframes.keys():
                    sd.kfamContentsInfo.append([True, k, getOciFromId(k), None, None, False])
    GUILayout.EndHorizontal()
    # name
    GUILayout.BeginHorizontal()
    GUILayout.Label("Clip Name:", GUILayout.Width(80))
    if sd.kfamCreateType != 2 or not sd.kfamImportClipName:
        sd.kfamCreateName = GUILayout.TextField(sd.kfamCreateName, GUILayout.Width(120))
    if sd.kfamCreateType == 2:
        sd.kfamImportClipName = GUILayout.Toggle(sd.kfamImportClipName, "Import clip's name")
    GUILayout.EndHorizontal()
    # file
    if sd.kfamCreateType == 2:
        GUILayout.BeginHorizontal()
        GUILayout.Label("File Name:", GUILayout.Width(80))
        sd.kfamCreateFile = GUILayout.TextField(sd.kfamCreateFile, GUILayout.Width(350))
        if GUILayout.Button("..."):
            def onSetImportClip(game, filename):
                sd.kfamCreateFile = filename
            stPath = path.join(game.pygamepath, "animeclips/")
            startFileBrower(sh, startPath=stPath, extFilter=(".txt", ".json"), openSave="open", title="Select a clip file to import:", onOk=onSetImportClip, onCancel=None)
        GUILayout.EndHorizontal()
    # version
    if sd.kfamCreateType == 0 or (sd.kfamCreateType == 1 and clip and clip.version == 1):
        GUILayout.BeginHorizontal()
        GUILayout.Label(" ", GUILayout.Width(80))
        if GUILayout.Toggle(sd.kfamCreateVer == 2, "Create clip version 2"):
            sd.kfamCreateVer = 2
        else:
            sd.kfamCreateVer = 1
        GUILayout.EndHorizontal()
    # copy contents
    if sd.kfamCreateType == 1 and clip and clip.version == 2:
        GUILayout.BeginHorizontal()
        GUILayout.Label("Select contents to copy, and can re-assign copied keyframe to other target:")
        GUILayout.EndHorizontal()
        sd.kfamContentsSclPos = GUILayout.BeginScrollView(sd.kfamContentsSclPos, GUI.skin.box)
        for cinfo in sd.kfamContentsInfo:
            encopy, tid, toci, rid, roci, rreged = cinfo
            GUILayout.BeginHorizontal()
            GUILayout.Space(5)
            if toci:
                cinfo[0] = GUILayout.Toggle(encopy, "%s: %s"%(tid, toci.text_name), GUILayout.Width(200))
            else:
                cinfo[0] = GUILayout.Toggle(encopy, "%s"%(tid), GUILayout.Width(200))
            if encopy and toci:   # can not re-assign non-obj
                if roci == None:
                    if GUILayout.Button("re-assign to selected object"):
                        rid, roci, rreged = checkReAssign(toci)
                        if rid != None or roci != None:
                            cinfo[3] = rid
                            cinfo[4] = roci
                            cinfo[5] = rreged
                else:
                    if rreged:
                        GUILayout.Label(u'\u2192' + " <color=#00ff00>%s: %s</color>"%(rid, roci.text_name))
                    else:
                        GUILayout.Label(u'\u2192' + " <color=#00ff00>%s</color> as "%(roci.text_name))
                        cinfo[3] = GUILayout.TextField(rid, GUILayout.Width(80))
                    if GUILayout.Button("X"):
                        cinfo[3] = None
                        cinfo[4] = None
                        cinfo[5] = False
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
        GUILayout.EndScrollView()

    # Control buttons
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Create", btnstyle, GUILayout.Width(fullw/2-2)):
        try:
            # if import
            if sd.kfamCreateType == 2:
                if sd.kfamCreateFile.lower().endswith(".json"):
                    from vnanime2 import loadScriptFromFile2
                    sd.kfamCreateVer = 2
                    clipScript = loadScriptFromFile2(sd.kfamCreateFile)
                    if sd.kfamImportClipName:
                        sd.kfamCreateName = clipScript["name"]
                else:
                    sd.kfamCreateVer = 1
                    clipScript = loadScriptFromFile(sd.kfamCreateFile)
                    if sd.kfamImportClipName:
                        sd.kfamCreateName = clipScript["setting"]["name"]
            # check name
            sd.kfamCreateName = sd.kfamCreateName.strip()
            iderr = verifyTagName(sd.kfamCreateName)
            if iderr != None:
                raise Exception("clip id error: " + iderr)
            # create clip
            if sd.kfamCreateType == 0:
                # new
                if sd.kfamCreateVer == 2:
                    from vnanime2 import KeyFrameClip2
                    newClip = KeyFrameClip2(game, None)
                else:
                    newClip = KeyFrameClip(game, None)
                newClip.name = sd.kfamCreateName
            elif sd.kfamCreateType == 1:
                # copy
                if sd.kfamCreateVer == clip.version:
                    if clip.version == 1:
                        newClip = clip.copy(sd.kfamCreateName)
                    else:
                        # with content selection and re-assign
                        assignInfo = {}
                        fregister = False
                        for cinfo in sd.kfamContentsInfo:
                            encopy, tid, _, rid, roci, rreged = cinfo
                            if encopy:
                                if roci == None:
                                    assignInfo[tid] = None
                                else:
                                    if not rreged:
                                        tagerr = addTagToObject(rid, roci, False)
                                        if tagerr != None:
                                            raise Exception("re-assign add tag error: " + tagerr)
                                        fregister = True
                                    assignInfo[tid] = rid
                        if fregister:
                            game.scenef_register_actorsprops()
                        newClip = clip.copy(sd.kfamCreateName, assignInfo)
                elif clip.version == 1 and sd.kfamCreateVer == 2:
                    from vnanime2 import KeyFrameClip2
                    newClip = KeyFrameClip2.createFromV1(clip, sd.kfamCreateName)
                else:
                    raise Exception("Downgrade copy not supported!")
            elif sd.kfamCreateType == 2:
                # import
                if sd.kfamCreateVer == 2:
                    from vnanime2 import KeyFrameClip2
                    newClip = KeyFrameClip2(game, clipScript)
                else:
                    newClip = KeyFrameClip(game, clipScript)
                if not sd.kfamImportClipName:
                    newClip.name = sd.kfamCreateName
            else:
                raise Exception("Unexpected Create Type!?")
            # register
            kfa_register(game, newClip)
            changeGuiScreen(sh, kfam_GUI_modify_clip, newClip)
        except Exception as e:
            traceback.print_exc()
            errMsg = "Create Clip Failed: " + str(e)
            if sd.kfamCreateType == 1 and sd.kfamCreateVer == 2:
                for cinfo in sd.kfamContentsInfo:
                    cinfo[3] = None
                    cinfo[4] = None
                    cinfo[5] = False
            scriptHelperGUIMessage(errMsg, ("OK",))
    if GUILayout.Button("Cancel", btnstyle, GUILayout.Width(fullw/2-2)):
        changeGuiScreen(sh, kfam_GUI_manager)
    GUILayout.EndHorizontal()

def kfam_GUI_modify_clip(sh):
    if sh.game.scenedata.kfamOnModifyClip.version == 2:
        from vnanime2 import kfam_GUI_modify_clip2
        kfam_GUI_modify_clip2(sh)
    elif sh.game.gdata.kfaSetting.useLegacyEditor:
        kfam_GUI_modify_clip_v1(sh)
    else:
        kfam_GUI_modify_clip_v2(sh)

def kfam_GUI_modify_clip_v1(sh):
    from UnityEngine import GUI, GUILayout, GUIStyle, Rect, Texture2D, Color, Color32, TextureFormat, RectOffset
    from System import String, Array
    fullw = sh.game.windowRect.width - 30
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    slableStyle = GUIStyle("Label")
    slableStyle.fontSize = 11
    sslableStyle = GUIStyle()
    sslableStyle.fontSize = 9
    cbwidth = fullw/6-3
    
    # shortcuts
    game = sh.game
    sd = game.scenedata
    clip = sd.kfamOnModifyClip
    mClips = game.gdata.kfaManagedClips
    sClips = game.gdata.kfaSortedClips

    # setting for modify
    if sd.kfamModifyTabIndex == -1:
        # on enter screen
        sd.kfamModifyTabIndex = 0
        sd.kfamTempName = clip.name
        sd.kfamTempPriority = "%d"%clip.priority
        sd.kfamTempType = clip.animeType
        sd.kfamTempLength = "%d"%clip.frameLength
        sd.kfamTempFPS = "%d"%clip.frameRate
        sd.kfamTempSpeed = "%.2f"%clip.speed
        sd.kfamTempLoop = "%d"%clip.loop
        sd.kfamTempAutorun = clip.autorun
        sd.kfLastOnEditId = None
        sd.kfLastOnEditFrame = -1
        sd.kfScrollPos = Vector2.zero
        sd.stScrollPos = Vector2.zero
        sd.kfSelectIndex = -1
        sd.clSelectIndex = -1
        sd.kfCurveTexture = Texture2D(100, 100, TextureFormat.RGBA32, False)
        sd.kfCurveRedraw = True
        sd.uiMore = False
        sd.uiPreset = False
        sd.uiShift = False

    def moreAction(objname):
        btnRect = Rect(fullw-100+15, 120, 100, 20)
        if GUI.Button(btnRect, "Import"):
            def onImportKeyframe(game, filename):
                try:
                    clipScript = loadScriptFromFile(filename)
                    clip.importSetting(clipScript, tgtId)
                    scriptHelperGUIMessage("Keyframes loaded.")
                except Exception as e:
                    scriptHelperGUIMessage("Fail to import setting: %s"%str(e), ("OK",))
            stPath = path.join(game.pygamepath, "keyframes/")
            startFileBrower(sh, startPath=stPath, extFilter=".txt", openSave="open", title="Select a keyframe file to import:", onOk=onImportKeyframe, onCancel=None)
        btnRect = Rect(fullw-100+15, 120+22, 100, 20)
        if len(tgtKeyframes) > 0 and GUI.Button(btnRect, "Export"):
            def onExportKeyframe(game, filename):
                try:
                    if saveStringToFile(filename, clip.exportSetting(tgtId), append=False, backup=False):
                        scriptHelperGUIMessage("All keyframes of %s(%s) were saved into <%s>."%(objname, tgtId, filename), ("OK",))
                except Exception as e:
                    scriptHelperGUIMessage("Fail to export setting: %s"%str(e), ("OK",))
            stPath = path.join(game.pygamepath, "keyframes/")
            startFileBrower(sh, startPath=stPath, extFilter=".txt", openSave="save", title="Export keyframes to:", onOk=onExportKeyframe, onCancel=None)
        btnRect = Rect(fullw-100+15, 120+22+22, 100, 20)
        if len(tgtKeyframes) > 0 and GUI.Button(btnRect, "Remove All"):
            scriptHelperGUIMessage("Delete <color=#ff0000>ALL</color> key frames for %s(%s)?"%(objname, tgtId), (("Sure, DELETE ALL!", clip.keyframes.pop, tgtId), "Cancel"))
        btnRect = Rect(fullw-100+15, 120+22+22+22, 100, 20)
        if len(tgtKeyframes) > 1 and (selectedTab == 0 or selectedTab == 2) and GUI.Button(btnRect, "Optimize"):
            scriptHelperGUIMessage("Clear <color=#ff0000>ALL unchanged status</color> in key frames for %s(%s)? Use the optimize command in <setting> tab if you want to choice which to clear."%(objname, tgtId), (("Sure, clean up!", clip.clearUnchangedStatus, tgtKeyframes[0].targetID), "Cancel"))

    def curveAndMoreAction(objname):
        # Curve Setting
        GUILayout.BeginVertical()
        GUILayout.BeginHorizontal()
        GUILayout.Label("Curve:", GUILayout.Width(60))
        if GUILayout.Button("<<<" if sd.uiPreset else ">>>", GUILayout.Width(40)):
            sd.uiPreset = not sd.uiPreset
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("<color=#00ffff>X1:</color>", slableStyle, GUILayout.Width(25))
        newX1 = GUILayout.HorizontalSlider(sd.kfCurveData[0], 0.00, 1, GUILayout.Width(75))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("<color=#00ffff>Y1:</color>", slableStyle, GUILayout.Width(25))
        newY1 = GUILayout.HorizontalSlider(sd.kfCurveData[2], 0.00, 1, GUILayout.Width(75))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("<color=#ff00ff>X2:</color>", slableStyle, GUILayout.Width(25))
        newX2 = GUILayout.HorizontalSlider(sd.kfCurveData[1], 0.00, 1, GUILayout.Width(75))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("<color=#f00fff>Y2:</color>", slableStyle, GUILayout.Width(25))
        newY2 = GUILayout.HorizontalSlider(sd.kfCurveData[3], 0.00, 1, GUILayout.Width(75))
        GUILayout.EndHorizontal()
        GUILayout.EndVertical()
        if newX1 != sd.kfCurveData[0] or newX2 != sd.kfCurveData[1] or newY1 != sd.kfCurveData[2] or newY2 != sd.kfCurveData[3]:
            sd.kfCurveData[0] = newX1
            sd.kfCurveData[1] = newX2
            sd.kfCurveData[2] = newY1
            sd.kfCurveData[3] = newY2
            sd.kfCurveRedraw = True

        # Curve Graph or more operation buttons
        if sd.uiMore:
            sd.uiPreset = False
            moreAction(objname)
        elif sd.uiPreset:
            sd.uiMore = False
            btnRect = Rect(fullw-100+15, 120, 100, 20)
            if GUI.Button(btnRect, "Linear"):
                sd.kfCurveData = [0.2, 0.8, 0.2, 0.8]
                sd.kfCurveRedraw = True
                sd.uiPreset = False
            btnRect = Rect(fullw-100+15, 120+22, 50, 20)
            if GUI.Button(btnRect, "F-S"):
                sd.kfCurveData = [0.1, 0.6, 0.4, 0.9]
                sd.kfCurveRedraw = True
                sd.uiPreset = False
            btnRect = Rect(fullw-50+15, 120+22, 50, 20)
            if GUI.Button(btnRect, "S-F"):
                sd.kfCurveData = [0.4, 0.9, 0.1, 0.6]
                sd.kfCurveRedraw = True
                sd.uiPreset = False
            btnRect = Rect(fullw-100+15, 120+22+22, 50, 20)
            if GUI.Button(btnRect, "F-S2"):
                sd.kfCurveData = [0.0, 0.5, 0.5, 1.0]
                sd.kfCurveRedraw = True
                sd.uiPreset = False
            btnRect = Rect(fullw-50+15, 120+22+22, 50, 20)
            if GUI.Button(btnRect, "S-F2"):
                sd.kfCurveData = [0.5, 1.0, 0.0, 0.5]
                sd.kfCurveRedraw = True
                sd.uiPreset = False
            btnRect = Rect(fullw-100+15, 120+22+22+22, 50, 20)
            if GUI.Button(btnRect, "F-S-F"):
                sd.kfCurveData = [0.0, 1.0, 0.4, 0.6]
                sd.kfCurveRedraw = True
                sd.uiPreset = False
            btnRect = Rect(fullw-50+15, 120+22+22+22, 50, 20)
            if GUI.Button(btnRect, "S-F-S"):
                sd.kfCurveData = [0.4, 0.6, 0.0, 1.0]
                sd.kfCurveRedraw = True
                sd.uiPreset = False
            btnRect = Rect(fullw-100+15, 120+22+22+22+22, 50, 20)
            if GUI.Button(btnRect, "Copy"):
                sd.kfCurveDataCopy = copy.copy(sd.kfCurveData)
                sd.uiPreset = False
            btnRect = Rect(fullw-50+15, 120+22+22+22+22, 50, 20)
            if GUI.Button(btnRect, "Paste"):
                if hasattr(sd, "kfCurveDataCopy"):
                    sd.kfCurveData = copy.copy(sd.kfCurveDataCopy)
                    sd.kfCurveRedraw = True
                else:
                    scriptHelperGUIMessage("Copy a curve data first.")
                sd.uiPreset = False
        else:
            curveRect = Rect(fullw-100+15, 120, 100, 100)
            if sd.kfCurveRedraw and not clip.isPlaying:
                drawCurve(sd.kfCurveTexture, sd.kfCurveData)
                sd.kfCurveRedraw = False
                # auto update curve setting
                if selectedTab == 0:    # actor/prop
                    (tgtId, tgtObj) = selectTaggedActorOrProp(game)
                    if tgtId and tgtObj and clip.keyframes.has_key(tgtId):
                        for kf in clip.keyframes[tgtId]:
                            if kf.frameNo == curFrame:
                                kf.curve = copy.copy(sd.kfCurveData)
                                break
                elif selectedTab == 1:  # camera
                    if clip.keyframes.has_key("cam"):
                        for kf in clip.keyframes["cam"]:
                            if kf.frameNo == curFrame:
                                kf.curve = copy.copy(sd.kfCurveData)
                elif selectedTab == 2:  # system
                    if clip.keyframes.has_key("sys"):
                        for kf in clip.keyframes["sys"]:
                            if kf.frameNo == curFrame:
                                kf.curve = copy.copy(sd.kfCurveData)
                else:   # no curve for clip or setting
                    pass
            GUI.DrawTexture(curveRect, sd.kfCurveTexture)
            labelRect = Rect(fullw-100+15, 220+1, 100, 20)
            GUI.Label(labelRect, "<color=#00ffff>(%.2f,%.2f)</color>  <color=#f00fff>(%.2f,%.2f)</color>"%(newX1, newY1, newX2, newY2), sslableStyle)
        
    # play control
    GUILayout.BeginHorizontal()
    newframe = GUILayout.HorizontalSlider(clip.curFrame, 0, clip.frameLength, GUILayout.Width(fullw))
    if not clip.isPlaying and int(newframe) != int(clip.curFrame):
        clip.seek(int(newframe))
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("||" if clip.isPlaying else u'\u25b6', GUILayout.Width(23)):
        if clip.isPlaying:
            clip.pause()
        else:
            clip.play()
    if GUILayout.Button(u'\u25a0', GUILayout.Width(23)):
        clip.stop()
    if GUILayout.Button("|<", GUILayout.Width(23)):
        clip.seek(0)
    if GUILayout.Button("<<", GUILayout.Width(23)):
        newframe = 0 if clip.curFrame < clip.frameRate else clip.curFrame -  clip.frameRate
        clip.seek(newframe)
    if GUILayout.Button("<", GUILayout.Width(23)):
        newframe = 0 if clip.curFrame < 1 else clip.curFrame - 1
        clip.seek(newframe)
    GUILayout.Label("%d/%d %.2fs/%.2fs"%(clip.curFrame, clip.frameLength, clip.curFrame/float(clip.frameRate), clip.frameLength/float(clip.frameRate)))
    if GUILayout.Button(">", GUILayout.Width(23)):
        newframe = clip.frameLength if clip.curFrame + 1 >= clip.frameLength else clip.curFrame + 1
        clip.seek(newframe)
    if GUILayout.Button(">>", GUILayout.Width(23)):
        newframe = clip.frameLength if clip.curFrame + clip.frameRate >= clip.frameLength else clip.curFrame +  clip.frameRate
        clip.seek(newframe)
    if GUILayout.Button(">|", GUILayout.Width(23)):
        clip.seek(clip.frameLength)
    GUILayout.EndHorizontal()

    # edit tabs
    GUILayout.BeginHorizontal()
    tabs = ["Actor/Prop", "Camera", "System", "Clip", "Setting"]
    if sd.kfamModifyTabIndex >= 0:
        tabs[sd.kfamModifyTabIndex] = "<color=#00ffff>" + tabs[sd.kfamModifyTabIndex] + "</color>"
    selectedTab = GUILayout.SelectionGrid(sd.kfamModifyTabIndex, Array[String](tabs), 5)
    if selectedTab != sd.kfamModifyTabIndex:
        # on tab change
        sd.kfamModifyTabIndex = selectedTab
        sd.kfLastOnEditId = None
        sd.kfLastOnEditFrame = -1
        sd.kfScrollPos = Vector2.zero
        sd.stScrollPos = Vector2.zero
        sd.kfSelectIndex = -1
        sd.clSelectIndex = -1
        sd.kfCurveRedraw = True
        sd.uiMore = False
        sd.uiPreset = False
        sd.uiShift = False
    GUILayout.EndHorizontal()

    # tab area
    if selectedTab == 0:    # actor/prop
        (tgtId, tgtObj) = selectTaggedActorOrProp(game)
        GUILayout.BeginHorizontal() # start actor/prop tab
        if tgtObj:
            if tgtId:
                pass
            else:
                GUILayout.Label("Selected object <%s> is not tagged yet."%tgtObj.text_name)
        else:
            GUILayout.Label("Selected an object to create animation.")
        # edit selected
        if tgtObj and tgtId:
            # prepare data
            tgtKeyframes = [] if not clip.keyframes.has_key(tgtId) else clip.keyframes[tgtId]
            tgtKeyframeNoList = [kf.frameNo for kf in tgtKeyframes]
            curStatus = tgtObj.export_full_status()
            curFrame = int(clip.curFrame)
            statusKeyList, statusKeyTranslateDict = translateAndSortStatusKeys(curStatus.keys())
            isKey = tgtKeyframeNoList.count(curFrame) > 0
            if isKey:
                keyIndex = tgtKeyframeNoList.index(curFrame)
            if sd.kfLastOnEditId != tgtId or sd.kfLastOnEditFrame != curFrame:
                # on actor/prop change or frame change
                sd.kfLastOnEditId = tgtId
                sd.kfLastOnEditFrame = curFrame
                sd.kfCurveRedraw = True
                sd.uiMore = False
                sd.uiPreset = False
                if isKey:
                    sd.kfSelectIndex = keyIndex
                    sd.kfCurveData = copy.copy(tgtKeyframes[keyIndex].curve)
                else:
                    sd.uiShift = False
                    sd.kfSelectIndex = -1
                    if not hasattr(sd, "kfCurveData"):
                        sd.kfCurveData = [0.2, 0.8, 0.2, 0.8]
                if not hasattr(sd, "apStatusSelection"):
                    sd.apStatusSelection = {}
                for st in statusKeyList:
                    if isKey:
                        sd.apStatusSelection[st] = tgtKeyframes[keyIndex].status.has_key(st)
                    elif not sd.apStatusSelection.has_key(st):
                        sd.apStatusSelection[st] = True
                for st in sd.apStatusSelection.keys():
                    if not st in statusKeyList:
                        sd.apStatusSelection.pop(st)

            # char name and key frame prompt
            if len(tgtKeyframes) > 0:
                stsMsg = "%d key frames"%len(tgtKeyframes)
                if isKey:
                    stsMsg += "<color=#ff0000> [KeyFrame]</color>"
            else:
                stsMsg = "No key frames"
            GUILayout.Label("[%s (%s)]: %s"%(tgtObj.text_name, tgtId, stsMsg), GUILayout.Width(fullw-100-4))
            if GUILayout.Button("- Hide -" if sd.uiMore else "+ More +", GUILayout.Width(100)):
                sd.uiMore = not sd.uiMore
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()

            # keyframe scroll list
            sd.kfScrollPos = GUILayout.BeginScrollView(sd.kfScrollPos, GUILayout.Width(80), GUILayout.Height(115))
            newKeyFrameIdx = GUILayout.SelectionGrid(sd.kfSelectIndex, Array[String](tuple(str(n) for n in tgtKeyframeNoList)), 1)
            if newKeyFrameIdx != sd.kfSelectIndex:
                clip.seek(tgtKeyframes[newKeyFrameIdx].frameNo)
            GUILayout.EndScrollView()

            # status scroll list
            sd.stScrollPos = GUILayout.BeginScrollView(sd.stScrollPos, GUILayout.Width(180), GUILayout.Height(115))
            GUILayout.BeginHorizontal()
            if GUILayout.Button("All"):
                for st in statusKeyList:
                    sd.apStatusSelection[st] = True
            if GUILayout.Button("None"):
                for st in statusKeyList:
                    sd.apStatusSelection[st] = False
            if GUILayout.Button("Ref"):
                prevKey = [kfn for kfn in tgtKeyframeNoList if kfn <= curFrame]
                if len(prevKey) > 0:
                    pi = tgtKeyframeNoList.index(prevKey[-1])
                    refStatus = tgtKeyframes[pi].status
                    for st in statusKeyList:
                        sd.apStatusSelection[st] = refStatus.has_key(st)
                else:
                    for st in statusKeyList:
                        sd.apStatusSelection[st] = True
            if GUILayout.Button("Diff"):
                prevKey = [kfn for kfn in tgtKeyframeNoList if kfn <= curFrame]
                if len(prevKey) > 0:
                    pi = tgtKeyframeNoList.index(prevKey[-1])
                    refStatus = tgtKeyframes[pi].status
                    for st in statusKeyList:
                        sd.apStatusSelection[st] = refStatus.has_key(st) and refStatus[st] != curStatus[st]
                else:
                    for st in statusKeyList:
                        sd.apStatusSelection[st] = True
            GUILayout.EndHorizontal()
            for st in statusKeyList:
                if sd.apStatusSelection[st]:
                    sd.apStatusSelection[st] = GUILayout.Toggle(sd.apStatusSelection[st], statusKeyTranslateDict[st])
            for st in statusKeyList:
                if not sd.apStatusSelection[st]:
                    sd.apStatusSelection[st] = GUILayout.Toggle(sd.apStatusSelection[st], statusKeyTranslateDict[st])
            GUILayout.EndScrollView()

            # Curve Setting
            curveAndMoreAction(tgtObj.text_name)

        GUILayout.EndHorizontal()   # end actor/prop tab

    elif selectedTab == 1:  # camera
        GUILayout.BeginHorizontal() # start camera tab
        tgtId = "cam"
        tgtObj = "Camera"
        # prepare data
        from Studio import Studio
        studio = Studio.Instance
        c = studio.cameraCtrl
        cdata = c.cameraData
        tgtKeyframes = [] if not clip.keyframes.has_key(tgtId) else clip.keyframes[tgtId]
        tgtKeyframeNoList = [kf.frameNo for kf in tgtKeyframes]
        curFrame = int(clip.curFrame)
        isKey = tgtKeyframeNoList.count(curFrame) > 0
        if isKey:
            keyIndex = tgtKeyframeNoList.index(curFrame)
        if sd.kfLastOnEditFrame != curFrame:
            # on frame change
            sd.kfLastOnEditFrame = curFrame
            sd.kfCurveRedraw = True
            sd.uiMore = False
            sd.uiPreset = False
            if isKey:
                sd.kfSelectIndex = keyIndex
                sd.kfCurveData = copy.copy(tgtKeyframes[keyIndex].curve)
            else:
                sd.uiShift = False
                sd.kfSelectIndex = -1
                if not hasattr(sd, "kfCurveData"):
                    sd.kfCurveData = [0.2, 0.8, 0.2, 0.8]

        # char name and key frame prompt
        if len(tgtKeyframes) > 0:
            stsMsg = "%d key frames"%len(tgtKeyframes)
            if isKey:
                stsMsg += "<color=#ff0000> [KeyFrame]</color>"
        else:
            stsMsg = "No key frames"
        GUILayout.Label("[%s]: %s"%(tgtObj, stsMsg), GUILayout.Width(fullw-100-4))
        if GUILayout.Button("- Hide -" if sd.uiMore else "+ More +", GUILayout.Width(100)):
            sd.uiMore = not sd.uiMore
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()

        # keyframe scroll list
        sd.kfScrollPos = GUILayout.BeginScrollView(sd.kfScrollPos, GUILayout.Width(80), GUILayout.Height(115))
        newKeyFrameIdx = GUILayout.SelectionGrid(sd.kfSelectIndex, Array[String](tuple(str(n) for n in tgtKeyframeNoList)), 1)
        if newKeyFrameIdx != sd.kfSelectIndex:
            clip.seek(tgtKeyframes[newKeyFrameIdx].frameNo)
        GUILayout.EndScrollView()

        # camera status
        GUILayout.BeginVertical(GUILayout.Width(180), GUILayout.Height(115))
        GUILayout.BeginHorizontal()
        GUILayout.Label("Pos: %.1f, %.1f, %.1f"%(cdata.pos.x, cdata.pos.y, cdata.pos.z))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("Rot: %.1f, %.1f, %.1f"%(cdata.rotate.x, cdata.rotate.y, cdata.rotate.z))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("Distance: %.1f"%(cdata.distance.z))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("FieldOfView: %.1f"%(c.fieldOfView))
        if GUILayout.Button("<", GUILayout.Width(18)):
            c.fieldOfView -= 1
        if GUILayout.Button(">", GUILayout.Width(18)):
            c.fieldOfView += 1
        GUILayout.EndHorizontal()
        GUILayout.EndVertical()

        # curve
        curveAndMoreAction(tgtObj)

        GUILayout.EndHorizontal()   # end camera tab

    elif selectedTab == 2:  # system
        GUILayout.BeginHorizontal() # start system tab
        tgtId = "sys"
        tgtObj = "System"
        # prepare data
        tgtKeyframes = [] if not clip.keyframes.has_key(tgtId) else clip.keyframes[tgtId]
        tgtKeyframeNoList = [kf.frameNo for kf in tgtKeyframes]
        curStatus = export_sys_status(game)
        curFrame = int(clip.curFrame)
        statusKeyList, statusKeyTranslateDict = translateAndSortStatusKeys(curStatus.keys())
        isKey = tgtKeyframeNoList.count(curFrame) > 0
        if isKey:
            keyIndex = tgtKeyframeNoList.index(curFrame)
        if sd.kfLastOnEditFrame != curFrame:
            # on frame change
            sd.kfLastOnEditFrame = curFrame
            sd.kfCurveRedraw = True
            sd.uiMore = False
            sd.uiPreset = False
            if isKey:
                sd.kfSelectIndex = keyIndex
                sd.kfCurveData = copy.copy(tgtKeyframes[keyIndex].curve)
            else:
                sd.uiShift = False
                sd.kfSelectIndex = -1
                if not hasattr(sd, "kfCurveData"):
                    sd.kfCurveData = [0.2, 0.8, 0.2, 0.8]
            if not hasattr(sd, "apStatusSelection"):
                sd.apStatusSelection = {}
            for st in statusKeyList:
                if isKey:
                    sd.apStatusSelection[st] = tgtKeyframes[keyIndex].status.has_key(st)
                elif not sd.apStatusSelection.has_key(st):
                    sd.apStatusSelection[st] = True
            for st in sd.apStatusSelection.keys():
                if not st in statusKeyList:
                    sd.apStatusSelection.pop(st)

        # char name and key frame prompt
        if len(tgtKeyframes) > 0:
            stsMsg = "%d key frames"%len(tgtKeyframes)
            if isKey:
                stsMsg += "<color=#ff0000> [KeyFrame]</color>"
        else:
            stsMsg = "No key frames"
        GUILayout.Label("[%s]: %s"%(tgtObj, stsMsg), GUILayout.Width(fullw-100-4))
        if GUILayout.Button("- Hide -" if sd.uiMore else "+ More +", GUILayout.Width(100)):
            sd.uiMore = not sd.uiMore
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()

        # keyframe scroll list
        sd.kfScrollPos = GUILayout.BeginScrollView(sd.kfScrollPos, GUILayout.Width(80), GUILayout.Height(115))
        newKeyFrameIdx = GUILayout.SelectionGrid(sd.kfSelectIndex, Array[String](tuple(str(n) for n in tgtKeyframeNoList)), 1)
        if newKeyFrameIdx != sd.kfSelectIndex:
            clip.seek(tgtKeyframes[newKeyFrameIdx].frameNo)
        GUILayout.EndScrollView()

        # status scroll list
        sd.stScrollPos = GUILayout.BeginScrollView(sd.stScrollPos, GUILayout.Width(180), GUILayout.Height(115))
        GUILayout.BeginHorizontal()
        if GUILayout.Button("All"):
            for st in statusKeyList:
                sd.apStatusSelection[st] = True
        if GUILayout.Button("None"):
            for st in statusKeyList:
                sd.apStatusSelection[st] = False
        if GUILayout.Button("Ref"):
            prevKey = [kfn for kfn in tgtKeyframeNoList if kfn <= curFrame]
            if len(prevKey) > 0:
                pi = tgtKeyframeNoList.index(prevKey[-1])
                refStatus = tgtKeyframes[pi].status
                for st in statusKeyList:
                    sd.apStatusSelection[st] = refStatus.has_key(st)
            else:
                for st in statusKeyList:
                    sd.apStatusSelection[st] = True
        if GUILayout.Button("Diff"):
            prevKey = [kfn for kfn in tgtKeyframeNoList if kfn <= curFrame]
            if len(prevKey) > 0:
                pi = tgtKeyframeNoList.index(prevKey[-1])
                refStatus = tgtKeyframes[pi].status
                for st in statusKeyList:
                    sd.apStatusSelection[st] = refStatus.has_key(st) and refStatus[st] != curStatus[st]
            else:
                for st in statusKeyList:
                    sd.apStatusSelection[st] = True
        GUILayout.EndHorizontal()
        for st in statusKeyList:
            if sd.apStatusSelection[st]:
                sd.apStatusSelection[st] = GUILayout.Toggle(sd.apStatusSelection[st], statusKeyTranslateDict[st])
        for st in statusKeyList:
            if not sd.apStatusSelection[st]:
                sd.apStatusSelection[st] = GUILayout.Toggle(sd.apStatusSelection[st], statusKeyTranslateDict[st])
        GUILayout.EndScrollView()

        # Curve Setting
        curveAndMoreAction(tgtObj)

        GUILayout.EndHorizontal()   # end actor/prop tab
    
    elif selectedTab == 3:  # clips
        GUILayout.BeginHorizontal() # start clip tab
        # clip scroll list
        clipNames = [c.name for c in sClips if c != clip]
        clipListNames = []
        for cn in clipNames:
            if clip.keyframes.has_key(cn):
                clipListNames.append("<color=#00ffff>" + cn + "</color>")
            else:
                clipListNames.append(cn)
        sd.stScrollPos = GUILayout.BeginScrollView(sd.stScrollPos, GUILayout.Width(100), GUILayout.Height(130))
        newClipIdx = GUILayout.SelectionGrid(sd.clSelectIndex, Array[String](tuple(clipListNames)), 1)
        if newClipIdx != sd.clSelectIndex:
            # on clip change
            sd.clSelectIndex = newClipIdx
            sd.kfLastOnEditFrame = -1   # force reset curframe info
            sd.clstPlay = 0
            sd.clstFrame = "0"
            sd.clstLoop = "-1"
            sd.clstSpeed = "1"
        if len(clipNames) == 0:
            sd.clSelectIndex = -1
        GUILayout.EndScrollView()
        if sd.clSelectIndex == -1:
            tgtId = None
            tgtObj = None
        else:
            tgtId = clipNames[sd.clSelectIndex]
            tgtObj = mClips[tgtId]

        # edit selected
        if tgtObj and tgtId:
            # prepare data
            tgtKeyframes = [] if not clip.keyframes.has_key(tgtId) else clip.keyframes[tgtId]
            tgtKeyframeNoList = [kf.frameNo for kf in tgtKeyframes]
            curFrame = int(clip.curFrame)
            statusKeyList = ("play", "frame", "loop", "speed")
            isKey = tgtKeyframeNoList.count(curFrame) > 0
            if isKey:
                keyIndex = tgtKeyframeNoList.index(curFrame)
            if sd.kfLastOnEditFrame != curFrame:
                # on frame change
                sd.kfLastOnEditFrame = curFrame
                if isKey:
                    sd.kfSelectIndex = keyIndex
                    if tgtKeyframes[keyIndex].status.has_key('play'):
                        sd.clstPlay = tgtKeyframes[keyIndex].status['play']
                    if tgtKeyframes[keyIndex].status.has_key('frame'):
                        sd.clstFrame = "%d"%tgtKeyframes[keyIndex].status['frame']
                    if tgtKeyframes[keyIndex].status.has_key('loop'):
                        sd.clstLoop = "%d"%tgtKeyframes[keyIndex].status['loop']
                    if tgtKeyframes[keyIndex].status.has_key('speed'):
                        sd.clstSpeed = "%.2f"%tgtKeyframes[keyIndex].status['speed']
                else:
                    sd.kfSelectIndex = -1
                if not hasattr(sd, "apStatusSelection"):
                    sd.apStatusSelection = {}
                for st in statusKeyList:
                    if isKey:
                        sd.apStatusSelection[st] = tgtKeyframes[keyIndex].status.has_key(st)
                    elif not sd.apStatusSelection.has_key(st):
                        sd.apStatusSelection[st] = False
                for st in sd.apStatusSelection.keys():
                    if not st in statusKeyList:
                        sd.apStatusSelection.pop(st)

            # clip name and key frame prompt
            GUILayout.BeginVertical() 
            if len(tgtKeyframes) > 0:
                stsMsg = "%d key frames"%len(tgtKeyframes)
                if isKey:
                    stsMsg += "<color=#ff0000> [KeyFrame]</color>"
            else:
                stsMsg = "No key frames"
            GUILayout.BeginHorizontal()
            GUILayout.Label("[Clip %s]: %s"%(tgtId, stsMsg), GUILayout.Width(fullw-100-4-100))
            if GUILayout.Button("- Hide -" if sd.uiMore else "+ More +", GUILayout.Width(100)):
                sd.uiMore = not sd.uiMore
            GUILayout.EndHorizontal()

            # keyframe scroll list
            GUILayout.BeginHorizontal()
            sd.kfScrollPos = GUILayout.BeginScrollView(sd.kfScrollPos, GUILayout.Width(80), GUILayout.Height(95))
            newKeyFrameIdx = GUILayout.SelectionGrid(sd.kfSelectIndex, Array[String](tuple(str(n) for n in tgtKeyframeNoList)), 1)
            if newKeyFrameIdx != sd.kfSelectIndex:
                clip.seek(tgtKeyframes[newKeyFrameIdx].frameNo)
            GUILayout.EndScrollView()

            GUILayout.BeginVertical()
            GUILayout.BeginHorizontal()
            sd.apStatusSelection['play'] = GUILayout.Toggle(sd.apStatusSelection['play'], "Play:", GUILayout.Width(60))
            if sd.clstPlay == 0:
                playbtn = "<color=#ff0000>Stop</color>"
            elif sd.clstPlay == 1:
                playbtn = "<color=#00ff00>Play</color>"
            else:
                playbtn = "<color=#ffff00>Pause</color>"
            if sd.apStatusSelection['play'] and GUILayout.Button(playbtn, GUILayout.Width(60)):
                sd.clstPlay += 1
                sd.clstPlay %= 3
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            sd.apStatusSelection['frame'] = GUILayout.Toggle(sd.apStatusSelection['frame'], "Frame:", GUILayout.Width(60))
            if sd.apStatusSelection['frame']:
                sd.clstFrame = GUILayout.TextField(sd.clstFrame, GUILayout.Width(60))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            sd.apStatusSelection['loop'] = GUILayout.Toggle(sd.apStatusSelection['loop'], "Loop:", GUILayout.Width(60))
            if sd.apStatusSelection['loop']:
                sd.clstLoop = GUILayout.TextField(sd.clstLoop, GUILayout.Width(60))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            sd.apStatusSelection['speed'] = GUILayout.Toggle(sd.apStatusSelection['speed'], "Speed:", GUILayout.Width(60))
            if sd.apStatusSelection['speed']:
                sd.clstSpeed = GUILayout.TextField(sd.clstSpeed, GUILayout.Width(60))
            GUILayout.EndHorizontal()
            GUILayout.EndVertical()
            GUILayout.EndHorizontal()
            GUILayout.EndVertical()

            # more button
            if sd.uiMore:
                sd.uiPreset = False
                moreAction(tgtId)

        else:
            GUILayout.Label("Selected an clip to create animation.")

        GUILayout.EndHorizontal()   # end clip tab

    else:   # settings
        GUILayout.BeginHorizontal()
        GUILayout.Label("Name:", GUILayout.Width(50))
        sd.kfamTempName = GUILayout.TextField(sd.kfamTempName, GUILayout.Width(100))
        GUILayout.Space(5)
        GUILayout.Label("Priority:", GUILayout.Width(55))
        sd.kfamTempPriority = GUILayout.TextField(sd.kfamTempPriority, GUILayout.Width(40))
        GUILayout.Space(5)
        sd.kfamTempAutorun = GUILayout.Toggle(sd.kfamTempAutorun, "Auto run on load")
        #GUILayout.Label("Type:", GUILayout.Width(40))
        #if GUILayout.Button(sd.kfamTempType if sd.kfamTempType == "absolute" else "<i>" + sd.kfamTempType + "</i>", GUILayout.Width(100)):
        #    if sd.kfamTempType == "absolute":
        #        sd.kfamTempType = "relative"
        #    else:
        #        sd.kfamTempType = "absolute"
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Label("Length:", GUILayout.Width(50))
        sd.kfamTempLength = GUILayout.TextField(sd.kfamTempLength, GUILayout.Width(60))
        GUILayout.Space(5)
        GUILayout.Label("FPS:", GUILayout.Width(45))
        sd.kfamTempFPS = GUILayout.TextField(sd.kfamTempFPS, GUILayout.Width(35))
        GUILayout.Space(5)
        GUILayout.Label("Speed:", GUILayout.Width(50))
        sd.kfamTempSpeed = GUILayout.TextField(sd.kfamTempSpeed, GUILayout.Width(45))
        GUILayout.Space(5)
        GUILayout.Label("Loop:", GUILayout.Width(50))
        sd.kfamTempLoop = GUILayout.TextField(sd.kfamTempLoop, GUILayout.Width(40))
        GUILayout.EndHorizontal()
        # content list
        GUILayout.BeginHorizontal()
        GUILayout.Label("Content:", GUILayout.Width(60))
        sd.kfScrollPos = GUILayout.BeginScrollView(sd.kfScrollPos, GUILayout.Height(80))
        for kfId in clip.keyframes.keys():
            GUILayout.BeginHorizontal()
            if kfId in sd.actors:
                cName = "<color=#ff0000>Actor</color>:" + sd.actors[kfId].text_name + "(" + kfId + ")"
            elif kfId in sd.props:
                cName = "<color=#00ff00>Prop</color>:" + sd.props[kfId].text_name + "(" + kfId + ")"
            elif kfId in mClips:
                cName = "<color=#00ffff>Clip</color>:" + mClips[kfId].name
            elif kfId == "cam":
                cName = "<color=#ffff00>Camera</color>"
            elif kfId == "sys":
                cName = "<color=#ffff00>System</color>"
            else:
                cName = "<color=#ff0000>Unknown!?</color>"
            cKfCount = len(clip.keyframes[kfId])
            if cKfCount:
                cKfMin = clip.keyframes[kfId][0].frameNo
                cKfMax = clip.keyframes[kfId][-1].frameNo
                info = "%s with %d keyframes from %d to %d"%(cName, cKfCount, cKfMin, cKfMax)
            else:
                info = "%s with no keyframe"%(cName)
            GUILayout.Label(info)
            GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
        GUILayout.EndHorizontal()

    # Control buttons
    GUILayout.FlexibleSpace()
    if selectedTab in {0, 1, 2, 3}: # for keyframe
        GUILayout.BeginHorizontal()
        if not sd.uiShift:
            # normal mode
            if tgtObj:
                if tgtId:
                    if GUILayout.Button("Create" if not isKey else "Update", btnstyle, GUILayout.Width(cbwidth)):
                        try:
                            if selectedTab == 0:    # actor/prop
                                # check
                                saveStKey = [i for i in sd.apStatusSelection.keys() if sd.apStatusSelection[i]]
                                if len(saveStKey) == 0:
                                    raise Exception("No property selected!")
                                saveStatus = {}
                                for sk in saveStKey:
                                    saveStatus[sk] = curStatus[sk]
                                # build
                                from vnactor import Actor, Prop
                                if isinstance(tgtObj, Actor):
                                    newkf = ActorKeyFrame(curFrame, tgtId, saveStatus, sd.kfCurveData)
                                elif isinstance(tgtObj, Prop):
                                    newkf = PropKeyFrame(curFrame, tgtId, saveStatus, sd.kfCurveData)
                                else:
                                    raise Exception("Unknown target type: " + str(type(tgtObj)))
                            elif selectedTab == 1:  # camera
                                newkf = CameraKeyFrame(curFrame, sd.kfCurveData)
                            elif selectedTab == 2:  # system
                                # check
                                saveStKey = [i for i in sd.apStatusSelection.keys() if sd.apStatusSelection[i]]
                                if len(saveStKey) == 0:
                                    raise Exception("No property selected!")
                                saveStatus = {}
                                for sk in saveStKey:
                                    saveStatus[sk] = curStatus[sk]
                                newkf = SystemKeyFrame(curFrame, saveStatus, sd.kfCurveData)
                            elif selectedTab == 3:  # clip
                                # check
                                saveStKey = [i for i in sd.apStatusSelection.keys() if sd.apStatusSelection[i]]
                                if len(saveStKey) == 0:
                                    raise Exception("No property selected!")
                                # convert
                                saveStatus = {}
                                if sd.apStatusSelection['play']:
                                    saveStatus['play'] = sd.clstPlay
                                if sd.apStatusSelection['frame']:
                                    saveStatus['frame'] = int(sd.clstFrame)
                                if sd.apStatusSelection['loop']:
                                    saveStatus['loop'] = int(sd.clstLoop)
                                if sd.apStatusSelection['speed']:
                                    saveStatus['speed'] = float(sd.clstSpeed)
                                newkf = ClipKeyFrame(curFrame, tgtId, saveStatus)
                            else:
                                raise Exception("Unknown keyframe type")
                            # create/update
                            if not isKey or sh.masterMode:
                                clip.updateKeyFrame(newkf)
                            else:
                                def toClipboard():
                                    clip.clipboard = newkf
                                scriptHelperGUIMessage("Update key frame at <color=#ff0000>#%d</color> with current status?\nUpdate will overwrite the old status of current keyframe. If you want to make a new keyframe with current status, select [To Clipboard] and then paste on target frame later."%(curFrame), (("Update", clip.updateKeyFrame, newkf), ("To Clipboard", toClipboard), "Cancel"))
                        except Exception as e:
                            scriptHelperGUIMessage(str(e))
                else:
                    if GUILayout.Button("Tag it", btnstyle, GUILayout.Width(cbwidth)):
                        sh.tag_select()
            else:
                GUILayout.Button("", btnstyle, GUILayout.Width(cbwidth))
            if clip.isPlaying:
                GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth))
                GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth))
                GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth))
                GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth))
            else:
                if GUILayout.Button("Delete" if tgtId and isKey else "", btnstyle, GUILayout.Width(cbwidth)) and tgtId and isKey:
                    def onDeleteKeyframe():
                        clip.deleteKeyFrame(tgtId)
                        sd.kfSelectIndex = -1
                    scriptHelperGUIMessage("Delete key frame at #%d?"%(curFrame), (("Delete", onDeleteKeyframe), "Cancel"))
                if GUILayout.Button("Shift" if tgtId and isKey else "", btnstyle, GUILayout.Width(cbwidth)) and tgtId and isKey:
                    if not hasattr(sd, "shiftFollowers"):
                        sd.shiftFollowers = False
                    sd.shiftInput = "0"
                    sd.uiShift = True
                if GUILayout.Button("Copy" if tgtId and isKey else "", btnstyle, GUILayout.Width(cbwidth)) and tgtId and isKey:
                    scriptHelperGUIMessage("Keyframe at frame %d was copied into the clipboard."%(curFrame))
                    clip.copyKeyFrame(tgtId)
                if GUILayout.Button("Paste" if tgtId and clip.canPasteKeyFrame(tgtId) else "", btnstyle, GUILayout.Width(cbwidth)) and tgtId and clip.canPasteKeyFrame(tgtId):
                    clip.pasteKeyFrame(tgtId)
                    clip.applyFrames()
            if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
                changeGuiScreen(sh, kfam_GUI_manager)
        else:
            # shift mode
            if keyIndex == 0:
                sfMin = -curFrame
            else:
                sfMin = tgtKeyframeNoList[keyIndex - 1] - curFrame + 1
            if sd.shiftFollowers or curFrame == tgtKeyframeNoList[-1]:
                sfMax = clip.frameLength - tgtKeyframeNoList[-1]
            else:
                sfMax = tgtKeyframeNoList[keyIndex + 1] - curFrame - 1
            # shift UI
            GUILayout.Label("Shift (%d - %d)"%(sfMin, sfMax), GUILayout.Width(110))
            sd.shiftInput = GUILayout.TextField(sd.shiftInput, GUILayout.Width(40))
            GUILayout.Label("frames", GUILayout.Width(40))
            sd.shiftFollowers = GUILayout.Toggle(sd.shiftFollowers, "with followers", GUILayout.Width(110))
            if GUILayout.Button("Apply", btnstyle, GUILayout.Width(cbwidth)):
                try:
                    sfNum = int(sd.shiftInput)
                    if sfNum > sfMax:
                        raise Exception("Shift step is too big. (Max = %d)"%sfMax)
                    if sfNum < sfMin:
                        raise Exception("Shift step is too small. (Min = %d)"%sfMin)
                    clip.shiftKeyFrame(tgtId, sfNum, sd.shiftFollowers)
                    clip.seek(curFrame + sfNum)
                    sd.uiShift = False
                except Exception as e:
                    scriptHelperGUIMessage("Shift keyframe failed: " + str(e))
            if GUILayout.Button("Cancel", btnstyle, GUILayout.Width(cbwidth)):
                sd.uiShift = False
        GUILayout.EndHorizontal()
    else:   # for setting
        GUILayout.BeginHorizontal()
        settingModified = sd.kfamTempName != clip.name or sd.kfamTempPriority != "%d"%clip.priority or sd.kfamTempType != clip.animeType or sd.kfamTempLength != "%d"%clip.frameLength or sd.kfamTempFPS != "%d"%clip.frameRate or sd.kfamTempSpeed != "%.2f"%clip.speed or sd.kfamTempLoop != "%d"%clip.loop or sd.kfamTempAutorun != clip.autorun
        if GUILayout.Button("Apply" if settingModified else "", btnstyle, GUILayout.Width(cbwidth)) and settingModified:
            try:
                oldName = clip.name
                name = sd.kfamTempName.strip()
                if len(name) == 0:
                    raise Exception("Clip name can not be empty!")
                if name != clip.name and mClips.has_key(name):
                    raise Exception("Clip name dupulicated!")
                priority = int(sd.kfamTempPriority)
                frameLength = int(sd.kfamTempLength)
                if frameLength < clip.lastFrameNo:
                    raise Exception("Clip's length must be equal or greater than last key frame No. #%d!"%clip.lastFrameNo)
                frameRate = int(sd.kfamTempFPS)
                speed = float(sd.kfamTempSpeed)
                loop = int(sd.kfamTempLoop)
                if name != clip.name:
                    kfa_rename(game, oldName, name)
                if priority != clip.priority:
                    kfa_resort(game, name, priority)
                    sd.kfamSelectedClipIndex = -1
                if sd.kfamTempType != clip.animeType:
                    kfa_retype(game, name, sd.kfamTempType)
                clip.frameLength = frameLength
                clip.frameRate = frameRate
                clip.speed = speed
                clip.loop = loop
                clip.autorun = sd.kfamTempAutorun
            except Exception as e:
                scriptHelperGUIMessage("Apply setting failed: " + str(e))
        if GUILayout.Button("Import", btnstyle, GUILayout.Width(cbwidth)):
            def onImportClip(game, filename):
                try:
                    clipScript = loadScriptFromFile(filename)
                    oldName = clip.name
                    oldPriority = clip.priority
                    oldAnimeType = clip.animeType
                    clip.importSetting(clipScript)
                    clip.name = oldName # do not change clip name when import
                    if oldPriority != clip.priority:
                        kfa_resort(game, clip.name, clip.priority)
                        sd.kfamSelectedClipIndex = -1
                    if sd.kfamTempType != clip.animeType:
                        kfa_retype(game, clip.name, clip.animeType)
                    scriptHelperGUIMessage("Key frame anime clip loaded.")
                except Exception as e:
                    scriptHelperGUIMessage("Fail to import setting: %s"%str(e), ("OK",))
            stPath = path.join(game.pygamepath, "animeclips/")
            startFileBrower(sh, startPath=stPath, extFilter=".txt", openSave="open", title="Select a clip file to import:", onOk=onImportClip, onCancel=None)
        if GUILayout.Button("Export", btnstyle, GUILayout.Width(cbwidth)):
            def onExportClip(game, filename):
                if saveStringToFile(filename, clip.exportSetting(), append=False, backup=True):
                    scriptHelperGUIMessage("All clip settings and keyframes were saved into <%s>."%filename, ("OK",))
            stPath = path.join(game.pygamepath, "animeclips/")
            startFileBrower(sh, startPath=stPath, extFilter=".txt", openSave="save", title="Export current clip to:", onOk=onExportClip, onCancel=None)
        if GUILayout.Button("Dump", btnstyle, GUILayout.Width(cbwidth)):
            if saveStringToFile('dumppython.txt', clip.exportSetting(), append=True):
                scriptHelperGUIMessage("All clip settings and keyframes were dumpped into <dumppython.txt>.")
        if GUILayout.Button("Optimize", btnstyle, GUILayout.Width(cbwidth)):
            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"))
        if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
            changeGuiScreen(sh, kfam_GUI_manager)
        GUILayout.EndHorizontal()

def kfam_GUI_modify_clip_v2(sh):
    from libkfguictrl import KeyframeViewer, CurveEditor
    # memory shortcuts
    game = sh.game
    gd = game.gdata
    sd = game.scenedata
    clip = sd.kfamOnModifyClip
    mClips = gd.kfaManagedClips
    sClips = gd.kfaSortedClips
    # ui grid const
    wndWidthMargin = 10
    wndHeightMargin = 95
    kfvTopMargin = 60
    kfvGapSmall = 1
    kfvGapBig = 2
    curveWindowWidth = 124
    curveWindowHeight = 180
    clipWindowWidth = 500
    clipWindowHeight = 45

    def initClipEditorWindow():
        sd.kfamModifyTabIndex = 0
        # UI temp variable
        sd.kfamEditorTargetScroll = Vector2.zero
        sd.kfamLastPlayingStatus = clip.isPlaying
        sd.kfamCurSelTargetId = None
        sd.kfamCurSelKeyframeSetCache = None
        sd.kfamTargetKeyframeExpand = {}
        sd.kfamCustomStartFrame = -1
        sd.kfamCustomEndFrame = -1
        sd.kfamKeyframeClipboard = {}
        sd.kfamKeyframeClipMode = 0
        sd.kfamKeyframeClipboardLength = -1
        # 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 clip.keyframes.keys() if id in sd.actors]
        sd.kfamSortedTargetId.extend(sorted(actorids))
        propids = [id for id in clip.keyframes.keys() if id in sd.props]
        sd.kfamSortedTargetId.extend(sorted(propids))
        clipids = [id for id in clip.keyframes.keys() if id in mClips]
        sd.kfamSortedTargetId.extend(sorted(clipids))
        if "cam" in clip.keyframes:
            sd.kfamSortedTargetId.append("cam")
        if "sys" in clip.keyframes:
            sd.kfamSortedTargetId.append("sys")
        # build keyframeview
        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)
        sd.kfamResizeRequest = False
        sd.kfamReloadRequest = False
        sd.kfamHidePropertiesRequest = False
        reloadKeyframes()
        # build curve editor
        sd.kfamCurveWndId = None
        sd.kfamCurveEditor = CurveEditor(7, 20, 110, None)
        sd.kfamCurveEditor.onCurveUpdateCallback = curveChange
        # build sub-clip editor
        sd.kfamSubClipWndId = 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)
        # VideoExport mode
        sd.kfamEatMouseInputBackup = game.skin.eatMouseInput
        game.skin.eatMouseInput = not gd.kfaSetting.videoExportMode
    def quitClipEditorWindow():
        # restore mouse mode
        game.skin.eatMouseInput = sd.kfamEatMouseInputBackup
        # cleanup
        sd.kfamModifyTabIndex = -1
        gd.kfaSetting.clipEditorWindowRect = game.windowRect
        save_keyframe_anime_setting(game)
        game.windowRect = sd.kfamOldWindowRect
        closeCurveSubWindow()
        toggleClipSubWindow(None)
        changeGuiScreen(sh, kfam_GUI_manager)
    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"
            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, None)
        # 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():
        def collectInfoFromId(tid):
            # return (name, properties key, properties name, properties animetable check)
            if tid in sd.actors:
                tgtObj = sd.actors[tid]
                tname = tid + ":" + tgtObj.text_name
                pkeys, pnames = translateAndSortStatusKeys(tgtObj.export_full_status().keys(), False)
                return (tname, pkeys, pnames, None)
            elif tid in sd.props:
                tgtObj = sd.props[tid]
                tname = tid + ":" + tgtObj.text_name
                pkeys, pnames = translateAndSortStatusKeys(tgtObj.export_full_status().keys(), False)
                return (tname, pkeys, pnames, None)
            elif tid in game.gdata.kfaManagedClips:
                return (tid + ":Clip", ("play", "frame", "loop", "speed"), {"play": "Play status", "frame": "Seek to frame", "loop": "Loop", "speed": "Speed"}, None)
            elif tid == "cam":
                return ("Camera", ("camera",), {"camera": "Camera"}, None)
            elif tid == "sys":
                pkeys, pnames = translateAndSortStatusKeys(export_sys_status(game).keys(), False)
                return ("Scene", pkeys, pnames, None)
            else:
                raise Exception("Unknown target id <%s> in clip's keyframes"%tid)

        kfView = sd.kfamKeyframeView
        # clear up to start
        kfView.clearKeyframe()
        # setup namelist, always use export key
        for tid in sd.kfamSortedTargetId:
            (tname, pkeys, pnames, panimecheck) = collectInfoFromId(tid)
            for pkey in pkeys:
                kfView.importFrameName(tid, pkey)
                kfView.keyframeNameList.dispNameDic[pkey] = pnames[pkey]
            kfView.keyframeNameList.dispNameDic[tid] = tname
        # import keyframes
        for tid in sd.kfamSortedTargetId:
            for kf in clip.keyframes[tid]:
                for pid in kf.status.keys():
                    kfView.importKeyframe(tid, pid, kf.frameNo, kf)
            kfView.keyframeNameList.groupExpand[tid] = False
        # hide unused properties
        hideUnusedProperties(False)
        # done import
        kfView.arrangeImportedKeyframes()
        kfView.setFixFrameLengthMode(True, clip.frameLength)
        onFrameChange(int(clip.curFrame))
        # setup callback
        kfView.onFrameChangeCallback = kfViewFrameChange
        kfView.onRowSelecCallback = kfViewRowSelect
        kfView.onKeyframeMoveCallback = kfViewKeyframeMove
        kfView.onKeyframeSelectCallback = kfViewKeyframeSelect
        kfView.onKeyframeNameResizeCallback = kfViewNameResize
        # clear request
        sd.kfamReloadRequest = False
    def hideUnusedProperties(refresh):
        hiddenNames = sd.kfamKeyframeView.keyframeNameList.hiddenNames
        hiddenNames.clear()
        if gd.kfaSetting.hideUnusedProperties:
            for tid in clip.keyframes:
                if len(clip.keyframes[tid]) == 0:
                    continue    # no-keyframe at all, do not hide properties for it
                unusedP = set(sd.kfamKeyframeView.keyframeNameList.groupContents[tid])
                for kf in clip.keyframes[tid]:
                    for st in kf.status:
                        unusedP.discard(st)
                for st in unusedP:
                    key = tid + "#" + st
                    hiddenNames.add(key)
        if refresh:
            sd.kfamKeyframeView.refreshKeyframe()
        sd.kfamHidePropertiesRequest = False
    # keyframe view callback
    def kfViewFrameChange(kfView):
        clip.seek(kfView.curFrame)
    def kfViewRowSelect(kfView):
        sd.kfamCurSelKeyframeSetCache = None
    def kfViewKeyframeMove(kfView, keyMoveInfo):
        # UI is already processed 
        # TODO: partly move not supportted
        for (bKey, oldFrame, newFrame, tgtObj) in keyMoveInfo:
            tgtObj.frameNo = newFrame
        kfView.refreshKeyframe()
        kfViewRowSelect(kfView)
        # update clip
        clip.applyFrames()
    def kfViewKeyframeSelect(kfView):
        selectedKeyframes = set()
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            kf = kfView.keyframeBuffer[kfbtn.key][kfbtn.column]
            if hasattr(kf, "curve"):
                selectedKeyframes.add(kf)
        updateCurveInput(selectedKeyframes)
    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 = kfView.keyframeBuffer[kfbtn.key][kfbtn.column]
            if hasattr(kf, "curve"):
                selectedKeyframes.add(kf)
        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():
        def getOldKeyframe(keyframes, frameNo):
            for kf in keyframes:
                if kf.frameNo == frameNo:
                    return kf
                if kf.frameNo > frameNo:
                    return None
            return None
        # check keys
        kfView = sd.kfamKeyframeView
        selKeys = getCurSelectedKeys(kfView)
        if len(selKeys) == 0:
            scriptHelperGUIMessage("No properties of any object was selected!\nPlease select the properties you want to register into clip's keyframe first.", ("OK",))
            return
        # group up
        groupKeys = {}
        for key in selKeys:
            gn, pn = key.split("#")
            if gn in groupKeys:
                groupKeys[gn].add(pn)
            else:
                groupKeys[gn] = {pn}
        # build and add keyframe
        for gn in groupKeys.keys():
            oldkeyframe = getOldKeyframe(clip.keyframes[gn], kfView.curFrame)
            if gn in sd.actors:
                saveStatus = {}
                if oldkeyframe:
                    saveStatus.update(oldkeyframe.status)
                    curveData = oldkeyframe.curve
                else:
                    curveData = [0.2, 0.8, 0.2, 0.8]
                fullCurStatus = sd.actors[gn].export_full_status()
                for pn in groupKeys[gn]:
                    if pn in fullCurStatus:
                        saveStatus[pn] = fullCurStatus[pn]
                newkf = ActorKeyFrame(kfView.curFrame, gn, saveStatus, curveData)
            elif gn in sd.props:
                saveStatus = {}
                if oldkeyframe:
                    saveStatus.update(oldkeyframe.status)
                    curveData = oldkeyframe.curve
                else:
                    curveData = [0.2, 0.8, 0.2, 0.8]
                fullCurStatus = sd.props[gn].export_full_status()
                for pn in groupKeys[gn]:
                    if pn in fullCurStatus:
                        saveStatus[pn] = fullCurStatus[pn]
                newkf = PropKeyFrame(kfView.curFrame, gn, saveStatus, curveData)
            elif gn in mClips:
                if sd.kfamSubClipWndId == None or sd.kfamSubClipStatus.errorMessage != None or sd.kfamSubClipStatus.targetClip.name != gn:
                    scriptHelperGUIMessage("To register clip keyframe, you need to select the clip <%s> in object panel to open its status window and setup status."%(gn), ("OK",))
                    continue
                saveStatus = {}
                if oldkeyframe:
                    saveStatus.update(oldkeyframe.status)
                fullCurStatus = sd.kfamSubClipStatus.subClipStatus
                for pn in groupKeys[gn]:
                    if pn in fullCurStatus:
                        saveStatus[pn] = fullCurStatus[pn]
                newkf = ClipKeyFrame(kfView.curFrame, gn, saveStatus)
            elif gn == "cam":
                if oldkeyframe:
                    curveData = oldkeyframe.curve
                else:
                    curveData = [0.2, 0.8, 0.2, 0.8]
                newkf = CameraKeyFrame(kfView.curFrame, curveData)
            elif gn == "sys":
                saveStatus = {}
                if oldkeyframe:
                    saveStatus.update(oldkeyframe.status)
                    curveData = oldkeyframe.curve
                else:
                    curveData = [0.2, 0.8, 0.2, 0.8]
                fullCurStatus = export_sys_status(game)
                for pn in groupKeys[gn]:
                    if pn in fullCurStatus:
                        saveStatus[pn] = fullCurStatus[pn]
                newkf = SystemKeyFrame(kfView.curFrame, saveStatus, curveData)
            else:
                raise Exception("Unexpected id")
            # to clip
            clip.updateKeyFrame(newkf)
            # to UI
            for pn in newkf.status.keys():
                kfView.registerKeyframe(gn, pn, kfView.curFrame, newkf)
        hideUnusedProperties(False)
        kfView.refreshKeyframe()
        sd.kfamCurSelKeyframeSetCache = None
    def copyKeyframe():
        # check
        kfView = sd.kfamKeyframeView
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        # clear old clipboard
        sd.kfamKeyframeClipboard.clear()
        sd.kfamKeyframeClipboardLength = -1
        # build clipboard
        baseFrame = min([si[1] for si in selectInfo])
        for (kfKey, kfFrame) in selectInfo:
            gn, pn = kfKey.split("#")
            diffFrame = kfFrame - baseFrame
            orgFrame = kfView.keyframeBuffer[kfKey][kfFrame]
            gfkey = "%s#%d"%(gn, diffFrame)
            if gfkey in sd.kfamKeyframeClipboard:
                tgtFrame = sd.kfamKeyframeClipboard[gfkey]
                tgtFrame.status[pn] = scriptCopy(orgFrame.status[pn])
            else:
                tgtFrame = orgFrame.copy()
                tgtFrame.frameNo = diffFrame
                for p in tgtFrame.status.keys():
                    if p != pn:
                        del tgtFrame.status[p]
                sd.kfamKeyframeClipboard[gfkey] = tgtFrame
            if diffFrame > sd.kfamKeyframeClipboardLength:
                sd.kfamKeyframeClipboardLength = diffFrame
    def pasteKeyframe():
        def getOldKeyframe(keyframes, frameNo):
            for kf in keyframes:
                if kf.frameNo == frameNo:
                    return kf
                if kf.frameNo > frameNo:
                    return None
            return None
        # check
        kfView = sd.kfamKeyframeView
        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() 
        for gfKey in sd.kfamKeyframeClipboard:
            gn, diffFrameStr = gfKey.split("#")
            diffFrame = int(diffFrameStr)
            frameNo = kfView.curFrame + diffFrame
            oldkeyframe = getOldKeyframe(clip.keyframes[gn], frameNo)
            copiedKeyframe = sd.kfamKeyframeClipboard[gfKey].copy()
            # update keyframe
            if gn in sd.actors:
                if oldkeyframe:
                    oldkeyframe.status.update(copiedKeyframe.status)
                    oldkeyframe.curve = copy.copy(copiedKeyframe.curve)
                    newkf = oldkeyframe
                else:
                    newkf = ActorKeyFrame(frameNo, gn, copiedKeyframe.status, copiedKeyframe.curve)
            elif gn in sd.props:
                if oldkeyframe:
                    oldkeyframe.status.update(copiedKeyframe.status)
                    oldkeyframe.curve = copy.copy(copiedKeyframe.curve)
                    newkf = oldkeyframe
                else:
                    newkf = PropKeyFrame(frameNo, gn, copiedKeyframe.status, copiedKeyframe.curve)
            elif gn in mClips:
                if oldkeyframe:
                    oldkeyframe.status.update(copiedKeyframe.status)
                    newkf = oldkeyframe
                else:
                    newkf = ClipKeyFrame(frameNo, gn, copiedKeyframe.status)
            elif gn == "cam":
                newkf = copiedKeyframe.copy()
                newkf.frameNo = frameNo
            elif gn == "sys":
                if oldkeyframe:
                    oldkeyframe.status.update(copiedKeyframe.status)
                    oldkeyframe.curve = copy.copy(copiedKeyframe.curve)
                    newkf = oldkeyframe
                else:
                    newkf = SystemKeyFrame(frameNo, copiedKeyframe.status, copiedKeyframe.curve)
            else:
                raise Exception("Unexpected id")
            # update clip
            clip.updateKeyFrame(newkf)
            # update ui
            for pn in newkf.status.keys():
                kfView.registerKeyframe(gn, pn, frameNo, newkf)
                # select later
                kfKey = gn + "#" + pn
                newSelectKeyframeInfo.add((kfKey, frameNo))
        # update status
        hideUnusedProperties(False)
        kfView.refreshKeyframe()
        kfView.selectKeyframes(newSelectKeyframeInfo)
        sd.kfamCurSelKeyframeSetCache = None
        kfViewKeyframeSelect(kfView)
        # update clip
        clip.applyFrames()
    def deleteKeyframe():
        def getOldKeyframe(keyframes, frameNo):
            for kf in keyframes:
                if kf.frameNo == frameNo:
                    return kf
                if kf.frameNo > frameNo:
                    return None
            return None
        kfView = sd.kfamKeyframeView
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        # from clip
        affectedKeyframe = {}
        for (kfKey, kfFrame) in selectInfo:
            gn, pn = kfKey.split("#")
            key = gn + "#" + str(kfFrame)
            if key in affectedKeyframe:
                affectedKeyframe[key].add(pn)
            else:
                affectedKeyframe[key] = {pn}
        for key in affectedKeyframe.keys():
            gn, frameNo = key.split("#")
            frameNo = int(frameNo)
            frameToDel = getOldKeyframe(clip.keyframes[gn], frameNo)
            if not frameToDel:
                raise Exception("Keyframe not found for %s"%key)
            frameToDelIndex = clip.keyframes[gn].index(frameToDel)
            if gn != "cam":
                for pn in affectedKeyframe[key]:
                    del frameToDel.status[pn]
                if len(frameToDel.status) == 0:
                    del clip.keyframes[gn][frameToDelIndex]
            else:
                del clip.keyframes[gn][frameToDelIndex]
        # from UI
        kfView.deleteKeyframes(selectInfo)
        hideUnusedProperties(False)
        kfView.refreshKeyframe()
        sd.kfamCurSelKeyframeSetCache = None
        kfViewKeyframeSelect(kfView)
        # update clip
        clip.applyFrames()
    # 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 updateCurveInput(selectedKeyframes):
        if len(selectedKeyframes):
            openCurveSubWindow()
        else:
            closeCurveSubWindow()
            return
        sd.kfamCurveEditor.clearCurveData()
        for skf in selectedKeyframes:
            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
    def selectPropertiesById(tid):
        kfView = sd.kfamKeyframeView
        selkeys = []
        if tid in kfView.keyframeNameList.groupContents.keys():
            savedPname = set()
            for kf in clip.keyframes[tid]:
                for pname in kf.status:
                    savedPname.add(pname)
            if len(savedPname) == 0:
                savedPname = set(kfView.keyframeNameList.groupContents[tid])
            for pname in savedPname:
                key = tid + "#" + pname
                if pname not in kfView.keyframeNameList.hiddenNames and key not in kfView.keyframeNameList.hiddenNames:
                    selkeys.append(key)
        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 pn not in kfView.keyframeNameList.hiddenNames and (gn + "#" + pn) not in kfView.keyframeNameList.hiddenNames]
            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 pname not in kfView.keyframeNameList.hiddenNames and key not in kfView.keyframeNameList.hiddenNames:
                    newSelKeys.append(key)
        kfView.selectBonesByKey(newSelKeys)
    def selectPropertiesCHG():
        kfView = sd.kfamKeyframeView
        selkeys = []
        # get cur clip frame status
        curFrames = clip.getCurFrames()
        for tid in curFrames.keys():
            if curFrames[tid] == None:
                for pname in kfView.keyframeNameList.groupContents[tid]:
                    key = tid + "#" + pname
                    if pname not in kfView.keyframeNameList.hiddenNames and key not in kfView.keyframeNameList.hiddenNames:
                        selkeys.append(key)
            elif tid in sd.actors:
                tobj = sd.actors[tid]
                tstatus = tobj.export_full_status()
                for pname in kfView.keyframeNameList.groupContents[tid]:
                    if pname in tstatus and pname in curFrames[tid].status and tstatus[pname] != curFrames[tid].status[pname]:
                        key = tid + "#" + pname
                        if pname not in kfView.keyframeNameList.hiddenNames and key not in kfView.keyframeNameList.hiddenNames:
                            selkeys.append(key)
            elif tid in sd.props:
                tobj = sd.props[tid]
                tstatus = tobj.export_full_status()
                for pname in kfView.keyframeNameList.groupContents[tid]:
                    if pname in tstatus and pname in curFrames[tid].status and tstatus[pname] != curFrames[tid].status[pname]:
                        key = tid + "#" + pname
                        if pname not in kfView.keyframeNameList.hiddenNames and key not in kfView.keyframeNameList.hiddenNames:
                            selkeys.append(key)
            elif tid in mClips:
                pass
            elif tid == "cam":
                pass
            elif tid == "sys":
                pass
        kfView.selectBonesByKey(selkeys)
    def deleteTargetObjectId():
        del clip.keyframes[sd.kfamCurSelTargetId]
        if sd.kfamCurSelTargetId in mClips:
            toggleClipSubWindow(None)
        closeCurveSubWindow()
        sd.kfamReloadRequest = True

    # init clip editor window
    if sd.kfamModifyTabIndex == -1 or sd.kfamReloadRequest:
        initClipEditorWindow()
    if sd.kfamResizeRequest:
        resizeClipEditorWindow()
    if sd.kfamHidePropertiesRequest:
        hideUnusedProperties(True)
    fullw = game.windowRect.width - wndWidthMargin
    leftPanelHeight = 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(GUI.skin.box, GUILayout.Width(gd.kfaSetting.targetPanelWidth), GUILayout.Height(leftPanelHeight))
    # targets selector
    sd.kfamEditorTargetScroll = GUILayout.BeginScrollView(sd.kfamEditorTargetScroll)
    for tid in sd.kfamSortedTargetId:
        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 game.gdata.kfaManagedClips:
            tno = None
            isSel = False
            ttitle = "<color=#00ff00>" + tid + ":</color>" + "Clip"
        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)
        # +/- 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]:
            capped = True
            for i in range(len(clip.keyframes[tid])):
                kf = clip.keyframes[tid][i]
                if i % 3 == 0:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(10)
                    capped = False
                GUI.color = Color.yellow if kf.frameNo == sd.kfamKeyframeView.curFrame else oColor
                if GUILayout.Button(str(kf.frameNo), frmbtnstyle, GUILayout.Width(39), GUILayout.Height(16)):
                    clip.seek(kf.frameNo)
                    onFrameChange(kf.frameNo)
                if i % 3 == 2:
                    GUILayout.EndHorizontal()
                    capped = True
            if not capped:
                GUILayout.EndHorizontal()
        GUI.color = oColor
    GUILayout.EndScrollView()
    # target operations
    GUILayout.FlexibleSpace()
    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 and 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 sd.kfamCurSelTargetId and GUILayout.Button(">>", smbtnstyle, GUILayout.Width(25)):
    #    pass    # TODO
    GUILayout.EndHorizontal()
    GUILayout.EndVertical()

    # right panel: 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()

    # control buttons
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    #GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Register", btnstyle):
        registerKeyframe()
    canCopy = len(getCurSelectedKeyframeInfo(sd.kfamKeyframeView)) > 0
    if GUILayout.Button("Copy" if canCopy else "<color=#808080>Copy</color>", btnstyle) and canCopy:
        copyKeyframe()
    canPaste = 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("M-Sel", btnstyle):
    #    pass
    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_setting)
    if GUILayout.Button("Back", btnstyle):
        quitClipEditorWindow()
    GUILayout.EndHorizontal()

    # 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))

def kfam_GUI_modify_clip_min(sh):
    # seekbar only
    kfam_GUI_parts_seekbar(sh, None, None)

def kfam_GUI_modify_clip_add(sh):
    from vnactor import Actor, Prop
    from libkfguictrl import ComboListBox
    from vnframe import verifyTagName
    # memory shortcuts
    game = sh.game
    sd = game.scenedata
    clip = sd.kfamOnModifyClip
    mClips = game.gdata.kfaManagedClips

    def autoNameAnId(obj):
        i = 0
        if isinstance(obj, Actor):
            newId = "act" + str(i)
            while newId in sd.actors:
                i += 1
                newId = "act" + str(i)
        else:
            newId = "prp" + str(i)
            while newId in sd.props:
                i += 1
                newId = "prp" + str(i)
        return newId
    def checkAndAdd():
        if sd.kfamAddInfo.catelog == "actorprop" and sd.kfamAddInfo.lastSelectObj != None:
            from vnframe import addTagToObject
            errMsg = addTagToObject(sd.kfamAddInfo.id, sd.kfamAddInfo.obj.objctrl)
            if errMsg:
                scriptHelperGUIMessage(errMsg, ("OK",))
                return
        # add new id into clip
        clip.addTarget(sd.kfamAddInfo.id)
        sd.kfamReloadRequest = True
        backToEditor()
    def backToEditor():
        changeGuiScreen(sh, kfam_GUI_modify_clip)
        del sd.kfamAddInfo

    # init
    if not hasattr(sd, "kfamAddInfo"):
        sd.kfamAddInfo = GData()
        sd.kfamAddInfo.catelog = "actorprop"
        sd.kfamAddInfo.id = None
        sd.kfamAddInfo.obj = None
        sd.kfamAddInfo.lastSelectObj = None
        sd.kfamAddInfo.clipTempNames = [cp.name for cp in game.gdata.kfaSortedClips if cp != clip and cp.name not in clip.keyframes]
        sd.kfamAddInfo.clipSelector = ComboListBox(Rect(220, 133, 120, 21), 80, sd.kfamAddInfo.clipTempNames, 0)
    
    # Title
    GUILayout.BeginHorizontal()
    GUILayout.Space(10)
    GUILayout.Label("Add object to controll by the clip:")
    GUILayout.EndHorizontal()
    # chara/object
    GUILayout.BeginHorizontal()
    if GUILayout.Toggle(sd.kfamAddInfo.catelog == "actorprop", "Charactor or Item, and plugin related with char/item."):
        sd.kfamAddInfo.catelog = "actorprop"
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Space(20)
        if len(game.studio.treeNodeCtrl.selectNodes) != 1:
            GUILayout.Label("<color=#00ff00>Select ONE Charactor/Item from workspace, please</color>")
            sd.kfamAddInfo.id = None
            sd.kfamAddInfo.obj = None
            sd.kfamAddInfo.lastSelectObj = None
        else:
            tid, tobj = selectTaggedActorOrProp(game)
            if tid:
                if tid in clip.keyframes:
                    GUILayout.Label("<color=#ff0000><%s:%s> is already added</color>"%(tid, tobj.text_name))
                    sd.kfamAddInfo.id = None
                    sd.kfamAddInfo.obj = None
                    sd.kfamAddInfo.lastSelectObj = None
                else:
                    GUILayout.Label("<color=#00ff00>Add <%s:%s> to the clip</color>"%(tid, tobj.text_name))
                    sd.kfamAddInfo.id = tid
                    sd.kfamAddInfo.obj = tobj
                    sd.kfamAddInfo.lastSelectObj = None
            elif tobj:
                if not sd.kfamAddInfo.id or sd.kfamAddInfo.lastSelectObj != tobj.objctrl:
                    sd.kfamAddInfo.id = autoNameAnId(tobj)
                    sd.kfamAddInfo.lastSelectObj = tobj.objctrl
                GUILayout.Label("<color=#00ff00>Add <%s> to the clip, tag as</color>"%(tobj.text_name))
                sd.kfamAddInfo.id = GUILayout.TextField(sd.kfamAddInfo.id, GUILayout.Width(70))
                sd.kfamAddInfo.obj = tobj
            else:
                GUILayout.Label("<color=#ff0000>Selected object is not supportted</color>")
                sd.kfamAddInfo.id = None
                sd.kfamAddInfo.obj = None
                sd.kfamAddInfo.lastSelectObj = None
        GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    # check camera
    GUILayout.BeginHorizontal()
    if not clip.keyframes.has_key("cam"):
        if GUILayout.Toggle(sd.kfamAddInfo.catelog == "camera", "Main camera work" + (" and object camera switch" if clip.version == 2 else "")):
            sd.kfamAddInfo.catelog = "camera"
            sd.kfamAddInfo.id = "cam"
    else:
        GUILayout.Toggle(False, "<color=#808080>Main Camera is already added</color>")
    GUILayout.EndHorizontal()
    # check system 
    GUILayout.BeginHorizontal()
    if not clip.keyframes.has_key("sys"):
        if GUILayout.Toggle(sd.kfamAddInfo.catelog == "system", "Studio scene setting, and plugin work on scene."):
            sd.kfamAddInfo.catelog = "system"
            sd.kfamAddInfo.id = "sys"
    else:
        GUILayout.Toggle(False, "<color=#808080>Scene setting is already added</color>")
    GUILayout.EndHorizontal()
    # check clip
    GUILayout.BeginHorizontal()
    if len(sd.kfamAddInfo.clipTempNames):
        if GUILayout.Toggle(sd.kfamAddInfo.catelog == "clip", "Control another VNAnime clip"):
            sd.kfamAddInfo.catelog = "clip"
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(20)
            GUILayout.Label("<color=#00ff00>Select target clip:</color>")
            sd.kfamAddInfo.clipSelector.paint()
            sd.kfamAddInfo.id = sd.kfamAddInfo.clipSelector.curText
            GUILayout.FlexibleSpace()
    else:
        GUILayout.Toggle(False, "<color=#808080>No more VNAnime clip found</color>")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    # addition support for vnanime2
    if clip.version == 2:
        # check vntext
        if not hasattr(sd.kfamAddInfo, "vntextSelector"):
            from vntext import get_vntext_manager
            vntxt_mgr = get_vntext_manager(game)
            sd.kfamAddInfo.vntextIds = [vntxtid for vntxtid in vntxt_mgr.allTextIds if vntxtid not in clip.keyframes]
            sd.kfamAddInfo.vntextSelector = ComboListBox(Rect(220, 155, 120, 21), 60, sd.kfamAddInfo.vntextIds, 0)
        GUILayout.BeginHorizontal()
        if len(sd.kfamAddInfo.vntextIds) > 0:
            if GUILayout.Toggle(sd.kfamAddInfo.catelog == "vntext", "Control VNText object"):
                sd.kfamAddInfo.catelog = "vntext"
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUILayout.Space(20)
                GUILayout.Label("<color=#00ff00>Select target VNText:</color>")
                sd.kfamAddInfo.vntextSelector.paint()
                sd.kfamAddInfo.id = sd.kfamAddInfo.vntextSelector.curText
                GUILayout.FlexibleSpace()
        else:
            GUILayout.Toggle(False, "<color=#808080>No more VNText object found</color>")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # check vnsound
        if not hasattr(sd.kfamAddInfo, "vnsoundSelector"):
            from vnsound import get_vnsound_manager
            vnsnd_mgr = get_vnsound_manager(game)
            sd.kfamAddInfo.vnsoundIds = [vnsndid for vnsndid in vnsnd_mgr.soundAlias if vnsndid not in clip.keyframes]
            sd.kfamAddInfo.vnsoundSelector = ComboListBox(Rect(220, 177, 120, 21), -60, sd.kfamAddInfo.vnsoundIds, 0)
        GUILayout.BeginHorizontal()
        if len(sd.kfamAddInfo.vnsoundIds) > 0:
            if GUILayout.Toggle(sd.kfamAddInfo.catelog == "vnsound", "Control VNSound object"):
                sd.kfamAddInfo.catelog = "vnsound"
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUILayout.Space(20)
                GUILayout.Label("<color=#00ff00>Select target VNSound:</color>")
                sd.kfamAddInfo.vnsoundSelector.paint()
                sd.kfamAddInfo.id = sd.kfamAddInfo.vnsoundSelector.curText
                GUILayout.FlexibleSpace()
        else:
            GUILayout.Toggle(False, "<color=#808080>No more VNSound object found</color>")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # check vnphy
        if not hasattr(sd.kfamAddInfo, "vnphySelector"):
            from vnphysics import initialize
            vnphy_mgr = initialize(game)
            sd.kfamAddInfo.actionIds = [action.id for action in vnphy_mgr.actions if action.id not in clip.keyframes]
            sd.kfamAddInfo.triggerIds = [trg.id for trg in vnphy_mgr.triggers if trg.id not in clip.keyframes]
            sd.kfamAddInfo.phyIds = sd.kfamAddInfo.triggerIds + sd.kfamAddInfo.actionIds
            sd.kfamAddInfo.vnphySelector = ComboListBox(Rect(220, 199, 120, 21), -60, sd.kfamAddInfo.phyIds, 0)
        GUILayout.BeginHorizontal()
        if len(sd.kfamAddInfo.phyIds) > 0:
            if GUILayout.Toggle(sd.kfamAddInfo.catelog == "vnphy", "Fire VNPhysics trigger or action"):
                sd.kfamAddInfo.catelog = "vnphy"
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUILayout.Space(20)
                GUILayout.Label("<color=#00ff00>Select target trigger/action:</color>")
                sd.kfamAddInfo.vnphySelector.paint()
                sd.kfamAddInfo.id = sd.kfamAddInfo.vnphySelector.curText
                GUILayout.FlexibleSpace()
        else:
            GUILayout.Toggle(False, "<color=#808080>No more VNPhysics object found</color>")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()


    # control buttons
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 14
    btnwidth = game.windowRect.width / 2 - 15
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Add" if sd.kfamAddInfo.id else "<color=#808080>Add</color>", btnstyle, GUILayout.Width(btnwidth)) and sd.kfamAddInfo.id:
        checkAndAdd()
    if GUILayout.Button("Cancel", btnstyle, GUILayout.Width(btnwidth)):
        backToEditor()
    GUILayout.EndHorizontal()

def kfam_GUI_modify_clip_setting(sh):
    # memory shortcuts
    game = sh.game
    sd = game.scenedata
    gd = game.gdata
    clip = sd.kfamOnModifyClip
    mClips = gd.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
            # 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
    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
    def exportClip():
        def onExportClip(game, filename):
            if saveStringToFile(filename, clip.exportSetting(), append=False, backup=True):
                scriptHelperGUIMessage("All clip settings and keyframes were saved into <%s>."%filename, ("OK",))
        stPath = path.join(game.pygamepath, "animeclips/")
        startFileBrower(sh, startPath=stPath, extFilter=".txt", openSave="save", title="Export current clip to:", onOk=onExportClip, onCancel=None)
    def importClip():
        def onImportClip(game, filename):
            try:
                clipScript = loadScriptFromFile(filename)
                oldName = clip.name
                oldPriority = clip.priority
                oldAnimeType = clip.animeType
                clip.importSetting(clipScript)
                clip.name = oldName # do not change clip name when import
                if oldPriority != clip.priority:
                    kfa_resort(game, clip.name, clip.priority)
                    sd.kfamSelectedClipIndex = -1
                if sd.kfamTempType != clip.animeType:
                    kfa_retype(game, clip.name, clip.animeType)
                scriptHelperGUIMessage("Key frame anime clip loaded.")
            except Exception as e:
                scriptHelperGUIMessage("Fail to import setting: %s"%str(e), ("OK",))
        stPath = path.join(game.pygamepath, "animeclips/")
        startFileBrower(sh, startPath=stPath, extFilter=".txt", openSave="open", title="Select a clip file to import:", onOk=onImportClip, onCancel=None)

    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.wndWidth = "%d"%game.windowRect.width
        td.wndHeight = "%d"%game.windowRect.height
        td.tgtPanelWidth = "%d"%gd.kfaSetting.targetPanelWidth
        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()
    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("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()
    if GUILayout.Button("Export", GUILayout.Width(obwidth)):
        exportClip()
    GUILayout.Label("Save clip data to text file")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    if GUILayout.Button("Import", GUILayout.Width(obwidth)):
        importClip()
    GUILayout.Label("Load clip data from text file")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
 
    # global setting
    GUILayout.Label("<color=#00ff00>Global setting:</color>", blableStyle)
    GUILayout.BeginHorizontal() # Use legacy editor
    gd.kfaSetting.useLegacyEditor = GUILayout.Toggle(gd.kfaSetting.useLegacyEditor, "Use legacy editor UI")
    if gd.kfaSetting.useLegacyEditor:
        GUILayout.EndHorizontal()
        GUILayout.Space(10)
        GUILayout.Label("<color=#ff0000>WARNING: Use legacy editor mode until restart editor. If you want to switch back to legacy editor forever, edit vnanime.ini manually. Legacy mode may no longer supported in future, so, please get used to new editor. XD</color>")
        GUILayout.BeginHorizontal()
    GUILayout.EndHorizontal()
    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 (Mouse click though clip UI)")
    if gd.kfaSetting.videoExportMode != veMode:
        gd.kfaSetting.videoExportMode = veMode
        game.skin.eatMouseInput = not veMode
    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 size = %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()
    
    # end for body
    GUILayout.EndScrollView()

    # Control button
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
        if gd.kfaSetting.useLegacyEditor:
            sd.kfamModifyTabIndex = -1
        if td.reloadRequest:
            sd.kfamReloadRequest = True
        del sd.kfamSettingTempData
        changeGuiScreen(sh, kfam_GUI_modify_clip)
    GUILayout.EndHorizontal()

def kfam_GUI_parts_seekbar(sh, frameUpdateCallback, jumpToKeyframeCallback):
    game = sh.game
    sd = game.scenedata
    clip = sd.kfamOnModifyClip
    fullw = game.windowRect.width - 20
    curEnent = Event.current

    def frameUpdate(nf):
        clip.seek(nf)
        if frameUpdateCallback:
            frameUpdateCallback(int(clip.curFrame))

    minimized = sd.kfaGuiFunction == kfam_GUI_modify_clip_min
    if not hasattr(sd, "kfamSeekbarMode"):
        sd.kfamSeekbarMode = "normal"

    GUILayout.BeginHorizontal()
    newframe = GUILayout.HorizontalSlider(clip.curFrame, 0, clip.frameLength, GUILayout.Width(fullw))
    if not clip.isPlaying and int(newframe) != int(clip.curFrame):
        frameUpdate(int(newframe))
    GUILayout.EndHorizontal()
    if clip.isPlaying:
        # playing mode
        GUILayout.BeginHorizontal()
        if GUILayout.Button("<color=#ff0000>" + u'\u25a0' + "</color>", GUILayout.Width(25)):
            clip.pause()
        GUILayout.FlexibleSpace()
        # time
        GUILayout.Label("frame: %d/%d (%.2fs/%.2fs)"%(clip.curFrame, clip.frameLength, clip.curTime, clip.timeLength))
        GUILayout.FlexibleSpace()
        if GUILayout.Button("+" if minimized else "-", GUILayout.Width(25)):
            if minimized:
                changeGuiScreen(sh, kfam_GUI_modify_clip)
            else:
                changeGuiScreen(sh, kfam_GUI_modify_clip_min)
        GUILayout.EndHorizontal()
    else:
        # editing mode
        GUILayout.BeginHorizontal()
        if GUILayout.Button("<color=#00ff00>" + u'\u25b6' + "</color>", GUILayout.Width(25)):
            clip.play()
        if GUILayout.Button("|<" if sd.kfamCustomStartFrame == -1 else "<color=#00ffff>|<</color>", GUILayout.Width(25)):
            if curEnent.button == 0:
                frameUpdate(0 if sd.kfamCustomStartFrame == -1 else sd.kfamCustomStartFrame)
            elif curEnent.button == 1:
                sd.kfamCustomStartFrame = int(clip.curFrame)
            elif curEnent.button == 2:
                sd.kfamCustomStartFrame = -1
        if GUILayout.Button(".<" if jumpToKeyframeCallback else "<<", GUILayout.Width(25)):
            if jumpToKeyframeCallback:
                jf = jumpToKeyframeCallback(int(clip.curFrame), -1)
                if jf != None:
                    frameUpdate(jf)
            else:
                newframe = 0 if clip.curFrame < clip.frameRate else clip.curFrame - clip.frameRate
                frameUpdate(newframe)
        if GUILayout.Button("<", GUILayout.Width(25)):
            newframe = 0 if clip.curFrame < 1 else clip.curFrame - 1
            frameUpdate(newframe)
        if sd.kfamSeekbarMode == "normal":
            if GUILayout.Button("%d/%d (%.2fs/%.2fs)"%(clip.curFrame, clip.frameLength, clip.curTime, clip.timeLength)):
                if curEnent.button == 0:
                    sd.kfamSeekbarMode = "goto"
                    sd.seekbarInput = str(int(clip.curFrame))
                elif curEnent.button == 1:
                    frameUpdate(clip.curFrame)
        elif sd.kfamSeekbarMode == "goto":
            isEnterKey = curEnent.type == EventType.KeyDown and (curEnent.keyCode == KeyCode.Return or curEnent.keyCode == KeyCode.KeypadEnter)
            GUILayout.FlexibleSpace()
            GUILayout.Label("Jump to:", GUILayout.Width(55))
            sd.seekbarInput = GUILayout.TextField(sd.seekbarInput, GUILayout.Width(60))
            if isEnterKey or GUILayout.Button("Goto", GUILayout.Width(50)):
                try:
                    if sd.seekbarInput.endswith("s"):
                        sectxt = sd.seekbarInput[:-1]
                        secval = float(sectxt)
                        frmval = int(secval * clip.frameRate)
                    elif 0 < sd.seekbarInput.count(":") <= 2:
                        hms = [float(v) if len(v) > 0 else 0 for v in sd.seekbarInput.split(":")]
                        if len(hms) == 3:
                            secval = hms[0] * 3600 + hms[1] * 60 + hms[2]
                        else:
                            secval = hms[0] * 60 + hms[1]
                        frmval = int(secval * clip.frameRate)
                    else:
                        frmval = int(sd.seekbarInput)
                    frameUpdate(frmval)
                    sd.kfamSeekbarMode = "normal"
                except:
                    pass
            if GUILayout.Button("Back", GUILayout.Width(50)):
                sd.kfamSeekbarMode = "normal"
            GUILayout.FlexibleSpace()
        else:
            sd.kfamSeekbarMode = "normal"
        if GUILayout.Button(">", GUILayout.Width(25)):
            newframe = clip.frameLength if clip.curFrame + 1 >= clip.frameLength else clip.curFrame + 1
            frameUpdate(newframe)
        if GUILayout.Button(">." if jumpToKeyframeCallback else ">>", GUILayout.Width(25)):
            if jumpToKeyframeCallback:
                jf = jumpToKeyframeCallback(int(clip.curFrame), +1)
                if jf != None:
                    frameUpdate(jf)
            else:
                newframe = clip.frameLength if clip.curFrame + clip.frameRate >= clip.frameLength else clip.curFrame +  clip.frameRate
                frameUpdate(newframe)
        if GUILayout.Button(">|" if sd.kfamCustomEndFrame == -1 else "<color=#00ffff>>|</color>", GUILayout.Width(25)):
            if curEnent.button == 0:
                frameUpdate(clip.frameLength if sd.kfamCustomEndFrame == -1 else sd.kfamCustomEndFrame)
            elif curEnent.button == 1:
                sd.kfamCustomEndFrame = int(clip.curFrame)
            elif curEnent.button == 2:
                sd.kfamCustomEndFrame = -1
        if GUILayout.Button("+" if minimized else "-", GUILayout.Width(25)):
            if minimized:
                changeGuiScreen(sh, kfam_GUI_modify_clip)
            else:
                changeGuiScreen(sh, kfam_GUI_modify_clip_min)
        GUILayout.EndHorizontal()

def kfam_GUI_curve_subwindow(baseController, windowNo):
    from vnframe import _sh
    game = _sh.game
    sd = game.scenedata
    wndRect = game.get_extra_window(windowNo).windowRect


    # value area
    if sd.kfamOnModifyClip.version == 2:
        btnStyle = GUIStyle("button")
        btnStyle.fontSize = 12
        # limit selection
        if sd.kfamValueNameOnEditor == None:
            sd.kfamCurveValueMode = "curve"
        if len(sd.kfamCurveEditor.curveDataSet) == 0:
            sd.kfamCurveValueMode = "value"
        # curve value selection buttons
        oldColor = GUI.color
        GUILayout.BeginHorizontal()
        GUI.color = Color.gray if len(sd.kfamCurveEditor.curveDataSet) == 0 else (Color.green if sd.kfamCurveValueMode == "curve" else oldColor)
        if GUILayout.Button("Curve", btnStyle) and len(sd.kfamCurveEditor.curveDataSet) > 0:
            sd.kfamCurveValueMode = "curve"
        GUI.color = Color.gray if sd.kfamValueNameOnEditor == None else (Color.green if sd.kfamCurveValueMode == "value" else oldColor)
        if GUILayout.Button("Value", btnStyle) and sd.kfamValueNameOnEditor != None:
            sd.kfamCurveValueMode = "value"
        GUILayout.EndHorizontal()
        GUI.color = oldColor
        # draw curve or value
        if sd.kfamCurveValueMode == "curve":
            sd.kfamCurveEditor.paint()
        else:
            #GUILayout.Label(sd.kfamValueNameOnEditor)
            #GUILayout.Label("<color=#00ffff>" + "data type" + "</color>")
            sd.kfamValueInputTemp = GUILayout.TextArea(sd.kfamValueInputTemp, GUILayout.Height(wndRect.height - 72))
            GUI.color = Color.gray if sd.kfamValueInputTemp == sd.kfamValueOrginal else oldColor
            if GUILayout.Button("Update", btnStyle) and sd.kfamValueInputTemp != sd.kfamValueOrginal:
                from vnframe import string2script
                try:
                    refValue = sd.kfamValueKeyframesOnEditor[0].value
                    newValue = string2script(sd.kfamValueInputTemp, refValue, True)
                    sd.kfamValueOrginal = sd.kfamValueInputTemp
                    for kf in sd.kfamValueKeyframesOnEditor:
                        kf.value = newValue
                except Exception as e:
                    sd.kfamValueInputTemp = sd.kfamValueOrginal
                    message = "Cannot update value: " + str(e)
                    game.logger_log_message(message, "[VNAnime] ")
            GUI.color = oldColor
    else:
        # always draw curve
        sd.kfamCurveEditor.paint()

    # close btn
    cbRect = Rect(wndRect.width - 16, 3, 13, 13)
    oldColor = GUI.color
    GUI.color = Color.red
    if GUI.Button(cbRect, ""):
        game.close_extra_window(windowNo)
    GUI.color = oldColor

def kfam_GUI_subclip_subwindow(baseController, windowNo):
    from vnframe import _sh
    game = _sh.game
    sd = game.scenedata
    scs = sd.kfamSubClipStatus
    nWidth = 50

    def checkModify():
        return (scs.subClipStatus["play"] == 1) != scs.tempPlaying or \
            scs.tempFrame != scs.subClipStatus["frame"] or \
            scs.tempFrameStr != "%d"%scs.subClipStatus["frame"] or \
            scs.tempLoopStr != "%d"%scs.subClipStatus["loop"] or \
            scs.tempSpeedStr != "%.2f"%scs.subClipStatus["speed"]
    def updateStatus():
        cp = ""
        try:
            cp = "frame"
            nFrame = int(scs.tempFrameStr)
            cp = "loop"
            nLoop = int(scs.tempLoopStr)
            cp = "speed"
            nSpeed = float(scs.tempSpeedStr)
            cp = "status"
            scs.subClipStatus["play"] = 1 if scs.tempPlaying else 2
            scs.subClipStatus["frame"] = scs.tempFrame = nFrame
            scs.subClipStatus["loop"] = nLoop
            scs.subClipStatus["speed"] = nSpeed
            scs.errorMessage = None
            revertStatus()
        except:
            scs.errorMessage = "Invalid input for " + cp
    def revertStatus():
        scs.tempPlaying = scs.subClipStatus["play"] == 1
        scs.tempFrame = scs.subClipStatus["frame"]
        scs.tempFrameStr = "%d"%scs.tempFrame 
        scs.tempLoopStr = "%d"%scs.subClipStatus["loop"]
        scs.tempSpeedStr = "%.2f"%scs.subClipStatus["speed"]
        scs.errorMessage = None
    def getStatus():
        scs.subClipStatus = scs.targetClip.export_full_status()
        scs.subClipStatus["frame"] = scs.targetClip.curFrame
        revertStatus()
    def setStatus():
        updateStatus()
        if scs.errorMessage == None:
            scs.targetClip.import_status(scs.subClipStatus)

    # body 
    GUILayout.BeginHorizontal()
    if scs.errorMessage:
        # error
        GUILayout.Label("<color=#ff0000>" + scs.errorMessage + "</color>")
        GUILayout.FlexibleSpace()
        if GUILayout.Button("OK", GUILayout.Width(50)):
            revertStatus()
    else:
        # status
        if scs.tempPlaying:
            #if GUILayout.Button("<color=#ff0000>" + u'\u25a0' + "</color>", GUILayout.Width(25)):
            if GUILayout.Button("<color=#00ff00>Play</color>", GUILayout.Width(50)):
                scs.tempPlaying = False
        else:
            #if GUILayout.Button("<color=#00ff00>" + u'\u25b6' + "</color>", GUILayout.Width(25)):
            if GUILayout.Button("<color=#ff0000>Stop</color>", GUILayout.Width(50)):
                scs.tempPlaying = True
        if GUILayout.Button("|<", GUILayout.Width(25)):
            scs.tempFrame = 0
            scs.tempFrameStr = "%d"%scs.tempFrame
        if GUILayout.Button("<", GUILayout.Width(25)):
            scs.tempFrame = scs.tempFrame - 1 if scs.tempFrame > 0 else 0
            scs.tempFrameStr = "%d"%scs.tempFrame
        scs.tempFrameStr = GUILayout.TextField(scs.tempFrameStr, GUILayout.Width(40))
        if GUILayout.Button(">", GUILayout.Width(25)):
            scs.tempFrame = scs.tempFrame + 1 if scs.tempFrame < scs.targetClip.frameLength else scs.targetClip.frameLength
            scs.tempFrameStr = "%d"%scs.tempFrame
        if GUILayout.Button(">|", GUILayout.Width(25)):
            scs.tempFrame = scs.targetClip.frameLength
            scs.tempFrameStr = "%d"%scs.tempFrame
        GUILayout.Space(5)
        GUILayout.Label("Loop:")
        scs.tempLoopStr = GUILayout.TextField(scs.tempLoopStr, GUILayout.Width(40))
        GUILayout.Space(5)
        GUILayout.Label("Speed:")
        scs.tempSpeedStr = GUILayout.TextField(scs.tempSpeedStr, GUILayout.Width(40))
        #isModified = checkModify()
        #if GUILayout.Button("<color=#ff0000>Update</color>" if isModified else "<color=#808080>Update</color>") and isModified:
        #    updateStatus()
        if GUILayout.Button("<color=#00ff00>Get</color>"):
            getStatus()
        if GUILayout.Button("<color=#ff0000>Set</color>"):
            setStatus()
    GUILayout.EndHorizontal()

    # close btn
    wndRect = game.get_extra_window(windowNo).windowRect
    cbRect = Rect(wndRect.width - 16, 3, 13, 13)
    oldColor = GUI.color
    GUI.color = Color.red
    if GUI.Button(cbRect, ""):
        game.close_extra_window(windowNo)
    GUI.color = oldColor


# PNG internal data save/load (orig by Keitaro)
def saveClipsToString(game):
    sClips = game.gdata.kfaSortedClips
    # build output for all managered clips
    if len(sClips) > 0:
        output = ""
        for clip in sClips:
            if clip.version != 1:
                continue    # this method only for v1 now
            output += clip.exportSettingForScene()
            output += ",\n"
        output = "["+output+"]"
    else:
        output = "[]"

    return output

def loadStringToClips(game,string):
    try:
        import ast
        ar = ast.literal_eval(string)
        # this is more secure construction, then eval. It doesn't run Python code, only eval literal

        # now postprocessing

        for clip in ar:
            kfa_import(game,clip)
        return True
    except Exception as e:
        print("can't parse Kfa string or import")
        return False

def scene_saved(game):

    print("KFA1&2: scene_saving...")
    try:
        if check_keyframe_anime(game):
            str = saveClipsToString(game)
            game.dataPNG_save("vnge_kfa",str,save_png_version)

            from vnanime2 import clipExportToString2
            str2 = clipExportToString2(game)
            game.dataPNG_save("vnge_kfa2",str2,save_png_version2)
        else:
            game.dataPNG_save("vnge_kfa","[]",save_png_version)
            game.dataPNG_save("vnge_kfa2","[]",save_png_version2)
        print("KFA1&2: scene_saved!...")
    except Exception as e:
        print("can't save kfA data in PNG")