#vngame;all;Utilities/MMD Director v2.0
# this util help your girl dance 
# by Countd360 
from UnityEngine import Quaternion, Vector3, Vector2, Color, Rect, GameObject, Transform
from UnityEngine import GUI, GUILayout, GUIStyle, GUIContent, GUIUtility, TextAnchor
from UnityEngine import Time, Screen, Input, KeyCode, Event, EventType
from System import String, Array, Action, GC
from vngameengine import HSNeoOCI, HSNeoOCIItem, HSNeoOCIChar, HSNeoOCIFolder, GData
from vngameengine import importOrReload, parseKeyCode
from vnframe import script2string
from vnanime import init_keyframe_anime, check_keyframe_anime, kfa_register, kfa_pauseAll, kfa_seek, KeyFrameClip
from vnactor import set_camera_data, get_camera_data
from vntext import init_vntext_manager, check_vnText 
import struct
import math
import copy
import traceback
import time
import os
import unity_util
import codecs

_mmdd = None

def start(game):
    print "\n\nStarting MMD Director..."
    importOrReload("mmddplaylist")
    importOrReload("vnactor")
    importOrReload("vnanime")
    importOrReload("vnframe")
    importOrReload("vntext")
    importOrReload("extplugins")
    importOrReload("vmdlib")
    importOrReload("libkfguictrl")

    # engine check
    if not game.isNEOV2 and not game.isCharaStudio:
        game.show_blocking_message_time(LC("MMD Director works on KK CharaStudio, AI and HS2 StudioNeoV2. This game engine is not supported, sorry."))
        return

    # dependence check
    from extplugins import ExtPlugin
    if ExtPlugin.exists("AIAPI"):
        api = ExtPlugin("AIAPI")
    elif ExtPlugin.exists("HS2API"):
        api = ExtPlugin("HS2API")
    elif ExtPlugin.exists("KKAPI"):
        api = ExtPlugin("KKAPI")
    else:
        api = None
    if not api:
        game.show_blocking_message_time(LC("MMD Director needs KKAPI/AIAPI/HS2API.dll"))
        return

    # init vnframe
    game.scenef_register_actorsprops()

    # init keyframe clips
    if not check_keyframe_anime(game):
        init_keyframe_anime(game)

    # init vntext if not
    if not check_vnText(game):
        init_vntext_manager(game)

    # build main object
    global _mmdd
    if _mmdd == None:
        _mmdd = MMDDirector(game)
    game.gdata.mmdd = _mmdd
    _mmdd.onStartFunc = onMmddStart
    _mmdd.onStopFunc = onMmddStop
    if not _mmdd.charaAnimePlugin:
        game.show_blocking_message_time(LC("CharaAnime.dll is not loaded correctly! Check your plugins please."))
        _mmdd = None
        del game.gdata.mmdd
        return

    # load config
    initConfig(game)
    loadConfig(game)
    solveConfig(game)

    # check and load setting
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    missFiles = checkSaveData()
    altarget = checkAutoLoadTarget()
    game.scenedata.startupAutoLoadError = None
    game.scenedata.startupAutoLoadSuspend = False
    game.scenedata.startupMissFiles = missFiles
    if altarget and mmddConfig.autoLoad:
        saveMissFiles = [fn for fn in missFiles.keys() if altarget in missFiles[fn][0]]
        if len(saveMissFiles) == 0:
            alerr = mmdd.loadFromScene(altarget)
            if alerr:
                print "MMDD: Fail to load %s data!"%altarget, alerr
                game.scenedata.startupAutoLoadError = alerr
                mmdd.cleanup()
            else:
                print "MMDD: %s data loaded!"%altarget
        else:
            game.scenedata.startupAutoLoadSuspend = True

    # extend memory heap
    if mmddConfig.gcOptimize == "bigheap" and mmddConfig.gcBigHeapSize_solved != None:
        toextend = mmddConfig.gcBigHeapSize_solved - GetWorkingSetMemorySize()
        extstart = time.clock()
        dummyHeap = {}
        i = 0
        class dummyGarbage():
            def __init__(self):
                self.data = [i for i in range(5000000)]
        while GetWorkingSetMemorySize() < mmddConfig.gcBigHeapSize_solved:
            dummyHeap[i] = dummyGarbage()
            i += 1
        print "build %d elm to extend %.1fMb, used %.3f sec"%(len(dummyHeap), toextend / 1024 / 1024, time.clock() - extstart)
        del dummyHeap
        DoGarbageCollect()

    # install event hook
    game.event_unreg_listener("update", hook_update, "mmdd_update")
    game.event_reg_listener("update", hook_update, "mmdd_update")
    game.event_unreg_listener("scene_loaded_after", hook_loaded, "mmdd_sceneload")
    game.event_reg_listener("scene_loaded_after", hook_loaded, "mmdd_sceneload")
    game.event_unreg_listener("scene_saved", hook_saved, "mmdd_scenesave")
    game.event_reg_listener("scene_saved", hook_saved, "mmdd_scenesave")

    # prepare ui and runtime
    game.scenedata.assetViewMode = "asset"
    game.scenedata.mainScrollPos = Vector2.zero
    game.scenedata.autoSaveTimer = time.clock()
    game.scenedata.autoVerifyTimer = time.clock()
    game.scenedata.mmddSyncClipCtrl = None
    game.scenedata.activedKeyframeView = None
    game.scenedata.playlistNextCallback = None

    # start UI
    from skin_customwindow import SkinCustomWindow
    skin = SkinCustomWindow()
    skin.funcSetup = mmddSkinSetup
    skin.funcWindowGUI = mmddSkinWindowGUI
    game.skin_set(skin)

def quit(game):
    # unhook
    game.event_unreg_listener("update", hook_update, "mmdd_update")
    game.event_unreg_listener("scene_loaded_after", hook_loaded, "mmdd_sceneload")
    # save config
    saveConfig(game)
    game.windowDraggable = True
    # del object
    global _mmdd
    _mmdd = None
    del game.gdata.mmdd
    # quit
    game.clear_timers()
    game.init_start_params()
    game.return_to_start_screen()

def CheckGarbageCollector():
    try:
        from UnityEngine.Scripting import GarbageCollector
        return GarbageCollector.GCMode == GarbageCollector.Mode.Enabled
    except:
        return None

def EnableGarbageCollector(enable):
    try:
        from UnityEngine.Scripting import GarbageCollector
        if enable:
            GarbageCollector.GCMode = GarbageCollector.Mode.Enabled
        else:
            GarbageCollector.GCMode = GarbageCollector.Mode.Disabled
    except:
        traceback.print_exc()

def DoGarbageCollect():
    GC.Collect(GC.MaxGeneration)    # add a parameter to avoid hook by ResourceUnloadOptimizations

def GetGarbageSize():
    return GC.GetTotalMemory(False)

def GetWorkingSetMemorySize():
    from CharaAnime import MemoryInfo
    q = MemoryInfo.QueryProcessMemStatus()
    return q.WorkingSetSize

def GetAvailPhyMemorySize():
    from CharaAnime import MemoryInfo
    q = MemoryInfo.QuerySystemMemStatus()
    return q.ullAvailPhys

def LC(txt):
    return txt

def initConfig(game):
    # build config data set in gdata
    mmddConfig = GData()
    game.gdata.mmdd_config = mmddConfig
    game.gdata.garbage_collector_valid = CheckGarbageCollector() != None

    # general setting
    mmddConfig.vmdHomeDir = ""
    mmddConfig.autoLoad = True
    mmddConfig.autoSave = True
    mmddConfig.autoBackup = True
    mmddConfig.autoBackupInterval = 600
    mmddConfig.boneNameDisplay = "dummy"
    mmddConfig.boneListMode = "group"
    mmddConfig.hideBone = {"non-opt": True, "no-frm": True, "1-frm": True, "2-frm": True, "finger": True}
    mmddConfig.morphNameDisplay = "dummy"
    mmddConfig.hideMorph = {"no-frm": True, "1-frm": True, "2-frm": True}
    mmddConfig.gcOptimize = "none"
    mmddConfig.gcBigHeapSize = "50%"
    mmddConfig.gcSuspendLimit = "inf"
    mmddConfig.scanRelatives = True

    # playlist setting
    mmddConfig.defPlaylist = ""
    mmddConfig.playlistLoop = 0
    mmddConfig.dancersLoop = 0
    mmddConfig.hideNonActiveDancer = True
    mmddConfig.extendMultiDancer = True
    mmddConfig.extendMultiLimit = 0
    mmddConfig.remainDancerPosY = True
    mmddConfig.disableCamera = False
    mmddConfig.autoSwitchCamera = True
    mmddConfig.playlistSaveRelPath = True

    # gui setting
    mmddConfig.guiX = -1
    mmddConfig.guiY = -1
    mmddConfig.guiWidth = 600
    mmddConfig.guiHeight = 320
    mmddConfig.guiLock = False
    mmddConfig.guiLanguage = ""
    mmddConfig.guiOnPlaying = "normal"

    # shortcut setting
    mmdd = game.gdata.mmdd
    def nextDanceFunc():
        if game.scenedata.playlistNextCallback:
            game.scenedata.playlistNextCallback(game)
    mmddConfig.shortcuts = {
        "Start/Stop": [mmdd.startStop, None, False],
        "Next Frame": [mmdd.gotoNextFrame, None, False],
        "Prev Frame": [mmdd.gotoPrevFrame, None, False],
        "Next Second": [mmdd.gotoNextSecond, None, False],
        "Prev Second": [mmdd.gotoPrevSecond, None, False],
        "Goto Start": [mmdd.gotoStartFrame, None, False],
        "Goto End": [mmdd.gotoEndFrame, None, False],
        "Go Forward": [mmdd.gotoNextFrame, None, True],
        "Go Backward": [mmdd.gotoPrevFrame, None, True],
        "Next Dance": [nextDanceFunc, None, False],
    }
    game.gdata.mmddSuspendShortcut = False

    # default camera multi target setting
    mmddConfig.switchMinFramePeriod = 60
    mmddConfig.switchMaxFramePeriod = 600
    mmddConfig.switchProbability = 50

    # drag intensity
    mmddConfig.dragIntensityPosX = 0.1 if game.isNEOV2 else 0.01
    mmddConfig.dragIntensityPosY = 0.01 if game.isNEOV2 else 0.001
    mmddConfig.dragIntensityRotX = 1
    mmddConfig.dragIntensityRotY = 0.1
    mmddConfig.dragIntensitySclX = 0.01
    mmddConfig.dragIntensitySclY = 0.001

    # keyframe color setting
    from libkfguictrl import KeyframeViewer, unity_adjust_color
    mmddConfig.keyframeColors = []
    for key in sorted(KeyframeViewer.DEFAULT_COLOR.keys()):
        color = KeyframeViewer.DEFAULT_COLOR[key]
        if not game.isNEOV2:
            color = unity_adjust_color(color)
        mmddConfig.keyframeColors.append([key, color])

    # curve color setting
    from libkfguictrl import CurveEditor
    mmddConfig.curveColors = []
    for key in sorted(CurveEditor.DEFAULT_COLOR.keys()):
        color = CurveEditor.DEFAULT_COLOR[key]
        if not game.isNEOV2:
            color = unity_adjust_color(color)
        mmddConfig.curveColors.append([key, color])

    # curve preset setting
    mmddConfig.curvePresets = (
        ("Linear", (0.2, 0.2, 0.8, 0.8)),
        ("F-S", (0.1, 0.4, 0.6, 0.9)),
        ("S-F", (0.4, 0.1, 0.9, 0.6)),
        ("F-S2", (0.0, 0.5, 0.5, 1.0)),
        ("S-F2", (0.5, 0.0, 1.0, 0.5)),
        ("F-S-F", (0.0, 0.4, 1.0, 0.6)),
        ("S-F-S", (0.4, 0.0, 0.6, 1.0)),
        ("F-S-F2", (0.0, 0.6, 1.0, 0.6)),
        ("S-F-S2", (0.6, 0.0, 0.4, 1.0)),
    )

def loadConfig(game):
    import ConfigParser, sys, os.path
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config

    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 == "vmdhomedir":
                    mmddConfig.vmdHomeDir = v.decode("utf8")
                if k == "autoload":
                    mmddConfig.autoLoad = bool(eval(v))
                if k == "autosave":
                    mmddConfig.autoSave = bool(eval(v))
                if k == "autobackup":
                    mmddConfig.autoBackup = bool(eval(v))
                if k == "autobackupInterval":
                    mmddConfig.autoBackupInterval = int(v)
                if k == "bonenamedisplay":
                    mmddConfig.boneNameDisplay = v
                if k == "bonelistmode":
                    mmddConfig.boneListMode = v
                if k == "hidebone":
                    mmddConfig.hideBone = eval(v)
                if k == "morphnamedisplay":
                    mmddConfig.morphNameDisplay = v
                if k == "hidemorph":
                    mmddConfig.hideMorph = eval(v)
                if k == "gcoptimize":
                    mmddConfig.gcOptimize = v
                if k == "gcbigheapsize":
                    mmddConfig.gcBigHeapSize = v
                if k == "gcsuspendlimit":
                    mmddConfig.gcSuspendLimit = v
                if k == "scanrelatives":
                    mmddConfig.scanRelatives = bool(eval(v))
        if "Playlist" in config.sections():
            for k, v in config.items("Playlist"):
                if k == "defaultlist":
                    mmddConfig.defPlaylist = v.decode("utf8")
                if k == "playlistloop":
                    mmddConfig.playlistLoop = int(v)
                if k == "dancersloop":
                    mmddConfig.dancersLoop = int(v)
                if k == "hidenonactivedancer":
                    mmddConfig.hideNonActiveDancer = bool(eval(v))
                if k == "extendmultidancer":
                    mmddConfig.extendMultiDancer = bool(eval(v))
                if k == "extendmultilimit":
                    mmddConfig.extendMultiLimit = int(v)
                if k == "remaindancerposy":
                    mmddConfig.remainDancerPosY = bool(eval(v))
                if k == "disablecamera":
                    mmddConfig.disableCamera = bool(eval(v))
                if k == "autoswitchcamera":
                    mmddConfig.autoSwitchCamera = bool(eval(v))
                if k == "playlistsaverelpath":
                    mmddConfig.playlistSaveRelPath = bool(eval(v))
        if "GUI" in config.sections():
            for k, v in config.items("GUI"):
                if k == "guix":
                    mmddConfig.guiX = int(v)
                if k == "guiy":
                    mmddConfig.guiY = int(v)
                if k == "guiwidth":
                    mmddConfig.guiWidth = int(v)
                if k == "guiheight":
                    mmddConfig.guiHeight = int(v)
                if k == "guilock":
                    mmddConfig.guiLock = bool(eval(v))
                if k == "guilanguage":
                    mmddConfig.guiLanguage = v
                if k == "guionplaying":
                    mmddConfig.guiOnPlaying = v
        if "Shortcuts" in config.sections():
            for k, v in config.items("Shortcuts"):
                for ktitle in mmddConfig.shortcuts.keys():
                    if k == ktitle.lower():
                        mmddConfig.shortcuts[ktitle][1] = parseKeyCode(v)
                        break
        if "DefaultCameraMultiTarget" in config.sections():
            for k, v in config.items("DefaultCameraMultiTarget"):
                if k == "switchminframeperiod":
                    mmddConfig.switchMinFramePeriod = int(v)
                if k == "switchmaxframeperiod":
                    mmddConfig.switchMaxFramePeriod = int(v)
                if k == "switchprobability":
                    mmddConfig.switchProbability = int(v)
        if "DragInputIntensity" in config.sections():
            for k, v in config.items("DragInputIntensity"):
                if k == "positionintensityx":
                    mmddConfig.dragIntensityPosX = float(v)
                if k == "positionintensityy":
                    mmddConfig.dragIntensityPosY = float(v)
                if k == "rotationintensityx":
                    mmddConfig.dragIntensityRotX = float(v)
                if k == "rotationintensityy":
                    mmddConfig.dragIntensityRotY = float(v)
                if k == "scaleintensityx":
                    mmddConfig.dragIntensitySclX = float(v)
                if k == "scaleintensityy":
                    mmddConfig.dragIntensitySclY = float(v)
        if "KeyframeViewColor" in config.sections():
            for k, v in config.items("KeyframeViewColor"):
                for kcPair in mmddConfig.keyframeColors:
                    if k == kcPair[0].lower():
                        r, g, b, a = v.split(",")
                        kcPair[1] = Color(float(r), float(g), float(b), float(a))
                        break
        if "CurveEditorColor" in config.sections():
            for k, v in config.items("CurveEditorColor"):
                for kcPair in mmddConfig.curveColors:
                    if k == kcPair[0].lower():
                        r, g, b, a = v.split(",")
                        kcPair[1] = Color(float(r), float(g), float(b), float(a))
                        break
        if "CurvePreset" in config.sections():
            curvePresets = []
            for k, v in config.items("CurvePreset"):
                try:
                    cSeg = v.split(",")
                    cp = (cSeg[0], (float(cSeg[1]), float(cSeg[2]), float(cSeg[3]), float(cSeg[4])))
                    curvePresets.append(cp)
                except:
                    pass
            if len(curvePresets):
                from libkfguictrl import CurveEditor
                mmddConfig.curvePresets = tuple(curvePresets)
                CurveEditor.CURVE_PRESET_DATA = mmddConfig.curvePresets
    except Exception, e:
        print "Fail to load config from ini file: " + str(e)

def solveConfig(game):
    mmddConfig = game.gdata.mmdd_config
    def solveMemoryValue(inputtxt, percentTotal):
        inputtxt = inputtxt.lower()
        try:
            if inputtxt.endswith("%") and percentTotal:
                ptxt = inputtxt[:-1]
                pvalue = float(ptxt)
                return pvalue * percentTotal / 100.0
            elif inputtxt.endswith("m"):
                mtxt = inputtxt[:-1]
                mvalue = float(mtxt)
                return mvalue * 1024 * 1024
            elif inputtxt.endswith("g"):
                gtxt = inputtxt[:-1]
                gvalue = float(gtxt)
                return gvalue * 1024 * 1024 * 1024
            else:
                return float(inputtxt)
        except:
            return None
    # gc optimize mode
    if mmddConfig.gcOptimize == "suspend" and not game.gdata.garbage_collector_valid:
        print "!! Config Invalid: GC suspend not supported, change to none"
        mmddConfig.gcOptimize = "none"
    # gc optimize setting
    mmddConfig.gcBigHeapSize_solved = solveMemoryValue(mmddConfig.gcBigHeapSize, GetWorkingSetMemorySize() + GetAvailPhyMemorySize())
    if mmddConfig.gcBigHeapSize_solved == None:
        print "!! Config Error: fail to solve GcBigHeapSize [%s]"%mmddConfig.gcBigHeapSize
    else:
        print "MMDD Config Solved: gcBigHeapSize = %.0fM (%s)"%(mmddConfig.gcBigHeapSize_solved/1024/1024, str(mmddConfig.gcBigHeapSize_solved))
        pass
    mmddConfig.gcSuspendLimit_solved = solveMemoryValue(mmddConfig.gcSuspendLimit, None)
    if mmddConfig.gcSuspendLimit_solved == None:
        print "!! Config Error: fail to solve GcSuspendLimit [%s]"%mmddConfig.gcSuspendLimit
    else:
        print "MMDD Config Solved: gcSuspendLimit = %.0fM (%s)"%(mmddConfig.gcSuspendLimit_solved/1024/1024, str(mmddConfig.gcSuspendLimit_solved))
        pass
    # Done

def saveConfig(game):
    mmddConfig = game.gdata.mmdd_config

    # update gui position
    mmddConfig.guiX = int(game.windowRect.x)
    mmddConfig.guiY = int(game.windowRect.y)

    # build contents
    content = "; MMDDirector setting\n"
    # general
    content += "[General]\n"
    content += "VmdHomeDir" + " = " + mmddConfig.vmdHomeDir + "\n"
    content += "AutoLoad" + " = " + str(mmddConfig.autoLoad) + "\n"
    content += "AutoSave" + " = " + str(mmddConfig.autoSave) + "\n"
    content += "AutoBackup" + " = " + str(mmddConfig.autoBackup) + "\n"
    content += "AutoBackupInterval" + " = " + str(mmddConfig.autoBackupInterval) + "\n"
    content += "BoneNameDisplay" + " = " + mmddConfig.boneNameDisplay + "\n"
    content += "BoneListMode" + " = " + mmddConfig.boneListMode + "\n"
    content += "HideBone" + " = " + script2string(mmddConfig.hideBone) + "\n"
    content += "MorphNameDisplay" + " = " + mmddConfig.morphNameDisplay + "\n"
    content += "HideMorph" + "=" + script2string(mmddConfig.hideMorph) + "\n"
    content += "GcOptimize" + " = " + mmddConfig.gcOptimize + "\n"
    content += "GcBigHeapSize" + " = " + mmddConfig.gcBigHeapSize + ("%" if mmddConfig.gcBigHeapSize.endswith("%") else "") + "\n"
    content += "GcSuspendLimit" + " = " + mmddConfig.gcSuspendLimit + "\n"
    content += "ScanRelatives" + " = " + str(mmddConfig.scanRelatives) + "\n"
    content += "\n"
    # playlist
    content += "[Playlist]\n"
    content += "DefaultList" + " = " + mmddConfig.defPlaylist + "\n"
    content += "PlaylistLoop" + " = " + str(mmddConfig.playlistLoop) + "\n"
    content += "DancersLoop" + " = " + str(mmddConfig.dancersLoop) + "\n"
    content += "HideNonActiveDancer" + " = " + str(mmddConfig.hideNonActiveDancer) + "\n"
    content += "ExtendMultiDancer" + " = " + str(mmddConfig.extendMultiDancer) + "\n"
    content += "ExtendMultiLimit" + " = " + str(mmddConfig.extendMultiLimit) + "\n"
    content += "RemainDancerPosY" + " = " + str(mmddConfig.remainDancerPosY) + "\n"
    content += "DisableCamera" + " = " + str(mmddConfig.disableCamera) + "\n"
    content += "AutoSwitchCamera" + " = " + str(mmddConfig.autoSwitchCamera) + "\n"
    content += "PlaylistSaveRelPath" + " = " + str(mmddConfig.playlistSaveRelPath) + "\n"
    content += "\n"
    # GUI
    content += "[GUI]\n"
    content += "GuiX" + " = " + str(mmddConfig.guiX) + "\n"
    content += "GuiY" + " = " + str(mmddConfig.guiY) + "\n"
    content += "GuiWidth" + " = " + str(mmddConfig.guiWidth) + "\n"
    content += "GuiHeight" + " = " + str(mmddConfig.guiHeight) + "\n"
    content += "GuiLock" + " = " + str(mmddConfig.guiLock) + "\n"
    content += "GuiLanguage" + " = " + mmddConfig.guiLanguage + "\n"
    content += "GuiOnPlaying" + " = " + mmddConfig.guiOnPlaying + "\n"
    content += "\n"
    # Shortcuts
    content += "[Shortcuts]\n"
    for keytitle, keyset in mmddConfig.shortcuts.items():
        if keyset[1] == None:
            content += keytitle + " = \n"
        else:
            content += keytitle + " = " + keyset[1][0] + "\n"
    content += "\n"
    # default camera multi target setting
    content += "[DefaultCameraMultiTarget]\n"
    content += "SwitchMinFramePeriod" + " = " + str(mmddConfig.switchMinFramePeriod) + "\n"
    content += "SwitchMaxFramePeriod" + " = " + str(mmddConfig.switchMaxFramePeriod) + "\n"
    content += "SwitchProbability" + " = " + str(mmddConfig.switchProbability) + "\n"
    content += "\n"
    # Drag intensity
    content += "[DragInputIntensity]\n"
    content += "PositionIntensityX" + " = " + str(mmddConfig.dragIntensityPosX) + "\n"
    content += "PositionIntensityY" + " = " + str(mmddConfig.dragIntensityPosY) + "\n"
    content += "RotationIntensityX" + " = " + str(mmddConfig.dragIntensityRotX) + "\n"
    content += "RotationIntensityY" + " = " + str(mmddConfig.dragIntensityRotY) + "\n"
    content += "ScaleIntensityX" + " = " + str(mmddConfig.dragIntensitySclX) + "\n"
    content += "ScaleIntensityY" + " = " + str(mmddConfig.dragIntensitySclY) + "\n"
    content += "\n"
    # Keyframe color
    content += "[KeyframeViewColor]\n"
    for (key, color) in mmddConfig.keyframeColors:
        content += key + " = " + "%.3f, %.3f, %.3f, %.3f"%(color.r, color.g, color.b, color.a) + "\n"
    content += "\n"
    # Curve color
    content += "[CurveEditorColor]\n"
    for (key, color) in mmddConfig.curveColors:
        content += key + " = " + "%.3f, %.3f, %.3f, %.3f"%(color.r, color.g, color.b, color.a) + "\n"
    content += "\n"
    # Curve preset
    content += "[CurvePreset]\n"
    pi = 0
    for (name, curve) in mmddConfig.curvePresets:
        content += "preset%d = "%pi + name + ", %.3f, %.3f, %.3f, %.3f"%(curve[0], curve[1], curve[2], curve[3]) + "\n"
        pi += 1
    content += "\n"

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

def hook_update(game, evid, u_param):
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    curTime = time.clock()
    # backup
    if mmddConfig.autoBackup and not mmdd.isPlaying:
        if curTime - game.scenedata.autoSaveTimer > mmddConfig.autoBackupInterval:
            mmdd.saveToScene("backup")
            game.scenedata.autoSaveTimer = curTime
            print "MMDD auto backup by timer"
    # shortcuts
    if not game.gdata.mmddSuspendShortcut:
        for commands, param in mmddConfig.shortcuts.items():
            if param[1] == None:
                continue
            (keyname, icode, ictrl, ialt, ishift) = param[1]
            if param[2] and Input.GetKey(icode):
                # unity sucks for checking meta keys
                ctrl, alt, shift = unity_util.metakey_state()
                if ctrl == ictrl and alt == ialt and shift == ishift:
                    # do the work
                    #print "* shortcut [%s] detected for [%s]"%(keyname, commands)
                    param[0]()
                    if sd.activedKeyframeView and not mmdd.isPlaying:
                        sd.activedKeyframeView.setCurrentFrame(int(mmdd.curFrame))
                    break
            elif not param[2] and Input.GetKeyDown(icode):
                # unity sucks for checking meta keys
                ctrl, alt, shift = unity_util.metakey_state()
                if ctrl == ictrl and alt == ialt and shift == ishift:
                    # do the work
                    #print "* shortcut [%s] detected for [%s]"%(keyname, commands)
                    param[0]()
                    if sd.activedKeyframeView and not mmdd.isPlaying:
                        sd.activedKeyframeView.setCurrentFrame(int(mmdd.curFrame))
                    break
    # GC
    if mmdd.isPlaying:
        if mmddConfig.gcOptimize == "suspend" and game.gdata.gc_collect_check_tick:
            if time.clock() > game.gdata.gc_collect_check_tick:
                game.gdata.gc_collect_check_tick = time.clock() + 3
                if GetGarbageSize() > mmddConfig.gcSuspendLimit_solved:
                    # do limited garbage collect
                    EnableGarbageCollector(True)
                    DoGarbageCollect()
                    EnableGarbageCollector(False)
        elif mmddConfig.gcOptimize == "periodic":
            game.gdata.gc_collect_count += 1
            if game.gdata.gc_collect_count >= 30:
                DoGarbageCollect()
                game.gdata.gc_collect_count = 0
    # clip sync
    if not mmdd.isPlaying and sd.mmddSyncClipCtrl:
        #print "sync with clip %s"%sd.mmddSyncClipCtrl.clipName
        mmddFrame = sd.mmddSyncClipCtrl.toMmddFrame(sd.mmddSyncClipStartFrame)
        if mmddFrame != None and mmdd.curFrame != mmddFrame:
            mmdd.curFrame = mmddFrame
            mmdd.update()
    # mmdd verify
    if curTime - game.scenedata.autoVerifyTimer > 1:
        game.scenedata.autoVerifyTimer = curTime
        mmdd.verify()

def hook_loaded(game, evid, u_param):
    mmddConfig = game.gdata.mmdd_config
    # load
    if mmddConfig.autoLoad:
        mmdd = game.gdata.mmdd
        mmdd.cleanup()
        err = mmdd.loadFromScene("save")
        if err:
            print "MMDD auto load failed:", err
        else:
            print "MMDD auto load OK"
        try:
            mmddGuiMain(game)
        except:
            pass # may cause exception

def hook_saved(game, evid, u_param):
    mmddConfig = game.gdata.mmdd_config
    # auto save
    if game.isSceneDataSaveSupported and mmddConfig.autoSave:
        try:
            mmdd = game.gdata.mmdd
            err = mmdd.saveToScene("save")
            print "MMDD auto save OK"
        except:
            print "MMDD auto save Failed"

def onMmddStart(game):
    mmddConfig = game.gdata.mmdd_config
    if mmddConfig.guiOnPlaying == "minimize":
        mmddGuiMinimize(game)
    elif mmddConfig.guiOnPlaying == "hide":
        game.visible = 0
    if mmddConfig.gcOptimize == "periodic":
        game.gdata.gc_collect_count = 0
    if mmddConfig.gcOptimize == "suspend":
        if mmddConfig.gcSuspendLimit_solved and not math.isinf(mmddConfig.gcSuspendLimit_solved):
            game.gdata.gc_collect_check_tick = time.clock() + 3     # check every 3 second
        else:
            game.gdata.gc_collect_check_tick = None
        EnableGarbageCollector(False)

def onMmddStop(game, stopByTimer):
    mmddConfig = game.gdata.mmdd_config
    if mmddConfig.guiOnPlaying == "minimize":
        mmddGuiRestore(game)
    elif mmddConfig.guiOnPlaying == "hide":
        game.visible = 1
    if mmddConfig.gcOptimize == "suspend":
        EnableGarbageCollector(True)
        DoGarbageCollect()
    elif mmddConfig.gcOptimize == "bigheap":
        DoGarbageCollect()

# ====================================  GUI function ====================================
def mmddSkinSetup(game):
    # window size
    mmddConfig = game.gdata.mmdd_config
    x = mmddConfig.guiX
    y = mmddConfig.guiY
    w = mmddConfig.guiWidth
    h = mmddConfig.guiHeight

    # auto position
    if x == -1 or y == -1:
        mmddConfig.guiX = x = (Screen.width - mmddConfig.guiWidth) / 2
        mmddConfig.guiY = y = Screen.height - mmddConfig.guiHeight - 10

    # window setting
    game.windowName = "MMD Director v2.0"
    game.windowRect = Rect(x,y,w,h)
    game.windowStyle = game.windowStyleDefault
    game.windowDraggable = not mmddConfig.guiLock

    # start up function
    game.gdata.guiFunction = None
    game.gdata.guiFunctionParam = None
    game.gdata.guiPrevFunction = None
    game.gdata.guiPrevFunctionParam = None
    if hasattr(game.scenedata, "startupAutoLoadError"):
        mmddGuiStartup(game)
    else:
        mmddGuiMain(game)

def mmddSkinWindowGUI(game, windowid):
    try:
        gd = game.gdata
        if gd.guiFunctionParam:
            gd.guiFunction(game, gd.guiFunctionParam)
        else:
            gd.guiFunction(game)
        GUI.DragWindow()
    except Exception:
        print "\n=== Skin Window GUI Error Track:"
        traceback.print_exc()

def mmddGuiSwitch(game, tgtGuiFunc, param = None):
    if tgtGuiFunc == None:
        raise Exception("Error, try to switch to None GUI")        
    gd = game.gdata
    if tgtGuiFunc == gd.guiFunction:
        return False
    gd.guiPrevFunction = gd.guiFunction
    gd.guiPrevFunctionParam = gd.guiFunctionParam
    gd.guiFunction = tgtGuiFunc
    gd.guiFunctionParam = param
    return True

def mmddGuiMinimize(game):
    if mmddGuiSwitch(game, mmddGuiMinimize, None):
        mmddConfig = game.gdata.mmdd_config
        mmddConfig.guiX = int(game.windowRect.x)
        mmddConfig.guiY = int(game.windowRect.y)
        game.windowRect = Rect(mmddConfig.guiX, mmddConfig.guiY + mmddConfig.guiHeight - 65, mmddConfig.guiWidth, 65)
        return
    # seek bar only
    guiPartsSeekbar(game, None, None, game.scenedata.playlistNextCallback)

def mmddGuiRestore(game):
    if game.gdata.guiFunction == mmddGuiMinimize:
        mmddConfig = game.gdata.mmdd_config
        mmddConfig.guiX = int(game.windowRect.x)
        mmddConfig.guiY = int(game.windowRect.y) - mmddConfig.guiHeight + 65
        game.windowRect = Rect(mmddConfig.guiX, mmddConfig.guiY, mmddConfig.guiWidth, mmddConfig.guiHeight)
        mmddGuiBack(game)

def mmddGuiBack(game):
    gd = game.gdata
    gd.guiFunction = gd.guiPrevFunction
    gd.guiFunctionParam = gd.guiPrevFunctionParam

def mmddGuiMain(game):
    if mmddGuiSwitch(game, mmddGuiMain):
        return
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    sd.mmddSyncClipCtrl = None

    from vmdlib import MotionController, MorphController, CameraController, SoundController, ClipController
    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    def deleteMotionCtrlWithKfView(aid):
        if hasattr(sd, "MotionKF_KeyframeViews"):
            if isinstance(aid, MotionController):
                mc = aid
            elif aid in mmdd.motionControllers:
                mc = mmdd.motionControllers[aid]
            else:
                mc = None
            if mc and sd.MotionKF_KeyframeViews.has_key(mc):
                del sd.MotionKF_KeyframeViews[mc]
        mmdd.deleteMotionController(aid)
        deleteMorphCtrlWithKfView(aid, True)
    def deleteMorphCtrlWithKfView(aid, skipDeleteCtrl = False):
        if hasattr(sd, "MorphKF_KeyframeViews"):
            if isinstance(aid, MorphController):
                mp = aid
            elif aid in mmdd.morphControllers:
                mp = mmdd.morphControllers[aid]
            else:
                mp = None
            if mp and sd.MorphKF_KeyframeViews.has_key(mp):
                del sd.MorphKF_KeyframeViews[mp]
        if not skipDeleteCtrl:
            mmdd.deleteMorphController(aid)
    def importVmd(filename):
        # prepare
        mmdd.lastVmdFilename = filename
        guiImportVmdData_normal(game)
    def importWav(filepath):
        err = mmdd.loadWavFile(filepath)
        if err:
            guiPromptDialog(game, LC("Fail to load ") + filepath)
        else:
            mmdd.refreshTotalFrame()
            guiPromptDialog(game, LC("Sound data loaded: ") + filepath)
    def updateDalay():
        mcl = mmdd.getAllControllerList()
        dvs = []
        for assetNo in range(len(mcl)):
            dt = sd.delayTempData[assetNo]
            try:
                dv = float(dt)
                if dv < 0: raise Exception()
                dvs.append(dv)
            except:
                return LC("Invalid delay setting <color=#ff0000>'%s'</color>, must be a non-negative number.")%dt
        for assetNo in range(len(mcl)):
            mcl[assetNo].delay = dvs[assetNo]
        mmdd.refreshTotalFrame()
        return None
    def delayInfo(mc):
        if mc.delay > 0:
            return LC(", delay %.3fs")%mc.delay
        else:
            return ""
    def linkClip():
        try:
            # select or new clip
            if sd.clipSelectTempExist:
                clipName = sd.clipSelectTempSelector.curText
            else:
                clipName = sd.clipSelectTempNewName.strip()
                if len(clipName) == 0:
                    raise Exception("Empty clip name!")
                if clipName == "cam" or clipName == "sys":
                    raise Exception("Reserved name as 'cam' and 'sys' can not be used as clip name!")
                if sd.actors.has_key(clipName):
                    raise Exception("<%s> is already registed as a actor's id!"%clipName)
                if sd.props.has_key(clipName):
                    raise Exception("<%s> is already registed as a prop's id!"%clipName)
                if clipName in sd.clipSelectTempNames:
                    raise Exception("<%s> is already registed as a clip's name!"%clipName)
                try:
                    newFrameLen = int(sd.clipSelectTempNewLength)
                except:
                    raise Exception(LC("Wrong new clip length setting, set an integer please."))
                if newFrameLen <= 0:
                    raise Exception(LC("Wrong new clip length setting, 0 or nagative length."))
                newClip = KeyFrameClip(game, None)
                newClip.name = clipName
                newClip.frameLength = newFrameLen
                kfa_register(game, newClip)
            # frame check
            try:
                tf = int(sd.clipSelectTempFrame)
            except:
                raise Exception(LC("Wrong frame setting, set an integer please."))
            if tf < 0:
                raise Exception(LC("Wrong frame setting, negative frame!"))
            if tf > mmdd.totalFrame:
                raise Exception(("Wrong frame setting, must not greater than %d")%mmdd.totalFrame)
            # build controller
            cc = ClipController(clipName, tf)
            for ecc in mmdd.clipControllers:
                if ecc.Id == cc.Id:
                    raise Exception(LC("This clip '%s' is already linked to frame %d")%(clipName, tf))
            cc.bindClip(game)
            if cc.clip == None:
                raise Exception(LC("Fail to bind clip '%s'!?")%(clipName))
        except Exception, e:
            sd.clipSelectTempError = str(e)
            return False
        # done and clean up
        mmdd.clipControllers.append(cc)
        mmdd.refreshTotalFrame()
        del sd.clipSelectTempNames
        del sd.clipSelectTempExist
        del sd.clipSelectTempSelector
        del sd.clipSelectTempFrame
        del sd.clipSelectTempNewName
        del sd.clipSelectTempNewLength
        del sd.clipSelectTempError
        return True

    # delete motion/morph controll if user deleted char/item
    for aid in mmdd.motionControllers.keys():
        notValidChar = aid not in game.scenef_get_all_actors() or game.scenef_get_actor(aid).objctrl.charInfo.objBody == None
        notValidItem = aid not in game.scenef_get_all_props() or game.scenef_get_propf(aid).objctrl.objectItem == None
        if notValidChar and notValidItem:
            # actor/prop deleted
            deleteMotionCtrlWithKfView(aid)
            break
    for aid in mmdd.morphControllers.keys():
        notValidChar = aid not in game.scenef_get_all_actors() or game.scenef_get_actor(aid).objctrl.charInfo.objBody == None
        notValidItem = aid not in game.scenef_get_all_props() or game.scenef_get_propf(aid).objctrl.objectItem == None
        if notValidChar and notValidItem:
            # actor/prop deleted
            deleteMorphCtrlWithKfView(aid)
            break

    # Seek bar
    guiPartsSeekbar(game)
    
    # asset view title
    GUILayout.BeginHorizontal()
    if mmdd.isPlaying:
        sd.assetViewMode = "asset"
    alTitle = {"asset": LC("MMD Assets:"),
               "add": LC("Add new asset:"),
               "set_delay": LC("Setup controller delay:"),
               "set_order": LC("Set motion / clip order:"),
               "link_clip": LC("Setup a VNAnime clip link:"),
               "add_prj": LC("Select a project file:"),
               "add_lib": LC("Select a play:"),
              }[sd.assetViewMode]
    GUILayout.Label(alTitle)
    GUILayout.FlexibleSpace()
    if not mmdd.isPlaying:
        if sd.assetViewMode == "asset" and GUILayout.Button(greenText(LC("Add Asset"))):
            sd.assetViewMode = "add"
        if sd.assetViewMode == "asset" and GUILayout.Button(LC("Set Delay")):
            sd.assetViewMode = "set_delay"
            sd.delayTempData = ["%.3f"%mc.delay for mc in mmdd.getAllControllerList()]
        if sd.assetViewMode == "asset" and GUILayout.Button(LC("Set Order")):
            sd.assetViewMode = "set_order"
        if sd.assetViewMode == "set_order" and GUILayout.Button(greenText(LC("OK"))):
            sd.assetViewMode = "asset"
        if sd.assetViewMode == "set_delay" and GUILayout.Button(greenText(LC("OK"))):
            err = updateDalay()
            if err:
                guiPromptDialog(game, err)
            else:
                guiPromptDialog(game, LC("Delay setting updated."))
                sd.assetViewMode = "asset"
        if sd.assetViewMode in ("set_delay", "add", "link_clip", "add_prj", "add_lib") and GUILayout.Button(redText(LC("Cancel"))):
            sd.assetViewMode = "asset"
    GUILayout.EndHorizontal()

    # asset view contents
    if sd.assetViewMode == "asset" or sd.assetViewMode == "set_delay":  # assets list
        sd.mainScrollPos = GUILayout.BeginScrollView(sd.mainScrollPos, GUI.skin.box, GUILayout.Width(fullw), GUILayout.Height(fullh - 110))
        mcl = mmdd.getAllControllerList()
        for assetNo in range(len(mcl)):
            mc = mcl[assetNo]
            if isinstance(mc, MotionController):
                # actor info line
                filename = mc.FileName
                if mc.keyframeCustomized:
                    filename += "<color=#ff0000>" + u"\u2605" + "</color>"
                if mc.Enable:
                    cTitle = LC("<color=#00ff00>%s</color> Motion: %s, %d frames%s")%(mc.CharName, filename, mc.maxFrameNo, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>%s Motion: %s, %d frames%s</color>")%(mc.CharName, filename, mc.maxFrameNo, delayInfo(mc))
                GUILayout.BeginHorizontal()
                mcEnable = GUILayout.Toggle(mc.Enable, cTitle)
                if mcEnable != mc.Enable:
                    mc.setEnable(mcEnable)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if mc.Baked:
                        if GUILayout.Button(LC("Edit"), GUILayout.Width(45)):
                            sd.tempToDel = mc
                            def debake(g):
                                sd.tempToDel.debakeFrame()
                                sd.tempToDel.setFrame(mmdd.curFrame)
                                mmddGuiBack(game)
                            guiConfirmDialog(game, LC("Discard all baked frames for <color=#00ff00>%s</color>?")%mc.CharName, debake)
                    else:
                        gv = mc.boneGizmosVisible()
                        if GUILayout.Button("<color=#00ff00>G</color>" if gv else "G", GUILayout.Width(25)):
                            mc.showBoneGizmos(not gv)
                        if GUILayout.Button(LC("Setup"), GUILayout.Width(45)):
                            mmddGuiMotionSetup(game, mc)
                        if GUILayout.Button(LC("Keys"), GUILayout.Width(45)):
                            if mc.delay == 0:
                                mmddGuiMotionKeyframe(game, mc)
                            else:
                                guiPromptDialog(game, LC("Keyframe view can not work properly with delay setted. Please clear delay to zero and then edit keyframes."))
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        sd.tempToDel = mc
                        def delMC(game):
                            deleteMotionCtrlWithKfView(sd.tempToDel)
                            del sd.tempToDel
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete motion %s for <color=#00ff00>%s</color>?\nIf morph is loaded for this actor, it will be deleted too.")%(mc.FileName, mc.CharName), delMC)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            elif isinstance(mc, MorphController):
                # morph info line
                filename = mc.FileName
                if mc.keyframeCustomized:
                    filename += "<color=#ff0000>" + u"\u2605" + "</color>"
                if mc.enable:
                    cTitle = LC("<color=#00ff00>%s</color> Morph: %s, %d frames%s")%(mc.CharName, filename, mc.maxFrameNo, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>%s Morph: %s, %d frames%s</color>")%(mc.CharName, filename, mc.maxFrameNo, delayInfo(mc))
                GUILayout.BeginHorizontal()
                mc.enable = GUILayout.Toggle(mc.enable, cTitle)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if GUILayout.Button(LC("Setup"), GUILayout.Width(45)):
                        mmddGuiMorphSetup(game, mc)
                    if GUILayout.Button(LC("Keys"), GUILayout.Width(45)):
                        if mc.delay == 0:
                            mmddGuiMorphKeyframe(game, mc)
                        else:
                            guiPromptDialog(game, LC("Keyframe view can not work properly with delay setted. Please clear delay to zero and then edit keyframes."))
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        sd.tempToDel = mc
                        def delMorphCtrl(game):
                            deleteMorphCtrlWithKfView(sd.tempToDel)
                            del sd.tempToDel
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete morph %s for <color=#00ff00>%s</color>?")%(mc.FileName, mc.CharName), delMorphCtrl)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            elif isinstance(mc, CameraController):
                # camera info line
                filename = mc.FileName
                if mc.keyframeCustomized:
                    filename += "<color=#ff0000>" + u"\u2605" + "</color>"
                if mc.enable:
                    cTitle = LC("Camera: <color=#00ff00>%s</color>, %d frames%s")%(filename, mc.maxFrameNo, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>Camera: %s, %d frames%s</color>")%(filename, mc.maxFrameNo, delayInfo(mc))
                GUILayout.BeginHorizontal()
                mc.enable = GUILayout.Toggle(mc.enable, cTitle)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if GUILayout.Button(LC("Setup"), GUILayout.Width(45)):
                        mmddGuiCameraSetup(game)
                    if GUILayout.Button(LC("Keys"), GUILayout.Width(45)):
                        if mc.delay == 0:
                            mmddGuiCameraKeyframe(game)
                        else:
                            guiPromptDialog(game, LC("Keyframe view can not work properly with delay setted. Please clear delay to zero and then edit keyframes."))
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        def delCamera(game):
                            mmdd.cameraController = None
                            mmdd.refreshTotalFrame()
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete imported camera <color=#00ff00>%s</color>?")%mc.FileName, delCamera)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            elif isinstance(mc, SoundController):
                # sound info line
                if mc.enable:
                    cTitle = LC("Sound: <color=#00ff00>%s</color>, %.2fsec%s")%(mc.FileName, mc.Length, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>Sound: %s, %.2fsec%s</color>")%(mc.FileName, mc.Length, delayInfo(mc))
                GUILayout.BeginHorizontal()
                mc.enable = GUILayout.Toggle(mc.enable, cTitle)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        def delSound(game):
                            mmdd.soundController.cleanup()
                            mmdd.soundController = None
                            mmdd.refreshTotalFrame()
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete imported sound <color=#00ff00>%s</color>?")%mc.FileName, delSound)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            elif isinstance(mc, ClipController):
                # clip info line
                if mc.enable:
                    cTitle = LC("Clip: <color=#00ff00>%s</color>, link at #%d lasts %s%s")%(mc.clipName, mc.linkFrame, mc.Lasts, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>Clip: %s, link at #%d lasts %s%s</color>")%(mc.clipName, mc.linkFrame, mc.Lasts, delayInfo(mc))
                GUILayout.BeginHorizontal()
                mc.enable = GUILayout.Toggle(mc.enable, cTitle)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if GUILayout.Button(LC("Setup"), GUILayout.Width(45)):
                        mmddGuiClipSetup(game, mc)
                    if GUILayout.Button(LC("Keys"), GUILayout.Width(45)):
                        if mc.delay == 0:
                            mmddGuiClipKeyframe(game, mc)
                        else:
                            guiPromptDialog(game, LC("Keyframe view can not work properly with delay setted. Please clear delay to zero and then edit keyframes."))
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        sd.tempToDel = mc
                        def delClip(game):
                            mmdd.deleteClipController(sd.tempToDel)
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete clip link <color=#00ff00>%s</color> at <color=#00ff00>%d</color>?")%(mc.clipName, mc.linkFrame), delClip)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            elif isinstance(mc, PluginMotionController):
                # VMD plugin controlled actor
                if mc.enable:
                    cTitle = LC("<color=#0000ff>%s</color> plugin motion: %s (%.2fs%s)")%(mc.actor.text_name, mc.filename, mc.length, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>%s plugin motion: %s (%.2fs%s)</color>")%(mc.actor.text_name, mc.filename, mc.length, delayInfo(mc))
                GUILayout.BeginHorizontal()
                if mmdd.isPlaying:
                    GUILayout.Label(cTitle)
                else:
                    mc.enable = GUILayout.Toggle(mc.enable, cTitle)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        sd.tempToDel = mc
                        def delPluginMotion(game):
                            mmdd.deletePluginMotionController(sd.tempToDel)
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete VMD plugin motion/morph <color=#00ff00>%s</color>?")%mc.actor.text_name, delPluginMotion)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            elif isinstance(mc, PluginCameraController):
                # VMD plugin camera
                if mc.enable:
                    cTitle = LC("Plugin Camera: <color=#0000ff>%s</color> (%.2fs%s)")%(mc.filename, mc.length, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>Plugin Camera: %s (%.2fs%s)</color>")%(mc.filename, mc.length, delayInfo(mc))
                GUILayout.BeginHorizontal()
                if mmdd.isPlaying:
                    GUILayout.Label(cTitle)
                else:
                    mc.enable = GUILayout.Toggle(mc.enable, cTitle)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        def delPluginCamera(game):
                            mmdd.vmdCameraCtrl = None
                            mmdd.refreshTotalFrame()
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete VMD plugin imported camera <color=#00ff00>%s</color>?")%mc.filename, delPluginCamera)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            elif isinstance(mc, PluginSoundController):
                # VMD plugin sound
                if mc.enable:
                    cTitle = LC("Plugin Sound: <color=#0000ff>%s</color> (%.2fs%s)")%(mc.filename, mc.length, delayInfo(mc))
                else:
                    cTitle = LC("<color=#808080>Plugin Sound: %s (%.2fs%s)</color>")%(mc.filename, mc.length, delayInfo(mc))
                GUILayout.BeginHorizontal()
                if mmdd.isPlaying:
                    GUILayout.Label(cTitle)
                else:
                    mc.enable = GUILayout.Toggle(mc.enable, cTitle)
                GUILayout.FlexibleSpace()
                if sd.assetViewMode == "asset":
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        def delPluginSound(game):
                            mmdd.vmdSoundCtrl = None
                            mmdd.refreshTotalFrame()
                            mmddGuiBack(game)
                        guiConfirmDialog(game, LC("Delete VMD plugin imported sound <color=#00ff00>%s</color>?")%mc.filename, delPluginSound)
                else:
                    GUILayout.Label(LC("Delay:"))
                    sd.delayTempData[assetNo] = GUILayout.TextField(sd.delayTempData[assetNo], GUILayout.Width(50))
                GUILayout.Space(10)
                GUILayout.EndHorizontal()
            else:
                #print "Unknown type of controller %s"%str(mc)
                pass
        GUILayout.EndScrollView()
    elif sd.assetViewMode == "set_order":
        sd.mainScrollPos = GUILayout.BeginScrollView(sd.mainScrollPos, GUI.skin.box, GUILayout.Width(fullw), GUILayout.Height(fullh - 110))
        for midx in range(len(mmdd.motionSequence)):
            # actor info line
            mc = mmdd.motionControllers[mmdd.motionSequence[midx]]
            canup = midx > 0
            candn = midx < len(mmdd.motionSequence) - 1
            cTitle = LC("<color=#00ff00>%s</color> Motion: %s, %d frames%s")%(mc.CharName, mc.FileName, mc.maxFrameNo, delayInfo(mc))
            GUILayout.BeginHorizontal()
            GUILayout.Label(cTitle)
            GUILayout.FlexibleSpace()
            if GUILayout.Button(u'\u2191' if canup else greyText(u'\u2191'), GUILayout.Width(25)) and canup:
                mmdd.motionSequence[midx-1], mmdd.motionSequence[midx] = mmdd.motionSequence[midx], mmdd.motionSequence[midx-1]
                break
            if GUILayout.Button(u'\u2193' if candn else greyText(u'\u2193'), GUILayout.Width(25)) and candn:
                mmdd.motionSequence[midx+1], mmdd.motionSequence[midx] = mmdd.motionSequence[midx], mmdd.motionSequence[midx+1]
                break
            GUILayout.EndHorizontal()
        for cidx in range(len(mmdd.clipControllers)):
            cc = mmdd.clipControllers[cidx]
            canup = cidx > 0
            candn = cidx < len(mmdd.clipControllers) - 1
            cTitle = LC("Clip: <color=#00ff00>%s</color>, link at #%d lasts %s")%(cc.clipName, cc.linkFrame, cc.Lasts)
            GUILayout.BeginHorizontal()
            GUILayout.Label(cTitle)
            GUILayout.FlexibleSpace()
            if GUILayout.Button(u'\u2191' if canup else greyText(u'\u2191'), GUILayout.Width(25)) and canup:
                mmdd.clipControllers[cidx-1], mmdd.clipControllers[cidx] = mmdd.clipControllers[cidx], mmdd.clipControllers[cidx-1]
                break
            if GUILayout.Button(u'\u2193' if candn else greyText(u'\u2193'), GUILayout.Width(25)) and candn:
                mmdd.clipControllers[cidx+1], mmdd.clipControllers[cidx] = mmdd.clipControllers[cidx], mmdd.clipControllers[cidx+1]
                break
            GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
    elif sd.assetViewMode == "add": # add menu
        GUILayout.BeginHorizontal() # add vmd
        GUILayout.Space(20)
        if GUILayout.Button(LC("Load VMD file"), GUILayout.Width(150)):
            def onLoadVmd(files):
                if files and len(files) > 0:
                    importVmd(files[0])
                sd.assetViewMode = "asset"
            guiOpenFileDialog(onLoadVmd, mmdd.lastVmdFilename if len(mmdd.lastVmdFilename) else mmddConfig.vmdHomeDir, True, "VMD")
        GUILayout.Space(10)
        GUILayout.Label(LC("Load motion/morph/camera from VMD file"))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal() # add sound
        GUILayout.Space(20)
        if GUILayout.Button(LC("Load WAV file"), GUILayout.Width(150)):
            def onLoadWav(files):
                if files and len(files) > 0:
                    importWav(files[0])
                sd.assetViewMode = "asset"
            guiOpenFileDialog(onLoadWav, mmdd.lastWavFilename if len(mmdd.lastWavFilename) else mmddConfig.vmdHomeDir, True, "WAV")
        GUILayout.Space(10)
        GUILayout.Label(LC("Load music (.WAV) file"))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal() # plugin asset
        GUILayout.Space(20)
        if GUILayout.Button(LC("Refresh plugin asset"), GUILayout.Width(150)):
            mmdd.loadVmdPluginAsset()
            sd.assetViewMode = "asset"
        GUILayout.Space(10)
        GUILayout.Label(LC("Refresh all MMD asset setted by ext plugins"))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal() # link clip
        GUILayout.Space(20)
        if GUILayout.Button(LC("Link VNAnime clip"), GUILayout.Width(150)):
            from libkfguictrl import ComboListBox
            sd.assetViewMode = "link_clip"
            sd.clipSelectTempNames = [cp.name for cp in game.gdata.kfaSortedClips]
            sd.clipSelectTempExist = len(sd.clipSelectTempNames) > 0
            sd.clipSelectTempSelector = ComboListBox(Rect(200, 86, 200, 21), 150, sd.clipSelectTempNames, 0)
            sd.clipSelectTempFrame = str(int(mmdd.curFrame))
            sd.clipSelectTempNewName = "mmdd_clip%d"%len(sd.clipSelectTempNames)
            sd.clipSelectTempNewLength = str(int(mmdd.totalFrame))
            sd.clipSelectTempError = None
        GUILayout.Space(10)
        GUILayout.Label(LC("Link a VNAnime keyframe clip to MMD."))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal() # add vmd advanced
        GUILayout.Space(20)
        if GUILayout.Button(LC("Load VMD Advanced"), GUILayout.Width(150)):
            sd.assetViewMode = "asset"
            guiImportVmdData_advanced(game)
        GUILayout.Space(10)
        GUILayout.Label(LC("Load VMD file in advanced mode."))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal() # add new camera
        GUILayout.Space(20)
        if GUILayout.Button(LC("New Camera"), GUILayout.Width(150)):
            def newc(g):
                mmdd.cameraController = CameraController()
                mmdd.cameraController.switchMinFramePeriod = mmddConfig.switchMinFramePeriod
                mmdd.cameraController.switchMaxFramePeriod = mmddConfig.switchMaxFramePeriod
                mmdd.cameraController.switchProbability = mmddConfig.switchProbability
                mmdd.refreshTotalFrame()
                sd.assetViewMode = "asset"
                mmddGuiMain(g)
            if mmdd.cameraController != None:
                guiConfirmDialog(game, LC("Create a new camera anime and overwrite current camera?"), newc)
            else:
                newc(game)
        GUILayout.Space(10)
        GUILayout.Label(LC("Create a new empty camera anime"))
        GUILayout.EndHorizontal()
    elif sd.assetViewMode == "link_clip":   # link vnanime clip
        GUILayout.BeginHorizontal()
        GUILayout.Space(10)
        GUILayout.Label(LC("Target clip:"), GUILayout.Width(80))
        if len(sd.clipSelectTempNames) > 0:
            sd.clipSelectTempExist = GUILayout.Toggle(sd.clipSelectTempExist, "Existed clip", GUILayout.Width(90))
            #GUILayout.TextField("For test")
            if sd.clipSelectTempExist:
                sd.clipSelectTempSelector.paint()
        else:
            GUILayout.Label(yellowText(LC("No VNAnime clip found!")))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Space(10)
        GUILayout.Label(" ", GUILayout.Width(80))
        if len(sd.clipSelectTempNames) > 0:
            sd.clipSelectTempExist = not GUILayout.Toggle(not sd.clipSelectTempExist, "New clip", GUILayout.Width(92))
        else:
            GUILayout.Label(LC("New clip"), GUILayout.Width(90))
        if not sd.clipSelectTempExist:
            GUILayout.Label(LC("Name:"))
            sd.clipSelectTempNewName = GUILayout.TextField(sd.clipSelectTempNewName, GUILayout.Width(120))
            GUILayout.Space(10)
            GUILayout.Label(LC("Length:"))
            sd.clipSelectTempNewLength = GUILayout.TextField(sd.clipSelectTempNewLength, GUILayout.Width(60))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        GUILayout.Space(10)
        GUILayout.BeginHorizontal()
        GUILayout.Space(10)
        GUILayout.Label(LC("At frame:"), GUILayout.Width(80))
        sd.clipSelectTempFrame = GUILayout.TextField(sd.clipSelectTempFrame, GUILayout.Width(60))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Space(10)
        GUILayout.Label(" ", GUILayout.Width(80))
        if GUILayout.Button(greenText("OK"), GUILayout.Width(80)):
            if linkClip():
                sd.assetViewMode = "asset"
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        if hasattr(sd, "clipSelectTempError") and sd.clipSelectTempError:
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label(redText(sd.clipSelectTempError))
            GUILayout.EndHorizontal()
    elif sd.assetViewMode in ("add_prj", "add_lib"):    # Load file line
        GUILayout.BeginHorizontal()
        GUILayout.Space(20)
        GUILayout.Label("Under Construction  m(_ _)m")
        GUILayout.EndHorizontal()

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button(LC("Ext.Parent"), btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiExtParentSetup(game)
    if GUILayout.Button(LC("Playlist"), btnstyle, GUILayout.Width(cbwidth)):
        from mmddplaylist import mmddGuiPlaylist
        mmddGuiPlaylist(game)
    if GUILayout.Button(LC("Setting"), btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiSetting(game)
    if GUILayout.Button(LC("Quit"), btnstyle, GUILayout.Width(cbwidth)):
        def cleanAndQuit(game):
            mmdd.cleanup()
            quit(game)
        def saveAndQuit(game):
            mmdd.saveToScene("save")
            print "MMDD auto saved on mmdd quit."
            if len(mmdd.clipControllers):
                from vnanime import scene_saved
                scene_saved(game)
            cleanAndQuit(game)
        def backupAndQuit(game):
            mmdd.saveToScene("backup")
            print "MMDD auto backup on mmdd quit."
            if len(mmdd.clipControllers):
                from vnanime import scene_saved
                scene_saved(game)
            cleanAndQuit(game)
        msg = LC("Sure to quit MMD Director?\n\n")
        if mmddConfig.autoSave:
            msg += greenText(LC("Data will be auto saved to scene."))
            guiSelectDialog(game, (msg, (LC("Save & Quit"), saveAndQuit), (LC("Quit without Save"), cleanAndQuit), ("Cancel", None)))
        elif mmddConfig.autoBackup:
            msg += yellowText(LC("Data will be auto backuped to scene."))
            guiSelectDialog(game, (msg, (LC("Backup & Quit"), backupAndQuit), (LC("Quit without Backup"), cleanAndQuit), ("Cancel", None)))
        else:
            msg += redText(LC("Unsaved data will be lost!"))
            guiConfirmDialog(game, msg, cleanAndQuit)
    GUILayout.EndHorizontal()

def mmddGuiMotionSetup(game, motionCtrl):
    from libkfguictrl import DragInputUnit
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    def initMotionSetup():
        sd.MotionSetup_Page = "general"
        sd.MotionSetup_SelectedBoneName = ""
        sd.MotionSetup_LastFrameNo = -1
        sd.MotionSetup_SelectedBoneAdjust = None
        sd.MotionSetup_Symme = True
        sd.MotionSetup_PositionInfo = ""
        sd.MotionSetup_RotationInfo = ""
        sd.MotionSetup_BoneEditFilter = ""
        sd.MotionSetup_BoneListScrollPos = Vector2.zero
        sd.MotionSetup_GeneralScrollPos = Vector2.zero
        sd.MotionSetup_LastGuideGizmos = None
        sd.MotionSetup_BoneGroupExpand = {}
        for gkey in motionCtrl.boneGroupList:
            sd.MotionSetup_BoneGroupExpand[gkey] = False

        # chara scale
        sd.MotionSetup_CharaScaleAdj = DragInputUnit(Rect(114, 4, 120, 21), motionCtrl.boneType, False)
        sd.MotionSetup_CharaScaleAdj.setValueFormat(vFormat="%.3f")
        sd.MotionSetup_CharaScaleAdj.setDragRate(mmddConfig.dragIntensitySclX, mmddConfig.dragIntensitySclY)
        sd.MotionSetup_CharaScaleAdj.setValue(motionCtrl.charScale)
        # rotation adjust
        sd.MotionSetup_RotateAdj = (
            DragInputUnit(Rect(172, 111, 120, 21), LC("X Rotate"), False, 65),
            DragInputUnit(Rect(172, 134, 120, 21), LC("Y Rotate"), False, 65),
            DragInputUnit(Rect(172, 157, 120, 21), LC("Z Rotate"), False, 65),
            DragInputUnit(Rect(295, 111, 120, 21), LC("X Axis"), False, 65),
            DragInputUnit(Rect(295, 134, 120, 21), LC("Y Axis"), False, 65),
            DragInputUnit(Rect(295, 157, 120, 21), LC("Z Axis"), False, 65),
        )
        for adjctrl in sd.MotionSetup_RotateAdj:
            adjctrl.setValueFormat(-180, 180, True, "%.3f")
            adjctrl.setDragRate(mmddConfig.dragIntensityRotX, mmddConfig.dragIntensityRotY)
        # rotation scale
        sd.MotionSetup_RotateScaleAdj = DragInputUnit(Rect(172, 180, 243, 21), "Rotation Scale", False, 120)
        sd.MotionSetup_RotateScaleAdj.setValueFormat(vFormat="%.3f")
        sd.MotionSetup_RotateScaleAdj.setDragRate(mmddConfig.dragIntensitySclX, mmddConfig.dragIntensitySclY)
        # position adjust
        sd.MotionSetup_OffsetAdj = (
            DragInputUnit(Rect(430, 111, 150, 21), LC("X Offset"), False, 95),
            DragInputUnit(Rect(430, 134, 150, 21), LC("Y Offset"), False, 95),
            DragInputUnit(Rect(430, 157, 150, 21), LC("Z Offset"), False, 95),
        )
        for adjctrl in sd.MotionSetup_OffsetAdj:
            adjctrl.setValueFormat(vFormat="%.3f")
            adjctrl.setDragRate(mmddConfig.dragIntensityPosX, mmddConfig.dragIntensityPosY)
        # position scale
        sd.MotionSetup_OffsetScaleAdj = DragInputUnit(Rect(430, 180, 150, 21), "Offset Scale", False, 95)
        sd.MotionSetup_OffsetScaleAdj.setValueFormat(vFormat="%.3f")
        sd.MotionSetup_OffsetScaleAdj.setDragRate(mmddConfig.dragIntensitySclX, mmddConfig.dragIntensitySclY)
    def refreshBoneAdj():
        boneAdj = sd.MotionSetup_SelectedBoneAdjust
        sd.MotionSetup_RotateAdj[0].setValue(boneAdj.rotAdjustmentVec.x)
        sd.MotionSetup_RotateAdj[1].setValue(boneAdj.rotAdjustmentVec.y)
        sd.MotionSetup_RotateAdj[2].setValue(boneAdj.rotAdjustmentVec.z)
        sd.MotionSetup_RotateAdj[3].setValue(boneAdj.rotAxisAdjustmentVec.x)
        sd.MotionSetup_RotateAdj[4].setValue(boneAdj.rotAxisAdjustmentVec.y)
        sd.MotionSetup_RotateAdj[5].setValue(boneAdj.rotAxisAdjustmentVec.z)
        sd.MotionSetup_RotateAdj[3].enable = not boneAdj.rotAxisAuto
        sd.MotionSetup_RotateAdj[4].enable = not boneAdj.rotAxisAuto
        sd.MotionSetup_RotateAdj[5].enable = not boneAdj.rotAxisAuto
        sd.MotionSetup_RotateScaleAdj.setValue(boneAdj.rotationScale)
        sd.MotionSetup_OffsetAdj[0].setValue(boneAdj.basePosition.x)
        sd.MotionSetup_OffsetAdj[1].setValue(boneAdj.basePosition.y)
        sd.MotionSetup_OffsetAdj[2].setValue(boneAdj.basePosition.z)
        sd.MotionSetup_OffsetScaleAdj.setValue(boneAdj.positionLocalScale)
    def selectBone(boneName):
        sd.MotionSetup_SelectedBoneName = boneName
        sd.MotionSetup_SelectedBoneAdjust = motionCtrl.boneAdjustMap[boneName]
        refreshBoneAdj()
        refreshFrameInfo()
        showGuideGizmos()
    def updateFrameNo():
        if sd.MotionSetup_LastFrameNo != int(mmdd.curFrame):
            sd.MotionSetup_LastFrameNo = int(mmdd.curFrame)
            refreshFrameInfo()
    def refreshFrameInfo():
        if sd.MotionSetup_SelectedBoneName != "":
            vname = motionCtrl.boneDefineMap[sd.MotionSetup_SelectedBoneName].vmdBone
            curBoneFrame = motionCtrl.getVmdBoneFrame(sd.MotionSetup_LastFrameNo, vname)
        else:
            curBoneFrame = None
        if curBoneFrame:
            bDefine = motionCtrl.boneDefineMap[sd.MotionSetup_SelectedBoneName]
            resPos = sd.MotionSetup_SelectedBoneAdjust.GetAdjustedPosition(curBoneFrame.position)
            resRot = sd.MotionSetup_SelectedBoneAdjust.GetAdjustedRotation(curBoneFrame.rotation)
            sd.MotionSetup_RotationInfo = LC("Rotation: (%.2f, %.2f, %.2f) ")%(curBoneFrame.rotation.eulerAngles.x, curBoneFrame.rotation.eulerAngles.y, curBoneFrame.rotation.eulerAngles.z) + u"\u2192"
            sd.MotionSetup_RotationInfo += " (%.2f, %.2f, %.2f)"%(resRot.eulerAngles.x, resRot.eulerAngles.y, resRot.eulerAngles.z)
            if not bDefine.isRotation:
                sd.MotionSetup_RotationInfo += " " + redText("N/A")
            sd.MotionSetup_PositionInfo = LC("Position: (%.2f, %.2f, %.2f) ")%(curBoneFrame.position.x, curBoneFrame.position.y, curBoneFrame.position.z) + u"\u2192"
            sd.MotionSetup_PositionInfo += " (%.2f, %.2f, %.2f)"%(resPos.x, resPos.y, resPos.z)
            if not bDefine.isPosition:
                sd.MotionSetup_PositionInfo += " " + redText("N/A")
        else:
            sd.MotionSetup_RotationInfo = LC("Selected bone '%s' has no frame at all.")%greenText(sd.MotionSetup_SelectedBoneName)
            sd.MotionSetup_PositionInfo = ""
    # util
    def rotMod(value):
        if value > 180:
            return value - 360
        elif value < -180:
            return value + 360
        else:
            return value
    def isSymmePair(bName):
        return bName.endswith("_l") or bName.endswith("_r")
    def getSymmePair(bName):
        if bName.endswith("_l"):
            return bName[:-1] + "r"
        else:
            return bName[:-1] + "l"
    def showGuideGizmos():
        clearGuideGizmos()
        from CharaAnime import CubeGizmos
        cubeSize = 0.20 if game.isNEOV2 else 0.020
        sd.MotionSetup_LastGuideGizmos = CubeGizmos(motionCtrl.getVmdBone(sd.MotionSetup_SelectedBoneName).transform, cubeSize)
        mmdd.charaAnimePlugin.GizmosMgr.ShowGizmos(sd.MotionSetup_LastGuideGizmos)
    def clearGuideGizmos():
        if sd.MotionSetup_LastGuideGizmos:
            mmdd.charaAnimePlugin.GizmosMgr.RemoveGizmos(sd.MotionSetup_LastGuideGizmos)
            sd.MotionSetup_LastGuideGizmos = None
    def getKeyframeCount(bname):
        vname = motionCtrl.boneDefineMap[bname].vmdBone
        if vname in motionCtrl.boneKeyFrames:
            return len(motionCtrl.boneKeyFrames[vname])
        else:
            return 0 

    # init switch
    if mmddGuiSwitch(game, mmddGuiMotionSetup, motionCtrl):
        initMotionSetup()
        return

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    leftw = 150
    rightw = fullw - leftw - 4

    # sync frame no
    updateFrameNo()

    # Seek bar
    guiPartsSeekbar(game)

    try:
        #  title line
        GUILayout.BeginHorizontal()
        GUILayout.Space(5)
        GUILayout.Label("Filter", GUILayout.Width(35))
        sd.MotionSetup_BoneEditFilter = GUILayout.TextField(sd.MotionSetup_BoneEditFilter, GUILayout.Width(leftw - 45))
        GUILayout.Space(5)
        if GUILayout.Button("<color=#00ff00>General</color>" if sd.MotionSetup_Page == "general" else "General", GUILayout.Width(rightw / 3 - 3)):
            sd.MotionSetup_Page = "general"
        if GUILayout.Button("<color=#00ff00>Bone Adjust</color>" if sd.MotionSetup_Page == "adjust" else "Bone Adjust", GUILayout.Width(rightw / 3 - 3)):
            sd.MotionSetup_Page = "adjust"
        if GUILayout.Button("<color=#00ff00>Bone Info</color>" if sd.MotionSetup_Page == "info" else "Bone Info", GUILayout.Width(rightw / 3 - 3)):
            sd.MotionSetup_Page = "info"
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        # left panel: bone list
        sd.MotionSetup_BoneListScrollPos = GUILayout.BeginScrollView(sd.MotionSetup_BoneListScrollPos, GUI.skin.box, GUILayout.Width(leftw), GUILayout.Height(fullh - 105))
        boneListContants = []
        for bname in motionCtrl.boneNameList:
            bDefine = motionCtrl.boneDefineMap[bname]
            # hide setting
            if mmddConfig.hideBone["non-opt"] and not bDefine.isRotation and not bDefine.isPosition:
                continue
            kc = getKeyframeCount(bname)
            if mmddConfig.hideBone["no-frm"] and kc == 0:
                continue
            if mmddConfig.hideBone["1-frm"] and kc == 1:
                continue
            if mmddConfig.hideBone["2-frm"] and kc == 2:
                continue
            if mmddConfig.hideBone["finger"] and bname.startswith("fig_"):
                continue
            # vmd name
            if mmddConfig.boneNameDisplay == "dummy":
                bdname = bname
            else:
                bdname = bDefine.vmdBone
            # filter
            if len(sd.MotionSetup_BoneEditFilter) > 0:
                filters = sd.MotionSetup_BoneEditFilter.lower().split("+")
                match = False
                for fil in filters:
                    if len(fil) > 0 and (bdname.find(fil) >= 0 or bname.find(fil) >= 0):
                        match = True
                        break
                if not match:
                    continue
            # bone title
            bTitle = bdname + (" (%d)"%kc if kc > 0 else "")
            if not bDefine.enable:
                bTitle = "<color=#808080>" + bTitle + "</color>"
            if bname in motionCtrl.boneAdjustDirtySet:
                bTitle += " <color=#ff0000>" + u"\u2605" + "</color>"
            # to list contants
            boneListContants.append((bDefine.group, bname, bTitle))
        if mmddConfig.boneListMode == "group":  # render in group
            for gkey in motionCtrl.boneGroupList:
                big = [tb for tb in boneListContants if tb[0] == gkey]
                if len(big) == 0:
                    continue
                GUILayout.BeginHorizontal()
                if GUILayout.Button("-" if sd.MotionSetup_BoneGroupExpand[gkey] else "+", GUILayout.Width(20)):
                    sd.MotionSetup_BoneGroupExpand[gkey] = not sd.MotionSetup_BoneGroupExpand[gkey]
                GUILayout.Label(yellowText(motionCtrl.boneGroupName[gkey]))
                GUILayout.EndHorizontal()
                if sd.MotionSetup_BoneGroupExpand[gkey]:
                    for blc in big:
                        bname = blc[1]
                        bTitle = blc[2]
                        color = GUI.color
                        if bname == sd.MotionSetup_SelectedBoneName:
                            GUI.color = Color.green
                        if GUILayout.Button(bTitle, GUILayout.Width(leftw - 24)):
                            selectBone(bname)
                        GUI.color = color
        else:   # render by sequence
            for blc in boneListContants:
                bname = blc[1]
                bTitle = blc[2]
                color = GUI.color
                if bname == sd.MotionSetup_SelectedBoneName:
                    GUI.color = Color.green
                if GUILayout.Button(bTitle, GUILayout.Width(leftw - 24)):
                    selectBone(bname)
                GUI.color = color
        GUILayout.EndScrollView()
        # right panel: bone adjust
        GUILayout.BeginVertical(GUILayout.Width(rightw))
        if sd.MotionSetup_Page == "general":
            upd = False
            titleLen = 100
            # general title
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label("Motion Controller General Setting:")
            GUILayout.EndHorizontal()
            sd.MotionSetup_GeneralScrollPos = GUILayout.BeginScrollView(sd.MotionSetup_GeneralScrollPos, GUILayout.Width(rightw), GUILayout.Height(fullh - 135))
            # Chara scale
            GUILayout.BeginHorizontal() 
            GUILayout.Space(10)
            GUILayout.Label("Chara Scale:", GUILayout.Width(titleLen))
            if sd.MotionSetup_CharaScaleAdj.paint():
                motionCtrl.setCharScale(sd.MotionSetup_CharaScaleAdj.value)
                upd = True
            GUILayout.Space(125)
            if GUILayout.Button("Auto", GUILayout.Width(50)):
                motionCtrl.autoCharScale()
                sd.MotionSetup_CharaScaleAdj.setValue(motionCtrl.charScale)
                upd = True
            GUILayout.EndHorizontal()
            # IK ON/OFF
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label("IK On/Off:", GUILayout.Width(titleLen))
            for ikname in motionCtrl.ikSettingMap.keys():
                iken = motionCtrl.ikSettingMap[ikname].enable
                if iken:
                    ikbtntxt = ikname + ": <color=#00ff00>ON</color>"
                else:
                    ikbtntxt = ikname + ": <color=#ff0000>OFF</color>"
                if GUILayout.Button(ikbtntxt):
                    motionCtrl.ikSettingMap[ikname].enable = not iken
                    motionCtrl.buildWorkBoneIndex()
                    upd = True
                GUILayout.Space(5)
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # Bone group ON/OFF
            paired = 0
            for bskey in motionCtrl.boneGroupList:
                if not motionCtrl.boneGroupSet.has_key(bskey):
                    continue
                contentBone = 0
                enabledBone = 0
                for vmdBone in motionCtrl.boneGroupSet[bskey]:
                    bDefine = motionCtrl.boneDefineMap[motionCtrl.vmd2BoneMap[vmdBone]]
                    if motionCtrl.boneKeyFrames.has_key(vmdBone):
                        contentBone += 1
                    if bDefine.enable:
                        enabledBone += 1
                if contentBone == 0:
                    continue
                if paired % 2 == 0:
                    GUILayout.BeginHorizontal()
                    if paired == 0:
                        GUILayout.Space(10)
                        GUILayout.Label("Group On/Off:", GUILayout.Width(titleLen))
                    else:
                        GUILayout.Space(10)
                        GUILayout.Label(" ", GUILayout.Width(titleLen))
                else:
                    GUILayout.Space(5)
                if enabledBone == 0:
                    bsbtntxt = motionCtrl.boneGroupName[bskey] + ": <color=#ff0000>OFF</color>"
                elif enabledBone == len(motionCtrl.boneGroupSet[bskey]):
                    bsbtntxt = motionCtrl.boneGroupName[bskey] + ": <color=#00ff00>ON</color>"
                else:
                    bsbtntxt = motionCtrl.boneGroupName[bskey] + ": <color=#ffa500>PARTLY ON</color>"
                if GUILayout.Button(bsbtntxt):
                    for vmdBone in motionCtrl.boneGroupSet[bskey]:
                        bname = motionCtrl.vmd2BoneMap[vmdBone]
                        bDefine = motionCtrl.boneDefineMap[bname]
                        bDefine.enable = True if enabledBone == 0 else False
                        if not bDefine.enable:
                            motionCtrl.restoreWeightBone(bname)
                    motionCtrl.bindWeightBone()
                    motionCtrl.buildWorkBoneIndex()
                    upd = True
                if paired % 2 == 1:
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                paired += 1
            if paired % 2 == 1:
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            # Twist disperse
            twistGroupName = []
            twistGroupInfo = {}
            for tdi in motionCtrl.twistDisperse:
                if tdi.group not in twistGroupName:
                    twistGroupName.append(tdi.group)
                    twistGroupInfo[tdi.group] = [tdi.group, tdi.enable, tdi.disperseRate]
            for i in range(len(twistGroupName)):
                GUILayout.BeginHorizontal()
                if i == 0:
                    GUILayout.Space(10)
                    GUILayout.Label("Twist Disperse:", GUILayout.Width(titleLen))
                else:
                    GUILayout.Space(10)
                    GUILayout.Label(" ", GUILayout.Width(titleLen))
                tdgi = twistGroupInfo[twistGroupName[i]]
                enable = GUILayout.Toggle(tdgi[1], tdgi[0], GUILayout.Width(100))
                if enable != tdgi[1]:
                    for tdi in motionCtrl.twistDisperse:
                        if tdi.group == tdgi[0]:
                            tdi.enable = enable
                    upd = True
                if enable:
                    GUILayout.Space(10)
                    rateInput = GUILayout.HorizontalSlider(tdgi[2], 0, 1.0, GUILayout.Width(100))
                    if "%.2f"%rateInput != "%.2f"%tdgi[2]:
                        for tdi in motionCtrl.twistDisperse:
                            if tdi.group == tdgi[0]:
                                tdi.disperseRate = rateInput
                        upd = True
                    rateTxt = GUILayout.TextField("%.2f"%tdgi[2], GUILayout.Width(35))
                    if rateTxt != "%.2f"%tdgi[2]:
                        try:
                            rateInput = float(rateTxt)
                            if 0 <= rateInput <= 1.0:
                                for tdi in motionCtrl.twistDisperse:
                                    if tdi.group == tdgi[0]:
                                        tdi.disperseRate = rateInput
                                upd = True
                        except:
                            pass
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            # Interpolate setting
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label("Interpolate:", GUILayout.Width(titleLen))
            motionCtrl.interpolateConsecutive = GUILayout.Toggle(motionCtrl.interpolateConsecutive, "Enable for consecutive keyframes")
            GUILayout.EndHorizontal()
            # Bonename display
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label("Bone Name:", GUILayout.Width(titleLen))
            if GUILayout.Toggle(mmddConfig.boneNameDisplay == "dummy", LC("Dummy name")):
                mmddConfig.boneNameDisplay = "dummy"
            if GUILayout.Toggle(mmddConfig.boneNameDisplay == "mmd", LC("MMD name")):
                mmddConfig.boneNameDisplay = "mmd"
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # Bone list mode
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label("Bone List:", GUILayout.Width(titleLen))
            if GUILayout.Toggle(mmddConfig.boneListMode == "group", LC("By Group")):
                mmddConfig.boneListMode = "group"
            if GUILayout.Toggle(mmddConfig.boneListMode == "sequence", LC("By Sequence")):
                mmddConfig.boneListMode = "sequence"
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # Bone hide
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label("Hide Bone:", GUILayout.Width(titleLen))
            mmddConfig.hideBone["non-opt"] = GUILayout.Toggle(mmddConfig.hideBone["non-opt"], LC("Hide non-operational bones"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label(" ", GUILayout.Width(titleLen))
            mmddConfig.hideBone["no-frm"] = GUILayout.Toggle(mmddConfig.hideBone["no-frm"], LC("Hide bones without any keyframe"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label(" ", GUILayout.Width(titleLen))
            mmddConfig.hideBone["1-frm"] = GUILayout.Toggle(mmddConfig.hideBone["1-frm"], LC("Hide bones with only 1 keyframe"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label(" ", GUILayout.Width(titleLen))
            mmddConfig.hideBone["2-frm"] = GUILayout.Toggle(mmddConfig.hideBone["2-frm"], LC("Hide bones with only 2 keyframes"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(10)
            GUILayout.Label(" ", GUILayout.Width(titleLen))
            mmddConfig.hideBone["finger"] = GUILayout.Toggle(mmddConfig.hideBone["finger"], LC("Hide finger bones"))
            GUILayout.EndHorizontal()
            # Bone define filename
            GUILayout.BeginHorizontal() 
            GUILayout.Space(10)
            GUILayout.Label("Define File:", GUILayout.Width(titleLen))
            GUILayout.Label(os.path.split(motionCtrl.boneDefineFilename)[1])
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUILayout.EndScrollView()
            if upd:
                motionCtrl.setFrame(mmdd.curFrame)
                mmdd.refreshBind()
        elif sd.MotionSetup_Page == "adjust":
            if sd.MotionSetup_SelectedBoneName == "":
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Select a bone to setup adjustment."))
                GUILayout.EndHorizontal()                
            else:
                motionUpd = False
                adjustUpd = False
                # rotation title                
                GUILayout.BeginHorizontal() 
                GUILayout.Space(10)
                GUILayout.Label("Rotate Adjust", GUILayout.Width(120))
                GUILayout.Label("Axis Adjust", GUILayout.Width(130))
                GUILayout.Label("Offset Adjust", GUILayout.Width(110))
                GUILayout.EndHorizontal()
                # adjust controls
                for n in range(3):
                    if sd.MotionSetup_RotateAdj[n].paint():
                        newRotAdjV = Vector3(sd.MotionSetup_RotateAdj[0].value, sd.MotionSetup_RotateAdj[1].value, sd.MotionSetup_RotateAdj[2].value)
                        sd.MotionSetup_SelectedBoneAdjust.SetRotAdjustment(newRotAdjV)
                        adjustUpd = True
                for n in range(3,6):
                    if sd.MotionSetup_RotateAdj[n].paint():
                        newAxisAdjV = Vector3(sd.MotionSetup_RotateAdj[3].value, sd.MotionSetup_RotateAdj[4].value, sd.MotionSetup_RotateAdj[5].value)
                        sd.MotionSetup_SelectedBoneAdjust.SetAxisAdjustment(newAxisAdjV)
                        adjustUpd = True
                for n in range(3):
                    if sd.MotionSetup_OffsetAdj[n].paint():
                        newBaseV = Vector3(sd.MotionSetup_OffsetAdj[0].value, sd.MotionSetup_OffsetAdj[1].value, sd.MotionSetup_OffsetAdj[2].value)
                        sd.MotionSetup_SelectedBoneAdjust.basePosition = newBaseV
                        adjustUpd = True
                # Scale
                if sd.MotionSetup_RotateScaleAdj.paint():
                    sd.MotionSetup_SelectedBoneAdjust.SetRotScale(sd.MotionSetup_RotateScaleAdj.value)
                    adjustUpd = True
                if sd.MotionSetup_OffsetScaleAdj.paint():
                    sd.MotionSetup_SelectedBoneAdjust.SetPosLocalScale(sd.MotionSetup_OffsetScaleAdj.value)
                    adjustUpd = True
                # symmetrical/Auto Axis Adjust/Revert
                GUILayout.Space(95)
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                sd.MotionSetup_Symme = GUILayout.Toggle(sd.MotionSetup_Symme, "Symmetrical")
                GUILayout.Space(10)
                raa = GUILayout.Toggle(sd.MotionSetup_SelectedBoneAdjust.rotAxisAuto, "Auto Axis Adjust", GUILayout.Width(140))
                if raa != sd.MotionSetup_SelectedBoneAdjust.rotAxisAuto:
                    sd.MotionSetup_SelectedBoneAdjust.rotAxisAuto = raa
                    adjustUpd = True
                GUILayout.Space(10)
                needRevert = sd.MotionSetup_SelectedBoneName in motionCtrl.boneAdjustDirtySet
                if GUILayout.Button(cyanText(LC("Restore Default")) if needRevert else greyText(LC("Restore Default"))) and needRevert:
                    motionCtrl.boneAdjustMap[sd.MotionSetup_SelectedBoneName].setAll(motionCtrl.boneAdjustMapBackup[sd.MotionSetup_SelectedBoneName])
                    motionCtrl.boneAdjustDirtySet.discard(sd.MotionSetup_SelectedBoneName)
                    if sd.MotionSetup_Symme and isSymmePair(sd.MotionSetup_SelectedBoneName):
                        sbname = getSymmePair(sd.MotionSetup_SelectedBoneName)
                        motionCtrl.boneAdjustMap[sbname].setAll(motionCtrl.boneAdjustMapBackup[sbname])
                        motionCtrl.boneAdjustDirtySet.discard(sbname)
                    motionUpd = True
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                # input->output info
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(sd.MotionSetup_RotationInfo)
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(sd.MotionSetup_PositionInfo)
                GUILayout.EndHorizontal()
                # update
                if adjustUpd:
                    motionCtrl.boneAdjustDirtySet.add(sd.MotionSetup_SelectedBoneName)
                    if sd.MotionSetup_Symme and isSymmePair(sd.MotionSetup_SelectedBoneName):
                        sbname = getSymmePair(sd.MotionSetup_SelectedBoneName)
                        boneAdj = sd.MotionSetup_SelectedBoneAdjust
                        sboneAdj = motionCtrl.boneAdjustMap[sbname]
                        sboneAdj.SetRotAdjustment(Vector3(boneAdj.rotAdjustmentVec.x, -boneAdj.rotAdjustmentVec.y, -boneAdj.rotAdjustmentVec.z))
                        sboneAdj.SetAxisAdjustment(Vector3(boneAdj.rotAxisAdjustmentVec.x, -boneAdj.rotAxisAdjustmentVec.y, -boneAdj.rotAxisAdjustmentVec.z))
                        sboneAdj.SetRotScale(boneAdj.rotationScale)
                        sboneAdj.rotAxisAuto = boneAdj.rotAxisAuto
                        sboneAdj.basePosition = Vector3(-boneAdj.basePosition.x, boneAdj.basePosition.y, boneAdj.basePosition.z)
                        sboneAdj.SetPosLocalScale(boneAdj.positionLocalScale)
                        motionCtrl.boneAdjustDirtySet.add(sbname)
                    motionUpd = True
                if motionUpd:
                    motionCtrl.autoBoneAxisAdjust()
                    motionCtrl.setFrame(mmdd.curFrame)
                    mmdd.refreshBind()
                    refreshBoneAdj()
                    refreshFrameInfo()
        elif sd.MotionSetup_Page == "info":
            upd = False
            def redOrGreen(val):
                if val:
                    return greenText(str(val))
                else:
                    return redText(str(val))
            def jumpToBone(bname):
                if GUILayout.Button(yellowText(bname)):
                    sd.MotionSetup_SelectedBoneName = bname
                    showGuideGizmos()
            if len(sd.MotionSetup_SelectedBoneName) > 0:
                bDefine = motionCtrl.boneDefineMap[sd.MotionSetup_SelectedBoneName]
                # name info
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Dummy bone: %s, in group: %s, status:")%(magentaText(sd.MotionSetup_SelectedBoneName), redText("None") if not bDefine.group else greenText(motionCtrl.boneGroupName[bDefine.group])))
                if GUILayout.Button(greenText("ENABLED") if bDefine.enable else redText("DISABLED")):
                    bDefine.enable = not bDefine.enable
                    if not bDefine.enable:
                        motionCtrl.restoreWeightBone(sd.MotionSetup_SelectedBoneName)
                    motionCtrl.bindWeightBone()
                    motionCtrl.buildWorkBoneIndex()
                    upd = True
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                # map info
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Map to MMD bone: %s, studio object bone: %s")%(redOrGreen(bDefine.vmdBone), redOrGreen(bDefine.weightBone)))
                GUILayout.EndHorizontal()
                # parent, symmetrical
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Parent bone:"))
                if bDefine.parent:
                    jumpToBone(bDefine.parent)
                else:
                    GUILayout.Label(redText("None"))
                GUILayout.Label(LC(", symmetrical bone:"))
                if isSymmePair(sd.MotionSetup_SelectedBoneName):
                    jumpToBone(getSymmePair(sd.MotionSetup_SelectedBoneName))
                else:
                    GUILayout.Label(redText("None"))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                # children
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Child bone:"))
                if len(bDefine.child):
                    for childbone in bDefine.child:
                        jumpToBone(childbone)
                else:
                    GUILayout.Label(redText("None"))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                # position, rotation
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Set position: %s, Set rotation: %s")%(redOrGreen(bDefine.isPosition), redOrGreen(bDefine.isRotation)))
                GUILayout.EndHorizontal()
                # IK, Track, limitAxis
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Is IK bone: %s, is track bone: %s, has limit axis: %s")%(redOrGreen(bDefine.isIK), redOrGreen(bDefine.isTrack), redOrGreen(bDefine.isLimitAxis)))
                GUILayout.EndHorizontal()
                # IK detail
                if bDefine.isIK:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(10)
                    GUILayout.Label("IK Chain:")
                    for ikcbone in bDefine.ikData:
                        jumpToBone(ikcbone)
                        if ikcbone != bDefine.ikData[-1]:
                            GUILayout.Label(u"\u2190")
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                # Track detail
                if bDefine.isTrack:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(10)
                    GUILayout.Label("Track mode: %s, rate: %s, target: "%(greenText("Position" if bDefine.trackData[0] == "p" else "Rotation"), greenText("%.1f"%bDefine.trackData[1])))
                    jumpToBone(bDefine.trackData[2])
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                # limitAxis detail
                if bDefine.isLimitAxis:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(10)
                    GUILayout.Label("Limit axis: (<color=#00ff00>%.2f, %.2f, %.2f</color>), reference axis: (<color=#00ff00>%.2f, %.2f, %.2f</color>)"%(bDefine.limitAxisData[0].x, bDefine.limitAxisData[0].y, bDefine.limitAxisData[0].z, bDefine.limitAxisData[1].x, bDefine.limitAxisData[1].y, bDefine.limitAxisData[1].z))
                    GUILayout.EndHorizontal()
            else:
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(LC("Select a bone to check detail info."))
                GUILayout.EndHorizontal()
            if upd:
                motionCtrl.setFrame(mmdd.curFrame)
                mmdd.refreshBind()
        else:
            sd.MotionSetup_Page = "general"
        GUILayout.EndVertical()
        GUILayout.EndHorizontal()

    except:
        traceback.print_exc()
        quit(game)

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    gv = motionCtrl.boneGizmosVisible()
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button("Bake", btnstyle, GUILayout.Width(cbwidth)):
        def bakex(osmpl):
            motionCtrl.showBoneGizmos(False)
            guiWaitDialog(game, LC("Baking in process..."))
            game.updFuncParam = osmpl
            game.updFunc = bakewait
        def bakewait(param):
            game.updFuncParam = param
            game.updFunc = bakedo
        def bakedo(param):
            motionCtrl.bakeFrame(param)
            motionCtrl.setFrame(mmdd.curFrame)
            clearGuideGizmos()
            mmddGuiMain(game)
        def bake1(g):
            bakex(1)
        def bake2(g):
            bakex(2)
        def bake4(g):
            bakex(4)
        msg = LC("Do you want to bake frames?\n\nBaked frames is faster when play but can not be adjusted until baked data dismissed.\nSelect an oversampling rate to start. More oversampling get better animation but takes more time.")
        guiSelectDialog(game, (msg, ("Bake 1x", bake1), ("Bake 2x", bake2), ("Bake 4x", bake4), ("Cancel", None)))
    if GUILayout.Button("<color=#00ff00>Gizmos</color>" if gv else "Gizmos", btnstyle, GUILayout.Width(cbwidth)):
        motionCtrl.showBoneGizmos(not gv)
    if GUILayout.Button("Keyframes", btnstyle, GUILayout.Width(cbwidth)):
        clearGuideGizmos()
        mmddGuiMotionKeyframe(game, motionCtrl)
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
        clearGuideGizmos()
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

def mmddGuiMotionKeyframe(game, motionCtrl):
    from libkfguictrl import KeyframeViewer, DragInputUnit, CurveEditor
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    ctrl, alt, shift = unity_util.metakey_state()

    def initKeyframeUI():
        # cood const
        kfx = 10
        kfy = 60
        kfw = fullw - 110
        kfh = fullh - 100
        bth = 20
        bcw = 105
        bcx = kfx + kfw + 5
        xColor = Color(1, 0.2, 0.2, 1)
        yColor = Color(0.5, 1, 0.5, 1)
        zColor = Color(0.2, 0.5, 1, 1)
        xyzxyzColor = (xColor, yColor, zColor, xColor, yColor, zColor)
        try:
            # init keyframe viewer
            if not hasattr(sd, "MotionKF_KeyframeViews"):
                sd.MotionKF_KeyframeViews = {}
            else:
                for mc in sd.MotionKF_KeyframeViews.keys():
                    if mc not in mmdd.motionControllers.values():
                        del sd.MotionKF_KeyframeViews[mc]
            kfRect = Rect(kfx, kfy, kfw, kfh)
            if motionCtrl not in sd.MotionKF_KeyframeViews:
                sd.MotionKF_KeyframeViews[motionCtrl] = KeyframeViewer(kfRect)
                setupKeyframeView(motionCtrl)
            else:
                sd.MotionKF_KeyframeViews[motionCtrl].resize(kfRect, None)
                updateKeyframeView(motionCtrl, True)
            sd.activedKeyframeView = sd.MotionKF_KeyframeViews[motionCtrl]
            # keyframe opt ui mode
            sd.MotionKF_KeyframeOptUiMode = "normal"
            # init keyframe normal ui
            kfBtnRect = Rect(kfx, kfy + kfh + 2, kfw, bth)
            kfBtnContents = ([Rect.zero, LC("Copy"), whenHasKeyframeSelected, keyframeCopy, False], 
                            [Rect.zero, LC("Paste"), whenKeyframeClipboard, keyframePaste, False], 
                            [Rect.zero, LC("Reverse-P"), whenKeyframeClipboard, keyframeRevPaste, False], 
                            [Rect.zero, LC("V-Select"), whenHasKeyframeSelected, keyframeVSelect, False], 
                            [Rect.zero, LC("Delete"), whenHasKeyframeSelected, keyframeDelete, False], 
                            [Rect.zero, LC("Range-Sel"), whenStopped, keyframeRangeSelect, False])
            kfBtnCount = len(kfBtnContents)
            kfBtnGap = 1
            kfBtnWidth = (kfw - (kfBtnCount - 1) * kfBtnGap) / kfBtnCount
            for i in range(kfBtnCount):
                kfBtnContents[i][0].Set(kfBtnRect.x + i * (kfBtnWidth + kfBtnGap), kfBtnRect.y, kfBtnWidth, kfBtnRect.height)
                if i == kfBtnCount - 1:
                    kfBtnContents[i][0].xMax = kfBtnRect.xMax + 1
            sd.MotionKF_KeyframeOptNormalUI = kfBtnContents
            # init keyframe rngsel ui
            sd.MotionKF_KeyframeOptRngSelUI = (
                Rect(kfBtnRect.x, kfBtnRect.y, 110, kfBtnRect.height),    # 0 "Selected Bones"
                Rect(kfBtnRect.x, kfBtnRect.y,  70, kfBtnRect.height),    # 1 "All Bones"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 2 "From"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 3 From-input
                Rect(kfBtnRect.x, kfBtnRect.y,  30, kfBtnRect.height),    # 4 "To"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 5 To-input
                Rect(kfBtnRect.x, kfBtnRect.y,   0, kfBtnRect.height),    # 6 space
                Rect(kfBtnRect.x, kfBtnRect.y,  60, kfBtnRect.height),    # 7 "Select"
                Rect(kfBtnRect.x, kfBtnRect.y,  25, kfBtnRect.height),    # 8 "X"
            )
            fixWidth = 8*kfBtnGap
            for i in range(9):
                fixWidth += sd.MotionKF_KeyframeOptRngSelUI[i].width
            sd.MotionKF_KeyframeOptRngSelUI[6].width = kfBtnRect.width - fixWidth + 1
            for i in range(1,9):
                sd.MotionKF_KeyframeOptRngSelUI[i].x = sd.MotionKF_KeyframeOptRngSelUI[i-1].xMax + kfBtnGap
            sd.MotionKF_KeyframeOptRngSelBone = "selected"
            sd.MotionKF_KeyframeOptRngStart = ""
            sd.MotionKF_KeyframeOptRngEnd = ""
            # init keyframe expand ui
            # bone/curve opt ui mode
            bFullHeight = (bth + 1 + bth * 10 + 4)
            bcFullHeight = bFullHeight + (bth + 1 + bcw + 20 * 3 + 2 * 3) - 3
            if kfh + bth > bcFullHeight:
                sd.MotionKF_BoneCurveOptUiMode = "both"
            else:
                sd.MotionKF_BoneCurveOptUiMode = "bone"
            boneCurveSelContents = ([Rect.zero, LC("Bone"), True, "bone"], [Rect.zero, LC("Curve"), True, "curve"])
            if sd.MotionKF_BoneCurveOptUiMode == "both":
                boneCurveSelContents[0][0].Set(bcx, kfy, bcw, bth)
                boneCurveSelContents[1][0].Set(bcx, kfy + bFullHeight, bcw, bth)
                boneCurveSelContents[0][2] = False
                boneCurveSelContents[1][2] = False
            else:
                boneCurveSelContents[1][1] = LC("C")
                boneCurveSelContents[0][0].Set(bcx, kfy, bcw-25, bth)
                boneCurveSelContents[1][0].Set(bcx + bcw - 25 + 1, kfy, 25, bth)
                boneCurveSelContents[0][2] = True
                boneCurveSelContents[1][2] = True
            sd.MotionKF_BoneCurveSelUI = boneCurveSelContents
            # init bone adj ui
            diuRect = Rect(0, 0, bcw, bth)
            sd.MotionKF_BoneAdjUiText = (LC("Pos X"), LC("Pos Y"), LC("Pos Z"), LC("Rot X"), LC("Rot Y"), LC("Rot Z"))
            boneAdjContents = []
            for i in range(6):
                diunit = DragInputUnit(diuRect, sd.MotionKF_BoneAdjUiText[i], False)
                diunit.color = xyzxyzColor[i]
                diunit.onInputChangeCallback = boneAdjChange
                if i in (0, 1, 2):
                    diunit.setDragRate(mmddConfig.dragIntensityPosX, mmddConfig.dragIntensityPosY)
                else:
                    diunit.setDragRate(mmddConfig.dragIntensityRotX, mmddConfig.dragIntensityRotY)
                    diunit.setValueFormat(-180, 180, True, "%.1f")
                boneAdjContents.append(diunit)
            sd.MotionKF_BoneAdjUI = tuple(boneAdjContents)
            boneOptContents = ([Rect.zero, LC("Copy"), whenHasBoneSelected, boneCopy, False],
                            [Rect.zero, LC("Paste"), whenBoneClipboard, bonePaste, False],
                            [Rect.zero, LC("Rev-P"), whenBoneClipboard, boneRevPaste, False],
                            [Rect.zero, LC("Reset"), whenHasBoneSelected, boneReset, True, LC("Revert"), whenHasBoneSelected, boneRevert],
                            [Rect.zero, LC("Select-Unreg"), whenStopped, boneSelectUnreg, True, LC("Select-All"), whenStopped, boneSelectAll],
                            [Rect.zero, LC("Register"), whenHasBoneSelected, boneRegister, False])
            sd.MotionKF_BoneOptUI = boneOptContents
            setupBoneInput(sd.MotionKF_KeyframeViews[motionCtrl])
            bacOffY = boneCurveSelContents[0][0].yMax + 1
            for i in range(6):
                diuRect = Rect(bcx, bacOffY + i * bth, bcw, bth)
                boneAdjContents[i].resize(diuRect, False)
            bocOffY = boneCurveSelContents[0][0].yMax + bth * 6 + 2
            boneOptContents[0][0].Set(bcx, bocOffY, bcw/2, bth)
            boneOptContents[1][0].Set(bcx + bcw/2 + 1, bocOffY, bcw/2, bth)
            boneOptContents[2][0].Set(bcx, bocOffY + bth, bcw/2, bth)
            boneOptContents[3][0].Set(bcx + bcw/2 + 1, bocOffY + bth, bcw/2, bth)
            boneOptContents[4][0].Set(bcx, bocOffY + bth * 2, bcw, bth)
            boneOptContents[5][0].Set(bcx, bocOffY + bth * 3, bcw, bth)
            # init bone adj clipboard
            if not hasattr(sd, "MotionKF_BoneClipboard"):
                sd.MotionKF_BoneClipboard = {}
            # init keyframe clipboard
            if not hasattr(sd, "MotionKF_KeyframeClipboard"):
                sd.MotionKF_KeyframeClipboard = set()
            # init curve adj ui
            cecOffY = boneCurveSelContents[1][0].yMax + 1
            if not hasattr(sd, "MotionKF_CurveEdit"):
                from vmdlib import VmdBoneKeyFrame
                sd.MotionKF_CurveEdit = CurveEditor(bcx, cecOffY, bcw, VmdBoneKeyFrame.curveSetName)
                sd.MotionKF_CurveEdit.changeColors(mmddConfig.curveColors)
            else:
                sd.MotionKF_CurveEdit.resize(bcx, cecOffY, bcw)
            sd.MotionKF_CurveEdit.onCurveUpdateCallback = curveChange
            # update frame
            onFrameChange(int(mmdd.curFrame))
        except:
            traceback.print_exc()
            quit(game)
    def setupKeyframeView(mc):
        kfView = sd.MotionKF_KeyframeViews[mc]
        kfView.motionCtrl = mc
        kfView.NameSet = ""
        kfView.changeColors(mmddConfig.keyframeColors)
        boneBelongMap = {}
        # clear up to start
        kfView.clearKeyframe()
        # setup known namelist, always use vmd name
        for gKey in mc.boneGroupList:
            if gKey in mc.boneGroupSet:
                for vName in mc.boneGroupSet[gKey]:
                    boneBelongMap[vName] = gKey
                    kfView.importFrameName(mc.boneGroupName[gKey], vName)
        for bname in mc.boneNameList:
            vName = mc.boneDefineMap[bname].vmdBone
            if vName not in boneBelongMap:
                kfView.importFrameName(mc.boneGroupName[mc.boneGroupList[-2]], vName)
        # import keyframes
        for kfname in mc.boneKeyFrames.keys():
            if kfname in boneBelongMap:
                gKey = boneBelongMap[kfname]
            elif kfname in mc.vmd2BoneMap:
                gKey = mc.boneGroupList[-2]
            else:
                gKey = mc.boneGroupList[-1]
                continue
            for kf in mc.boneKeyFrames[kfname]:
                kfView.importKeyframe(mc.boneGroupName[gKey], kfname, kf.frameNo, kf)
        # update nameset and hide setting
        updateKeyframeView(mc, False)
        # done import
        kfView.arrangeImportedKeyframes()
        # setup callback
        kfView.onFrameChangeCallback = frameChange
        kfView.onRowSelecCallback = rowSelect
        kfView.onKeyframeMoveCallback = keyframeMove
        kfView.onKeyframeSelectCallback = keyframeSelect
    def updateKeyframeView(mc, refresh):
        kfView = sd.MotionKF_KeyframeViews[mc]
        # check name set change
        if kfView.NameSet != mmddConfig.boneNameDisplay:
            kfView.NameSet = mmddConfig.boneNameDisplay
            for bname in mc.boneNameList:
                vname = mc.boneDefineMap[bname].vmdBone
                kfView.keyframeNameList.dispNameDic[vname] = bname if kfView.NameSet == "dummy" else vname
        # check hide setting
        kfView.keyframeNameList.hiddenNames.clear()
        for bname in motionCtrl.boneNameList:
            if isHiddenBone(bname):
                vname = mc.boneDefineMap[bname].vmdBone
                kfView.keyframeNameList.hiddenNames.add(vname)
        # clear select bone if hidden
        selKeys = getCurSelectedBoneKeys(kfView)
        newSelKeys = []
        for bKey in selKeys:
            vmdBoneName = bKey.split("#")[1]
            dmyBoneName = motionCtrl.vmd2BoneMap[vmdBoneName]
            if not isHiddenBone(dmyBoneName):
                newSelKeys.append(bKey)
        if len(selKeys) != len(newSelKeys):
            kfView.selectBonesByKey(newSelKeys)
            rowSelect(kfView)
        # refresh
        if refresh:
            kfView.refreshKeyframe()
    def setupBoneInput(kfView):
        selKeys = getCurSelectedBoneKeys(kfView)
        if len(selKeys) == 1:
            bKey = selKeys[0]
            vmdBoneName = bKey.split("#")[1]
            dmyBoneName = motionCtrl.vmd2BoneMap[vmdBoneName]
            bPose = motionCtrl.poseData[dmyBoneName]
            sd.MotionKF_CurSelBoneInfo = (dmyBoneName, vmdBoneName, bPose)
        else:
            sd.MotionKF_CurSelBoneInfo = None
        sd.MotionKF_CurSelBonesKeyframeSetCache = None
        updateBoneInput()
    def updateBoneInput():
        if not mmdd.isPlaying and sd.MotionKF_CurSelBoneInfo != None:
            vmdBoneName = sd.MotionKF_CurSelBoneInfo[1]
            bPose = sd.MotionKF_CurSelBoneInfo[2]
            frameNo = int(mmdd.curFrame)
            if vmdBoneName in sd.MotionKF_DirtyFrameCache:
                vmdBoneFrame = sd.MotionKF_DirtyFrameCache[vmdBoneName]
            else:
                vmdBoneFrame = motionCtrl.getVmdBoneFrame(frameNo, vmdBoneName)
            if vmdBoneFrame:
                # position
                sd.MotionKF_BoneAdjUI[0].setValue(vmdBoneFrame.position.x)
                sd.MotionKF_BoneAdjUI[1].setValue(vmdBoneFrame.position.y)
                sd.MotionKF_BoneAdjUI[2].setValue(vmdBoneFrame.position.z)
                sd.MotionKF_BoneAdjUI[0].enable = bPose[1] != None
                sd.MotionKF_BoneAdjUI[1].enable = bPose[1] != None
                sd.MotionKF_BoneAdjUI[2].enable = bPose[1] != None
                # rotation
                sd.MotionKF_BoneAdjUI[3].setValue(vmdBoneFrame.rotation.eulerAngles.x)
                sd.MotionKF_BoneAdjUI[4].setValue(vmdBoneFrame.rotation.eulerAngles.y)
                sd.MotionKF_BoneAdjUI[5].setValue(vmdBoneFrame.rotation.eulerAngles.z)
                sd.MotionKF_BoneAdjUI[3].enable = bPose[0] != None
                sd.MotionKF_BoneAdjUI[4].enable = bPose[0] != None
                sd.MotionKF_BoneAdjUI[5].enable = bPose[0] != None
            else:
                # no keyframe for selected bone, clear value
                for boneInput in sd.MotionKF_BoneAdjUI:
                    boneInput.setValue(0)
                    boneInput.enable = False
        else:
            # can not adjust bone, keep value
            for boneInput in sd.MotionKF_BoneAdjUI:
                boneInput.enable = False
    def updateCurveInput(selectedKeyframes):
        sd.MotionKF_CurveEdit.clearCurveData()
        for skf in selectedKeyframes:
            sd.MotionKF_CurveEdit.importCurveData(skf.curveSet, False)
        sd.MotionKF_CurveEdit.selectTargetCurve()
        sd.MotionKF_CurveEdit.curveNeedRepaint = True
    # keyframe view and bone input unit callback
    def frameChange(kfView):
        mmdd.gotoFrame(kfView.curFrame)
        onFrameChange(kfView.curFrame)
    def rowSelect(kfView):
        setupBoneInput(kfView)
        switchGuideGizmos()
    def keyframeMove(kfView, keyMoveInfo):
        # UI is already processed
        for (bKey, oldFrame, newFrame, tgtObj) in keyMoveInfo:
            tgtObj.frameNo = newFrame
        motionCtrl.buildWorkBoneIndex()
        buildSelBonesKeyframesSetCache(getCurSelectedBoneKeys(kfView))
        mmdd.refreshTotalFrame()
        kfView.refreshKeyframe()
        rowSelect(kfView)
        motionCtrl.setFrame(kfView.curFrame)
        mmdd.refreshBind()
    def keyframeSelect(kfView):
        selectedKeyframes = set()
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            selectedKeyframes.add(kfView.keyframeBuffer[kfbtn.key][kfbtn.column])
        updateCurveInput(selectedKeyframes)
    def boneAdjChange(tgtAdj, value, delta):
        if not mmdd.isPlaying and sd.MotionKF_CurSelBoneInfo:
            bidx = sd.MotionKF_BoneAdjUiText.index(tgtAdj.text)
            dmyBoneName = sd.MotionKF_CurSelBoneInfo[0]
            vmdBoneName = sd.MotionKF_CurSelBoneInfo[1]
            bPose = sd.MotionKF_CurSelBoneInfo[2]
            dFrame = getDirtyFrame(vmdBoneName)
            if delta == None:   # direct input
                if bidx in (0, 1, 2):
                    newPos = Vector3(sd.MotionKF_BoneAdjUI[0].value, sd.MotionKF_BoneAdjUI[1].value, sd.MotionKF_BoneAdjUI[2].value)
                    dFrame.position = newPos
                    bPose[1] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedPosition(newPos)
                else:
                    newRot = Quaternion.Euler(sd.MotionKF_BoneAdjUI[3].value, sd.MotionKF_BoneAdjUI[4].value, sd.MotionKF_BoneAdjUI[5].value)
                    dFrame.rotation = newRot
                    bPose[0] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedRotation(newRot)
            else:   # drag input
                if bidx in (0, 1, 2):
                    newPos = Vector3(sd.MotionKF_BoneAdjUI[0].value, sd.MotionKF_BoneAdjUI[1].value, sd.MotionKF_BoneAdjUI[2].value)
                    dFrame.position = newPos
                    bPose[1] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedPosition(newPos)
                else:
                    newRot = Quaternion.Euler(sd.MotionKF_BoneAdjUI[3].value, sd.MotionKF_BoneAdjUI[4].value, sd.MotionKF_BoneAdjUI[5].value)
                    dFrame.rotation = newRot
                    bPose[0] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedRotation(newRot)
            motionCtrl.updatePose()
            mmdd.refreshBind()
    def curveChange(curveEditor):
        motionCtrl.keyframeCustomized = True
    # seekbar callback
    def onFrameChange(newFrameNo):
        sd.MotionKF_DirtyFrameCache = {}
        if not mmdd.isPlaying:
            kfView = sd.MotionKF_KeyframeViews[motionCtrl]
            if newFrameNo != kfView.curFrame:
                kfView.setCurrentFrame(newFrameNo)
            updateBoneInput()
    def onJumpToKeyframe(curFrame, direction):
        kfView = sd.MotionKF_KeyframeViews[motionCtrl]
        selKeys = getCurSelectedBoneKeys(kfView)
        if not mmdd.isPlaying and len(selKeys) > 0:
            if sd.MotionKF_CurSelBonesKeyframeSetCache:
                kfset = sd.MotionKF_CurSelBonesKeyframeSetCache
            else:
                kfset = buildSelBonesKeyframesSetCache(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
    # util
    def quitMotionKeyframeView():
        sd.activedKeyframeView = None
    def switchGuideGizmos():
        clearGuideGizmos()
        if sd.MotionKF_CurSelBoneInfo:
            from CharaAnime import CubeGizmos
            cubeSize = 0.20 if game.isNEOV2 else 0.020
            sd.MotionKF_LastGuideGizmos = CubeGizmos(motionCtrl.getVmdBone(sd.MotionKF_CurSelBoneInfo[0]).transform, cubeSize)
            mmdd.charaAnimePlugin.GizmosMgr.ShowGizmos(sd.MotionKF_LastGuideGizmos)
    def clearGuideGizmos():
        if sd.MotionKF_LastGuideGizmos:
            mmdd.charaAnimePlugin.GizmosMgr.RemoveGizmos(sd.MotionKF_LastGuideGizmos)
            sd.MotionKF_LastGuideGizmos = None
    def isHiddenBone(bName):
        bDefine = motionCtrl.boneDefineMap[bName]
        if mmddConfig.hideBone["non-opt"] and not bDefine.isRotation and not bDefine.isPosition:
            return True
        if mmddConfig.hideBone["finger"] and bName.startswith("fig_"):
            return True
        if bDefine.vmdBone in motionCtrl.boneKeyFrames:
            kc = len(motionCtrl.boneKeyFrames[bDefine.vmdBone])
        else:
            kc = 0
        if mmddConfig.hideBone["no-frm"] and kc == 0:
            return True
        if mmddConfig.hideBone["1-frm"] and kc == 1:
            return True
        if mmddConfig.hideBone["2-frm"] and kc == 2:
            return True
        return False
    def buildSelBonesKeyframesSetCache(selKeys):
        sd.MotionKF_CurSelBonesKeyframeSetCache = set()
        kfView = sd.MotionKF_KeyframeViews[motionCtrl]
        for bKey in selKeys:
            sd.MotionKF_CurSelBonesKeyframeSetCache.update(kfView.keyframeBuffer[bKey].keys())
        return sd.MotionKF_CurSelBonesKeyframeSetCache
    def getCurSelectedBoneKeys(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 getDirtyFrame(vmdBoneName):
        if vmdBoneName in sd.MotionKF_DirtyFrameCache:
            return sd.MotionKF_DirtyFrameCache[vmdBoneName]
        else:
            kfView = sd.MotionKF_KeyframeViews[motionCtrl]
            orgFrame = motionCtrl.getVmdBoneFrame(kfView.curFrame, vmdBoneName)
            if orgFrame == None:
                return None
            vmdBoneFrame = orgFrame.copy()
            vmdBoneFrame.frameNo = kfView.curFrame
            sd.MotionKF_DirtyFrameCache[vmdBoneName] = vmdBoneFrame
            return vmdBoneFrame
    def tryGetSymmetricVmdBone(vmdBoneName):
        dmyBoneName = motionCtrl.vmd2BoneMap[vmdBoneName]
        if dmyBoneName.endswith("_l"):
            dmyBoneName = dmyBoneName[:-1] + "r"
            vmdBoneName = motionCtrl.boneDefineMap[dmyBoneName].vmdBone
        elif dmyBoneName.endswith("_r"):
            dmyBoneName = dmyBoneName[:-1] + "l"
            vmdBoneName = motionCtrl.boneDefineMap[dmyBoneName].vmdBone
        return vmdBoneName, dmyBoneName
    def reversePosRot(pos, rot):
        rPos = Vector3(- pos.x, pos.y, pos.z)
        rRot = Quaternion.Euler(rot.eulerAngles.x, - rot.eulerAngles.y, - rot.eulerAngles.z)
        return rPos, rRot
    # operation button functions
    def renderOptButton(btnInfo, kfView):
        # btnInfo = [0 rect, 1 text, 2 enable-func, 3 call-func, 4 hasShift[, 5 shift-text, 6 shift-enable-func, 7 shift-call-func]]
        # 5 shift-text, 6 shift-enable-func, 7 shift-call-func only valid when 4 hasShift == True
        if btnInfo[4] and shift:
            # shift function
            btnValid = btnInfo[6](kfView)
            if GUI.Button(btnInfo[0], btnInfo[5] if btnValid else "<color=#808080>" + btnInfo[5] + "</color>") and btnValid:
                btnInfo[7](kfView)
        else:
            # normal function
            btnValid = btnInfo[2](kfView)
            if GUI.Button(btnInfo[0], btnInfo[1] if btnValid else "<color=#808080>" + btnInfo[1] + "</color>") and btnValid:
                btnInfo[3](kfView)
    def whenStopped(kfView):
        return not mmdd.isPlaying
    def whenBoneClipboard(kfView):
        return not mmdd.isPlaying and len(sd.MotionKF_BoneClipboard) > 0
    def whenHasBoneSelected(kfView):
        return not mmdd.isPlaying and len(getCurSelectedBoneKeys(kfView)) > 0
    def whenKeyframeClipboard(kfView):
        return not mmdd.isPlaying and len(sd.MotionKF_KeyframeClipboard) > 0
    def whenHasKeyframeSelected(kfView):
        return not mmdd.isPlaying and len(kfView.keyframeBox.selectedKeyframes) > 0
    def boneCopy(kfView):
        selKeys = getCurSelectedBoneKeys(kfView)
        sd.MotionKF_BoneClipboard.clear()
        for bKey in selKeys:
            vmdBoneName = bKey.split("#")[1]
            if vmdBoneName in sd.MotionKF_DirtyFrameCache:
                nFrame = sd.MotionKF_DirtyFrameCache[vmdBoneName]
            else:
                nFrame = motionCtrl.getVmdBoneFrame(kfView.curFrame, vmdBoneName)
            if nFrame:
                sd.MotionKF_BoneClipboard[bKey] = (nFrame.position, nFrame.rotation)
    def bonePaste(kfView):
        for bKey in sd.MotionKF_BoneClipboard.keys():
            (nPos, nRot) = sd.MotionKF_BoneClipboard[bKey]
            vmdBoneName = bKey.split("#")[1]
            dmyBoneName = motionCtrl.vmd2BoneMap[vmdBoneName]
            bPose = motionCtrl.poseData[dmyBoneName]
            dFrame = getDirtyFrame(vmdBoneName)
            if bPose[1]:
                dFrame.position = nPos
                bPose[1] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedPosition(nPos)
            if bPose[0]:
                dFrame.rotation = nRot
                bPose[0] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedRotation(nRot)
        kfView.selectBonesByKey(sd.MotionKF_BoneClipboard.keys())
        rowSelect(kfView)
        motionCtrl.updatePose()
        mmdd.refreshBind()
    def boneRevPaste(kfView):
        revBoneNames = []
        for bKey in sd.MotionKF_BoneClipboard.keys():
            (nPos, nRot) = sd.MotionKF_BoneClipboard[bKey]
            vmdBoneName = bKey.split("#")[1]
            vmdBoneName, dmyBoneName = tryGetSymmetricVmdBone(vmdBoneName)
            rPos, rRot = reversePosRot(nPos, nRot)
            bPose = motionCtrl.poseData[dmyBoneName]
            dFrame = getDirtyFrame(vmdBoneName)
            if bPose[1]:
                dFrame.position = rPos
                bPose[1] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedPosition(rPos)
            if bPose[0]:
                dFrame.rotation = rRot
                bPose[0] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedRotation(rRot)
            revBoneNames.append(vmdBoneName)
        kfView.selectBonesByName(revBoneNames)
        rowSelect(kfView)
        motionCtrl.updatePose()
        mmdd.refreshBind()
    def boneReset(kfView):
        for bKey in getCurSelectedBoneKeys(kfView):
            vmdBoneName = bKey.split("#")[1]
            dmyBoneName = motionCtrl.vmd2BoneMap[vmdBoneName]
            bPose = motionCtrl.poseData[dmyBoneName]
            dFrame = getDirtyFrame(vmdBoneName)
            if not dFrame: 
                continue
            if bPose[1]:
                dFrame.position = Vector3.zero
                bPose[1] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedPosition(Vector3.zero)
            if bPose[0]:
                dFrame.rotation = Quaternion.identity
                bPose[0] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedRotation(Quaternion.identity)
        for boneAdj in sd.MotionKF_BoneAdjUI:
            boneAdj.setValue(0)
        motionCtrl.updatePose()
        mmdd.refreshBind()
    def boneRevert(kfView):
        for bKey in getCurSelectedBoneKeys(kfView):
            vmdBoneName = bKey.split("#")[1]
            dmyBoneName = motionCtrl.vmd2BoneMap[vmdBoneName]
            bPose = motionCtrl.poseData[dmyBoneName]
            oldBoneFrame = motionCtrl.getVmdBoneFrame(kfView.curFrame, vmdBoneName)
            if vmdBoneName in sd.MotionKF_DirtyFrameCache:
                del sd.MotionKF_DirtyFrameCache[vmdBoneName]
            if bPose[1]:
                bPose[1] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedPosition(oldBoneFrame.position)
            if bPose[0]:
                bPose[0] = motionCtrl.boneAdjustMap[dmyBoneName].GetAdjustedRotation(oldBoneFrame.rotation)
        rowSelect(kfView)
        motionCtrl.updatePose()
        mmdd.refreshBind()
    def boneSelectUnreg(kfView):
        kfView.selectBonesByName(sd.MotionKF_DirtyFrameCache.keys())
        rowSelect(kfView)
    def boneSelectAll(kfView):
        allUnhideVmdBoneName = [motionCtrl.boneDefineMap[bname].vmdBone for bname in motionCtrl.boneNameList if not isHiddenBone(bname)]
        kfView.selectBonesByName(allUnhideVmdBoneName)
        rowSelect(kfView)
    def boneRegister(kfView):
        selKeys = getCurSelectedBoneKeys(kfView)
        hasNew = False
        newKeyframes = set()
        for bKey in selKeys:
            vmdBoneName = bKey.split("#")[1]
            orgFrame = motionCtrl.getVmdBoneFrame(kfView.curFrame, vmdBoneName)
            if vmdBoneName in sd.MotionKF_DirtyFrameCache:
                nFrame = sd.MotionKF_DirtyFrameCache.pop(vmdBoneName)
                if orgFrame:
                    nFrame.importCurveSet(orgFrame.curveSet)
            else:
                if orgFrame:
                    nFrame = orgFrame.copy()
                else:
                    from vmdlib import VmdBoneKeyFrame
                    nFrame = VmdBoneKeyFrame()
                    nFrame.name = vmdBoneName
                nFrame.frameNo = kfView.curFrame
            groupName = kfView.keyframeNameList.getGroup(vmdBoneName)
            # to UI
            hasNew |= kfView.registerKeyframe(groupName, vmdBoneName, kfView.curFrame, nFrame)
            # to MotionCtrl
            newKeyframes.add(nFrame)
        motionCtrl.updateKeyframes(newKeyframes)
        if hasNew:
            rowSelect(kfView)
            buildSelBonesKeyframesSetCache(selKeys)
            mmdd.refreshTotalFrame()
        kfView.refreshKeyframe()
    def keyframeCopy(kfView):
        sTime = time.time()
        sd.MotionKF_KeyframeClipboard.clear()
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        baseFrame = min([si[1] for si in selectInfo])
        for (kfKey, kfFrame) in selectInfo:
            orgFrame = kfView.keyframeBuffer[kfKey][kfFrame]
            sd.MotionKF_KeyframeClipboard.add((kfKey, kfFrame - baseFrame, orgFrame.copy()))
        print "done copy %d keyframes in %f sec"%(len(sd.MotionKF_KeyframeClipboard), time.time() - sTime)
    def keyframePaste(kfView):
        sTime = time.time()
        hasNew = False
        newKeyframes = set()
        newSelectKeyframeInfo = set() 
        for (kfKey, kfOffset, kfOrgFrame) in sd.MotionKF_KeyframeClipboard:
            groupName, vmdBoneName = kfKey.split("#")
            nFrame = kfOrgFrame.copy()
            nFrame.frameNo = kfView.curFrame + kfOffset
            # to UI
            hasNew |= kfView.registerKeyframe(groupName, vmdBoneName, nFrame.frameNo, nFrame)
            # to MotionCtrl
            newKeyframes.add(nFrame)
            # select later
            newSelectKeyframeInfo.add((kfKey, nFrame.frameNo))
        motionCtrl.updateKeyframes(newKeyframes)
        if hasNew:
            buildSelBonesKeyframesSetCache(getCurSelectedBoneKeys(kfView))
            mmdd.refreshTotalFrame()
        kfView.selectKeyframes(newSelectKeyframeInfo)
        kfView.refreshKeyframe()
        rowSelect(kfView)
        keyframeSelect(kfView)
        motionCtrl.setFrame(kfView.curFrame)
        mmdd.refreshBind()
        print "done paste %d keyframes in %f sec"%(len(sd.MotionKF_KeyframeClipboard), time.time() - sTime)
    def keyframeRevPaste(kfView):
        sTime = time.time()
        hasNew = False
        newKeyframes = set()
        newSelectKeyframeInfo = set() 
        for (kfKey, kfOffset, kfOrgFrame) in sd.MotionKF_KeyframeClipboard:
            groupName, vmdBoneName = kfKey.split("#")
            revVmdBoneName, dmy = tryGetSymmetricVmdBone(vmdBoneName)
            if revVmdBoneName != vmdBoneName:
                vmdBoneName = revVmdBoneName # new vmdBoneName
                groupName = kfView.keyframeNameList.getGroup(vmdBoneName) # new groupName
            nFrame = kfOrgFrame.copy()
            nFrame.name = vmdBoneName
            nFrame.frameNo = kfView.curFrame + kfOffset
            nFrame.position, nFrame.rotation = reversePosRot(nFrame.position, nFrame.rotation)
            # to UI
            hasNew |= kfView.registerKeyframe(groupName, vmdBoneName, nFrame.frameNo, nFrame)
            # to MotionCtrl
            newKeyframes.add(nFrame)
            # select later
            newSelectKeyframeInfo.add((groupName + "#" + vmdBoneName, nFrame.frameNo))
        motionCtrl.updateKeyframes(newKeyframes)
        if hasNew:
            buildSelBonesKeyframesSetCache(getCurSelectedBoneKeys(kfView))
            mmdd.refreshTotalFrame()
        kfView.selectKeyframes(newSelectKeyframeInfo)
        kfView.refreshKeyframe()
        rowSelect(kfView)
        keyframeSelect(kfView)
        motionCtrl.setFrame(kfView.curFrame)
        mmdd.refreshBind()
        print "done reverse paste %d keyframes in %f sec"%(len(sd.MotionKF_KeyframeClipboard), time.time() - sTime)
    def keyframeVSelect(kfView):
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        toSelectFrame = set()
        for (kfKey, kfFrame) in selectInfo:
            toSelectFrame.add(kfFrame)
        for kfFrame in toSelectFrame:
            for bKey in kfView.keyframeBuffer.keys():
                if kfFrame in kfView.keyframeBuffer[bKey]:
                    selectInfo.add((bKey, kfFrame))
        kfView.selectKeyframes(selectInfo)
        kfView.refreshKeyframe()
        keyframeSelect(kfView)
    def keyframeDelete(kfView):
        sTime = time.time()
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        kfc = len(selectInfo)
        sTime1 = time.time()
        # to MotionCtrl
        delInfoPair = set()
        for (kfKey, kfFrame) in selectInfo:
            groupName, vmdBoneName = kfKey.split("#")
            delInfoPair.add((vmdBoneName, kfFrame))
        motionCtrl.deleteKeyframes(delInfoPair)
        # UI
        sTime2 = time.time()
        kfView.deleteKeyframes(selectInfo)
        kfView.refreshKeyframe()
        buildSelBonesKeyframesSetCache(getCurSelectedBoneKeys(kfView))
        rowSelect(kfView)
        keyframeSelect(kfView)
        motionCtrl.setFrame(kfView.curFrame)
        mmdd.refreshTotalFrame()
        mmdd.refreshBind()
        sTime3 = time.time()
        print "done delete %d keyframes in %f sec, prep=%f, mc=%f, ui=%f"%(kfc, sTime3 - sTime, sTime1-sTime, sTime2-sTime1, sTime3-sTime2)
    def keyframeRangeSelect(kfView):
        sd.MotionKF_KeyframeOptUiMode = "rngsel"
        sd.MotionKF_KeyframeOptRngSelBone = "selected" if whenHasBoneSelected(kfView) else "all"
    def keyframeRangeSelectDo(kfView):
        sTime = time.time()
        if sd.MotionKF_KeyframeOptRngSelBone == "selected":
            bonekeys = getCurSelectedBoneKeys(kfView)
        else:
            bonekeys = kfView.keyframeBuffer.keys()
        if len(bonekeys) == 0:
            return False
        try:
            rngStart = int(sd.MotionKF_KeyframeOptRngStart)
        except:
            sd.MotionKF_KeyframeOptRngStart = ""
            return False
        try:
            rngEnd = int(sd.MotionKF_KeyframeOptRngEnd)
        except:
            sd.MotionKF_KeyframeOptRngEnd = ""
            return False
        if rngStart > rngEnd:
            sd.MotionKF_KeyframeOptRngEnd = ""
            return False
        selectInfo = set()
        for bKey in bonekeys:
            frameInRange = [f for f in kfView.keyframeBuffer[bKey].keys() if rngStart <= f <= rngEnd]
            for kfFrame in frameInRange:
                selectInfo.add((bKey, kfFrame))
        kfView.selectKeyframes(selectInfo)
        kfView.refreshKeyframe()
        keyframeSelect(kfView)
        print "done range-select %d keyframes in %f sec"%(len(selectInfo), time.time() - sTime)
        return True
    def keyframeExpand(kfView):
        # TODO
        pass
    # sepcial render
    def renderKeyframeRngSel(kfView):
        labelStyle = GUIStyle("label")
        labelStyle.alignment = TextAnchor.MiddleRight
        if GUI.Toggle(sd.MotionKF_KeyframeOptRngSelUI[0], sd.MotionKF_KeyframeOptRngSelBone == "selected", LC("Selected Bones")):
            sd.MotionKF_KeyframeOptRngSelBone = "selected"
        if GUI.Toggle(sd.MotionKF_KeyframeOptRngSelUI[1], sd.MotionKF_KeyframeOptRngSelBone == "all", LC("All Bones")):
            sd.MotionKF_KeyframeOptRngSelBone = "all"
        GUI.Label(sd.MotionKF_KeyframeOptRngSelUI[2], LC("From "), labelStyle)
        sd.MotionKF_KeyframeOptRngStart = GUI.TextField(sd.MotionKF_KeyframeOptRngSelUI[3], sd.MotionKF_KeyframeOptRngStart)
        GUI.Label(sd.MotionKF_KeyframeOptRngSelUI[4], LC("To "), labelStyle)
        sd.MotionKF_KeyframeOptRngEnd = GUI.TextField(sd.MotionKF_KeyframeOptRngSelUI[5], sd.MotionKF_KeyframeOptRngEnd)
        GUI.Label(sd.MotionKF_KeyframeOptRngSelUI[6], LC(" "))
        if GUI.Button(sd.MotionKF_KeyframeOptRngSelUI[7], LC("Select")):
            if keyframeRangeSelectDo(kfView):
                sd.MotionKF_KeyframeOptUiMode = "normal"
        if GUI.Button(sd.MotionKF_KeyframeOptRngSelUI[8], "X"):
            sd.MotionKF_KeyframeOptUiMode = "normal"

    # init switch
    if mmddGuiSwitch(game, mmddGuiMotionKeyframe, motionCtrl):
        sd.LastPlayingStatus = mmdd.isPlaying
        sd.MotionKF_LastGuideGizmos = None
        # setup ui
        initKeyframeUI()
        return
    
    # Seek bar
    guiPartsSeekbar(game, onFrameChange, onJumpToKeyframe)

    # keyframe view
    kfView = sd.MotionKF_KeyframeViews[motionCtrl]
    kfView.paint()

    # keyframe opt buttons
    if sd.MotionKF_KeyframeOptUiMode == "normal":
        for kfBtn in sd.MotionKF_KeyframeOptNormalUI:
            renderOptButton(kfBtn, kfView)
    elif sd.MotionKF_KeyframeOptUiMode == "rngsel":
        renderKeyframeRngSel(kfView)

    # bone/curve select
    for bcselBtn in sd.MotionKF_BoneCurveSelUI:
        if bcselBtn[3] == "bone" and sd.MotionKF_CurSelBoneInfo:
            bcselTitle = sd.MotionKF_CurSelBoneInfo[0] if kfView.NameSet == "dummy" else sd.MotionKF_CurSelBoneInfo[1]
        else:
            bcselTitle = bcselBtn[1]
        if bcselBtn[2]:
            if sd.MotionKF_BoneCurveOptUiMode == bcselBtn[3]:
                GUI.Box(bcselBtn[0], "<b>" + greenText(bcselTitle) + "</b>")
            else:
                if GUI.Button(bcselBtn[0], bcselTitle):
                    sd.MotionKF_BoneCurveOptUiMode = bcselBtn[3]
        else:
            GUI.Label(bcselBtn[0], "<b>" + greenText(bcselTitle) + "</b>")

    # curve and bone area
    if sd.MotionKF_BoneCurveOptUiMode in ("bone", "both"):
        for boneAdjUnit in sd.MotionKF_BoneAdjUI:
            boneAdjUnit.paint()
        for boneOptBtn in sd.MotionKF_BoneOptUI:
            renderOptButton(boneOptBtn, kfView)
    if sd.MotionKF_BoneCurveOptUiMode in ("curve", "both"):
        sd.MotionKF_CurveEdit.paint()

    # Control buttons
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    cbwidth = (fullw - 4*4) / 5
    gv = motionCtrl.boneGizmosVisible()
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button("Export" if not mmdd.isPlaying else " ", btnstyle, GUILayout.Width(cbwidth)) and not mmdd.isPlaying:
        guiExportVmdData(game, motionCtrl)
    if GUILayout.Button("<color=#00ff00>Gizmos</color>" if gv else "Gizmos", btnstyle, GUILayout.Width(cbwidth)):
        motionCtrl.showBoneGizmos(not gv)
    if GUILayout.Button("Setup", btnstyle, GUILayout.Width(cbwidth)):
        clearGuideGizmos()
        quitMotionKeyframeView()
        mmddGuiMotionSetup(game, motionCtrl)
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
        clearGuideGizmos()
        quitMotionKeyframeView()
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

    # updates
    if sd.LastPlayingStatus != mmdd.isPlaying:
        sd.LastPlayingStatus = mmdd.isPlaying
        kfView.setEnable(not sd.LastPlayingStatus)
        sd.MotionKF_CurveEdit.enable = not sd.LastPlayingStatus
        updateBoneInput()
        if sd.LastPlayingStatus:
            clearGuideGizmos()
        else:
            onFrameChange(int(mmdd.curFrame))
            switchGuideGizmos()

def mmddGuiMorphSetup(game, morphCtrl):
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    def initMorphSetup():
        sd.MorphSetup_Mode = "general"
        sd.MorphSetup_SelectedName = ""
        sd.MorphSetup_LastFrameNo = -1
        sd.MorphSetup_ListScrollPos = Vector2.zero
        sd.MorphSetup_SettingScrollPos = Vector2.zero
        sd.MorphSetup_ListMode = "list"
        sd.MorphSetup_ActiveMouthMorph = ""
        sd.MorphSetup_ActiveEyeMorph = ""
        sd.MorphSetup_ActiveEyebrowMorph = ""
        sd.MorphSetup_DefaultMouthPtn = str(morphCtrl.defaultMouthPtn)
        sd.MorphSetup_DefaultEyesPtn = str(morphCtrl.defaultEyesPtn)
        sd.MorphSetup_DefaultEyebrowPtn = str(morphCtrl.defaultEyebrowPtn)
        sd.MorphSetup_MorphGroupExpand = {}
        for gkey in morphCtrl.morphGroupList:
            sd.MorphSetup_MorphGroupExpand[gkey] = True
        # dict by engine
        if game.isCharaStudio:
            sd.MorphSetup_RenderNameDict = {
                "cf_O_face":        "Eyes / Mouth",
                "cf_O_tooth":       "Teeth",
                "cf_O_canine":      "Canines",
                "cf_O_eyeline":     "Upper Eyelashes",
                "cf_O_eyeline_low": "Lower Eyelashes",
                "cf_O_mayuge":      "Eyebrows",
                "cf_Ohitomi_L":     "Left Eye White",
                "cf_Ohitomi_R":     "Right Eye White",
                "o_tang":           "Tongue",
                "cf_O_namida_L":    "Tears L",
                "cf_O_namida_M":    "Tears M",
                "cf_O_namida_S":    "Tears S",
            }
        else:
            sd.MorphSetup_RenderNameDict = {
                "o_eyelashes":      "Eyelashes",
                "o_head":           "Head",
                "o_namida":         "Tears",
                "o_tang":           "Tongue",
                "o_tooth":          "Teeth",
            }
    def selectMorph(vname):
        if sd.MorphSetup_SelectedName == vname:
            sd.MorphSetup_SelectedName = ""
            sd.MorphSetup_Mode = "general"
            sd.MorphSetup_DirtyDefine = None
        else:
            sd.MorphSetup_SelectedName = vname
            sd.MorphSetup_Mode = "detail"
            sd.MorphSetup_DirtyDefine = morphCtrl.morphDefineMap[vname].copy()
    def numberInputHelper(orgValue, format, width):
        tempo = format%orgValue
        tempi = GUILayout.TextField(tempo, GUILayout.Width(width))
        if tempi != tempo:
            try:
                if format[-1] == "f":
                    nval = float(tempi)
                else:
                    nval = int(tempi)
                return True, nval
            except:
                return False, None
        else:
            return False, None
    def transRendererName(orgName):
        if orgName in sd.MorphSetup_RenderNameDict:
            return sd.MorphSetup_RenderNameDict[orgName]
        else:
            return orgName
    def checkBlendSetting(blendInfo):
        # check skinned mesh renderer name
        if blendInfo[0] in sd.MorphSetup_RenderNameDict:
            renderName = blendInfo[0]
        else:
            renderName = None
            for rn in sd.MorphSetup_RenderNameDict:
                if sd.MorphSetup_RenderNameDict[rn].lower() == blendInfo[0].lower():
                    renderName = rn
                    break
        if not renderName:
            return LC("Renderer error: '%s' is not a valid renderer. check model's SkinnedMeshRenderer or refer to KKPE/AIPE/HS2PE's blend shapes.")%(blendInfo[0])
        # check blend name
        _, bsNameList, _ = morphCtrl.getBlendRendererInfo(renderName)
        if not bsNameList:
            return LC("Renderer error: '%s' is not a valid renderer.")%(renderName)
        if blendInfo[1] in bsNameList:
            blendName = blendInfo[1]
        else:
            blendName = None
            for bn in bsNameList:
                if bn.endswith(blendInfo[1]):
                    if blendName == None:
                        blendName = blendInfo[1]
                    else:
                        return LC("Blend shape error: renderer '%s' does has more than 2 blend shapes ends with '%s'."%(renderName, blendInfo[1]))
        if not blendName:
            return LC("Blend shape error: renderer '%s' does not content blend shape '%s'."%(renderName, blendInfo[1]))
        # done
        blendInfo[0] = renderName
        blendInfo[1] = blendName
        return ""
    def updateMorphDefine(vname, dirtyDefine):
        err = ""
        if not dirtyDefine.valid:
            err = "morpy define is not valid."
        if dirtyDefine.type == "blend":
            for bi in dirtyDefine.blendList:
                err += checkBlendSetting(bi)
        if len(err) == 0:
            try:
                morphCtrl.updateMorphDefine(vname, dirtyDefine.copy())
            except Exception, e:
                err = "Unexpected error: " + str(e)
        if len(err):
            guiPromptDialog(game, LC("Morph setting error") + "\n" + err)
            return False
        else:
            return True

    # init switch
    if mmddGuiSwitch(game, mmddGuiMorphSetup, morphCtrl):
        initMorphSetup()
        return

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    leftw = 150
    rightw = fullw - leftw - 4
    updflg = False
    legacyMorphType = ["eye", "mouth", "eyebrow"]

    # refresh frameNo, get active mouth/eye/eyebrow morph
    if sd.MorphSetup_LastFrameNo != int(mmdd.curFrame):
        sd.MorphSetup_LastFrameNo = int(mmdd.curFrame)
        frameNoDelay = sd.MorphSetup_LastFrameNo - int(morphCtrl.delay * 30.0)
        mi = morphCtrl.getMouthFrameIndexed(frameNoDelay)
        sd.MorphSetup_ActiveMouthMorph = "" if not mi else mi.vmdName
        mi = morphCtrl.getEyeFrameIndexed(frameNoDelay)
        sd.MorphSetup_ActiveEyeMorph = "" if not mi else mi.vmdName
        mi = morphCtrl.getEyebrowFrameIndexed(frameNoDelay)
        sd.MorphSetup_ActiveEyebrowMorph = "" if not mi else mi.vmdName

    # Seek bar
    guiPartsSeekbar(game)

    try:
        GUILayout.BeginHorizontal()
        # left panel: morph list
        GUILayout.BeginVertical(GUILayout.Width(leftw))
        #  left title line
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Morphs:"))
        GUILayout.EndHorizontal()
        # morph list
        sd.MorphSetup_ListScrollPos = GUILayout.BeginScrollView(sd.MorphSetup_ListScrollPos, GUI.skin.box, GUILayout.Width(leftw), GUILayout.Height(fullh - 111))
        morphListContants = []
        if sd.MorphSetup_ListMode == "list":    # list mode, alwayse group
            for vname in morphCtrl.morphNameList:
                mdefine = morphCtrl.morphDefineMap[vname]
                frmCount = morphCtrl.getMorphKeyframeCountByVmdName(vname)
                isHide = (frmCount == 0 and mmddConfig.hideMorph["no-frm"]) or (frmCount == 1 and mmddConfig.hideMorph["1-frm"]) or (frmCount == 2 and mmddConfig.hideMorph["2-frm"])
                if isHide:
                    continue
                mTitle = mdefine.name if mmddConfig.morphNameDisplay == "dummy" else vname
                if frmCount: mTitle += " (%d)"%frmCount
                if not mdefine.enable:
                    mTitle = "<color=#808080>" + mTitle + "</color>"
                elif mdefine.type in legacyMorphType:
                    mTitle = orangeText(mTitle)
                if morphCtrl.isMorphDefineChanged(vname):
                    mTitle += " <color=#ff0000>" + u"\u2605" + "</color>"
                # to list contants
                morphListContants.append((mdefine.group, vname, mTitle))
            for gkey in morphCtrl.morphGroupList:
                mig = [tb for tb in morphListContants if tb[0] == gkey]
                if len(mig) == 0:
                    continue
                GUILayout.BeginHorizontal()
                if GUILayout.Button("-" if sd.MorphSetup_MorphGroupExpand[gkey] else "+", GUILayout.Width(20)):
                    sd.MorphSetup_MorphGroupExpand[gkey] = not sd.MorphSetup_MorphGroupExpand[gkey]
                GUILayout.Label(yellowText(morphCtrl.morphGroupName[gkey]))
                GUILayout.EndHorizontal()
                if sd.MorphSetup_MorphGroupExpand[gkey]:
                    for g, vname, mTitle in mig:
                        color = GUI.color
                        if vname in {sd.MorphSetup_ActiveMouthMorph, sd.MorphSetup_ActiveEyeMorph, sd.MorphSetup_ActiveEyebrowMorph}:
                            GUI.color = Color.magenta
                        if vname == sd.MorphSetup_SelectedName:
                            GUI.color = Color.green
                        if GUILayout.Button(mTitle, GUILayout.Width(leftw - 24)):
                            selectMorph(vname)
                        GUI.color = color
        else:   # set order mode
            for tgtType in legacyMorphType:
                GUILayout.BeginHorizontal()
                GUILayout.Label(yellowText(tgtType))
                GUILayout.EndHorizontal()
                mig = [vn for vn in morphCtrl.morphNameList if morphCtrl.morphDefineMap[vn].type == tgtType]
                for vname in mig:
                    vidx = mig.index(vname)
                    canup = vidx > 0
                    candn = vidx < len(mig) - 1
                    mdefine = morphCtrl.morphDefineMap[vname]
                    frmCount = morphCtrl.getMorphKeyframeCountByVmdName(vname)
                    mTitle = (mdefine.name if mmddConfig.morphNameDisplay == "dummy" else vname) + " (%d)"%frmCount
                    GUILayout.BeginHorizontal()
                    GUILayout.Label(mTitle)
                    GUILayout.FlexibleSpace()
                    if GUILayout.Button(u'\u2191' if canup else greyText(u'\u2191'), GUILayout.Width(25)) and canup:
                        midx = morphCtrl.morphNameList.index(vname)
                        nidx = morphCtrl.morphNameList.index(mig[vidx-1])
                        morphCtrl.morphNameList[nidx], morphCtrl.morphNameList[midx] = morphCtrl.morphNameList[midx], morphCtrl.morphNameList[nidx]
                        morphCtrl.morphSequenceChanged = True
                        updflg = True
                        break
                    if GUILayout.Button(u'\u2193' if candn else greyText(u'\u2193'), GUILayout.Width(25)) and candn:
                        midx = morphCtrl.morphNameList.index(vname)
                        nidx = morphCtrl.morphNameList.index(mig[vidx+1])
                        morphCtrl.morphNameList[nidx], morphCtrl.morphNameList[midx] = morphCtrl.morphNameList[midx], morphCtrl.morphNameList[nidx]
                        morphCtrl.morphSequenceChanged = True
                        updflg = True
                        break
                    GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
        GUILayout.EndVertical()

        # right panel: morph setting by mode
        GUILayout.BeginVertical(GUILayout.Width(rightw))
        #  right title line
        hdrSpace = 10
        titleLen = 100
        if sd.MorphSetup_Mode == "general":
            # title
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(LC("Morph general setting:"))
            GUILayout.EndHorizontal()
            sd.MorphSetup_SettingScrollPos = GUILayout.BeginScrollView(sd.MorphSetup_SettingScrollPos)
            # legacy option
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(LC("Legacy Morph:"), GUILayout.Width(titleLen))
            if morphCtrl.HasLegacyData:
                if morphCtrl.converted:
                    GUILayout.Label(LC("Legacy morph data converted. Change setting after de-convert."))
                else:
                    GUILayout.Label(LC("Legacy morph data found. These morph and relative settings are marked with <color=#ffa500>orange</color> color."))
            else:
                GUILayout.Label(greenText(LC("No legacy morph data found")))
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # active morph
            if morphCtrl.HasLegacyData and not morphCtrl.converted:
                GUILayout.BeginHorizontal() # descirption
                GUILayout.Space(hdrSpace)
                GUILayout.Label(orangeText(LC("Actived Morph:")), GUILayout.Width(titleLen))
                GUILayout.Label(LC("Current actived legacy morph info"))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                mInfo = ""
                if morphCtrl.hasMouthEye:
                    mInfo += LC("Mouth: %s")%(magentaText(sd.MorphSetup_ActiveMouthMorph) if len(sd.MorphSetup_ActiveMouthMorph) else "---")
                    mInfo += LC(", Eyes: %s")%(magentaText(sd.MorphSetup_ActiveEyeMorph) if len(sd.MorphSetup_ActiveEyeMorph) else "---")
                if morphCtrl.hasEyebrow:
                    mInfo += LC(", Eyebrow: %s")%(magentaText(sd.MorphSetup_ActiveEyebrowMorph) if len(sd.MorphSetup_ActiveEyebrowMorph) else "---")
                GUILayout.BeginHorizontal() # active mouth/eyes/eyebrow
                GUILayout.Space(hdrSpace)
                GUILayout.Label(" ", GUILayout.Width(titleLen))
                GUILayout.Label(mInfo)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            # defualt pattern
            if morphCtrl.HasLegacyData and not morphCtrl.converted:
                GUILayout.BeginHorizontal() # descirption
                GUILayout.Space(hdrSpace)
                GUILayout.Label(orangeText(LC("Default Pattern:")), GUILayout.Width(titleLen))
                GUILayout.Label(LC("Default pattern when no morph active"))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal() # mouth
                GUILayout.Space(hdrSpace)
                GUILayout.Label(" ", GUILayout.Width(titleLen))
                GUILayout.Label(LC("Mouth"), GUILayout.Width(70))
                sd.MorphSetup_DefaultMouthPtn = GUILayout.TextField(sd.MorphSetup_DefaultMouthPtn, GUILayout.Width(30))
                if GUILayout.Button(LC("Current")):
                    ptn = morphCtrl.actor.get_mouth_ptn()
                    sd.MorphSetup_DefaultMouthPtn = str(ptn)
                if sd.MorphSetup_DefaultMouthPtn.isnumeric() and sd.MorphSetup_DefaultMouthPtn != str(morphCtrl.defaultMouthPtn):
                    if GUILayout.Button(greenText(u"\u2713")):
                        morphCtrl.defaultMouthPtn = int(sd.MorphSetup_DefaultMouthPtn)
                        updflg = True
                    if GUILayout.Button(redText(u"\u2715")):
                        sd.MorphSetup_DefaultMouthPtn = str(morphCtrl.defaultMouthPtn)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal() # eyes
                GUILayout.Space(hdrSpace)
                GUILayout.Label(" ", GUILayout.Width(titleLen))
                GUILayout.Label(LC("Eyes"), GUILayout.Width(70))
                sd.MorphSetup_DefaultEyesPtn = GUILayout.TextField(sd.MorphSetup_DefaultEyesPtn, GUILayout.Width(30))
                if GUILayout.Button(LC("Current")):
                    ptn = morphCtrl.actor.get_eyes_ptn()
                    sd.MorphSetup_DefaultEyesPtn = str(ptn)
                if sd.MorphSetup_DefaultEyesPtn.isnumeric() and sd.MorphSetup_DefaultEyesPtn != str(morphCtrl.defaultEyesPtn):
                    if GUILayout.Button(greenText(u"\u2713")):
                        morphCtrl.defaultEyesPtn = int(sd.MorphSetup_DefaultEyesPtn)
                        updflg = True
                    if GUILayout.Button(redText(u"\u2715")):
                        sd.MorphSetup_DefaultEyesPtn = str(morphCtrl.defaultEyesPtn)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                if morphCtrl.hasEyebrow:
                    GUILayout.BeginHorizontal() # eyebrow
                    GUILayout.Space(hdrSpace)
                    GUILayout.Label(" ", GUILayout.Width(titleLen))
                    GUILayout.Label(LC("Eyebrow"), GUILayout.Width(70))
                    sd.MorphSetup_DefaultEyebrowPtn = GUILayout.TextField(sd.MorphSetup_DefaultEyebrowPtn, GUILayout.Width(30))
                    if GUILayout.Button(LC("Current")):
                        ptn = morphCtrl.actor.get_eyebrow_ptn()
                        sd.MorphSetup_DefaultEyebrowPtn = str(ptn)
                    if sd.MorphSetup_DefaultEyebrowPtn.isnumeric() and sd.MorphSetup_DefaultEyebrowPtn != str(morphCtrl.defaultEyebrowPtn):
                        if GUILayout.Button(greenText(u"\u2713")):
                            morphCtrl.defaultEyebrowPtn = int(sd.MorphSetup_DefaultEyebrowPtn)
                            updflg = True
                        if GUILayout.Button(redText(u"\u2715")):
                            sd.MorphSetup_DefaultEyebrowPtn = str(morphCtrl.defaultEyebrowPtn)
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
            # resort mode
            if morphCtrl.HasLegacyData and not morphCtrl.converted:
                GUILayout.BeginHorizontal() # descirption
                GUILayout.Space(hdrSpace)
                GUILayout.Label(orangeText(LC("Priority Order:")), GUILayout.Width(titleLen))
                GUILayout.Label(LC("Priority to decide active morph"))
                if sd.MorphSetup_ListMode == "list":
                    if GUILayout.Button("Setup"):
                        sd.MorphSetup_ListMode = "set-order"
                else:
                    if GUILayout.Button("Done"):
                        sd.MorphSetup_ListMode = "list"
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            # morph display name
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(LC("Display Name:"), GUILayout.Width(titleLen))
            if GUILayout.Toggle(mmddConfig.morphNameDisplay == "dummy", LC("Dummy name")):
                mmddConfig.morphNameDisplay = "dummy"
            if GUILayout.Toggle(mmddConfig.morphNameDisplay == "mmd", LC("MMD name")):
                mmddConfig.morphNameDisplay = "mmd"
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # morph hide
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(LC("Hide Morph:"), GUILayout.Width(titleLen))
            mmddConfig.hideMorph["no-frm"] = GUILayout.Toggle(mmddConfig.hideMorph["no-frm"], LC("Hide morphs without any keyframe"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(" ", GUILayout.Width(titleLen))
            mmddConfig.hideMorph["1-frm"] = GUILayout.Toggle(mmddConfig.hideMorph["1-frm"], LC("Hide morphs with only 1 keyframe"))
            GUILayout.EndHorizontal()
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(" ", GUILayout.Width(titleLen))
            mmddConfig.hideMorph["2-frm"] = GUILayout.Toggle(mmddConfig.hideMorph["2-frm"], LC("Hide morphs with only 2 keyframes"))
            GUILayout.EndHorizontal()
            # Bone define filename
            GUILayout.BeginHorizontal() 
            GUILayout.Space(hdrSpace)
            GUILayout.Label("Define File:", GUILayout.Width(titleLen))
            GUILayout.Label(os.path.split(morphCtrl.motionCtrl.boneDefineFilename)[1])
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUILayout.EndScrollView()
        if sd.MorphSetup_Mode == "detail":
            vname = sd.MorphSetup_SelectedName
            mdefine = sd.MorphSetup_DirtyDefine
            mname = mdefine.name
            dspName = mname if mmddConfig.morphNameDisplay == "dummy" else vname
            # title
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(LC("Morph detail for %s, maps to %s:")%(greenText(mname), greenText(dspName)))
            GUILayout.EndHorizontal()
            # general
            GUILayout.BeginHorizontal()
            GUILayout.Space(hdrSpace)
            GUILayout.Label(LC("Morph Type: %s")%(orangeText(mdefine.type) if mdefine.type in legacyMorphType else greenText(mdefine.type)))
            GUILayout.Space(10)
            GUILayout.Label(LC("Status:"))
            if mdefine.enable:
                if GUILayout.Button(greenText("ENABLED")):
                    mdefine.enable = False
                    updflg = updateMorphDefine(vname, mdefine)
            else:
                if GUILayout.Button(redText("DISABLED")):
                    mdefine.enable = True
                    updflg = updateMorphDefine(vname, mdefine)
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            # by type
            if mdefine.type in legacyMorphType:
                sd.MorphSetup_SettingScrollPos = GUILayout.BeginScrollView(sd.MorphSetup_SettingScrollPos, GUI.skin.box)
                #  pattern
                if mdefine.pattern != None:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(hdrSpace)
                    GUILayout.Label(LC("pattern:"), GUILayout.Width(60))
                    GUILayout.Space(5)
                    chg, val = numberInputHelper(mdefine.pattern, "%d", 40)
                    if chg and val >= 0: mdefine.pattern = val
                    GUILayout.Space(5)
                    if GUILayout.Button(LC("Current")):
                        if mdefine.type == "mouth":
                            mdefine.pattern = morphCtrl.actor.get_mouth_ptn()
                        elif mdefine.type == "eye":
                            mdefine.pattern = morphCtrl.actor.get_eyes_ptn()
                        elif mdefine.type == "eyebrow":
                            mdefine.pattern = morphCtrl.actor.get_eyebrow_ptn()
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                #  range
                if mdefine.range != None:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(hdrSpace)
                    GUILayout.Label(LC("Range:"), GUILayout.Width(60))
                    GUILayout.Space(5)
                    chg, val = numberInputHelper(mdefine.range[0], "%.1f", 40)
                    if chg and 0 <= val <= 1: mdefine.range[0] = val
                    GUILayout.Label("-")
                    chg, val = numberInputHelper(mdefine.range[1], "%.1f", 40)
                    if chg and 0 <= val <= 1: mdefine.range[1] = val
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                # active morph info
                if morphCtrl.converted:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(hdrSpace)
                    GUILayout.Label(redText(LC("Morph setting was CONVERTED!!\nChanging morph define will not take effect until re-convert.")))
                    GUILayout.EndHorizontal()
                else:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(hdrSpace)
                    if mdefine.type == "mouth":
                        mInfo = LC("Current actived mouth morph: ")
                        mInfo += magentaText(sd.MorphSetup_ActiveMouthMorph) if len(sd.MorphSetup_ActiveMouthMorph) else "---"
                    elif mdefine.type == "eye":
                        mInfo = LC("Current actived eye morph: ")
                        mInfo += magentaText(sd.MorphSetup_ActiveEyeMorph) if len(sd.MorphSetup_ActiveEyeMorph) else "---"
                    elif mdefine.type == "eyebrow":
                        mInfo = LC("Current actived eyebrow morph: ")
                        mInfo += magentaText(sd.MorphSetup_ActiveEyebrowMorph) if len(sd.MorphSetup_ActiveEyebrowMorph) else "---"            
                    GUILayout.Label(mInfo)
                    GUILayout.EndHorizontal()
                GUILayout.EndScrollView()
            elif mdefine.type == "blend":
                GUILayout.BeginHorizontal()
                GUILayout.Label(" ", GUILayout.Width(75))
                GUILayout.Label("Renderer", GUILayout.Width(100))
                GUILayout.Label("Blend Shape", GUILayout.Width(110))
                GUILayout.Label("From", GUILayout.Width(35))
                GUILayout.Label("To", GUILayout.Width(35))
                GUILayout.EndHorizontal()
                sd.MorphSetup_SettingScrollPos = GUILayout.BeginScrollView(sd.MorphSetup_SettingScrollPos, GUI.skin.box)
                for i in range(len(mdefine.blendList)):
                    blendInfo = mdefine.blendList[i]
                    GUILayout.BeginHorizontal()
                    GUILayout.Label("Blend #%d:"%i, GUILayout.Width(70))
                    blendInfo[0] = GUILayout.TextField(transRendererName(blendInfo[0]), GUILayout.Width(100))
                    blendInfo[1] = GUILayout.TextField(blendInfo[1], GUILayout.Width(110))
                    chg, val = numberInputHelper(blendInfo[2], "%.0f", 35)
                    if chg and 0 <= val <= 100: blendInfo[2] = val
                    chg, val = numberInputHelper(blendInfo[3], "%.0f", 35)
                    if chg and 0 <= val <= 100: blendInfo[3] = val
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        del mdefine.blendList[i]
                        break
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                if GUILayout.Button(LC("Add"), GUILayout.Width(50)):
                    mdefine.blendList.append(["", "", 0, 100])
                GUILayout.EndHorizontal()
                GUILayout.EndScrollView()
            elif mdefine.type == "func":
                GUILayout.BeginHorizontal()
                GUILayout.Label(" ", GUILayout.Width(65))
                GUILayout.Label("Get method", GUILayout.Width(80))
                GUILayout.Label("Set method", GUILayout.Width(80))
                GUILayout.Label("Mode", GUILayout.Width(50))
                GUILayout.Label("From", GUILayout.Width(40))
                GUILayout.Label("To", GUILayout.Width(40))
                GUILayout.EndHorizontal()
                sd.MorphSetup_SettingScrollPos = GUILayout.BeginScrollView(sd.MorphSetup_SettingScrollPos, GUI.skin.box)
                for i in range(len(mdefine.funcList)):
                    funcInfo = mdefine.funcList[i]
                    GUILayout.BeginHorizontal()
                    GUILayout.Label("Func #%d:"%i, GUILayout.Width(60))
                    funcInfo[0] = GUILayout.TextField(transRendererName(funcInfo[0]), GUILayout.Width(80))
                    funcInfo[1] = GUILayout.TextField(funcInfo[1], GUILayout.Width(80))
                    funcInfo[2] = GUILayout.TextField(funcInfo[2], GUILayout.Width(50))
                    chg, val = numberInputHelper(funcInfo[3], "%.2f", 40)
                    if chg: funcInfo[3] = val
                    chg, val = numberInputHelper(funcInfo[4], "%.2f", 40)
                    if chg: funcInfo[4] = val
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        del mdefine.funcList[i]
                        break
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                if GUILayout.Button(LC("Add"), GUILayout.Width(50)):
                    mdefine.funcList.append(["", "", "scale", 0, 1])
                GUILayout.EndHorizontal()
                GUILayout.EndScrollView()
            elif mdefine.type == "bone":
                GUILayout.BeginHorizontal()
                GUILayout.Label("Bone name", GUILayout.Width(100))
                GUILayout.Label(" ", GUILayout.Width(80))
                GUILayout.Label("X", GUILayout.Width(60))
                GUILayout.Label("Y", GUILayout.Width(60))
                GUILayout.Label("Z", GUILayout.Width(60))
                GUILayout.EndHorizontal()
                sd.MorphSetup_SettingScrollPos = GUILayout.BeginScrollView(sd.MorphSetup_SettingScrollPos, GUI.skin.box)
                for i in range(len(mdefine.boneList)):
                    boneInfo = mdefine.boneList[i]
                    GUILayout.BeginHorizontal() # line1
                    GUILayout.Label("Bone #%d:"%i, GUILayout.Width(100))
                    hasRot = boneInfo[1] != None
                    needRot = GUILayout.Toggle(hasRot, LC("Rotation"), GUILayout.Width(70))
                    if hasRot and not needRot:
                        boneInfo[1] = None
                    elif not hasRot and needRot:
                        boneInfo[1] = Quaternion.identity
                    if needRot:
                        vec = boneInfo[1].eulerAngles
                        chg, val = numberInputHelper(vec.x, "%.3f", 60)
                        if chg: boneInfo[1] = Quaternion.Euler(val, vec.y, vec.z)
                        chg, val = numberInputHelper(vec.y, "%.3f", 60)
                        if chg: boneInfo[1] = Quaternion.Euler(vec.x, val, vec.z)
                        chg, val = numberInputHelper(vec.z, "%.3f", 60)
                        if chg: boneInfo[1] = Quaternion.Euler(vec.x, vec.y, val)
                    else:
                        GUILayout.Space(192)
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        del mdefine.boneList[i]
                        break
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                    GUILayout.BeginHorizontal() # line2
                    boneInfo[0] = GUILayout.TextField(boneInfo[0], GUILayout.Width(100))
                    hasPos = boneInfo[2] != None
                    needPos = GUILayout.Toggle(hasPos, LC("Position"), GUILayout.Width(70))
                    if hasPos and not needPos:
                        boneInfo[2] = None
                    elif not hasPos and needPos:
                        boneInfo[2] = Vector3.zero
                    if needPos:
                        vec = boneInfo[2]
                        chg, val = numberInputHelper(vec.x, "%.3f", 60)
                        if chg: boneInfo[2] = Vector3(val, vec.y, vec.z)
                        chg, val = numberInputHelper(vec.y, "%.3f", 60)
                        if chg: boneInfo[2] = Vector3(vec.x, val, vec.z)
                        chg, val = numberInputHelper(vec.z, "%.3f", 60)
                        if chg: boneInfo[2] = Vector3(vec.x, vec.y, val)
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                    GUILayout.BeginHorizontal() # line3
                    GUILayout.Label(" ", GUILayout.Width(100))
                    hasScl = boneInfo[3] != None
                    needScl = GUILayout.Toggle(hasScl, LC("Scale"), GUILayout.Width(70))
                    if hasScl and not needScl:
                        boneInfo[3] = None
                    elif not hasScl and needScl:
                        boneInfo[3] = Vector3(1,1,1)
                    if needScl:
                        vec = boneInfo[3]
                        chg, val = numberInputHelper(vec.x, "%.3f", 60)
                        if chg: boneInfo[3] = Vector3(val, vec.y, vec.z)
                        chg, val = numberInputHelper(vec.y, "%.3f", 60)
                        if chg: boneInfo[3] = Vector3(vec.x, val, vec.z)
                        chg, val = numberInputHelper(vec.z, "%.3f", 60)
                        if chg: boneInfo[3] = Vector3(vec.x, vec.y, val)
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                    GUILayout.Space(5)
                GUILayout.BeginHorizontal()
                if GUILayout.Button(LC("Add"), GUILayout.Width(50)):
                    mdefine.boneList.append(["", None, None, None])
                GUILayout.EndHorizontal()
                GUILayout.EndScrollView()
            # define operation button
            GUILayout.BeginHorizontal()
            GUILayout.Space(5)
            if GUILayout.Button(redText(LC("Reset"))):
                sd.MorphSetup_DirtyDefine = morphCtrl.morphDefineMap[vname].copy()
            if GUILayout.Button(greenText(LC("Update"))):
                updflg = updateMorphDefine(vname, mdefine)
            if morphCtrl.isMorphDefineChanged(vname) and GUILayout.Button(cyanText(LC("Revert"))):
                sd.MorphSetup_DirtyDefine = morphCtrl.restoreMorphDefine(vname).copy()
                updflg = True
            if mdefine.type in legacyMorphType and GUILayout.Button(LC("About legacy setting")):
                infoText = LC("All mouth, eye, eyebrow type morph setting is called legacy setting from MMDD v2. Legacy morph will be convert to corresponding mouth/eye/eyebrow pattern when VMD morph weight is not zero.\n")
                infoText += LC("If there are more morphs have weight > 0, the high priority one will be the actived morph. And if no morph's weight > 0 the default pattern will be used. Priority order and default pattern can be set in general page.\n")
                infoText += LC("For mouth and eye, the vmd morph weight can be map to mouth open or eye open value, with setting range (From - To).\n")
                guiLongPromptDialog(game, "About legacy morph setting", infoText)
            if mdefine.type == "blend" and GUILayout.Button(LC("About blend setting")):
                infoText = LC("Blend type morph setting map MMD morph to some blend shape change. You should use KKPE/AIPE/HS2PE to help you setup.\n\n")
                infoText += LC("<b>Renderer:</b>\n")
                infoText += LC("In renderer field you should input a SkinnedMeshRenderer name. Valid renderer name for this game are:\n")
                for bn in sd.MorphSetup_RenderNameDict.keys():
                    infoText += "  " + greenText(bn) + " aka " + yellowText(sd.MorphSetup_RenderNameDict[bn]) + "\n"
                infoText += "Note the green name is the actual name and yellow name is the corresponding name in KKPE/AIPE/HS2PE. You can use both the green name or yellow name for renderer field here, but if you want to edit the morph define in bone define (ini) file, green name must be used."
                infoText += LC("\n\n<b>Blend shape:</b>\n")
                infoText += LC("Blend shape name in renderer, you can found these names in KKPE/AIPE/HS2PE.")
                infoText += LC("\n\n<b>From and To:</b>\n")
                infoText += LC("Range map for blend shape. The morph's weight (0 - 1) will be map to (From - To). From and To field should be a float between 0 to 100.")
                guiLongPromptDialog(game, "About blend morph setting", infoText)
            if mdefine.type == "func" and GUILayout.Button(LC("About func setting")):
                infoText = LC("Function type morph setting map MMD morph to data and call vnactor function.\n")
                infoText += redText(LC("\nDetail under Construction! No check for setting!"))
                guiLongPromptDialog(game, "About function morph setting", infoText)
            if mdefine.type == "bone" and GUILayout.Button(LC("About bone setting")):
                infoText = LC("Bone type morph setting map MMD morph to adjust bone position/rotation/scale.\n")
                infoText += redText(LC("\nDetail under Construction! No check for setting!"))
                guiLongPromptDialog(game, "About bone morph setting", infoText)
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUILayout.FlexibleSpace()
        GUILayout.EndVertical()
        GUILayout.EndHorizontal()
    except:
        traceback.print_exc()
        quit(game)    

    # update morph force fresh
    if updflg:
        morphCtrl.setFrame(sd.MorphSetup_LastFrameNo, True)

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if morphCtrl.HasLegacyData:
        if not morphCtrl.converted and GUILayout.Button(orangeText(LC("Convert")), btnstyle, GUILayout.Width(cbwidth)):
            msg = LC("Convert morph keyframes of mouth/eyes/eyebrow base on legacy morph setting, make sets of new keyframes for mouth, eyes and eyebrow which can be adjust in studio directly. Converted keyframes are more easier to edit sometimes.\n\nNote: Bone/Blend/Func morph setting will not be converted, and these morph keyframe can not be adjust anymore after convert!")
            def doConvert(g):
                morphCtrl.convertKeyframes()
                mmddGuiBack(game)
            guiConfirmDialog(game, msg, doConvert)
        if morphCtrl.converted and GUILayout.Button(orangeText(LC("De-Convert")), btnstyle, GUILayout.Width(cbwidth)):
            msg = LC("Discard all converted keyframe and go back to VMD based morph setting.\nAll modified converted keyframes will be lost forever.")
            def doDeConvert(g):
                morphCtrl.deconvertKeyframes()
                mmddGuiBack(game)
            guiConfirmDialog(game, msg, doDeConvert)
    else:
        if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
            pass
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button("Keyframes", btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiMorphKeyframe(game, morphCtrl)
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

def mmddGuiMorphKeyframe(game, morphCtrl):
    from libkfguictrl import KeyframeViewer, DragInputUnit
    from vmdlib import MorphController
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    ctrl, alt, shift = unity_util.metakey_state()

    def initKeyframeUI():
        # cood const
        kfx = 10
        kfy = 60
        kfw = fullw
        kfh = fullh - 144
        bth = 20
        try:
            # init keyframe viewer
            if not hasattr(sd, "MorphKF_KeyframeViews"):
                sd.MorphKF_KeyframeViews = {}
            else:
                for mp in sd.MorphKF_KeyframeViews.keys():
                    if mp not in mmdd.morphControllers.values():
                        del sd.MorphKF_KeyframeViews[mp]
            kfRect = Rect(kfx, kfy, kfw, kfh)
            if morphCtrl not in sd.MorphKF_KeyframeViews:
                sd.MorphKF_KeyframeViews[morphCtrl] = KeyframeViewer(kfRect)
                setupKeyframeView(morphCtrl)
            else:
                kfView = sd.MorphKF_KeyframeViews[morphCtrl]
                if kfView.convertState == morphCtrl.converted:
                    kfView.resize(kfRect, None)
                    updateKeyframeView(morphCtrl, True)
                else:
                    # converted state changed, need re-import keyframes
                    setupKeyframeView(morphCtrl)
            sd.activedKeyframeView = sd.MorphKF_KeyframeViews[morphCtrl]
            # keyframe opt ui mode
            sd.MorphKF_KeyframeOptUiMode = "normal"
            # init keyframe normal ui
            kfBtnRect = Rect(kfx, kfy + kfh + 2, kfw, bth)
            kfBtnContents = ([Rect.zero, LC("Copy"), whenHasKeyframeSelected, keyframeCopy, False], 
                            [Rect.zero, LC("Paste"), whenKeyframeClipboard, keyframePaste, False], 
                            [Rect.zero, LC("V-Select"), whenHasKeyframeSelected, keyframeVSelect, False], 
                            [Rect.zero, LC("Delete"), whenHasKeyframeSelected, keyframeDelete, False], 
                            [Rect.zero, LC("Range-Sel"), whenStopped, keyframeRangeSelect, False])
            kfBtnCount = len(kfBtnContents)
            kfBtnGap = 1
            kfBtnWidth = (kfw - (kfBtnCount - 1) * kfBtnGap) / kfBtnCount
            for i in range(kfBtnCount):
                kfBtnContents[i][0].Set(kfBtnRect.x + i * (kfBtnWidth + kfBtnGap), kfBtnRect.y, kfBtnWidth, kfBtnRect.height)
            kfBtnContents[-1][0].xMax = kfBtnRect.xMax + 1
            sd.MorphKF_KeyframeOptNormalUI = kfBtnContents
            # init keyframe rngsel ui
            sd.MorphKF_KeyframeOptRngSelUI = (
                Rect(kfBtnRect.x, kfBtnRect.y, 120, kfBtnRect.height),    # 0 "Selected Morphs"
                Rect(kfBtnRect.x, kfBtnRect.y,  80, kfBtnRect.height),    # 1 "All Morphs"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 2 "From"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 3 From-input
                Rect(kfBtnRect.x, kfBtnRect.y,  30, kfBtnRect.height),    # 4 "To"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 5 To-input
                Rect(kfBtnRect.x, kfBtnRect.y,   0, kfBtnRect.height),    # 6 space
                Rect(kfBtnRect.x, kfBtnRect.y,  60, kfBtnRect.height),    # 7 "Select"
                Rect(kfBtnRect.x, kfBtnRect.y,  25, kfBtnRect.height),    # 8 "X"
            )
            fixWidth = (len(sd.MorphKF_KeyframeOptRngSelUI)-1)*kfBtnGap
            for i in range(len(sd.MorphKF_KeyframeOptRngSelUI)):
                fixWidth += sd.MorphKF_KeyframeOptRngSelUI[i].width
            sd.MorphKF_KeyframeOptRngSelUI[6].width = kfBtnRect.width - fixWidth + 1
            for i in range(1,len(sd.MorphKF_KeyframeOptRngSelUI)):
                sd.MorphKF_KeyframeOptRngSelUI[i].x = sd.MorphKF_KeyframeOptRngSelUI[i-1].xMax + kfBtnGap
            sd.MorphKF_KeyframeOptRngSelBone = "selected"
            sd.MorphKF_KeyframeOptRngStart = ""
            sd.MorphKF_KeyframeOptRngEnd = ""
            # init morph adjust ui
            mdh = kfy + kfh + bth + 4
            sd.MorphKF_MorphNameRect = Rect(kfx, mdh, 85, bth)
            sd.MorphKF_MorphName = ""
            sd.MorphKF_MorphInfo = LC("Select a morph to adjust")
            wadjw = 190 if morphCtrl.converted else 160
            diuRect = Rect(sd.MorphKF_MorphNameRect.xMax, mdh, wadjw, bth)
            sd.MorphKF_WeightAdj = DragInputUnit(diuRect, LC("Weight"), False, wadjw - 50)
            sd.MorphKF_WeightAdj.setDragRate(0.01, 0.01)
            sd.MorphKF_WeightAdj.setValueFormat(0, 1)
            sd.MorphKF_WeightAdj.onInputChangeCallback = morphAdjChange
            # init morph operate ui
            mdbx = diuRect.xMax + kfBtnGap
            if morphCtrl.converted:
                morphOptContents = (
                    [Rect(mdbx, mdh, 80, bth), LC("Revert"), whenHasMorphSelected, morphRevert, False],
                    [Rect(mdbx, mdh, 110, bth), LC("Select-Unreg"), whenStopped, morphSelectUnreg, True, LC("Select-All"), whenStopped, morphSelectAll],
                    [Rect(mdbx, mdh, 110, bth), LC("Register"), whenHasMorphSelected, morphRegister, False],
                )
            else:
                morphOptContents = (
                    [Rect(mdbx, mdh, 25, bth), "0", whenHasMorphSelected, morphSet0, False],
                    [Rect(mdbx, mdh, 25, bth), "1", whenHasMorphSelected, morphSet1, False],
                    [Rect(mdbx, mdh, 80, bth), LC("Revert"), whenHasMorphSelected, morphRevert, False],
                    [Rect(mdbx, mdh, 110, bth), LC("Select-Unreg"), whenStopped, morphSelectUnreg, True, LC("Select-All"), whenStopped, morphSelectAll],
                    [Rect(mdbx, mdh, 110, bth), LC("Register"), whenHasMorphSelected, morphRegister, False],
                )
            for n in range(1, len(morphOptContents)):
                morphOptContents[n][0].x = morphOptContents[n-1][0].xMax + kfBtnGap
            morphOptContents[-1][0].xMax = kfBtnRect.xMax + 1
            sd.MorphKF_MorphOptUI = morphOptContents
            setupMorphInput(sd.MorphKF_KeyframeViews[morphCtrl])
            sd.MorphKF_MorphInfoRect = Rect(kfx, mdh + bth + kfBtnGap, kfw, bth)
            # init keyframe clipboard
            if not hasattr(sd, "MorphKF_KeyframeClipboard"):
                sd.MorphKF_KeyframeClipboard = set()
            # update frame
            onFrameChange(int(mmdd.curFrame))
        except:
            traceback.print_exc()
            quit(game)
    # util
    def setupKeyframeView(mp):
        kfView = sd.MorphKF_KeyframeViews[mp]
        kfView.morphCtrl = mp
        kfView.convertState = mp.converted
        kfView.NameSet = ""
        kfView.changeColors(mmddConfig.keyframeColors)
        morphBelongMap = {}
        # clear up to start
        kfView.clearKeyframe()
        if mp.converted:
            gName = "Studio"
            # setup namelist
            for cname in mp.convertedFrameName:
                kfView.importFrameName(gName, cname)
            # import keyframes
            for k in mp.convertedKeyframes.keys():
                cname = mp.convertedFrameName[k]
                for kf in mp.convertedKeyframes[k]:
                    kfView.importKeyframe(gName, cname, kf.frameNo, kf)
        else:
            # setup known namelist, always use vmd name
            for gKey in mp.morphGroupList:
                if gKey in mp.morphGroupSet:
                    for vName in mp.morphGroupSet[gKey]:
                        morphBelongMap[vName] = gKey
                        kfView.importFrameName(mp.morphGroupName[gKey], vName)
            for vName in mp.morphNameList:
                if vName not in morphBelongMap:
                    kfView.importFrameName(mp.morphGroupName[mp.morphGroupList[-2]], vName)
            # import keyframes
            for kfname in mp.morphKeyFrames.keys():
                if kfname in morphBelongMap:
                    gKey = morphBelongMap[kfname]
                elif kfname in mp.morphNameList:
                    gKey = mp.morphGroupList[-2]
                else:
                    gKey = mp.morphGroupList[-1]
                    continue
                for kf in mp.morphKeyFrames[kfname]:
                    kfView.importKeyframe(mp.morphGroupName[gKey], kfname, kf.frameNo, kf)
        # update nameset and hide setting
        updateKeyframeView(mp, False)
        # done import
        kfView.arrangeImportedKeyframes()
        # setup callback
        kfView.onFrameChangeCallback = frameChange
        kfView.onRowSelecCallback = rowSelect
        kfView.onKeyframeMoveCallback = keyframeMove
        kfView.onKeyframeSelectCallback = keyframeSelect
    def updateKeyframeView(mp, refresh):
        kfView = sd.MorphKF_KeyframeViews[mp]
        # check name set change
        if kfView.NameSet != mmddConfig.morphNameDisplay:
            kfView.NameSet = mmddConfig.morphNameDisplay
            for vname in mp.morphNameList:
                dname = mp.morphDefineMap[vname].name
                kfView.keyframeNameList.dispNameDic[vname] = dname if kfView.NameSet == "dummy" else vname
        # check hide setting
        kfView.keyframeNameList.hiddenNames.clear()
        for vname in mp.morphNameList:
            if isHiddenMorph(vname):
                kfView.keyframeNameList.hiddenNames.add(vname)
        # clear select bone if hidden
        selKeys = getCurSelectedMorphKeys(kfView)
        newSelKeys = []
        for bKey in selKeys:
            vname = bKey.split("#")[1]
            if not isHiddenMorph(vname):
                newSelKeys.append(bKey)
        if len(selKeys) != len(newSelKeys):
            kfView.selectBonesByKey(newSelKeys)
            rowSelect(kfView)
        # refresh
        if refresh:
            kfView.refreshKeyframe()
    def setupMorphInput(kfView):
        selKeys = getCurSelectedMorphKeys(kfView)
        if len(selKeys) == 1:
            bKey = selKeys[0]
            vname = bKey.split("#")[1]
            if kfView.convertState:
                dname = morphCtrl.getConvertedMorphNameIndex(vname)
                mi = morphCtrl.getConvertedIndexInfo(vname)
            else:
                dname = morphCtrl.morphDefineMap[vname].name
                mi = morphCtrl.getMorphIndexInfo(vname)
            sd.MorphKF_CurSelMorphInfo = (dname, vname, mi)
        else:
            sd.MorphKF_CurSelMorphInfo = None
        sd.MorphKF_CurSelMorphsKeyframeSetCache = None
        updateMorphInfo(kfView)
        updateMorphInput()
    def updateMorphInfo(kfView):
        if sd.MorphKF_CurSelMorphInfo:
            vname = sd.MorphKF_CurSelMorphInfo[1]
            if kfView.convertState:
                # for converted
                sd.MorphKF_MorphName = "<b>" + greenText(vname) + "</b>"
                sd.MorphKF_MorphInfo = LC("Adjust %s parameter in studio please.")%sd.MorphKF_MorphName
            else:
                dname = sd.MorphKF_CurSelMorphInfo[0]
                sd.MorphKF_MorphName = "<b>" + greenText(dname if kfView.NameSet == "dummy" else vname) + "</b>"
                # get current morph of selected morph type
                mtype = morphCtrl.morphDefineMap[vname].type
                if mtype == "mouth":
                    curMi = morphCtrl.getMouthFrameIndexed(kfView.curFrame)
                elif mtype == "eye":
                    curMi = morphCtrl.getEyeFrameIndexed(kfView.curFrame)
                elif mtype == "eyebrow":
                    curMi = morphCtrl.getEyebrowFrameIndexed(kfView.curFrame)
                else:
                    sd.MorphKF_MorphInfo = "" #LC("No priroty check needed for %s type morph %s.")%(mtype, sd.MorphKF_MorphName)
                    return
                curMorphName = "" if not curMi else curMi.vmdName
                # set morph info text
                if curMorphName == "" or curMorphName == vname:
                    sd.MorphKF_MorphInfo = LC("Current selection %s is the active morph.")%sd.MorphKF_MorphName
                elif morphCtrl.morphNameList.index(vname) < morphCtrl.morphNameList.index(curMorphName):
                    dspName = curMorphName if kfView.NameSet != "dummy" else morphCtrl.getDummyNameByVmdName(curMorphName)
                    sd.MorphKF_MorphInfo = LC("Priority of %s is higher than active morph %s and will overwrite it.")%(sd.MorphKF_MorphName, greenText("<b>"+dspName+"</b>"))
                else:
                    dspName = curMorphName if kfView.NameSet != "dummy" else morphCtrl.getDummyNameByVmdName(curMorphName)
                    sd.MorphKF_MorphInfo = LC("Priority of %s is lower than active morph %s and <color=#ff0000>may be invalid.</color>")%(sd.MorphKF_MorphName, greenText("<b>"+dspName+"</b>"))
        else:
            sd.MorphKF_MorphName = "---"
            sd.MorphKF_MorphInfo = LC("Select a morph to adjust")
    def updateMorphInput():
        if not mmdd.isPlaying and sd.MorphKF_CurSelMorphInfo != None:
            vname = sd.MorphKF_CurSelMorphInfo[1]
            mi = sd.MorphKF_CurSelMorphInfo[2]
            if morphCtrl.converted:
                # set format
                di = sd.MorphKF_CurSelMorphInfo[0]
                if di in (0, 2, 4):
                    sd.MorphKF_WeightAdj.setValueFormat(vFormat="%d")
                else:
                    sd.MorphKF_WeightAdj.setValueFormat(vFormat="%.2f")
                # update value
                mvalue = sd.MorphKF_CharaMorphStatus[di]
                sd.MorphKF_WeightAdj.setValue(mvalue)
                """
                if vname in sd.MorphKF_DirtyFrameCache:
                    mvalue = sd.MorphKF_DirtyFrameCache[vname].value
                    sd.MorphKF_WeightAdj.setValue(mvalue)
                elif mi:
                    mvalue = mi.wVal
                    sd.MorphKF_WeightAdj.setValue(mvalue)
                else:
                    # no keyframe for selected morph, clear value
                    sd.MorphKF_WeightAdj.setValue(0)
                """
                # always disable
                sd.MorphKF_WeightAdj.enable = False
            else:
                # update value and enable
                if vname in sd.MorphKF_DirtyFrameCache:
                    mvalue = sd.MorphKF_DirtyFrameCache[vname].weight
                    sd.MorphKF_WeightAdj.setValue(mvalue)
                    sd.MorphKF_WeightAdj.enable = True
                elif mi:
                    mvalue = mi.weight
                    sd.MorphKF_WeightAdj.setValue(mvalue)
                    sd.MorphKF_WeightAdj.enable = True
                else:
                    # no keyframe for selected morph, clear value
                    sd.MorphKF_WeightAdj.setValue(0)
                    sd.MorphKF_WeightAdj.enable = False
        else:
            # can not adjust bone, keep value
            sd.MorphKF_WeightAdj.enable = False
        sd.MorphKF_WeightAdj.setText(sd.MorphKF_MorphName)
    # keyframe view and bone input unit callback
    def frameChange(kfView):
        mmdd.gotoFrame(kfView.curFrame)
        onFrameChange(kfView.curFrame)
    def rowSelect(kfView):
        setupMorphInput(kfView)
    def keyframeMove(kfView, keyMoveInfo):
        # UI is already processed
        for (bKey, oldFrame, newFrame, tgtObj) in keyMoveInfo:
            tgtObj.frameNo = newFrame
        if morphCtrl.converted:
            morphCtrl.buildConvertIndex()
        else:
            morphCtrl.buildMorphIndex()
        buildSelMorphsKeyframesSetCache(getCurSelectedMorphKeys(kfView))
        mmdd.refreshTotalFrame()
        kfView.refreshKeyframe()
        rowSelect(kfView)
        morphCtrl.setFrame(kfView.curFrame)
    def keyframeSelect(kfView):
        selectedKeyframes = set()
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            selectedKeyframes.add(kfView.keyframeBuffer[kfbtn.key][kfbtn.column])
    def morphAdjChange(tgtAdj, value, delta):
        if not mmdd.isPlaying and sd.MorphKF_CurSelMorphInfo:
            vname = sd.MorphKF_CurSelMorphInfo[1]
            dframe = getDirtyFrame(vname)
            dframe.weight = value
            updateCurrentMorphTemp(vname, value)
    # seekbar callback
    def onFrameChange(newFrameNo):
        sd.MorphKF_DirtyFrameCache = {}
        if not mmdd.isPlaying:
            kfView = sd.MorphKF_KeyframeViews[morphCtrl]
            if newFrameNo != kfView.curFrame:
                kfView.setCurrentFrame(newFrameNo)
            refreshCharaMorphStatus()
            updateMorphInfo(kfView)
            updateMorphInput()
    def onJumpToKeyframe(curFrame, direction):
        kfView = sd.MorphKF_KeyframeViews[morphCtrl]
        selKeys = getCurSelectedMorphKeys(kfView)
        if not mmdd.isPlaying and len(selKeys) > 0:
            if sd.MorphKF_CurSelMorphsKeyframeSetCache:
                kfset = sd.MorphKF_CurSelMorphsKeyframeSetCache
            else:
                kfset = buildSelMorphsKeyframesSetCache(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
    # util
    def quitMorphKeyframeView():
        sd.activedKeyframeView = None
    def isHiddenMorph(vname):
        kc = morphCtrl.getMorphKeyframeCountByVmdName(vname)
        if morphCtrl.converted and vname in morphCtrl.convertedFrameName:
            return False
        if mmddConfig.hideMorph["no-frm"] and kc == 0:
            return True
        if mmddConfig.hideMorph["1-frm"] and kc == 1:
            return True
        if mmddConfig.hideMorph["2-frm"] and kc == 2:
            return True
        return False
    def buildSelMorphsKeyframesSetCache(selKeys):
        sd.MorphKF_CurSelMorphsKeyframeSetCache = set()
        kfView = sd.MorphKF_KeyframeViews[morphCtrl]
        for bKey in selKeys:
            sd.MorphKF_CurSelMorphsKeyframeSetCache.update(kfView.keyframeBuffer[bKey].keys())
        return sd.MorphKF_CurSelMorphsKeyframeSetCache
    def getCurSelectedMorphKeys(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 getDirtyFrame(vname):
        if vname in sd.MorphKF_DirtyFrameCache:
            return sd.MorphKF_DirtyFrameCache[vname]
        else:
            kfView = sd.MorphKF_KeyframeViews[morphCtrl]
            if kfView.convertState:
                ni = morphCtrl.getConvertedMorphNameIndex(vname)
                ndf = MorphController.CnvMorphKeyframe(ni, kfView.curFrame, 0, True)
            else:
                mi = morphCtrl.getMorphIndexInfo(vname)
                if mi == None:
                    return None
                from vmdlib import VmdMorphKeyFrame
                ndf = VmdMorphKeyFrame()
                ndf.name = vname
                ndf.frameNo = kfView.curFrame
                ndf.weight = mi.weight
            sd.MorphKF_DirtyFrameCache[vname] = ndf
            return ndf
    # operation button functions
    def renderOptButton(btnInfo, kfView):
        # btnInfo = [0 rect, 1 text, 2 enable-func, 3 call-func, 4 hasShift[, 5 shift-text, 6 shift-enable-func, 7 shift-call-func]]
        # 5 shift-text, 6 shift-enable-func, 7 shift-call-func only valid when 4 hasShift == True
        if btnInfo[4] and shift:
            # shift function
            btnValid = btnInfo[6](kfView)
            if GUI.Button(btnInfo[0], btnInfo[5] if btnValid else "<color=#808080>" + btnInfo[5] + "</color>") and btnValid:
                btnInfo[7](kfView)
        else:
            # normal function
            btnValid = btnInfo[2](kfView)
            if GUI.Button(btnInfo[0], btnInfo[1] if btnValid else "<color=#808080>" + btnInfo[1] + "</color>") and btnValid:
                btnInfo[3](kfView)
    def whenStopped(kfView):
        return not mmdd.isPlaying
    def whenHasMorphSelected(kfView):
        return not mmdd.isPlaying and len(getCurSelectedMorphKeys(kfView)) > 0
    def whenKeyframeClipboard(kfView):
        return not mmdd.isPlaying and len(sd.MorphKF_KeyframeClipboard) > 0
    def whenHasKeyframeSelected(kfView):
        return not mmdd.isPlaying and len(kfView.keyframeBox.selectedKeyframes) > 0
    def morphSet0(kfView):
        morphToUpdate = set()
        for bKey in getCurSelectedMorphKeys(kfView):
            vname = bKey.split("#")[1]
            dFrame = getDirtyFrame(vname)
            if not dFrame: 
                continue
            dFrame.weight = 0
            morphToUpdate.add(vname)
        updateSeq = [vn for vn in morphCtrl.morphNameList if vn in morphToUpdate]
        updateSeq.reverse()
        for vname in updateSeq:
            updateCurrentMorphTemp(vname, 0)
        sd.MorphKF_WeightAdj.setValue(0)
    def morphSet1(kfView):
        morphToUpdate = set()
        for bKey in getCurSelectedMorphKeys(kfView):
            vname = bKey.split("#")[1]
            dFrame = getDirtyFrame(vname)
            if not dFrame: 
                continue
            dFrame.weight = 1
            morphToUpdate.add(vname)
        updateSeq = [vn for vn in morphCtrl.morphNameList if vn in morphToUpdate]
        updateSeq.reverse()
        for vname in updateSeq:
            updateCurrentMorphTemp(vname, 1)
        sd.MorphKF_WeightAdj.setValue(1)
    def morphRevert(kfView):
        for bKey in getCurSelectedMorphKeys(kfView):
            vname = bKey.split("#")[1]
            if vname in sd.MorphKF_DirtyFrameCache:
                del sd.MorphKF_DirtyFrameCache[vname]
        morphCtrl.setFrame(kfView.curFrame, True)
        refreshCharaMorphStatus()
        rowSelect(kfView)
    def morphSelectUnreg(kfView):
        kfView.selectBonesByName(sd.MorphKF_DirtyFrameCache.keys())
        rowSelect(kfView)
    def morphSelectAll(kfView):
        if kfView.convertState:
            allUnhideVmdMorphName = [vname for vname in morphCtrl.convertedFrameName]
        else:
            allUnhideVmdMorphName = [vname for vname in morphCtrl.morphNameList if not isHiddenMorph(vname)]
        kfView.selectBonesByName(allUnhideVmdMorphName)
        rowSelect(kfView)
    def morphRegister(kfView):
        selKeys = getCurSelectedMorphKeys(kfView)
        hasNew = False
        newKeyframes = set()
        for bKey in selKeys:
            groupName, vname = bKey.split("#")
            if vname in sd.MorphKF_DirtyFrameCache:
                nFrame = sd.MorphKF_DirtyFrameCache.pop(vname)
            elif kfView.convertState:
                ni = morphCtrl.getConvertedMorphNameIndex(vname)
                value = sd.MorphKF_CharaMorphStatus[ni]
                interp = ni in {1, 3}
                nFrame = MorphController.CnvMorphKeyframe(ni, kfView.curFrame, value, interp)
            else:
                from vmdlib import VmdMorphKeyFrame
                nFrame = VmdMorphKeyFrame()
                nFrame.name = vname
                nFrame.frameNo = kfView.curFrame
                nFrame.weight = morphCtrl.getMorphWeight(vname)
            # to UI
            hasNew |= kfView.registerKeyframe(groupName, vname, kfView.curFrame, nFrame)
            # to MorphCtrl
            newKeyframes.add(nFrame)
        if morphCtrl.converted:
            morphCtrl.updateConvertKeyframes(newKeyframes)
        else:
            morphCtrl.updateMorphKeyframes(newKeyframes)
        morphCtrl.setFrame(kfView.curFrame, True)
        if hasNew:
            buildSelMorphsKeyframesSetCache(selKeys)
            mmdd.refreshTotalFrame()
        rowSelect(kfView)
        kfView.refreshKeyframe()
    def keyframeCopy(kfView):
        sd.MorphKF_KeyframeClipboard.clear()
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        baseFrame = min([si[1] for si in selectInfo])
        for (kfKey, kfFrame) in selectInfo:
            orgFrame = kfView.keyframeBuffer[kfKey][kfFrame]
            sd.MorphKF_KeyframeClipboard.add((kfKey, kfFrame - baseFrame, orgFrame.copy()))
    def keyframePaste(kfView):
        hasNew = False
        newKeyframes = set()
        newSelectKeyframeInfo = set() 
        for (kfKey, kfOffset, kfOrgFrame) in sd.MorphKF_KeyframeClipboard:
            groupName, vmdName = kfKey.split("#")
            nFrame = kfOrgFrame.copy()
            nFrame.frameNo = kfView.curFrame + kfOffset
            # to UI
            hasNew |= kfView.registerKeyframe(groupName, vmdName, nFrame.frameNo, nFrame)
            # to MotionCtrl
            newKeyframes.add(nFrame)
            # select later
            newSelectKeyframeInfo.add((kfKey, nFrame.frameNo))
        if morphCtrl.converted:
            morphCtrl.updateConvertKeyframes(newKeyframes)
        else:
            morphCtrl.updateMorphKeyframes(newKeyframes)
        morphCtrl.setFrame(kfView.curFrame, True)
        if hasNew:
            buildSelMorphsKeyframesSetCache(getCurSelectedMorphKeys(kfView))
            mmdd.refreshTotalFrame()
        kfView.selectKeyframes(newSelectKeyframeInfo)
        kfView.refreshKeyframe()
        rowSelect(kfView)
        keyframeSelect(kfView)
    def keyframeVSelect(kfView):
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        toSelectFrame = set()
        for (kfKey, kfFrame) in selectInfo:
            toSelectFrame.add(kfFrame)
        for kfFrame in toSelectFrame:
            for bKey in kfView.keyframeBuffer.keys():
                if kfFrame in kfView.keyframeBuffer[bKey]:
                    selectInfo.add((bKey, kfFrame))
        kfView.selectKeyframes(selectInfo)
        kfView.refreshKeyframe()
        keyframeSelect(kfView)
    def keyframeDelete(kfView):
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        kfc = len(selectInfo)
        # to MorphCtrl
        delInfoPair = set()
        if morphCtrl.converted:
            for (kfKey, kfFrame) in selectInfo:
                groupName, vmdName = kfKey.split("#")
                ni = morphCtrl.getConvertedMorphNameIndex(vmdName)
                delInfoPair.add((ni, kfFrame))
            morphCtrl.deleteConvertKeyframes(delInfoPair)
        else:
            for (kfKey, kfFrame) in selectInfo:
                groupName, vmdName = kfKey.split("#")
                delInfoPair.add((vmdName, kfFrame))
            morphCtrl.deleteMorphKeyframes(delInfoPair)
        morphCtrl.setFrame(kfView.curFrame, True)
        # UI
        kfView.deleteKeyframes(selectInfo)
        kfView.refreshKeyframe()
        buildSelMorphsKeyframesSetCache(getCurSelectedMorphKeys(kfView))
        rowSelect(kfView)
        keyframeSelect(kfView)
        mmdd.refreshTotalFrame()
    def keyframeRangeSelect(kfView):
        sd.MorphKF_KeyframeOptUiMode = "rngsel"
        sd.MorphKF_KeyframeOptRngSelBone = "selected" if whenHasMorphSelected(kfView) else "all"
    def keyframeRangeSelectDo(kfView):
        if sd.MorphKF_KeyframeOptRngSelBone == "selected":
            bonekeys = getCurSelectedMorphKeys(kfView)
        else:
            bonekeys = kfView.keyframeBuffer.keys()
        if len(bonekeys) == 0:
            return False
        try:
            rngStart = int(sd.MorphKF_KeyframeOptRngStart)
        except:
            sd.MorphKF_KeyframeOptRngStart = ""
            return False
        try:
            rngEnd = int(sd.MorphKF_KeyframeOptRngEnd)
        except:
            sd.MorphKF_KeyframeOptRngEnd = ""
            return False
        if rngStart > rngEnd:
            sd.MorphKF_KeyframeOptRngEnd = ""
            return False
        selectInfo = set()
        for bKey in bonekeys:
            frameInRange = [f for f in kfView.keyframeBuffer[bKey].keys() if rngStart <= f <= rngEnd]
            for kfFrame in frameInRange:
                selectInfo.add((bKey, kfFrame))
        kfView.selectKeyframes(selectInfo)
        kfView.refreshKeyframe()
        keyframeSelect(kfView)
        return True
    def keyframeExpand(kfView):
        # TODO
        pass
    def updateCurrentMorphTemp(vname, weight):
        mdefine = morphCtrl.morphDefineMap[vname]
        if mdefine.type == "mouth":
            morphCtrl.actor.set_mouth_ptn(mdefine.pattern)
            morphCtrl.actor.set_mouth_open(mdefine.getValue(weight))
        elif mdefine.type == "eye":
            morphCtrl.actor.set_eyes_ptn(mdefine.pattern)
            morphCtrl.actor.set_eyes_open(mdefine.getValue(weight))
        elif mdefine.type == "eyebrow":
            morphCtrl.actor.set_eyebrow_ptn(mdefine.pattern)
        elif mdefine.type == "bone":
            mi = morphCtrl.getMorphIndexInfo(vname)
            mi.weight = weight
            morphCtrl.setBoneFrameIndexed(mi, True)
        elif mdefine.type == "blend":
            mi = morphCtrl.getMorphIndexInfo(vname)
            mi.weight = weight
            morphCtrl.setAllBlendFrameIndexed(True)
        elif mdefine.type == "func":
            mi = morphCtrl.getMorphIndexInfo(vname)
            mi.weight = weight
            morphCtrl.setFuncFrameIndexed(mi, True)
    def refreshCharaMorphStatus():
        if mmdd.isPlaying or not morphCtrl.converted:
            return
        actor = morphCtrl.actor
        sd.MorphKF_CharaMorphStatus = [
            actor.get_mouth_ptn(),
            actor.get_mouth_open(),
            actor.get_eyes_ptn(),
            actor.get_eyes_open(),
            actor.get_eyebrow_ptn(),
        ]
    def trackCharaMorphChange():
        if mmdd.isPlaying or not morphCtrl.converted:
            return
        actor = morphCtrl.actor
        curMorphState = (
            actor.get_mouth_ptn(),
            actor.get_mouth_open(),
            actor.get_eyes_ptn(),
            actor.get_eyes_open(),
            actor.get_eyebrow_ptn(),
        )
        updsts = set()
        for i in range(len(curMorphState)):
            if curMorphState[i] != sd.MorphKF_CharaMorphStatus[i]:
                sd.MorphKF_CharaMorphStatus[i] = curMorphState[i]
                vname = morphCtrl.convertedFrameName[i]
                dfrm = getDirtyFrame(vname)
                dfrm.value = curMorphState[i]
                dfrm.interpolate = i in {1, 3}
                updsts.add(vname)
        if sd.MorphKF_CurSelMorphInfo and sd.MorphKF_CurSelMorphInfo[1] in updsts:
            updateMorphInput()
    # sepcial render
    def renderKeyframeRngSel(kfView):
        labelStyle = GUIStyle("label")
        labelStyle.alignment = TextAnchor.MiddleRight
        if GUI.Toggle(sd.MorphKF_KeyframeOptRngSelUI[0], sd.MorphKF_KeyframeOptRngSelBone == "selected", LC("Selected Morphs")):
            sd.MorphKF_KeyframeOptRngSelBone = "selected"
        if GUI.Toggle(sd.MorphKF_KeyframeOptRngSelUI[1], sd.MorphKF_KeyframeOptRngSelBone == "all", LC("All Morphs")):
            sd.MorphKF_KeyframeOptRngSelBone = "all"
        GUI.Label(sd.MorphKF_KeyframeOptRngSelUI[2], LC("From "), labelStyle)
        sd.MorphKF_KeyframeOptRngStart = GUI.TextField(sd.MorphKF_KeyframeOptRngSelUI[3], sd.MorphKF_KeyframeOptRngStart)
        GUI.Label(sd.MorphKF_KeyframeOptRngSelUI[4], LC("To "), labelStyle)
        sd.MorphKF_KeyframeOptRngEnd = GUI.TextField(sd.MorphKF_KeyframeOptRngSelUI[5], sd.MorphKF_KeyframeOptRngEnd)
        GUI.Label(sd.MorphKF_KeyframeOptRngSelUI[6], LC(" "))
        if GUI.Button(sd.MorphKF_KeyframeOptRngSelUI[7], LC("Select")):
            if keyframeRangeSelectDo(kfView):
                sd.MorphKF_KeyframeOptUiMode = "normal"
        if GUI.Button(sd.MorphKF_KeyframeOptRngSelUI[8], "X"):
            sd.MorphKF_KeyframeOptUiMode = "normal"    

    # init switch
    if mmddGuiSwitch(game, mmddGuiMorphKeyframe, morphCtrl):
        sd.LastPlayingStatus = mmdd.isPlaying
        # init ui
        initKeyframeUI()
        return

    # Seek bar
    guiPartsSeekbar(game, onFrameChange, onJumpToKeyframe)

    # keyframe view
    kfView = sd.MorphKF_KeyframeViews[morphCtrl]
    kfView.paint()

    # keyframe opt buttons
    if sd.MorphKF_KeyframeOptUiMode == "normal":
        for kfBtn in sd.MorphKF_KeyframeOptNormalUI:
            renderOptButton(kfBtn, kfView)
    elif sd.MorphKF_KeyframeOptUiMode == "rngsel":
        renderKeyframeRngSel(kfView)

    # morph adjust area
    GUI.Label(sd.MorphKF_MorphNameRect, LC("Morph Adjust"))
    sd.MorphKF_WeightAdj.paint()
    for wOptBtn in sd.MorphKF_MorphOptUI:
        renderOptButton(wOptBtn, kfView)
    GUI.Label(sd.MorphKF_MorphInfoRect, sd.MorphKF_MorphInfo)

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button(LC("Export") if not mmdd.isPlaying else " ", btnstyle, GUILayout.Width(cbwidth)) and not mmdd.isPlaying:
        guiExportVmdData(game, morphCtrl)
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button(LC("Setup"), btnstyle, GUILayout.Width(cbwidth)):
        quitMorphKeyframeView()
        mmddGuiMorphSetup(game, morphCtrl)
    if GUILayout.Button(LC("Back"), btnstyle, GUILayout.Width(cbwidth)):
        quitMorphKeyframeView()
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

    # updates
    if sd.LastPlayingStatus != mmdd.isPlaying:
        sd.LastPlayingStatus = mmdd.isPlaying
        kfView.setEnable(not sd.LastPlayingStatus)
        updateMorphInput()
        if not sd.LastPlayingStatus:
            onFrameChange(int(mmdd.curFrame))
    trackCharaMorphChange()

def mmddGuiCameraSetup(game):
    from libkfguictrl import DragInputUnit
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    cc = mmdd.cameraController

    def initCameraSetup():
        # adjusts
        sd.CameraSetup_OffsetAdj = (
            DragInputUnit(Rect(110, 86, 150, 21), LC("Position X"), False, 100),
            DragInputUnit(Rect(264, 86, 150, 21), LC("Position Y"), False, 100),
            DragInputUnit(Rect(418, 86, 150, 21), LC("Position Z"), False, 100),
        )
        for adjctrl in sd.CameraSetup_OffsetAdj:
            adjctrl.setValueFormat(vFormat="%.3f")
            adjctrl.setDragRate(mmddConfig.dragIntensityPosX, mmddConfig.dragIntensityPosY)
        sd.CameraSetup_RotateAdj = (
            DragInputUnit(Rect(110, 111, 150, 21), LC("Rotation X"), False, 100),
            DragInputUnit(Rect(264, 111, 150, 21), LC("Rotation Y"), False, 100),
            DragInputUnit(Rect(418, 111, 150, 21), LC("Rotation Z"), False, 100),
        )
        for adjctrl in sd.CameraSetup_RotateAdj:
            adjctrl.setValueFormat(-180, 180, True, "%.3f")
            adjctrl.setDragRate(mmddConfig.dragIntensityRotX, mmddConfig.dragIntensityRotY)
        sd.CameraSetup_PositionScaleAdj = DragInputUnit(Rect(110, 166, 150, 21), LC("Position Scale"), False, 100)
        sd.CameraSetup_PositionScaleAdj.setValueFormat(vFormat="%.3f")
        sd.CameraSetup_PositionScaleAdj.setDragRate(mmddConfig.dragIntensitySclX, mmddConfig.dragIntensitySclY)
        sd.CameraSetup_DistanceScaleAdj = DragInputUnit(Rect(264, 166, 150, 21), LC("Distance Scale"), False, 100)
        sd.CameraSetup_DistanceScaleAdj.setValueFormat(vFormat="%.3f")
        sd.CameraSetup_DistanceScaleAdj.setDragRate(mmddConfig.dragIntensitySclX, mmddConfig.dragIntensitySclY)
        # switch
        sd.CameraSetup_SwitchAdj = (
            DragInputUnit(Rect(110, 86, 150, 21), LC("Min frame"), False, 100),
            DragInputUnit(Rect(264, 86, 150, 21), LC("Max frame"), False, 100),
            DragInputUnit(Rect(418, 86, 150, 21), LC("Probability %"), False, 100),
        )
        sd.CameraSetup_SwitchAdj[0].setValueFormat(minValue=0, vFormat="%d")
        sd.CameraSetup_SwitchAdj[0].setDragRate(1, 1)
        sd.CameraSetup_SwitchAdj[1].setValueFormat(minValue=0, vFormat="%d")
        sd.CameraSetup_SwitchAdj[1].setDragRate(1, 1)
        sd.CameraSetup_SwitchAdj[2].setValueFormat(minValue=0, maxValue=100, vFormat="%d")
        sd.CameraSetup_SwitchAdj[2].setDragRate(1, 1)
        # refresh
        refreshCameraAdj()
    def refreshCameraAdj():
        sd.CameraSetup_OffsetAdj[0].setValue(cc.offPos.x)
        sd.CameraSetup_OffsetAdj[1].setValue(cc.offPos.y)
        sd.CameraSetup_OffsetAdj[2].setValue(cc.offPos.z)
        sd.CameraSetup_RotateAdj[0].setValue(cc.offRot.x)
        sd.CameraSetup_RotateAdj[1].setValue(cc.offRot.y)
        sd.CameraSetup_RotateAdj[2].setValue(cc.offRot.z)
        sd.CameraSetup_PositionScaleAdj.setValue(cc.posScale)
        sd.CameraSetup_DistanceScaleAdj.setValue(cc.dstScale)
        sd.CameraSetup_SwitchAdj[0].setValue(cc.switchMinFramePeriod)
        sd.CameraSetup_SwitchAdj[1].setValue(cc.switchMaxFramePeriod)
        sd.CameraSetup_SwitchAdj[2].setValue(cc.switchProbability)
    def offsetAlignToSelect():
        try:
            sel = HSNeoOCI.create_from_selected()
        except:
            sel = None
        if isinstance(sel, HSNeoOCIItem):
            cc.offPos = sel.pos
            cc.offRot = sel.rot 
            msg = LC("Camera align to '%s': pos%s, rot%s")%(sel.text_name, str(sel.pos), str(sel.rot))
            updateFlg = True
        elif isinstance(sel, HSNeoOCIChar):
            cc.offPos = sel.as_actor.pos
            cc.offRot = sel.as_actor.rot
            msg = LC("Camera align to '%s': pos%s, rot%s")%(sel.text_name, str(sel.as_actor.pos), str(sel.as_actor.rot))
            updateFlg = True
        else:
            msg = LC("<color=#ff0000>No char or item is selected.</color>\nSelect an object in workspace first.")
            updateFlg = False
        guiPromptDialog(game, msg)
        return updateFlg
    def offsetAlignToCurrent():
        cdata = game.studio.cameraCtrl.cameraData
        #print "Current camera: pos=%s, rot=%s"%(str(cdata.pos), str(cdata.rotate))
        camFrame = cc.getFrame(mmdd.curFrame)
        if camFrame == None:
            msg = "<color=#ff0000>Fail to get current camera frame.</color>"
            updateFlg = False
        else:
            cpos = Vector3(-camFrame.posX * cc.posScale, camFrame.posY * cc.posScale, -camFrame.posZ * cc.posScale)
            crot = Vector3(-camFrame.rotX, -camFrame.rotY+180, -camFrame.rotZ)
            #print "Current frame: pos=%s, rot=%s"%(str(cpos), str(crot))
            opos = cdata.pos - cpos
            orot = cdata.rotate - crot
            cc.offPos = opos
            cc.offRot = orot
            msg = "Camera align to current camera: pos%s, rot%s"%(str(opos), str(orot))
            updateFlg = True
        guiPromptDialog(game, msg)
        return updateFlg
    def offsetResetZero():
        cc.offPos = Vector3.zero
        cc.offRot = Vector3.zero
        msg = "Camera position and rotation offset reseted to zero."
        guiPromptDialog(game, msg)
        return True

    # init switch
    if mmddGuiSwitch(game, mmddGuiCameraSetup):
        initCameraSetup()
        return

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    # Seek bar
    guiPartsSeekbar(game)

    # offset mode
    adjUpdate = False
    camUpdate = False
    GUILayout.BeginHorizontal()
    GUILayout.Label("Offset Mode:", GUILayout.Width(100))
    cc.enableMultiTarget = not GUILayout.Toggle(not cc.enableMultiTarget, LC("Fix offset mode"))
    GUILayout.Space(10)
    cc.enableMultiTarget = GUILayout.Toggle(cc.enableMultiTarget, LC("Auto multi target randomly") + (yellowText(LC(" (need restart anime)")) if mmdd.isPlaying else ""))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    if cc.enableMultiTarget:
        # multi target setting
        GUILayout.BeginHorizontal()
        GUILayout.Label("Switch setting:", GUILayout.Width(100))
        GUILayout.EndHorizontal()
        if sd.CameraSetup_SwitchAdj[0].paint():
            cc.switchMinFramePeriod = int(sd.CameraSetup_SwitchAdj[0].value)
            mmddConfig.switchMinFramePeriod = cc.switchMinFramePeriod
        if sd.CameraSetup_SwitchAdj[1].paint():
            cc.switchMaxFramePeriod = int(sd.CameraSetup_SwitchAdj[1].value)
            mmddConfig.switchMaxFramePeriod = cc.switchMaxFramePeriod
        if sd.CameraSetup_SwitchAdj[2].paint():
            cc.switchProbability = int(sd.CameraSetup_SwitchAdj[2].value)
            mmddConfig.switchProbability = cc.switchProbability
        GUILayout.BeginHorizontal()
        GUILayout.Space(100)
        GUILayout.Label(LC("In auto multi target mode, camera will focus on random dancer when playing."))
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Space(100)
        GUILayout.Label(LC("Setting here is to control how often should switch camera happens."))
        GUILayout.EndHorizontal()
    else:
        # offset adjust
        GUILayout.BeginHorizontal()
        GUILayout.Label("Camera offset:", GUILayout.Width(100))
        GUILayout.EndHorizontal()
        for adjctrl in sd.CameraSetup_OffsetAdj:
            if adjctrl.paint():
                cc.offPos = Vector3(sd.CameraSetup_OffsetAdj[0].value, sd.CameraSetup_OffsetAdj[1].value, sd.CameraSetup_OffsetAdj[2].value)
                camUpdate = True
        GUILayout.BeginHorizontal()
        GUILayout.Label(" ", GUILayout.Width(100))
        GUILayout.EndHorizontal()
        for adjctrl in sd.CameraSetup_RotateAdj:
            if adjctrl.paint():
                cc.offRot = Vector3(sd.CameraSetup_RotateAdj[0].value, sd.CameraSetup_RotateAdj[1].value, sd.CameraSetup_RotateAdj[2].value)
                camUpdate = True
        GUILayout.BeginHorizontal()
        GUILayout.Space(100)
        if GUILayout.Button("Align to selected", GUILayout.Width(150)):
            adjUpdate = offsetAlignToSelect()
        if GUILayout.Button("Offset to current", GUILayout.Width(150)):
            adjUpdate = offsetAlignToCurrent()
        if GUILayout.Button("  Reset to zero  ", GUILayout.Width(150)):
            adjUpdate = offsetResetZero()
        GUILayout.EndHorizontal()
    # scale adjustt
    GUILayout.Space(5)
    GUILayout.BeginHorizontal()
    GUILayout.Label("Camera scale:", GUILayout.Width(100))
    GUILayout.Space(304)
    if GUILayout.Button("Reset to Default", GUILayout.Width(150)):
        cc.posScale = cc.dstScale = cc.backupScale
        adjUpdate = True
    GUILayout.EndHorizontal()
    if sd.CameraSetup_PositionScaleAdj.paint():
        cc.posScale = sd.CameraSetup_PositionScaleAdj.value
        if cc.syncScale: 
            cc.dstScale = cc.posScale
            adjUpdate = True
        else:
            camUpdate = True
    if sd.CameraSetup_DistanceScaleAdj.paint():
        cc.dstScale = sd.CameraSetup_DistanceScaleAdj.value
        if cc.syncScale: 
            cc.posScale = cc.dstScale
            adjUpdate = True
        else:
            camUpdate = True

    GUILayout.BeginHorizontal()
    GUILayout.Label(" ", GUILayout.Width(100))
    cc.syncScale = GUILayout.Toggle(cc.syncScale, LC("Synchronize position scale and distance scale."))
    GUILayout.EndHorizontal()

    # interpolate
    GUILayout.Space(5)
    GUILayout.BeginHorizontal()
    GUILayout.Label("Interpolate:", GUILayout.Width(100))
    cc.useQuaternion = GUILayout.Toggle(cc.useQuaternion, LC("Use quaternion slerp interpolate"))
    GUILayout.EndHorizontal()

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button("Keyframes", btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiCameraKeyframe(game)
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

    #update
    if adjUpdate:
        refreshCameraAdj()
        camUpdate = True
    if camUpdate:
        mmdd.update()

def mmddGuiCameraKeyframe(game):
    from libkfguictrl import KeyframeViewer, DragInputUnit, CurveEditor
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    cc = mmdd.cameraController
    cameraGroup = ""
    cameraName = LC("Camera")
    cameraKey = LC("#Camera")

    def initKeyframeUI():
        # cood const
        kfx = 10
        kfy = 60
        kfw = fullw
        kfh = 45
        bth = 20
        cvw = 105
        vgap = 5
        xColor = Color(1, 0.2, 0.2, 1)
        yColor = Color(0.5, 1, 0.5, 1)
        zColor = Color(0.2, 0.5, 1, 1)
        dColor = Color.yellow
        fovColor = Color.yellow
        xyzxyzColor = (xColor, yColor, zColor, xColor, yColor, zColor, dColor, fovColor)
        try:
            # init keyframe viewer
            kfRect = Rect(kfx, kfy, kfw, kfh)
            if not hasattr(sd, "CameraKF_KeyframeView") or sd.CameraKF_KeyframeView.cameraCtrl != cc:
                sd.CameraKF_KeyframeView = KeyframeViewer(kfRect)
                setupKeyframeView(sd.CameraKF_KeyframeView)
            else:
                sd.CameraKF_KeyframeView.resize(kfRect, None)
                sd.CameraKF_KeyframeView.refreshKeyframe()
            sd.activedKeyframeView = sd.CameraKF_KeyframeView
            # keyframe opt ui mode
            sd.CameraKF_KeyframeOptUiMode = "normal"
            # init keyframe normal ui
            kfBtnRect = Rect(kfx, kfy + kfh + vgap, kfw - cvw - vgap * 3, bth)
            kfBtnContents = ([Rect.zero, LC("Copy"), whenHasKeyframeSelected, keyframeCopy, False], 
                                [Rect.zero, LC("Paste"), whenKeyframeClipboard, keyframePaste, False], 
                                [Rect.zero, LC("Delete"), whenHasKeyframeSelected, keyframeDelete, False], 
                                [Rect.zero, LC("Range-Sel"), whenStopped, keyframeRangeSelect, False])
            sd.CameraKF_KeyframeOptNormalUI = kfBtnContents
            kfBtnCount = len(kfBtnContents)
            kfBtnGap = 2
            kfBtnWidth = (kfBtnRect.width - (kfBtnCount - 1) * kfBtnGap) / kfBtnCount
            for i in range(kfBtnCount):
                kfBtnContents[i][0].Set(kfBtnRect.x + i * (kfBtnWidth + kfBtnGap), kfBtnRect.y, kfBtnWidth, kfBtnRect.height)
                if i == kfBtnCount - 1:
                    kfBtnContents[i][0].xMax = kfBtnRect.xMax + 1
            # init keyframe rngsel ui
            sd.CameraKF_KeyframeOptRngSelUI = (
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 0 "From"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 1 From-input
                Rect(kfBtnRect.x, kfBtnRect.y,  30, kfBtnRect.height),    # 2 "To"
                Rect(kfBtnRect.x, kfBtnRect.y,  50, kfBtnRect.height),    # 3 To-input
                Rect(kfBtnRect.x, kfBtnRect.y,   0, kfBtnRect.height),    # 4 space
                Rect(kfBtnRect.x, kfBtnRect.y,  60, kfBtnRect.height),    # 5 "Select"
                Rect(kfBtnRect.x, kfBtnRect.y,  25, kfBtnRect.height),    # 6 "X"
            )
            fixWidth = 6 * kfBtnGap
            for i in range(len(sd.CameraKF_KeyframeOptRngSelUI)):
                fixWidth += sd.CameraKF_KeyframeOptRngSelUI[i].width
            sd.CameraKF_KeyframeOptRngSelUI[4].width = kfBtnRect.width - fixWidth + 1
            for i in range(1,len(sd.CameraKF_KeyframeOptRngSelUI)):
                sd.CameraKF_KeyframeOptRngSelUI[i].x = sd.CameraKF_KeyframeOptRngSelUI[i-1].xMax + kfBtnGap
            sd.CameraKF_KeyframeOptRngStart = ""
            sd.CameraKF_KeyframeOptRngEnd = ""
            # init keyframe expand ui
            # init camera adj ui
            if not hasattr(sd, "CameraKF_CameraMmdMode"):
                sd.CameraKF_CameraMmdMode = False
            camVgap = 2
            camHgap = 2
            camAdjW1 = 150
            camAdjTW1 = 90
            camOptW = 80
            diuRect = Rect(0, 0, camAdjW1, bth)
            sd.CameraKF_CameraAdjUiText = (LC("Pos X"), LC("Pos Y"), LC("Pos Z"), LC("Pitch"), LC("Yaw"), LC("Roll"), LC("Distance"), LC("View Angle"))
            cameraAdjContents = []
            for i in range(len(sd.CameraKF_CameraAdjUiText)):
                diunit = DragInputUnit(diuRect, sd.CameraKF_CameraAdjUiText[i], False)
                diunit.vBtnWidth = camAdjTW1
                diunit.color = xyzxyzColor[i]
                diunit.onInputChangeCallback = cameraAdjChange
                if i in (0, 1, 2, 6, 7):
                    diunit.setDragRate(mmddConfig.dragIntensityPosX, mmddConfig.dragIntensityPosY)
                else:
                    diunit.setDragRate(mmddConfig.dragIntensityRotX, mmddConfig.dragIntensityRotY)
                cameraAdjContents.append(diunit)
            sd.CameraKF_CameraAdjUI = tuple(cameraAdjContents)
            cameraOptContents = ([Rect.zero, LC("Reset"), whenStopped, cameraReset, False],
                                    [Rect.zero, LC("Revert"), whenStopped, cameraRevert, False],
                                    [Rect.zero, LC("Register"), whenStopped, cameraRegister, False])
            sd.CameraKF_CameraOptUI = cameraOptContents
            cameraStsContents = ([Rect.zero, LC("Keyframes:")],
                                    [Rect.zero, LC("Camera Adjust:")]) 
            sd.CameraKF_CameraStsUI = cameraStsContents
            camRect = Rect(kfx, kfy + kfh + bth + vgap * 2, kfw - cvw - vgap, bth * 5 + camVgap * 5)
            cameraStsContents[0][0].Set(kfx, kfy-4, 80, bth)
            cameraStsContents[1][0].Set(camRect.x, camRect.y, camRect.width, bth)
            diuRect = Rect(camRect.x, camRect.y + (bth + camVgap) * 1, camAdjW1, bth)
            cameraAdjContents[0].resize(diuRect, False)
            diuRect.x += camAdjW1 + camHgap
            cameraAdjContents[1].resize(diuRect, False)
            diuRect.x += camAdjW1 + camHgap
            cameraAdjContents[2].resize(diuRect, False)
            diuRect = Rect(camRect.x, camRect.y + (bth + camVgap) * 2, camAdjW1, bth)
            cameraAdjContents[3].resize(diuRect, False)
            diuRect.x += camAdjW1 + camHgap
            cameraAdjContents[4].resize(diuRect, False)
            diuRect.x += camAdjW1 + camHgap
            cameraAdjContents[5].resize(diuRect, False)
            diuRect = Rect(camRect.x, camRect.y + (bth + camVgap) * 3, camAdjW1, bth)
            cameraAdjContents[6].resize(diuRect, False)
            diuRect.x += camAdjW1 + camHgap
            cameraAdjContents[7].resize(diuRect, False)
            sd.CameraKF_CameraMmdModeText = LC("Check camera parameter in MMD mode")
            mmdTglSize = GUI.skin.toggle.CalcSize(GUIContent(sd.CameraKF_CameraMmdModeText))
            sd.CameraKF_CameraMmdModeRect = Rect(camRect.x, camRect.y + (bth + camVgap) * 4, mmdTglSize.x, bth)
            for i in range(len(cameraOptContents)):
                cameraOptContents[i][0].Set(camRect.x + (camOptW + camHgap)*i, camRect.y + (bth + camVgap) * 5, camOptW, bth*1.5)
            # init keyframe clipboard
            if not hasattr(sd, "CameraKF_KeyframeClipboard"):
                sd.CameraKF_KeyframeClipboard = set()
            # init curve adj ui
            if not hasattr(sd, "CameraKF_CurveEdit"):
                from vmdlib import VmdCameraKeyFrame
                sd.CameraKF_CurveEdit = CurveEditor(kfx + kfw - cvw, kfy + kfh + vgap, cvw, VmdCameraKeyFrame.curveSetName)
                sd.CameraKF_CurveEdit.changeColors(mmddConfig.curveColors)
            else:
                sd.CameraKF_CurveEdit.resize(kfx + kfw - cvw, kfy + kfh + vgap, cvw)
            sd.CameraKF_CurveEdit.onCurveUpdateCallback = curveChange
            # update frame
            onFrameChange(int(mmdd.curFrame))
        except:
            traceback.print_exc()
            quit(game)
    def setupKeyframeView(kfView):
        # clear up to start
        kfView.cameraCtrl = cc
        kfView.changeColors(mmddConfig.keyframeColors)
        kfView.defaultScroll = "h"
        kfView.keyframeNameList.showGroup = False
        kfView.clearKeyframe()
        # only one frame
        kfView.importFrameName(cameraGroup, cameraName)
        # import keyframes
        for kf in cc.cameraKeyFrames:
            kfView.importKeyframe(cameraGroup, cameraName, kf.frameNo, kf)
        # done import
        kfView.arrangeImportedKeyframes()
        buildCameraKeyframesSetCache(kfView)
        # setup callback
        kfView.onFrameChangeCallback = frameChange
        kfView.onRowSelecCallback = cancelRowSelect
        kfView.onKeyframeMoveCallback = keyframeMove
        kfView.onKeyframeSelectCallback = keyframeSelect
    def updateCameraInput():
        if sd.CameraKF_CameraMmdMode:
            tfr = cc.createKeyframeFromCurrentCamera(0, None)  # frameNo and curve is not neccessary here
            sd.CameraKF_CameraAdjUI[0].setValue(tfr.posX)
            sd.CameraKF_CameraAdjUI[1].setValue(tfr.posY)
            sd.CameraKF_CameraAdjUI[2].setValue(tfr.posZ)
            sd.CameraKF_CameraAdjUI[3].setValue(-tfr.rotX)
            sd.CameraKF_CameraAdjUI[4].setValue(tfr.rotY)
            sd.CameraKF_CameraAdjUI[5].setValue(tfr.rotZ)
            sd.CameraKF_CameraAdjUI[6].setValue(-tfr.dist)
            sd.CameraKF_CameraAdjUI[7].setValue(tfr.viewAngle)
            for aui in sd.CameraKF_CameraAdjUI:
                aui.enable = False
        else:
            camdata = get_camera_data(game)
            sd.CameraKF_CameraAdjUI[0].setValue(camdata[0].x)
            sd.CameraKF_CameraAdjUI[1].setValue(camdata[0].y)
            sd.CameraKF_CameraAdjUI[2].setValue(camdata[0].z)
            sd.CameraKF_CameraAdjUI[3].setValue(camdata[2].x)
            sd.CameraKF_CameraAdjUI[4].setValue(camdata[2].y)
            sd.CameraKF_CameraAdjUI[5].setValue(camdata[2].z)
            sd.CameraKF_CameraAdjUI[6].setValue(camdata[1].z)
            sd.CameraKF_CameraAdjUI[7].setValue(camdata[3])
            for aui in sd.CameraKF_CameraAdjUI:
                aui.enable = not mmdd.isPlaying
    def updateCurveInput(selectedKeyframes):
        sd.CameraKF_CurveEdit.clearCurveData()
        for skf in selectedKeyframes:
            sd.CameraKF_CurveEdit.importCurveData(skf.curveSet)
    # keyframe view and camera input unit callback
    def frameChange(kfView):
        mmdd.gotoFrame(kfView.curFrame)
        onFrameChange(kfView.curFrame)
    def cancelRowSelect(kfView):
        kfView.selectBonesByKey(())
    def keyframeMove(kfView, keyMoveInfo):
        # UI is already processed
        for (bKey, oldFrame, newFrame, tgtObj) in keyMoveInfo:
            tgtObj.frameNo = newFrame
        buildCameraKeyframesSetCache(kfView)
        mmdd.refreshTotalFrame()
        kfView.refreshKeyframe()
        cc.setFrame(kfView.curFrame)
    def keyframeSelect(kfView):
        selectedKeyframes = set()
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            selectedKeyframes.add(kfView.keyframeBuffer[kfbtn.key][kfbtn.column])
        updateCurveInput(selectedKeyframes)
    def cameraAdjChange(tgtAdj, value, delta):
        if not mmdd.isPlaying:
            c = game.studio.cameraCtrl
            cdata = c.cameraData
            bidx = sd.CameraKF_CameraAdjUiText.index(tgtAdj.text)
            if bidx in (0, 1, 2):
                cdata.pos = Vector3(sd.CameraKF_CameraAdjUI[0].value, sd.CameraKF_CameraAdjUI[1].value, sd.CameraKF_CameraAdjUI[2].value)
            elif bidx in (3, 4, 5):
                cdata.rotate = Vector3(sd.CameraKF_CameraAdjUI[3].value, sd.CameraKF_CameraAdjUI[4].value, sd.CameraKF_CameraAdjUI[5].value)
            elif bidx == 6:
                cdata.distance = Vector3(0, 0, sd.CameraKF_CameraAdjUI[6].value)
            else:
                c.fieldOfView = sd.CameraKF_CameraAdjUI[7].value
    def curveChange(curveEditor):
        cc.keyframeCustomized = True    
    # seekbar callback
    def onFrameChange(newFrameNo):
        if not mmdd.isPlaying:
            if newFrameNo != sd.CameraKF_KeyframeView.curFrame:
                sd.CameraKF_KeyframeView.setCurrentFrame(newFrameNo)
    def onJumpToKeyframe(curFrame, direction):
        kfView = sd.CameraKF_KeyframeView
        if not mmdd.isPlaying:
            if sd.CameraKF_KeyframeSetCache:
                kfset = sd.CameraKF_KeyframeSetCache
            else:
                kfset = buildCameraKeyframesSetCache(kfView)
            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
    # util
    def quitCameraKeyframeView():
        sd.activedKeyframeView = None
    def getCurSelectedKeyframeInfo(kfView):
        selectedKfInfo = set()
        for kfbtn in kfView.keyframeBox.selectedKeyframes:
            selectedKfInfo.add(kfbtn.column)
        return selectedKfInfo
    def buildCameraKeyframesSetCache(kfView):
        sd.CameraKF_KeyframeSetCache = set(kfView.keyframeBuffer[cameraKey].keys())
        return sd.CameraKF_KeyframeSetCache
    def toggleCameraMode(nMode):
        sd.CameraKF_CameraMmdMode = nMode
        for aui in sd.CameraKF_CameraAdjUI:
            aui.enable = not nMode and not mmdd.isPlaying
    # operation button functions
    def renderOptButton(btnInfo, kfView):
        # btnInfo = [0 rect, 1 text, 2 enable-func, 3 call-func, 4 hasShift[, 5 shift-text, 6 shift-enable-func, 7 shift-call-func]]
        # 5 shift-text, 6 shift-enable-func, 7 shift-call-func only valid when 4 hasShift == True
        if btnInfo[4] and shift:
            # shift function
            btnValid = btnInfo[6](kfView)
            if GUI.Button(btnInfo[0], btnInfo[5] if btnValid else "<color=#808080>" + btnInfo[5] + "</color>") and btnValid:
                btnInfo[7](kfView)
        else:
            # normal function
            btnValid = btnInfo[2](kfView)
            if GUI.Button(btnInfo[0], btnInfo[1] if btnValid else "<color=#808080>" + btnInfo[1] + "</color>") and btnValid:
                btnInfo[3](kfView)
    def whenStopped(kfView):
        return not mmdd.isPlaying
    def whenKeyframeClipboard(kfView):
        return not mmdd.isPlaying and len(sd.CameraKF_KeyframeClipboard) > 0
    def whenHasKeyframeSelected(kfView):
        return not mmdd.isPlaying and len(getCurSelectedKeyframeInfo(kfView)) > 0
    def cameraReset(kfView):
        camdata0 = (Vector3(0, 10, 0), Vector3(0, 0, -50), Vector3(15, -180, 0), 23)
        set_camera_data(game, camdata0)
    def cameraRevert(kfView):
        cc.setFrame(kfView.curFrame)
    def cameraRegister(kfView):
        orgFrame, sof = cc.getFrame(kfView.curFrame)
        nFrame = cc.createKeyframeFromCurrentCamera(kfView.curFrame, orgFrame.curveSet if orgFrame else None)
        # to UI
        hasNew = kfView.registerKeyframe(cameraGroup, cameraName, kfView.curFrame, nFrame)
        # to ctrl
        cc.updateKeyframe(nFrame)
        if hasNew:
            buildCameraKeyframesSetCache(kfView)
            mmdd.refreshTotalFrame()
        kfView.refreshKeyframe()
    def keyframeCopy(kfView):
        sd.CameraKF_KeyframeClipboard.clear()
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        baseFrame = min(selectInfo)
        for kfFrame in selectInfo:
            orgFrame = kfView.keyframeBuffer[cameraKey][kfFrame]
            sd.CameraKF_KeyframeClipboard.add((kfFrame - baseFrame, orgFrame.copy()))
    def keyframePaste(kfView):
        hasNew = False
        newSelectKeyframeInfo = set()
        for (kfOffset, kfOrgFrame) in sd.CameraKF_KeyframeClipboard:
            nFrame = kfOrgFrame.copy()
            nFrame.frameNo = kfView.curFrame + kfOffset
            # to UI
            hasNew |= kfView.registerKeyframe(cameraGroup, cameraName, nFrame.frameNo, nFrame)
            # to MotionCtrl
            cc.updateKeyframe(nFrame)
            # select later
            newSelectKeyframeInfo.add((cameraKey, nFrame.frameNo))
        if hasNew:
            buildCameraKeyframesSetCache(kfView)
            mmdd.refreshTotalFrame()
        kfView.selectKeyframes(newSelectKeyframeInfo)
        keyframeSelect(kfView)
        kfView.refreshKeyframe()
        cc.setFrame(kfView.curFrame)
    def keyframeDelete(kfView):
        selectInfo = getCurSelectedKeyframeInfo(kfView)
        # to ctrl
        for kfFrame in selectInfo: 
            cc.deleteKeyframe(kfFrame)
        # UI
        kfView.deleteKeyframes([(cameraKey, i) for i in selectInfo])
        keyframeSelect(kfView)
        kfView.refreshKeyframe()
        buildCameraKeyframesSetCache(kfView)
        mmdd.refreshTotalFrame()
        cc.setFrame(kfView.curFrame)
    def keyframeRangeSelect(kfView):
        sd.CameraKF_KeyframeOptUiMode = "rngsel"
    def keyframeRangeSelectDo(kfView):
        try:
            rngStart = int(sd.CameraKF_KeyframeOptRngStart)
        except:
            sd.CameraKF_KeyframeOptRngStart = ""
            return False
        try:
            rngEnd = int(sd.CameraKF_KeyframeOptRngEnd)
        except:
            sd.CameraKF_KeyframeOptRngEnd = ""
            return False
        if rngStart > rngEnd:
            sd.CameraKF_KeyframeOptRngEnd = ""
            return False
        frameInRange = [f for f in kfView.keyframeBuffer[cameraKey].keys() if rngStart <= f <= rngEnd]
        selectInfo = set()
        for kfFrame in frameInRange:
            selectInfo.add((cameraKey, kfFrame))
        kfView.selectKeyframes(selectInfo)
        kfView.refreshKeyframe()
        keyframeSelect(kfView)
        return True
    # sepcial render
    def renderKeyframeRngSel(kfView):
        labelStyle = GUIStyle("label")
        labelStyle.alignment = TextAnchor.MiddleRight
        GUI.Label(sd.CameraKF_KeyframeOptRngSelUI[0], LC("From "), labelStyle)
        sd.CameraKF_KeyframeOptRngStart = GUI.TextField(sd.CameraKF_KeyframeOptRngSelUI[1], sd.CameraKF_KeyframeOptRngStart)
        GUI.Label(sd.CameraKF_KeyframeOptRngSelUI[2], LC("To "), labelStyle)
        sd.CameraKF_KeyframeOptRngEnd = GUI.TextField(sd.CameraKF_KeyframeOptRngSelUI[3], sd.CameraKF_KeyframeOptRngEnd)
        GUI.Label(sd.CameraKF_KeyframeOptRngSelUI[4], LC(" "))
        if GUI.Button(sd.CameraKF_KeyframeOptRngSelUI[5], LC("Select")):
            if keyframeRangeSelectDo(kfView):
                sd.CameraKF_KeyframeOptUiMode = "normal"
        if GUI.Button(sd.CameraKF_KeyframeOptRngSelUI[6], "X"):
            sd.CameraKF_KeyframeOptUiMode = "normal"

    # init switch
    if mmddGuiSwitch(game, mmddGuiCameraKeyframe):
        sd.LastPlayingStatus = mmdd.isPlaying
        # setup ui
        initKeyframeUI()
        return
    
    # Seek bar
    guiPartsSeekbar(game, onFrameChange, onJumpToKeyframe)

    # keyframe view
    kfView = sd.CameraKF_KeyframeView
    kfView.paint()

    # keyframe opt buttons
    if sd.CameraKF_KeyframeOptUiMode == "normal":
        for kfBtn in sd.CameraKF_KeyframeOptNormalUI:
            renderOptButton(kfBtn, kfView)
    elif sd.CameraKF_KeyframeOptUiMode == "rngsel":
        renderKeyframeRngSel(kfView)

    # camera adjust
    for st in sd.CameraKF_CameraStsUI:
        GUI.Label(st[0], st[1])
    for au in sd.CameraKF_CameraAdjUI:
        au.paint()
    nMode = GUI.Toggle(sd.CameraKF_CameraMmdModeRect, sd.CameraKF_CameraMmdMode, sd.CameraKF_CameraMmdModeText)
    if sd.CameraKF_CameraMmdMode != nMode:
        toggleCameraMode(nMode)
    for ob in sd.CameraKF_CameraOptUI:
        renderOptButton(ob, kfView)
    if not sd.LastPlayingStatus:
        updateCameraInput() # keep updating when stopped

    # curve
    sd.CameraKF_CurveEdit.paint()

    # Control buttons
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    cbwidth = (fullw - 4*4) / 5
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button("Export" if not mmdd.isPlaying else " ", btnstyle, GUILayout.Width(cbwidth)) and not mmdd.isPlaying:
        guiExportVmdData(game, cc)
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button("Setup", btnstyle, GUILayout.Width(cbwidth)):
        quitCameraKeyframeView()
        mmddGuiCameraSetup(game)
    if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
        quitCameraKeyframeView()
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

    # updates
    if sd.LastPlayingStatus != mmdd.isPlaying:
        sd.LastPlayingStatus = mmdd.isPlaying
        kfView.setEnable(not sd.LastPlayingStatus)
        sd.CameraKF_CurveEdit.enable = not sd.LastPlayingStatus
        updateCameraInput()
        if not sd.LastPlayingStatus:
            onFrameChange(int(mmdd.curFrame))

def mmddGuiClipSetup(game, clipCtrl):
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    if mmddGuiSwitch(game, mmddGuiClipSetup, clipCtrl):
        sd.clipSetupTempLinkFrame = str(clipCtrl.linkFrame)
        sd.clipSetupTempLinkFrameError = None
        sd.clipSetupTempLoopCount = str(clipCtrl.loopCount)
        sd.clipSetupTempLoopCountError = None
        sd.clipSetupTempPlaySpeed = str(clipCtrl.playSpeed)
        sd.clipSetupTempPlaySpeedError = None
        return

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    def checkUpd():
        err = False
        try:
            lf = int(sd.clipSetupTempLinkFrame)
            if lf < 0 or lf > mmdd.totalFrame:
                raise Exception()
            sd.clipSetupTempLinkFrameError = None
        except:
            sd.clipSetupTempLinkFrameError = LC("Input an integer between 0 - %d")%mmdd.totalFrame
            err = True
        try:
            lc = int(sd.clipSetupTempLoopCount)
            if lc < 0 and lc != -1:
                raise Exception()
            sd.clipSetupTempLoopCountError = None
        except:
            sd.clipSetupTempLoopCountError = LC("Input -1, 0 or positive integer")
            err = True
        try:
            ps = float(sd.clipSetupTempPlaySpeed)
            if ps <= 0:
                raise Exception()
            sd.clipSetupTempPlaySpeedError = None
        except:
            sd.clipSetupTempPlaySpeedError = LC("Input a float value greater than 0")
            err = True
        if err:
            return False
        else:
            clipCtrl.linkFrame = lf
            clipCtrl.loopCount = lc
            clipCtrl.playSpeed = ps
            return True

    # Seek bar
    guiPartsSeekbar(game)

    # scale adjustt
    GUILayout.Label(LC("Synchronize setting for clip <color=#00ff00>%s</color>:")%clipCtrl.clipName)
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Link frame:"), GUILayout.Width(80))
    sd.clipSetupTempLinkFrame = GUILayout.TextField(sd.clipSetupTempLinkFrame, GUILayout.Width(60))
    if sd.clipSetupTempLinkFrameError:
        GUILayout.Space(10)
        GUILayout.Label(redText(sd.clipSetupTempLinkFrameError))
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Space(85)
    GUILayout.Label(LC("* Set the frame when clip start to play."))
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Loop count:"), GUILayout.Width(80))
    sd.clipSetupTempLoopCount = GUILayout.TextField(sd.clipSetupTempLoopCount, GUILayout.Width(60))
    if sd.clipSetupTempLoopCountError:
        GUILayout.Space(10)
        GUILayout.Label(redText(sd.clipSetupTempLoopCountError))
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Space(85)
    GUILayout.Label(LC("* Set how many times clip loops. Value 0 means no loop, so clip plays once. Value 1 means loop once, so clip plays twice. And so on. Set to -1 for infinite loop."))
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Play speed:"), GUILayout.Width(80))
    sd.clipSetupTempPlaySpeed = GUILayout.TextField(sd.clipSetupTempPlaySpeed, GUILayout.Width(60))
    if sd.clipSetupTempPlaySpeedError:
        GUILayout.Space(10)
        GUILayout.Label(redText(sd.clipSetupTempPlaySpeedError))
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Space(85)
    GUILayout.Label(LC("* Set the speed rate use to playback this clip."))
    GUILayout.EndHorizontal()

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button(LC("Update"), btnstyle, GUILayout.Width(cbwidth)):
        if checkUpd():
            mmdd.refreshTotalFrame()
            mmddGuiMain(game)
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button(LC("Keyframes"), btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiClipKeyframe(game, clipCtrl)
    if GUILayout.Button(LC("Back"), btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

def mmddGuiClipKeyframe(game, clipCtrl):
    def editClip(tgtClip):
        from vnanime import changeGuiScreen, kfam_GUI_modify_clip
        from vnframe import _sh
        changeGuiScreen(_sh, kfam_GUI_modify_clip, tgtClip)

    # setup sync data
    sd = game.scenedata
    mmdd = game.gdata.mmdd
    sd.mmddSyncClipCtrl = clipCtrl
    sd.mmddSyncClipStartFrame = mmdd.curFrame

    # repare to jump to edit clip GUI
    from vnframe import toggle_devconsole, scriptHelperGUIToSceen
    toggle_devconsole(game)
    scriptHelperGUIToSceen(2)
    game.updFuncParam = clipCtrl.clip
    game.updFunc = editClip

def mmddGuiExtParentSetup(game):
    if mmddGuiSwitch(game, mmddGuiExtParentSetup):
        sd = game.scenedata
        sd.bindEditMode = "list"
        sd.bindOnSelect = None
        sd.bindScrollPos = Vector2.zero
        sd.bindSearch = ""
        sd.bindVmdBoneOnly = True
        sd.bindOpenedBones = set()
        return
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    def initBindEditInput():
        tgtBC = sd.bindOnSelect
        sd.bindPosOffsetX = "%.3f"%tgtBC.positionOffset.x
        sd.bindPosOffsetY = "%.3f"%tgtBC.positionOffset.y
        sd.bindPosOffsetZ = "%.3f"%tgtBC.positionOffset.z
        sd.bindRotOffsetX = "%.3f"%tgtBC.rotationOffset.eulerAngles.x
        sd.bindRotOffsetY = "%.3f"%tgtBC.rotationOffset.eulerAngles.y
        sd.bindRotOffsetZ = "%.3f"%tgtBC.rotationOffset.eulerAngles.z
        sd.bindBbsModeScale = "%.2f"%tgtBC.bbsModeScale
        sd.bindPosFollow = "%.2f"%tgtBC.positionBindFollow
        sd.bindRotFollow = "%.2f"%tgtBC.rotationBindFollow
        sd.bindFromFrame = str(tgtBC.startFrame)
        sd.bindEndFrame = str(tgtBC.endFrame) if tgtBC.endFrame != -1 else "inf"
        sd.bindPosAxis = [tgtBC.positionBindAxis == None or "x" in tgtBC.positionBindAxis, tgtBC.positionBindAxis == None or "y" in tgtBC.positionBindAxis, tgtBC.positionBindAxis == None or "z" in tgtBC.positionBindAxis]
        sd.bindRotAxis = [tgtBC.rotationBindAxis == None or "x" in tgtBC.rotationBindAxis, tgtBC.rotationBindAxis == None or "y" in tgtBC.rotationBindAxis, tgtBC.rotationBindAxis == None or "z" in tgtBC.rotationBindAxis]
    def verifyBindEditInput():
        tgtBC = sd.bindOnSelect
        if tgtBC.bindMode not in {"local", "global", "bbsLocal", "bbsGlobal"}:
            return LC("Unknown bind mode!")
        if not tgtBC.parent or not tgtBC.parentTransform:
            return LC("Parent bone not set!")
        if tgtBC.bindMode.startswith("bbs"):
            if not tgtBC.parent2nd or not tgtBC.parentTransform2nd:
                return LC("2nd parent bone not set!")
            try:
                tgtBC.bbsModeScale = float(sd.bindBbsModeScale)
            except:
                return LC("Input format error in parent scale")
        else:
            tgtBC.parent2nd = None
            tgtBC.parentTransform2nd = None
            tgtBC.bbsModeScale = 0.5
        if not tgtBC.child or not tgtBC.childTransform:
            return LC("Child bone not set!")
        if not tgtBC.positionBind and not tgtBC.rotationBind:
            return LC("Choose a bind type, position or rotation or both.")
        if tgtBC.positionBind:
            if not sd.bindPosAxis[0] and not sd.bindPosAxis[1] and not sd.bindPosAxis[2]:
                return LC("Choose at least one axis to bind for position")
            elif sd.bindPosAxis[0] and sd.bindPosAxis[1] and sd.bindPosAxis[2]:
                tgtBC.positionBindAxis = None
            else:
                tgtBC.positionBindAxis = set()
                if sd.bindPosAxis[0]: tgtBC.positionBindAxis.add("x")
                if sd.bindPosAxis[1]: tgtBC.positionBindAxis.add("y")
                if sd.bindPosAxis[2]: tgtBC.positionBindAxis.add("z")
        if tgtBC.rotationBind:
            if not sd.bindRotAxis[0] and not sd.bindRotAxis[1] and not sd.bindRotAxis[2]:
                return LC("Choose at least one axis to bind for position")
            elif sd.bindRotAxis[0] and sd.bindRotAxis[1] and sd.bindRotAxis[2]:
                tgtBC.rotationBindAxis = None
            else:
                tgtBC.rotationBindAxis = set()
                if sd.bindRotAxis[0]: tgtBC.rotationBindAxis.add("x")
                if sd.bindRotAxis[1]: tgtBC.rotationBindAxis.add("y")
                if sd.bindRotAxis[2]: tgtBC.rotationBindAxis.add("z")
        try:
            tgtBC.positionOffset = Vector3(float(sd.bindPosOffsetX), float(sd.bindPosOffsetY), float(sd.bindPosOffsetZ))
        except:
            return LC("Input format error in position offset.")
        try:
            tgtBC.rotationOffset = Quaternion.Euler(float(sd.bindRotOffsetX), float(sd.bindRotOffsetY), float(sd.bindRotOffsetZ))
        except:
            return LC("Input format error in rotation offset.")
        try:
            tgtBC.positionBindFollow = float(sd.bindPosFollow)
            if tgtBC.positionBindFollow <= 0 or tgtBC.positionBindFollow > 1.0:
                return LC("Position follow rate value must between 0 and 1.")
        except:
            return LC("Input format error in position follow rate.")
        try:
            tgtBC.rotationBindFollow = float(sd.bindRotFollow)
            if tgtBC.rotationBindFollow <= 0 or tgtBC.rotationBindFollow > 1.0:
                return LC("Rotation follow rate value must between 0 and 1.")
        except:
            return LC("Input format error in rotation follow rate.")
        try:
            tgtBC.startFrame = int(sd.bindFromFrame)
            tgtBC.endFrame = -1 if sd.bindEndFrame == "inf" else int(sd.bindEndFrame)
            if tgtBC.startFrame < 0:
                return LC("Start frame must >= 0.")
            if tgtBC.endFrame > 0:
                if tgtBC.endFrame <= tgtBC.startFrame:
                    return LC("End frame must greater than start frame.")
                if tgtBC.endFrame > mmdd.MaxFrameNo:
                    return LC("End frame must not greater than max frame(%d), set -1 for infinite")%mmdd.MaxFrameNo
            elif tgtBC.endFrame != -1:
                return LC("Invalid end frame input.")
        except:
            return LC("Input format error in start or end frame.")
        tgtBC.alias.strip()
        # all green
        return None
    def initParentEditInput():
        sd.bindLastFrame = int(mmdd.curFrame)
        sd.bindFromFrame = str(sd.bindLastFrame)
        sd.bindAdjOffsetIndex = -1
        sd.bindSelectedMotionId = None
        sd.bindSelectedMotionCtrl = None
        sd.bindSelectedBone = None
    def verifyAndAddParentFrame():
        try:
            fn = int(sd.bindFromFrame)
            if fn < 0 or fn > mmdd.MaxFrameNo:
                return LC("Please input a valid frame from 0 to %d")%mmdd.MaxFrameNo
        except:
            return LC("Input format error in frame setting.")
        # all green
        if sd.bindSelectedMotionId and sd.bindSelectedActorProp and sd.bindSelectedBone:
            sd.bindOnSelect.addKeyframe(fn, sd.bindSelectedMotionId, sd.bindSelectedActorProp, sd.bindSelectedBone)
        else:
            sd.bindOnSelect.addKeyframe(fn, None, None, None)
        return None
    def getCurrentSelectGuideObj():
        if len(game.studio.treeNodeCtrl.selectNodes) == 0:
            return None, None, redText("None, Please select object from WORKSPACE")
        if len(game.studio.treeNodeCtrl.selectNodes) > 1:
            return None, None, redText("Multi, Please select only one object")
        tgtObj = HSNeoOCI.create_from_treenode(game.studio.treeNodeCtrl.selectNodes[0])
        if isinstance(tgtObj, HSNeoOCIChar):
            tgtDesp = greenText(tgtObj.text_name) + " (" + greenText(LC("Charactor")) + ")"
        elif isinstance(tgtObj, HSNeoOCIItem):
            tgtDesp = greenText(tgtObj.text_name) + " (" + yellowText(LC("Item")) + ")"
        elif isinstance(tgtObj, HSNeoOCIFolder):
            tgtDesp = greenText(tgtObj.text_name) + " (" + cyanText(LC("Folder")) + ")"
        else:
            tgtDesp = greenText(str(tgtObj)) + " (" + redText(LC("Unsupported type!!")) + ")"
            tgtObj = None
        if tgtObj and hasattr(tgtObj.objctrl, "guideObject"):
            return tgtObj, tgtObj.objctrl.guideObject, tgtDesp
        else:
            return None, None, tgtDesp
    def displayObjectTree(go, indent):
        name = go.name
        if len(sd.bindSearch) == 0 or sd.bindSearch.lower() in go.name.lower():
            color = GUI.color
            if sd.bindSelectedBone == go.transform:
                GUI.color = Color.cyan
            elif hasattr(sd.bindOnSelect, "parentTransform") and sd.bindOnSelect.parentTransform == go.transform:
                GUI.color = Color.green
            elif sd.bindOnSelect.childTransform == go.transform:
                GUI.color = Color.magenta
            GUILayout.BeginHorizontal()
            if len(sd.bindSearch) == 0:
                GUILayout.Space(indent * 20)
                if go.transform.childCount != 0:
                    if GUILayout.Toggle(go in sd.bindOpenedBones, ""):
                        sd.bindOpenedBones.add(go)
                    else:
                        sd.bindOpenedBones.discard(go)
                else:
                    GUILayout.Space(20)
            if GUILayout.Button(name):
                sd.bindSelectedBone = go.transform
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUI.color = color
        if len(sd.bindSearch) > 0 or go in sd.bindOpenedBones:
            for i in range(go.transform.childCount):
                displayObjectTree(go.transform.GetChild(i).gameObject, indent + 1)

    # Seek bar
    guiPartsSeekbar(game)

    # Bind setting
    if sd.bindEditMode == "list":
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("External parent bind setting:"))
        GUILayout.FlexibleSpace()
        if GUILayout.Button(greenText(LC("Add Ext. Parent"))):
            from vmdlib import ParentController
            sd.bindOnSelect = ParentController()
            sd.bindSelectedMotionId = None
            sd.bindSelectedMotionCtrl = None
            sd.bindSelectedActorProp = None
            sd.bindSelectedBone = None
            sd.bindTreeScrollPos = Vector2.zero
            sd.bindEditModeOld = sd.bindEditMode
            sd.bindEditMode = "parent_select_c"
        if GUILayout.Button(yellowText(LC("Add Bind"))):
            from vmdlib import BindController
            sd.bindOnSelect = BindController()
            sd.bindOnSelect.startFrame = int(mmdd.curFrame)
            sd.bindOnSelect.endFrame = mmdd.MaxFrameNo
            initBindEditInput()
            sd.bindEditMode = "create_bind"
        if GUILayout.Button(LC("Set Order")):
            sd.bindEditMode = "set_order"
        GUILayout.EndHorizontal()
        sd.bindScrollPos = GUILayout.BeginScrollView(sd.bindScrollPos, GUI.skin.box, GUILayout.Width(fullw), GUILayout.Height(fullh - 110))
        for pc in mmdd.parentControllers:
            if pc.enable:
                if len(pc.alias):
                    pcDesp = LC("Ext. parent: %s (%d keys)")%(greenText(pc.alias), len(pc.keyframes))
                else:
                    pcDesp = LC("Ext. parent for %s (%d keys)")%(greenText(pc.ChildDesp), len(pc.keyframes))
            else:
                if len(pc.alias):
                    pcDesp = LC("Ext. parent: %s (%d keys)")%(pc.alias, len(pc.keyframes))
                else:
                    pcDesp = LC("Ext. parent for %s (%d keys)")%(pc.ChildDesp, len(pc.keyframes))
                pcDesp = colorText(pcDesp, "808080")
            GUILayout.BeginHorizontal()
            nPcEnable = GUILayout.Toggle(pc.enable, pcDesp)
            if nPcEnable != pc.enable:
                pc.enable = nPcEnable
                if nPcEnable:
                    pc.setFrame(mmdd.curFrame)
                else:
                    pc.cleanup()
            GUILayout.FlexibleSpace()
            if GUILayout.Button(LC("Keys"), GUILayout.Width(45)):
                from vmdlib import ParentController
                sd.bindOnSelect = ParentController()
                sd.bindOnSelect.copyFrom(pc)
                sd.bindUpdateTo = pc
                initParentEditInput()
                sd.bindEditMode = "update_parent"
            if GUILayout.Button("X", GUILayout.Width(25)):
                sd.tempToDel = pc
                def delParent(game):
                    mmdd.deleteParentController(sd.tempToDel)
                    mmddGuiBack(game)
                guiConfirmDialog(game, LC("Delete selected external parent setting for %s?")%(pcDesp), delParent)
            GUILayout.EndHorizontal()
        for bc in mmdd.bindControllers:
            bcEndFrame = str(bc.endFrame) if bc.endFrame != -1 else "inf"
            if bc.isWorking(mmdd.curFrame):
                if len(bc.alias):
                    bcDesp = LC("Bind: %s (%d - %s)")%(yellowText(bc.alias), bc.startFrame, bcEndFrame)
                else:
                    bcDesp = LC("Bind %s to %s (%d - %s)")%(yellowText(bc.childTransform.gameObject.name), yellowText(bc.parentTransform.gameObject.name), bc.startFrame, bcEndFrame)
            else:
                if len(bc.alias):
                    bcDesp = LC("Bind: %s (%d - %s)")%(bc.alias, bc.startFrame, bcEndFrame)
                else:
                    bcDesp = LC("Bind %s to %s (%d - %s)")%(bc.childTransform.gameObject.name, bc.parentTransform.gameObject.name, bc.startFrame, bcEndFrame)
                bcDesp = colorText(bcDesp, "808080")
            GUILayout.BeginHorizontal()
            nBcEnable = GUILayout.Toggle(bc.enable, bcDesp)
            if nBcEnable != bc.enable:
                bc.enable = nBcEnable
                if nBcEnable:
                    bc.setFrame(mmdd.curFrame)
                else:
                    bc.cleanup()
            GUILayout.FlexibleSpace()
            if GUILayout.Button(LC("Setup"), GUILayout.Width(45)):
                from vmdlib import BindController
                sd.bindOnSelect = BindController()
                sd.bindOnSelect.copyFrom(bc)
                sd.bindUpdateTo = bc
                initBindEditInput()
                sd.bindEditMode = "update_bind"
            if GUILayout.Button("X", GUILayout.Width(25)):
                sd.tempToDel = bc
                def delBind(game):
                    mmdd.deleteBindController(sd.tempToDel)
                    mmddGuiBack(game)
                guiConfirmDialog(game, LC("Delete selected external bind %s?")%(bcDesp), delBind)
            GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
    elif sd.bindEditMode == "set_order":
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Set Ext.parent / bind order:"))
        GUILayout.FlexibleSpace()
        if GUILayout.Button(greenText(LC("OK"))):
            sd.bindEditMode = "list"
        GUILayout.EndHorizontal()
        sd.bindScrollPos = GUILayout.BeginScrollView(sd.bindScrollPos, GUI.skin.box, GUILayout.Width(fullw), GUILayout.Height(fullh - 110))
        for pidx in range(len(mmdd.parentControllers)):
            pc = mmdd.parentControllers[pidx]
            canup = pidx > 0
            candn = pidx < len(mmdd.parentControllers) - 1
            if len(pc.alias):
                pcDesp = LC("Ext. parent: %s (%d keys)")%(greenText(pc.alias), len(pc.keyframes))
            else:
                pcDesp = LC("Ext. parent for %s (%d keys)")%(greenText(pc.ChildDesp), len(pc.keyframes))
            GUILayout.BeginHorizontal()
            GUILayout.Label(pcDesp)
            GUILayout.FlexibleSpace()
            if GUILayout.Button(u'\u2191' if canup else greyText(u'\u2191'), GUILayout.Width(25)) and canup:
                mmdd.parentControllers[pidx-1], mmdd.parentControllers[pidx] = mmdd.parentControllers[pidx], mmdd.parentControllers[pidx-1]
                break
            if GUILayout.Button(u'\u2193' if candn else greyText(u'\u2193'), GUILayout.Width(25)) and candn:
                mmdd.parentControllers[pidx+1], mmdd.parentControllers[pidx] = mmdd.parentControllers[pidx], mmdd.parentControllers[pidx+1]
                break
            GUILayout.EndHorizontal()
        for bidx in range(len(mmdd.bindControllers)):
            bc = mmdd.bindControllers[bidx]
            bcEndFrame = str(bc.endFrame) if bc.endFrame != -1 else "inf"
            canup = bidx > 0
            candn = bidx < len(mmdd.bindControllers) - 1
            if len(bc.alias):
                bcDesp = LC("Bind: %s (%d - %s)")%(yellowText(bc.alias), bc.startFrame, bcEndFrame)
            else:
                bcDesp = LC("Bind %s to %s (%d - %s)")%(yellowText(bc.childTransform.gameObject.name), yellowText(bc.parentTransform.gameObject.name), bc.startFrame, bcEndFrame)
            GUILayout.BeginHorizontal()
            GUILayout.Label(bcDesp)
            GUILayout.FlexibleSpace()
            if GUILayout.Button(u'\u2191' if canup else greyText(u'\u2191'), GUILayout.Width(25)) and canup:
                mmdd.bindControllers[bidx-1], mmdd.bindControllers[bidx] = mmdd.bindControllers[bidx], mmdd.bindControllers[bidx-1]
                break
            if GUILayout.Button(u'\u2193' if candn else greyText(u'\u2193'), GUILayout.Width(25)) and candn:
                mmdd.bindControllers[bidx+1], mmdd.bindControllers[bidx] = mmdd.bindControllers[bidx], mmdd.bindControllers[bidx+1]
                break
            GUILayout.EndHorizontal()
        GUILayout.EndScrollView()        
    elif sd.bindEditMode in {"create_bind", "update_bind"}:
        # title
        GUILayout.BeginHorizontal()
        if sd.bindEditMode == "create_bind":
            GUILayout.Label(LC("Create a new external bind setting:"))
            GUILayout.FlexibleSpace()
            if GUILayout.Button(greenText(LC("Create"))):
                err = verifyBindEditInput()
                if err:
                    guiPromptDialog(game, redText(LC("Invalid setting:\n\n")) + err)
                else:
                    sd.bindOnSelect.bind()
                    mmdd.bindControllers.append(sd.bindOnSelect)
                    sd.bindEditMode = "list"
                    mmdd.update()
            if GUILayout.Button(redText(LC("Cancel"))):
                sd.bindEditMode = "list"
        else:
            GUILayout.Label(LC("Edit selected external bind setting:"))
            GUILayout.FlexibleSpace()
            if GUILayout.Button(greenText(LC("Update"))):
                err = verifyBindEditInput()
                if err:
                    guiPromptDialog(game, redText(LC("Invalid setting:\n\n")) + err)
                else:
                    sd.bindUpdateTo.copyFrom(sd.bindOnSelect)
                    sd.bindEditMode = "list"
                    mmdd.update()
            if GUILayout.Button(redText(LC("Cancel"))):
                sd.bindEditMode = "list"
        GUILayout.EndHorizontal()
        # bind mode
        def checkGreenBtn(lblText, content):
            if GUILayout.Button(greenText(LC(lblText)) if sd.bindOnSelect.bindMode == content else LC(lblText)):
                sd.bindOnSelect.bindMode = content
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Bind Mode:"), GUILayout.Width(70))
        checkGreenBtn("Local", "local")
        checkGreenBtn("Global", "global")
        checkGreenBtn("Local 2", "bbsLocal")
        checkGreenBtn("Global 2", "bbsGlobal")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # parent
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Parent:"), GUILayout.Width(70))
        if len(sd.bindOnSelect.ParentDesp):
            pName = greenText(sd.bindOnSelect.ParentDesp)
        else:
            pName = LC("Click to select")
        if GUILayout.Button(pName):
            sd.bindSelectedBone = None
            sd.bindTreeScrollPos = Vector2.zero
            sd.bindEditModeOld = sd.bindEditMode
            sd.bindEditMode = "bind_select_p"
        # parent 2nd
        if sd.bindOnSelect.bindMode.startswith("bbs"):
            if len(sd.bindOnSelect.ParentDesp2nd):
                pName = greenText(sd.bindOnSelect.ParentDesp2nd)
            else:
                pName = LC("Click to select")
            if GUILayout.Button(pName):
                sd.bindSelectedBone = None
                sd.bindTreeScrollPos = Vector2.zero
                sd.bindEditModeOld = sd.bindEditMode
                sd.bindEditMode = "bind_select_p2"
            sd.bindBbsModeScale = GUILayout.TextField(sd.bindBbsModeScale, GUILayout.Width(50))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # child
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Child:"), GUILayout.Width(70))
        if len(sd.bindOnSelect.ChildDesp):
            cName = magentaText(sd.bindOnSelect.ChildDesp)
        else:
            cName = LC("Click to select")
        if GUILayout.Button(cName):
            sd.bindSelectedBone = None
            sd.bindTreeScrollPos = Vector2.zero
            sd.bindEditModeOld = sd.bindEditMode
            sd.bindEditMode = "bind_select_c"
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # position
        GUILayout.BeginHorizontal()
        sd.bindOnSelect.positionBind = GUILayout.Toggle(sd.bindOnSelect.positionBind, "Bind position", GUILayout.Width(100))
        sd.bindPosAxis[0] = GUILayout.Toggle(sd.bindPosAxis[0], "X")
        sd.bindPosOffsetX = GUILayout.TextField(sd.bindPosOffsetX, GUILayout.Width(60))
        sd.bindPosAxis[1] = GUILayout.Toggle(sd.bindPosAxis[1], "Y")
        sd.bindPosOffsetY = GUILayout.TextField(sd.bindPosOffsetY, GUILayout.Width(60))
        sd.bindPosAxis[2] = GUILayout.Toggle(sd.bindPosAxis[2], "Z")
        sd.bindPosOffsetZ = GUILayout.TextField(sd.bindPosOffsetZ, GUILayout.Width(60))
        if GUILayout.Button(LC("Use Current")) and sd.bindOnSelect.childTransform and sd.bindOnSelect.parentTransform:
            posoff = sd.bindOnSelect.parentTransform.InverseTransformPoint(sd.bindOnSelect.childTransform.position)
            sd.bindPosOffsetX = "%.3f"%posoff.x
            sd.bindPosOffsetY = "%.3f"%posoff.y
            sd.bindPosOffsetZ = "%.3f"%posoff.z
        if GUILayout.Button(LC("Reset")):
            sd.bindPosOffsetX = sd.bindPosOffsetY = sd.bindPosOffsetZ = "0.000"
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # rotation
        GUILayout.BeginHorizontal()
        sd.bindOnSelect.rotationBind = GUILayout.Toggle(sd.bindOnSelect.rotationBind, "Bind rotation", GUILayout.Width(100))
        sd.bindRotAxis[0] = GUILayout.Toggle(sd.bindRotAxis[0], "X")
        sd.bindRotOffsetX = GUILayout.TextField(sd.bindRotOffsetX, GUILayout.Width(60))
        sd.bindRotAxis[1] = GUILayout.Toggle(sd.bindRotAxis[1], "Y")
        sd.bindRotOffsetY = GUILayout.TextField(sd.bindRotOffsetY, GUILayout.Width(60))
        sd.bindRotAxis[2] = GUILayout.Toggle(sd.bindRotAxis[2], "Z")
        sd.bindRotOffsetZ = GUILayout.TextField(sd.bindRotOffsetZ, GUILayout.Width(60))
        if GUILayout.Button(LC("Use Current")) and sd.bindOnSelect.childTransform and sd.bindOnSelect.parentTransform:
            rotoff = Quaternion.Inverse(sd.bindOnSelect.parentTransform.rotation) * sd.bindOnSelect.childTransform.rotation
            rotoff = rotoff.eulerAngles
            sd.bindRotOffsetX = "%.3f"%rotoff.x
            sd.bindRotOffsetY = "%.3f"%rotoff.y
            sd.bindRotOffsetZ = "%.3f"%rotoff.z
        if GUILayout.Button(LC("Reset")):
            sd.bindRotOffsetX = sd.bindRotOffsetY = sd.bindRotOffsetZ = "0.000"
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # follow rate
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Position follow rate:"))
        sd.bindPosFollow = GUILayout.TextField(sd.bindPosFollow, GUILayout.Width(50))
        GUILayout.Space(10)
        GUILayout.Label(LC("Rotation follow rate:"))
        sd.bindRotFollow = GUILayout.TextField(sd.bindRotFollow, GUILayout.Width(50))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # others
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Work from frame:"))
        sd.bindFromFrame = GUILayout.TextField(sd.bindFromFrame, GUILayout.Width(50))
        GUILayout.Space(5)
        GUILayout.Label(LC("until frame:"))
        sd.bindEndFrame = GUILayout.TextField(sd.bindEndFrame, GUILayout.Width(50))
        GUILayout.Space(10)
        GUILayout.Label(LC("Alias:"))
        sd.bindOnSelect.alias = GUILayout.TextField(sd.bindOnSelect.alias, GUILayout.Width(120))
        if GUILayout.Button("X", GUILayout.Width(25)):
            sd.bindOnSelect.alias = ""
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    elif sd.bindEditMode == "update_parent":
        # title
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("External parent setting for %s")%magentaText(sd.bindOnSelect.ChildDesp))
        GUILayout.FlexibleSpace()
        if GUILayout.Button(greenText("Create") if sd.bindUpdateTo == None else greenText("Update")):
            if sd.bindUpdateTo:
                sd.bindUpdateTo.copyFrom(sd.bindOnSelect)
            else:
                mmdd.parentControllers.append(sd.bindOnSelect)
            sd.bindEditMode = "list"
            mmdd.update()
        if GUILayout.Button(redText(LC("Cancel"))):
            if sd.bindUpdateTo:
                pass
            else:
                sd.bindOnSelect.cleanup()
                sd.bindOnSelect = None
            sd.bindEditMode = "list"
            mmdd.update()
            return
        GUILayout.EndHorizontal()
        # keyframe list
        synReq = False
        sd.bindScrollPos = GUILayout.BeginScrollView(sd.bindScrollPos, GUI.skin.box, GUILayout.Width(fullw))
        if len(sd.bindOnSelect.keyframes):
            for idx in range(len(sd.bindOnSelect.keyframes)):
                if sd.bindOnSelect.parentIds[idx] and len(sd.bindOnSelect.parentIds[idx]) > 0:
                    pDesp = sd.bindOnSelect.parentTransforms[idx].name + "(" + sd.bindOnSelect.parentCharNames[idx] + ")"
                else:
                    pDesp = LC("None")
                frameDesp = LC("#%s: parent %s")%(greenText(str(sd.bindOnSelect.keyframes[idx])), greenText(pDesp))
                delKf = False
                GUILayout.BeginHorizontal()
                GUILayout.Label(frameDesp)
                GUILayout.FlexibleSpace()
                if sd.bindAdjOffsetIndex == idx:
                    if GUILayout.Button(LC("Current")):
                        sd.bindOnSelect.localPositions[idx] = sd.bindOnSelect.childTransform.localPosition
                        sd.bindOnSelect.localRotations[idx] = sd.bindOnSelect.childTransform.localRotation
                        synReq = True
                    if GUILayout.Button(LC("Zero")):
                        sd.bindOnSelect.localPositions[idx] = Vector3.zero
                        sd.bindOnSelect.localRotations[idx] = Quaternion.identity
                        synReq = True
                    if GUILayout.Button(LC("Origin")):
                        sd.bindOnSelect.localPositions[idx] = sd.bindOnSelect.orgChildPosition
                        sd.bindOnSelect.localRotations[idx] = sd.bindOnSelect.orgChildRotation
                        synReq = True
                    if GUILayout.Button(LC("Clear")):
                        sd.bindOnSelect.localPositions[idx] = None
                        sd.bindOnSelect.localRotations[idx] = None
                        synReq = True
                    if GUILayout.Button(LC("Cancel")):
                        sd.bindAdjOffsetIndex = -1
                else:
                    if GUILayout.Button(greenText(LC("Adjust Offset")) if sd.bindOnSelect.localPositions[idx] else orangeText(LC("Set Offset"))):
                        sd.bindAdjOffsetIndex = idx
                if GUILayout.Button("X", GUILayout.Width(25)):
                    sd.bindOnSelect.deleteKeyframeAt(idx)
                    delKf = True
                    synReq = True
                GUILayout.EndHorizontal()
                if delKf:
                    break
        else:
            GUILayout.Label("No parent keyframes")
        GUILayout.EndScrollView()
        # edit line
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Key at frame #"))
        if sd.bindLastFrame != int(mmdd.curFrame):
            sd.bindLastFrame = int(mmdd.curFrame)
            sd.bindFromFrame = str(sd.bindLastFrame)
        sd.bindFromFrame = GUILayout.TextField(sd.bindFromFrame, GUILayout.Width(50))
        GUILayout.Label(LC("set parent:"))
        if sd.bindSelectedBone:
            pDesp = greenText(sd.bindSelectedBone.name + "(" + sd.bindSelectedActorProp.text_name + ")")
        else:
            pDesp = LC("None")
        if GUILayout.Button(pDesp):
            sd.bindTreeScrollPos = Vector2.zero
            sd.bindEditModeOld = sd.bindEditMode
            sd.bindEditMode = "parent_select_p"
        if sd.bindSelectedBone and GUILayout.Button("X", GUILayout.Width(25)):
            sd.bindSelectedBone = None
        GUILayout.FlexibleSpace()
        if GUILayout.Button(magentaText(LC("Add Key"))):
            err = verifyAndAddParentFrame()
            if err:
                guiPromptDialog(game, redText(LC("Invalid setting:\n\n")) + err)
            else:
                synReq = True
        GUILayout.EndHorizontal()
        if synReq:
            sd.bindOnSelect.setFrame(sd.bindLastFrame)
            mc = mmdd.getMotionController(sd.bindOnSelect.childId)
            if mc: mc.setFrame(sd.bindLastFrame)
            sd.bindAdjOffsetIndex = -1
        # alias
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Setup alias:"), GUILayout.Width(100))
        sd.bindOnSelect.alias = GUILayout.TextField(sd.bindOnSelect.alias, GUILayout.Width(200))
        if GUILayout.Button("X", GUILayout.Width(25)):
            sd.bindOnSelect.alias = ""
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    elif sd.bindEditMode in {"bind_select_p", "bind_select_p2", "bind_select_c"}:
        tgtObj, tgtGuideObj, tgtDesp = getCurrentSelectGuideObj()
        # title
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Select parent bone from:") if sd.bindEditMode == "bind_select_p" else LC("Select child bone from:"))
        GUILayout.Label(tgtDesp)
        GUILayout.FlexibleSpace()
        if tgtObj and sd.bindSelectedBone and GUILayout.Button(greenText(LC("OK"))):
            if sd.bindEditMode == "bind_select_p":
                sd.bindOnSelect.setParent(tgtObj, sd.bindSelectedBone)
                sd.bindEditMode = sd.bindEditModeOld
            elif sd.bindEditMode == "bind_select_p2":
                sd.bindOnSelect.setParent2nd(tgtObj, sd.bindSelectedBone)
                sd.bindEditMode = sd.bindEditModeOld
            elif sd.bindEditMode == "bind_select_c":
                sd.bindOnSelect.setChild(tgtObj, sd.bindSelectedBone)
                sd.bindEditMode = sd.bindEditModeOld
            else:
                raise Exception("wront edit mode: %s"%sd.bindEditMode)
        if GUILayout.Button(redText(LC("Cancel"))):
            sd.bindEditMode = sd.bindEditModeOld
        GUILayout.EndHorizontal()
        # search line
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Search Bone:"), GUILayout.Width(100))
        sd.bindSearch = GUILayout.TextField(sd.bindSearch)
        if GUILayout.Button("X", GUILayout.Width(25)):
            sd.bindSearch = ""
        GUILayout.EndHorizontal()
        # bones tree
        sd.bindTreeScrollPos = GUILayout.BeginScrollView(sd.bindTreeScrollPos, GUI.skin.box, GUILayout.Height(fullh - 130))
        if tgtGuideObj:
            displayObjectTree(tgtGuideObj.transformTarget.gameObject, 0)
        GUILayout.EndScrollView()
    elif sd.bindEditMode in {"parent_select_p", "parent_select_c"}:
        # title
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("Select parent bone:") if sd.bindEditMode == "parent_select_p" else LC("Select child bone to setup external parent:"))
        #GUILayout.Label(tgtDesp)
        GUILayout.FlexibleSpace()
        if sd.bindSelectedActorProp and sd.bindSelectedBone and GUILayout.Button(greenText(LC("OK"))):
            if sd.bindEditMode == "parent_select_c":
                extPC = None
                for pc in mmdd.parentControllers:
                    if pc.childId == sd.bindSelectedMotionId and pc.childTransform == sd.bindSelectedBone:
                        extPC = pc
                        break
                if extPC:
                    sd.bindOnSelect.copyFrom(extPC)
                    sd.bindUpdateTo = extPC
                else:
                    sd.bindOnSelect.setChild(sd.bindSelectedMotionId, sd.bindSelectedActorProp, sd.bindSelectedBone, sd.bindSelectedMotionCtrl)
                    sd.bindUpdateTo = None
                initParentEditInput()
            elif sd.bindEditMode == "parent_select_p":
                pass
            else:
                raise Exception("Unknown edit mode!?")
            sd.bindEditMode = "update_parent"
        if GUILayout.Button(redText(LC("Cancel"))):
            sd.bindEditMode = sd.bindEditModeOld
        GUILayout.EndHorizontal()
        # info/search line
        GUILayout.BeginHorizontal()
        GUILayout.Label(LC("From tagged object:"), GUILayout.Width(200))
        GUILayout.Label(LC("Bone:"))
        GUILayout.Space(10)
        GUILayout.Label(LC("Search"))
        sd.bindSearch = GUILayout.TextField(sd.bindSearch, GUILayout.Width(100))
        if GUILayout.Button("X", GUILayout.Width(25)):
            sd.bindSearch = ""
        if sd.bindSelectedMotionCtrl:
            sd.bindVmdBoneOnly = GUILayout.Toggle(sd.bindVmdBoneOnly, "Vmd Bone Only")
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # obj list + tree list
        GUILayout.BeginHorizontal()
        sd.bindScrollPos = GUILayout.BeginScrollView(sd.bindScrollPos, GUI.skin.box, GUILayout.Width(200), GUILayout.Height(fullh-130))
        color = GUI.color
        for mcid in mmdd.motionSequence:
            mc = mmdd.motionControllers[mcid]
            if sd.bindSelectedMotionId == mcid:
                GUI.color = Color.green
            GUILayout.BeginHorizontal()
            if GUILayout.Button(greenText(mc.CharName + " (" + mcid + ")")):
                sd.bindSelectedMotionId = mcid
                sd.bindSelectedMotionCtrl = mc
                sd.bindSelectedBone = None
                sd.bindSelectedActorProp = game.scenef_get_actor(mcid)
                if not sd.bindSelectedActorProp:
                    sd.bindSelectedActorProp = game.scenef_get_propf(mcid)
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUI.color = color
        for aid in game.scenef_get_all_actors():
            if aid in mmdd.motionSequence:
                continue
            actor = game.scenef_get_actor(aid)
            if sd.bindSelectedMotionId == aid:
                GUI.color = Color.green
            GUILayout.BeginHorizontal()
            if GUILayout.Button(actor.text_name + " (" + aid + ")"):
                sd.bindSelectedMotionId = aid
                sd.bindSelectedMotionCtrl = None
                sd.bindSelectedBone = None
                sd.bindSelectedActorProp = actor
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUI.color = color
        for pid in game.scenef_get_all_props():
            if pid in mmdd.motionSequence:
                continue
            prop = game.scenef_get_propf(pid)
            if sd.bindSelectedMotionId == pid:
                GUI.color = Color.green
            GUILayout.BeginHorizontal()
            if GUILayout.Button(prop.text_name + " (" + pid + ")"):
                sd.bindSelectedMotionId = pid
                sd.bindSelectedMotionCtrl = None
                sd.bindSelectedBone = None
                sd.bindSelectedActorProp = prop
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
            GUI.color = color
        GUILayout.EndScrollView()
        sd.bindTreeScrollPos = GUILayout.BeginScrollView(sd.bindTreeScrollPos, GUI.skin.box, GUILayout.Height(fullh-130))
        if sd.bindSelectedMotionId in game.scenef_get_all_actors():
            guideObject = game.scenef_get_actor(sd.bindSelectedMotionId).objctrl.guideObject
        elif sd.bindSelectedMotionId in game.scenef_get_all_props():
            guideObject = game.scenef_get_propf(sd.bindSelectedMotionId).objctrl.guideObject
        else:
            guideObject = None
        if sd.bindVmdBoneOnly and sd.bindSelectedMotionCtrl:
            listed = 0
            for bname in sd.bindSelectedMotionCtrl.boneNameList:
                bDesp = bname + " (" + sd.bindSelectedMotionCtrl.boneDefineMap[bname].vmdBone + ")"
                bTran = sd.bindSelectedMotionCtrl.getVmdBone(bname).transform
                if len(sd.bindSearch) > 0 and not sd.bindSearch in bDesp:
                    continue
                color = GUI.color
                GUILayout.BeginHorizontal()
                if sd.bindSelectedBone == bTran:
                    GUI.color = Color.green
                if GUILayout.Button(bDesp):
                    sd.bindSelectedBone = bTran
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
                GUI.color = color
                listed += 1
            if not listed:
                GUILayout.Label(LC("No bone matches search keyword..."))
        elif guideObject:
            displayObjectTree(guideObject.transformTarget.gameObject, 0)
        else:
            GUILayout.Label(LC("Select an object from left panel."))
            GUILayout.Label(LC("Only object registed as Actor/Prop can be selected."))
        GUILayout.EndScrollView()
        GUILayout.EndHorizontal()
    else:
        sd.bindEditMode = "list"

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.RepeatButton("Update", btnstyle, GUILayout.Width(cbwidth)):
        mmdd.update()
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth)):
        pass
    if GUILayout.Button(LC("Back") if sd.bindEditMode == "list" else " ", btnstyle, GUILayout.Width(cbwidth)) and sd.bindEditMode == "list":
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

def mmddGuiSetting(game):
    if mmddGuiSwitch(game, mmddGuiSetting):
        sd = game.scenedata
        sd.settingScrollPos = Vector2.zero
        sd.settingShortcutInput = None
        return
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    # Seek bar
    guiPartsSeekbar(game)
    GUILayout.Space(10)
    
    sd.settingScrollPos = GUILayout.BeginScrollView(sd.settingScrollPos, GUILayout.Width(fullw))
    # test
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("DEBUG:"), GUILayout.Width(100))
    if GUILayout.Button(LC("Create motion controller")):
        actor = None
        wsSels = tuple(game.studio.treeNodeCtrl.selectNodes)
        for f in (game.scene_get_all_females() + game.scene_get_all_males()):
            fa = f.as_actor
            if fa.treeNodeObject in wsSels:
                actor = fa
                break
        if actor:
            mc = mmdd.createMotionController(actor)
            mmddGuiMain(game)
    if GUILayout.Button(LC("Clear all bone gizmos")):
        mmdd.clearBoneGizmos()
    if GUILayout.Button(LC("Cleanup ALL")):
        mmdd.cleanup()
    GUILayout.EndHorizontal()
    #GUILayout.BeginHorizontal()
    #GUILayout.Label(LC("DEBUG:"), GUILayout.Width(100))
    #GUILayout.EndHorizontal()

    # setup playback timer
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Play timer:"), GUILayout.Width(100))
    if GUILayout.Button(colorText(LC("For Record"), "00ff00" if mmdd.playTimer == "record" else "ffffff")):
        mmdd.playTimer = "record"
    if GUILayout.Button(colorText(LC("Realtime"), "00ff00" if mmdd.playTimer == "realtime" else "ffffff")):
        mmdd.playTimer = "realtime"
    GUILayout.EndHorizontal()

    # setup playback speed/loop
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Slow motion:"), GUILayout.Width(100))
    if mmdd.isPlaying:
        if mmdd.playFPS == 30.0:
            spd = LC("NORMAL")
        elif mmdd.playFPS == 15.0:
            spd = LC("1/2")
        elif mmdd.playFPS == 6.0:
            spd = LC("1/5")
        else:
            spd = LC("1/10")
        GUILayout.Label(LC("Playing at <color=#00ff00>%s</color> speed.")%spd)
    else:
        if GUILayout.Button(colorText(LC("NORMAL"), "00ff00" if mmdd.playFPS == 30.0 else "ffffff")):
            mmdd.setPlayFPS(30.0)
        if GUILayout.Button(colorText(LC("1/2"), "00ff00" if mmdd.playFPS == 15.0 else "ffffff")):
            mmdd.setPlayFPS(15.0)
        if GUILayout.Button(colorText(LC("1/5"), "00ff00" if mmdd.playFPS == 6.0 else "ffffff")):
            mmdd.setPlayFPS(6.0)
        if GUILayout.Button(colorText(LC("1/10"), "00ff00" if mmdd.playFPS == 3.0 else "ffffff")):
            mmdd.setPlayFPS(3.0)
    GUILayout.Space(10)
    GUILayout.Label(LC("Loop:"))
    mmdd.loop = GUILayout.Toggle(mmdd.loop, LC("Enable"))
    GUILayout.EndHorizontal()

    # auto save/load
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Auto S/L:"), GUILayout.Width(100))
    mmddConfig.autoLoad = GUILayout.Toggle(mmddConfig.autoLoad, LC("Auto load"))
    GUILayout.Space(10)
    mmddConfig.autoSave = GUILayout.Toggle(mmddConfig.autoSave, LC("Auto save"))
    GUILayout.Space(10)
    mmddConfig.autoBackup = GUILayout.Toggle(mmddConfig.autoBackup, LC("Auto backup"))
    temptxt = GUILayout.TextField("%d"%mmddConfig.autoBackupInterval, GUILayout.Width(40))
    if temptxt != "%d"%mmddConfig.autoBackupInterval:
        try:
            mmddConfig.autoBackupInterval = int(temptxt)
        except:
            pass
    GUILayout.Label(LC("sec"))
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # GUI
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("GUI on playing:"), GUILayout.Width(100))
    if GUILayout.Button(colorText(LC("Normal"), "00ff00" if mmddConfig.guiOnPlaying == "normal" else "ffffff")):
        mmddConfig.guiOnPlaying = "normal"
    if GUILayout.Button(colorText(LC("Minimize"), "00ff00" if mmddConfig.guiOnPlaying == "minimize" else "ffffff")):
        mmddConfig.guiOnPlaying = "minimize"
    if GUILayout.Button(colorText(LC("Hide"), "00ff00" if mmddConfig.guiOnPlaying == "hide" else "ffffff")):
        mmddConfig.guiOnPlaying = "hide"
    GUILayout.Space(10)
    GUILayout.Label(LC("GUI Pos/Size:"))
    guiRectUpd = False
    if not hasattr(sd, "tempSettingGui") or sd.tempSettingGui == "normal":
        if GUILayout.Button(LC("Move")):
            sd.tempSettingGui = "move"
            mmddConfig.guiX = int(game.windowRect.x)
            mmddConfig.guiY = int(game.windowRect.y)
        if GUILayout.Button(LC("Size")):
            sd.tempSettingGui = "size"
        tmpLock = GUILayout.Toggle(mmddConfig.guiLock, LC("Lock Pos"))
        if mmddConfig.guiLock != tmpLock:
            mmddConfig.guiLock = tmpLock
            game.windowDraggable = not mmddConfig.guiLock
    elif sd.tempSettingGui == "move":
        if GUILayout.Button(u"\u2190", GUILayout.Width(30)):
            mmddConfig.guiX -= 10
            guiRectUpd = True
        if GUILayout.Button(u"\u2192", GUILayout.Width(30)):
            mmddConfig.guiX += 10
            guiRectUpd = True
        if GUILayout.Button(u"\u2191", GUILayout.Width(30)):
            mmddConfig.guiY -= 10
            guiRectUpd = True
        if GUILayout.Button(u"\u2193", GUILayout.Width(30)):
            mmddConfig.guiY += 10
            guiRectUpd = True
        if GUILayout.Button(LC("Back"), GUILayout.Width(40)):
            sd.tempSettingGui = "normal"
    elif sd.tempSettingGui == "size":
        if GUILayout.Button(LC("W+"), GUILayout.Width(30)):
            mmddConfig.guiWidth += 10
            guiRectUpd = True
        if GUILayout.Button(LC("W-"), GUILayout.Width(30)):
            mmddConfig.guiWidth -= 10
            if mmddConfig.guiWidth < 600: mmddConfig.guiWidth = 600
            guiRectUpd = True
        if GUILayout.Button(LC("H+"), GUILayout.Width(30)):
            mmddConfig.guiHeight += 10
            guiRectUpd = True
        if GUILayout.Button(LC("H-"), GUILayout.Width(30)):
            mmddConfig.guiHeight -= 10
            if mmddConfig.guiHeight < 320: mmddConfig.guiHeight = 320
            guiRectUpd = True
        if GUILayout.Button(LC("Back")):
            sd.tempSettingGui = "normal"
    if guiRectUpd:
        game.windowRect = Rect(mmddConfig.guiX, mmddConfig.guiY, mmddConfig.guiWidth, mmddConfig.guiHeight)
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    # GC optimize
    def validSizeTxt(inputtxt, enablePercent):
        inputtxt = inputtxt.lower()
        try:
            if inputtxt.endswith("%") and enablePercent:
                ptxt = inputtxt[:-1]
                pvalue = float(ptxt)
            elif inputtxt.endswith("m") or inputtxt.endswith("g"):
                mtxt = inputtxt[:-1]
                mvalue = float(mtxt)
            else:
                return False
            return True
        except:
            return False
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("GC Optimize:"), GUILayout.Width(100))
    if GUILayout.Button(colorText(LC("None"), "00ff00" if mmddConfig.gcOptimize == "none" else "ffffff")):
        mmddConfig.gcOptimize = "none"
    #if GUILayout.Button(colorText(LC("Periodic Collect"), "00ff00" if mmddConfig.gcOptimize == "periodic" else "ffffff")):
    #    mmddConfig.gcOptimize = "periodic"
    if GUILayout.Button(colorText(LC("Pre-Build Big Heap"), "00ff00" if mmddConfig.gcOptimize == "bigheap" else "ffffff")):
        mmddConfig.gcOptimize = "bigheap"
    if game.gdata.garbage_collector_valid:
        if GUILayout.Button(colorText(LC("Suspend On Playing"), "00ff00" if mmddConfig.gcOptimize == "suspend" else "ffffff")):
            mmddConfig.gcOptimize = "suspend"
    GUILayout.EndHorizontal()
    if mmddConfig.gcOptimize == "periodic":
        GUILayout.BeginHorizontal()
        GUILayout.Label(" ", GUILayout.Width(100))
        GUILayout.Label(LC("Collect every"))
        GUILayout.TextField("30", GUILayout.Width(40))
        GUILayout.Label(LC("frames"))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    elif mmddConfig.gcOptimize == "bigheap":
        GUILayout.BeginHorizontal()
        GUILayout.Label(" ", GUILayout.Width(100))
        GUILayout.Label(LC("Pre-Build heap size"))
        inputTxt = GUILayout.TextField(mmddConfig.gcBigHeapSize, GUILayout.Width(60))
        if mmddConfig.gcBigHeapSize != inputTxt:
            mmddConfig.gcBigHeapSize = inputTxt
            sd.tempSettingGcBigHeapSizeChanged = True
        if validSizeTxt(mmddConfig.gcBigHeapSize, True):
            if hasattr(sd, "tempSettingGcBigHeapSizeChanged"):
                GUILayout.Label(yellowText(LC("** Restart MMDD to apply change **")))
            else:
                pass    # no message
        else:
            GUILayout.Label(redText(LC("Invalid! set number + M, G or %")))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    elif mmddConfig.gcOptimize == "suspend":
        GUILayout.BeginHorizontal()
        GUILayout.Label(" ", GUILayout.Width(100))
        GUILayout.Label(LC("Garbage size limit"))
        inputTxt = GUILayout.TextField(mmddConfig.gcSuspendLimit, GUILayout.Width(60))
        if GUILayout.Button("X", GUILayout.Width(25)):
            inputTxt = "inf"
        if mmddConfig.gcSuspendLimit != inputTxt:
            mmddConfig.gcSuspendLimit = inputTxt
            sd.tempSettingGcSuspendLimitChanged = True
        if validSizeTxt(mmddConfig.gcSuspendLimit, False) or mmddConfig.gcSuspendLimit == "inf":
            if hasattr(sd, "tempSettingGcSuspendLimitChanged"):
                GUILayout.Label(yellowText(LC("** Restart MMDD to apply change **")))
            elif mmddConfig.gcSuspendLimit == "inf":
                GUILayout.Label(redText(LC("** MIND YOUR MEMORY USAGE! **")))
        else:
            GUILayout.Label(redText(LC("Invalid! set number + M or G")))
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()

    # Vmd home folder
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("VMD Home Dir:"), GUILayout.Width(100))
    mmddConfig.vmdHomeDir = GUILayout.TextField(mmddConfig.vmdHomeDir)
    GUILayout.EndHorizontal()

    # Shortcuts
    def releaseShortcut(param):
        game.gdata.mmddSuspendShortcut = False
    def releaseShortcutLater():
        sd.settingShortcutInput = None
        game.updFunc = releaseShortcutLater1
    def releaseShortcutLater1(param):
        game.updFunc = releaseShortcut
    def dispNameForShortcut(shortcut):
        if sd.settingShortcutInput == shortcut:
            return "<i>" + cyanText(LC("-Press A Key-")) + "</i>"
        elif mmddConfig.shortcuts[shortcut][1] and len(mmddConfig.shortcuts[shortcut][1][0]):
            return mmddConfig.shortcuts[shortcut][1][0]
        else:
            return "<i>" + yellowText(LC("-Not Set-")) + "</i>"
    def setupShortcut(shortcut):
        if sd.settingShortcutInput == shortcut:
            mmddConfig.shortcuts[sd.settingShortcutInput][1] = None
            releaseShortcutLater()
        else:
            sd.settingShortcutInput = shortcut
            game.gdata.mmddSuspendShortcut = True
    shortcutTxtWidth = 80
    shortcutBtnWidth = 120
    if sd.settingShortcutInput:
        evt = Event.current
        if evt.isKey:
            if evt.keyCode == KeyCode.Escape:
                releaseShortcutLater()
            elif evt.keyCode != KeyCode.LeftControl and evt.keyCode != KeyCode.RightControl and \
               evt.keyCode != KeyCode.LeftAlt and evt.keyCode != KeyCode.RightAlt:
                keyname = str(evt.keyCode)
                if evt.shift: keyname = "Shift+" + keyname
                if evt.alt: keyname = "Alt+" + keyname
                if evt.control: keyname = "Ctrl+" + keyname
                print "detected <%s> press"%keyname
                mmddConfig.shortcuts[sd.settingShortcutInput][1] = parseKeyCode(keyname)
                releaseShortcutLater()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC("Shortcuts:"), GUILayout.Width(100))
    GUILayout.Label(LC("Start/Stop"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Start/Stop"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Start/Stop")
    GUILayout.FlexibleSpace()
    GUILayout.Label(LC("Next Dance"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Next Dance"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Next Dance")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC(" "), GUILayout.Width(100))
    GUILayout.Label(LC("Go Forward"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Go Forward"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Go Forward")
    GUILayout.FlexibleSpace()
    GUILayout.Label(LC("Go Backward"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Go Backward"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Go Backward")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC(" "), GUILayout.Width(100))
    GUILayout.Label(LC("Next Frame"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Next Frame"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Next Frame")
    GUILayout.FlexibleSpace()
    GUILayout.Label(LC("Prev Frame"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Prev Frame"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Prev Frame")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC(" "), GUILayout.Width(100))
    GUILayout.Label(LC("Next Second"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Next Second"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Next Second")
    GUILayout.FlexibleSpace()
    GUILayout.Label(LC("Prev Second"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Prev Second"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Prev Second")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()
    GUILayout.BeginHorizontal()
    GUILayout.Label(LC(" "), GUILayout.Width(100))
    GUILayout.Label(LC("Goto Start"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Goto Start"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Goto Start")
    GUILayout.FlexibleSpace()
    GUILayout.Label(LC("Goto End"), GUILayout.Width(shortcutTxtWidth))
    if GUILayout.Button(dispNameForShortcut("Goto End"), GUILayout.Width(shortcutBtnWidth)):
        setupShortcut("Goto End")
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

    GUILayout.EndScrollView()

    # Control buttons
    cbwidth = (fullw - 4*4) / 5
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    if GUILayout.Button(redText(LC("Stop")) if mmdd.isPlaying else greenText(LC("Start")), btnstyle, GUILayout.Width(cbwidth)):
        mmdd.startStop()
    if GUILayout.Button(LC("Clear Data"), btnstyle, GUILayout.Width(cbwidth)):
        def doClear(game):
            clearFromPNG("mmddirectorData")
            mmddGuiSetting(game)
        msg = LC("Clear all data saved in PNG file?\n\nClear data can prevent PNG file grows too big. This function only clear data saved by MMDD version 2.0 or later, MMDD version before 2.0 save data in scene folder, you can delete it manually.\n<color=#ff0000>You should save again to prevent lost current data.</color>")
        guiConfirmDialog(game, msg, doClear)
    if GUILayout.Button(LC("Save Data"), btnstyle, GUILayout.Width(cbwidth)):
        pldata = preloadMultiSceneData()
        def doSave(tgtFld):
            print "Save data to '%s' set..."%tgtFld
            try:
                mmddGuiSetting(game)    # cancel last input dialog
                mmdd.saveToScene(tgtFld)
                guiPromptDialog(game, LC("<color=#00ff00>Data saved OK.</color>\n\nDONT forget save you scene!"))
            except Exception, e:
                guiPromptDialog(game, LC("<color=#ff0000>Data save FAILED!</color>\n\n") + str(e))
        msg = LC("Save data to:")
        lines = [(["t1", True, magentaText(LC("Scene defaut save data"))], ("o", "save")), (("s", 10),), (("s", 10), ("l", LC("custom data set")))]
        for pd in pldata:
            if pd[0] in ("save", "backup"): continue
            tc = ["t1", False, "<color=#ff00ff>%s</color>: %s"%(pd[0], pd[1])]
            lelm = (tc, ("o", pd[0]))
            lines.append(lelm)
        lines.append((["t1", False, cyanText(LC("New data set:"))], ["i", "", 100]))
        def saveSelectedData(game):
            for lc in lines:
                if lc[0][0] == "t1" and lc[0][1]:
                    doSave(lc[1][1])
                    return
        btns = ((LC("OK"), saveSelectedData), (LC("Cancel"), None))
        guiInputDialog(game, (msg, lines, btns))
    if GUILayout.Button(LC("Load Data"), btnstyle, GUILayout.Width(cbwidth)):
        pldata = preloadMultiSceneData()
        def doLoad(tgtIndex):
            mmdd.cleanup()
            guiWaitDialog(game, LC("Load data set: %s")%greenText(pldata[tgtIndex][0]))
            game.updFuncParam = pldata[tgtIndex]
            game.updFunc = doLoad2
        def doLoad2(param):
            game.updFuncParam = param
            game.updFunc = doLoad3
        def doLoad3(param):
            game.updFuncParam = param
            game.updFunc = doLoad4
        def doLoad4(param):
            game.updFuncParam = param
            game.updFunc = doLoadX
        def doLoadX(param):
            err = mmdd.loadFromScene(param[0])
            if err:
                guiPromptDialog(game, LC("Fail to load data from '%s':\n%s")%(param[0], err), mmddGuiSetting)
            else:
                guiPromptDialog(game, LC("Setting data from '%s' loaded.")%param[0], mmddGuiMain)
        if len(pldata) == 0:
            guiPromptDialog(game, LC("No save data found!"))
        elif len(pldata) == 1:
            doLoad(0)
        else:
            msg = LC("Load data from:")
            lines = []
            for pd in pldata:
                tc = ["t1", pd[0] == "save", "<color=#00ff00>%s</color>: %s"%(pd[0], pd[1])]
                lelm = (tc,)
                lines.append(lelm)
            def loadSelectedData(g):
                for i in range(len(lines)):
                    if lines[i][0][1]:
                        doLoad(i)
                        return
            btns = ((LC("OK"), loadSelectedData), (LC("Cancel"), None))
            guiInputDialog(game, (msg, lines, btns))
    if GUILayout.Button(LC("Back"), btnstyle, GUILayout.Width(cbwidth)):
        mmddGuiMain(game)
    GUILayout.EndHorizontal()

def mmddGuiStartup(game):
    if mmddGuiSwitch(game, mmddGuiStartup):
        sd = game.scenedata
        sd.startupScrollPos = Vector2.zero
        sd.startupStep = "autoloaderror"
        return
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata

    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20
    labelStyle = GUIStyle("label")
    labelStyle.richText = True
    labelStyle.fontSize = 16
    labelStyle.wordWrap = True
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    if sd.startupStep == "autoloaderror":
        if sd.startupAutoLoadError:
            GUILayout.Label(LC("Auto load from default save/backup data failed:"), labelStyle)
            GUILayout.Space(10)
            GUILayout.Label(redText(sd.startupAutoLoadError), labelStyle)
            GUILayout.Space(20)
            GUILayout.Label(yellowText(LC("Please try to fix it manually or try load from backup or other data set.")))
            GUILayout.FlexibleSpace()
            if GUILayout.Button("OK", btnstyle):
                if sd.startupAutoLoadSuspend:
                    sd.startupStep = "startmain"
                else:
                    sd.startupStep = "missfiles"
        else:
            sd.startupStep = "missfiles"
    if sd.startupStep == "missfiles":
        if len(sd.startupMissFiles) > 0:
            GUILayout.Label(LC("Following files in save data are missing, please try to fix them:"), labelStyle)
            sd.startupScrollPos = GUILayout.BeginScrollView(sd.startupScrollPos, GUI.skin.box)#, GUILayout.Width(fullw))
            for fn in sd.startupMissFiles.keys():
                GUILayout.BeginHorizontal()
                GUILayout.Label(LC("In data set %s for %s")%(magentaText(",".join(sd.startupMissFiles[fn][0])), cyanText("/".join(sd.startupMissFiles[fn][1]))))
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                GUILayout.Label(yellowText(fn), labelStyle)
                GUILayout.EndHorizontal()
                GUILayout.BeginHorizontal()
                GUILayout.Space(10)
                if sd.startupMissFiles[fn][2]:
                    GUILayout.Label(u'\u279C')
                    GUILayout.Label(greenText(sd.startupMissFiles[fn][2]))
                    GUILayout.Space(10)
                    if GUILayout.Button("X", GUILayout.Width(25)):
                        sd.startupMissFiles[fn][2] = None
                if GUILayout.Button(LC("re-select")):
                    onres = fn
                    ext = os.path.splitext(fn)[1][1:]
                    def reselect(files):
                        if files and len(files) > 0:
                            refile = files[0]
                            sd.startupMissFiles[onres][2] = refile
                            # try auto select file if folder changed
                            oldPathFile = os.path.split(onres)
                            newPathFile = os.path.split(refile)
                            if len(oldPathFile) == len(newPathFile) == 2 and oldPathFile[1] == newPathFile[1]:
                                oldFolder = oldPathFile[0]
                                newFolder = newPathFile[0]
                                for fm in sd.startupMissFiles.keys():
                                    if sd.startupMissFiles[fm][2]:
                                        continue
                                    pathFile = os.path.split(fm)
                                    if len(pathFile) != 2 or pathFile[0] != oldFolder:
                                        continue
                                    acFile = os.path.join(newFolder, pathFile[1])
                                    if os.path.isfile(acFile):
                                        sd.startupMissFiles[fm][2] = os.path.realpath(acFile)
                    guiOpenFileDialog(reselect, sd.startupMissFiles[onres][2] if sd.startupMissFiles[onres][2] else mmddConfig.vmdHomeDir, True, ext)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            GUILayout.EndScrollView()
            GUILayout.FlexibleSpace()
            if GUILayout.Button(LC("Continue"), btnstyle):
                replaceFiles = {}
                for fn in sd.startupMissFiles.keys():
                    if sd.startupMissFiles[fn][2]:
                        replaceFiles[fn] = sd.startupMissFiles[fn][2]
                if len(replaceFiles) > 0:
                    correctSaveData(replaceFiles)
                if sd.startupAutoLoadSuspend:
                    sd.startupStep = "autoloadretry"
                else:
                    sd.startupStep = "startmain"
        else:
            sd.startupStep = "startmain"
    if sd.startupStep == "autoloadretry":
        altarget = checkAutoLoadTarget()
        alerr = mmdd.loadFromScene(altarget)
        if alerr:
            sd.startupAutoLoadError = alerr
            mmdd.cleanup()
            sd.startupStep = "autoloaderror"
        else:
            sd.startupStep = "startmain"
    if sd.startupStep == "startmain":
        # start main gui
        if Event.current.type != EventType.Layout:
            del sd.startupScrollPos
            del sd.startupStep
            del sd.startupAutoLoadError
            del sd.startupAutoLoadSuspend
            del sd.startupMissFiles
            mmddGuiMain(game)
    
def guiPartsSeekbar(game, frameUpdateCallback = None, jumpToKeyframeCallback = None, jumpToNextCallback = None):
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    fullw = mmddConfig.guiWidth - 20

    def frameUpdate():
        if frameUpdateCallback:
            frameUpdateCallback(int(mmdd.curFrame))

    minimized = game.gdata.guiFunction == mmddGuiMinimize
    if not hasattr(sd, "seekbarMode"):
        sd.seekbarMode = "normal"

    GUILayout.BeginHorizontal()
    newframe = GUILayout.HorizontalSlider(mmdd.curFrame, 0, mmdd.totalFrame, GUILayout.Width(fullw))
    if not mmdd.isPlaying and int(newframe) != int(mmdd.curFrame):
        mmdd.gotoFrame(int(newframe))
        frameUpdate()
    GUILayout.EndHorizontal()
    if mmdd.isPlaying:
        # playing mode
        GUILayout.BeginHorizontal()
        if GUILayout.Button("<color=#ff0000>" + u'\u25a0' + "</color>", GUILayout.Width(25)):
            mmdd.startStop()
        if jumpToNextCallback and GUILayout.Button(LC("Next")):
            jumpToNextCallback(game)
        GUILayout.FlexibleSpace()
        # title or play
        if mmdd.onStopFunc != onMmddStop and hasattr(sd, "playlist") and sd.playlist and hasattr(sd, "currentPlayingIndex") and sd.currentPlayingIndex != None:
            GUILayout.Label("%s (%.0fs/%.0fs)"%(greenText(sd.playlist[sd.currentPlayingIndex].name), mmdd.curFrame/30.0, mmdd.totalFrame/30.0))
        else:
            GUILayout.Label("frame: %d/%d (%.2fs/%.2fs)"%(mmdd.curFrame, mmdd.totalFrame, mmdd.curFrame/30.0, mmdd.totalFrame/30.0))
        GUILayout.FlexibleSpace()
        if GUILayout.Button("+" if minimized else "-", GUILayout.Width(25)):
            if minimized:
                mmddGuiRestore(game)
            else:
                mmddGuiMinimize(game)
        GUILayout.EndHorizontal()
    else:
        # editing mode
        GUILayout.BeginHorizontal()
        if GUILayout.Button("<color=#00ff00>" + u'\u25b6' + "</color>", GUILayout.Width(25)):
            mmdd.startStop()
        if GUILayout.Button("|<" if mmdd.customStartFrame == -1 else "<color=#00ffff>|<</color>", GUILayout.Width(25)):
            if Input.GetMouseButtonUp(0):
                mmdd.gotoStartFrame()
                frameUpdate()
                if mmddConfig.gcOptimize != "none":
                    DoGarbageCollect()
            elif Input.GetMouseButtonUp(1):
                mmdd.setStartFrame(mmdd.curFrame)
            elif Input.GetMouseButtonUp(2):
                mmdd.setStartFrame(-1)
        if GUILayout.Button(".<" if jumpToKeyframeCallback else "<<", GUILayout.Width(25)):
            if jumpToKeyframeCallback:
                jf = jumpToKeyframeCallback(int(mmdd.curFrame), -1)
                if jf != None:
                    mmdd.gotoFrame(jf)
                    frameUpdate()
            else:
                mmdd.gotoPrevSecond()
                frameUpdate()
        if GUILayout.Button("<", GUILayout.Width(25)):
            mmdd.gotoPrevFrame()
            frameUpdate()
        if sd.seekbarMode == "normal":
            if GUILayout.Button("%d/%d (%.2fs/%.2fs)"%(mmdd.curFrame, mmdd.totalFrame, mmdd.curFrame/30.0, mmdd.totalFrame/30.0)):
                if Input.GetMouseButtonUp(0):
                    sd.seekbarMode = "goto"
                    sd.seekbarInput = str(int(mmdd.curFrame))
                elif Input.GetMouseButtonUp(1):
                    mmdd.update()
                    frameUpdate()
        elif sd.seekbarMode == "goto":
            isEnterKey = Event.current.type == EventType.KeyDown and (Event.current.keyCode == KeyCode.Return or Event.current.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 * 30.0)
                    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 * 30.0)
                    else:
                        frmval = int(sd.seekbarInput)
                    mmdd.gotoFrame(frmval)
                    frameUpdate()
                    sd.seekbarMode = "normal"
                except:
                    pass
            if GUILayout.Button("Back", GUILayout.Width(50)):
                sd.seekbarMode = "normal"
            GUILayout.FlexibleSpace()
        else:
            sd.seekbarMode = "normal"
        if GUILayout.Button(">", GUILayout.Width(25)):
            mmdd.gotoNextFrame()
            frameUpdate()
        if GUILayout.Button(">." if jumpToKeyframeCallback else ">>", GUILayout.Width(25)):
            if jumpToKeyframeCallback:
                jf = jumpToKeyframeCallback(int(mmdd.curFrame), +1)
                if jf != None:
                    mmdd.gotoFrame(jf)
                    frameUpdate()
            else:
                mmdd.gotoNextSecond()
                frameUpdate()
        if GUILayout.Button(">|" if mmdd.customEndFrame == -1 else "<color=#00ffff>>|</color>", GUILayout.Width(25)):
            if Input.GetMouseButtonUp(0):
                mmdd.gotoEndFrame()
                frameUpdate()
            elif Input.GetMouseButtonUp(1):
                mmdd.setEndFrame(mmdd.curFrame)
            elif Input.GetMouseButtonUp(2):
                mmdd.setEndFrame(-1)
        if GUILayout.Button("+" if minimized else "-", GUILayout.Width(25)):
            if minimized:
                mmddGuiRestore(game)
            else:
                mmddGuiMinimize(game)
        GUILayout.EndHorizontal()

def guiOpenFileDialog(cbFunc, lastFile = None, load = True, ext = None):
    from UnityEngine import Application
    from KKAPI.Utilities import OpenFileDialog
    from System import IntPtr
    from os import path

    if lastFile and path.isfile(lastFile):
        initialDir = path.split(path.realpath(lastFile))[0]
    elif lastFile and path.isdir(lastFile):
        initialDir = path.realpath(lastFile)
    elif lastFile and path.isdir(path.split(lastFile)[0]):
        initialDir = path.realpath(path.split(lastFile)[0])
    else:
        initialDir = path.realpath(path.join(Application.dataPath, '..'))
    if ext == None or len(ext) == 0:
        extFilter = "All files|*.*"
    else:
        extFilter = "%s files (.%s)|*.%s|All files|*.*"%(ext, ext.lower(), ext.lower())
    if load:
        title = "Load %s file"%ext
        flags = OpenFileDialog.OpenSaveFileDialgueFlags.OFN_FILEMUSTEXIST | OpenFileDialog.OpenSaveFileDialgueFlags.OFN_EXPLORER | OpenFileDialog.OpenSaveFileDialgueFlags.OFN_LONGNAMES
    else:
        title = "Save %s file"%ext
        flags = OpenFileDialog.OpenSaveFileDialgueFlags.OFN_EXPLORER | OpenFileDialog.OpenSaveFileDialgueFlags.OFN_LONGNAMES

    ca = Action[Array[String]](cbFunc)
    OpenFileDialog.Show(ca, title, initialDir, extFilter, ext, flags)

def guiInputDialog(game, param):
    # param = (message, contents, buttons)
    #  contents = (lineContents, lineContents, lineContents...)
    #   lineContents = (lineElm, lineElm, lineElm...)
    #    lineElm can be:
    #      Toggle: ["t", On/Off, "texts", width]
    #      Input:  ["i", "texts", width]
    #      Label:  ("l", "texts", width)
    #      Space:  ("s", width)
    #      other:  ("o", data)
    #  buttons = ((btn, func), (btn, func), (btn, func)...)
    if not isinstance(param, tuple) or len(param) != 3 or not isinstance(param[0], str):
        raise Exception("Wrong param format for guiInputDialog")
    if mmddGuiSwitch(game, guiInputDialog, param):
        game.scenedata.tempInputDialogScrollPos = Vector2.zero
        return
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    fullw = mmddConfig.guiWidth - 20

    labelStyle = GUIStyle("label")
    labelStyle.richText = True
    labelStyle.fontSize = 16
    labelStyle.wordWrap = True
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16

    # message part
    GUILayout.BeginHorizontal()
    message = param[0]
    GUILayout.Label(message, labelStyle, GUILayout.Width(fullw))
    GUILayout.EndHorizontal()
    GUILayout.Space(10)

    # contents part
    game.scenedata.tempInputDialogScrollPos = GUILayout.BeginScrollView(game.scenedata.tempInputDialogScrollPos, GUILayout.Width(fullw))
    for lineContents in param[1]:
        GUILayout.BeginHorizontal()
        for lineElm in lineContents:
            if lineElm[0][0] == "t":   # toggle
                if len(lineElm) == 4:
                    nsValue = GUILayout.Toggle(lineElm[1], lineElm[2], GUILayout.Width(lineElm[3]))
                else:
                    nsValue = GUILayout.Toggle(lineElm[1], lineElm[2])
                if nsValue:
                    for lc in param[1]:
                        for le in lc:
                            if le[0] == lineElm[0]:
                                le[1] = False
                    lineElm[1] = True
            elif lineElm[0] == "i": # text input
                if len(lineElm) == 3:
                    lineElm[1] = GUILayout.TextField(lineElm[1], GUILayout.Width(lineElm[2]))
                else:
                    lineElm[1] = GUILayout.TextField(lineElm[1])
            elif lineElm[0] == "l": # label
                if len(lineElm) == 3:
                    GUILayout.Label(lineElm[1], GUILayout.Width(lineElm[2]))
                else:
                    GUILayout.Label(lineElm[1])
            elif lineElm[0] == "s": # space
                GUILayout.Space(lineElm[1])
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
    GUILayout.EndScrollView()
    GUILayout.FlexibleSpace()

    # button part
    cbNum = len(param[2])
    cbwidth = (fullw - 20 - cbNum*4) / cbNum
    GUILayout.BeginHorizontal()
    GUILayout.FlexibleSpace()
    for btnSet in param[2]:
        btnTxt = btnSet[0]
        if btnSet[1] != None:
            btnFunc = btnSet[1]
        else:
            btnFunc = mmddGuiBack
        if GUILayout.Button(btnTxt, btnstyle, GUILayout.Width(cbwidth)):
            btnFunc(game)
    GUILayout.FlexibleSpace()
    GUILayout.EndHorizontal()

def guiSelectDialog(game, param):
    # param = (message, (btn, func), (btn, func), (btn, func)...)
    # if 'func' is none, call mmddGuiBack
    if not isinstance(param, tuple) or len(param) < 2 or not isinstance(param[0], str):
        raise Exception("Wrong param format for guiSelectDialog")
    for i in range(1, len(param)):
        if not isinstance(param[i], tuple) or len(param[i]) != 2:
            raise Exception("Wrong param format for guiSelectDialog")
    iparam = (param[0], (), param[1:])
    guiInputDialog(game, iparam)

def guiPromptDialog(game, message, okFunc = None):
    param = (message, ("OK", okFunc))
    guiSelectDialog(game, param)

def guiLongPromptDialog(game, title, message, okFunc = None):
    lineElm = ("l", message)
    lineContents = (lineElm, )
    contents = (lineContents, )
    iparam = (title, contents, (("OK", okFunc),))
    guiInputDialog(game, iparam)

def guiConfirmDialog(game, message, okFunc):
    param = (message, ("OK", okFunc), ("Cancel", None))
    guiSelectDialog(game, param)

def guiWaitDialog(game, message):
    if mmddGuiSwitch(game, guiWaitDialog, message):
        return
    mmdd = game.gdata.mmdd
    sd = game.scenedata

    labelStyle = GUIStyle("label")
    labelStyle.richText = True
    labelStyle.fontSize = 18
    labelStyle.wordWrap = True

    GUILayout.FlexibleSpace()
    GUILayout.Label(message, labelStyle)
    GUILayout.FlexibleSpace()

def guiImportVmdData_normal(game):
    from vmdlib import VmdLib
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    if not hasattr(sd, "tempImportVmdNormalGuiData"):
        sd.tempImportVmdNormalGuiData = GData()
    lsd = sd.tempImportVmdNormalGuiData
    relativeFileLimit = 20

    def initImport():
        try:
            # check
            vmdCheckRes = VmdLib.checkVmd(mmdd.lastVmdFilename)
            # check result
            if len(vmdCheckRes) == 1:
                raise Exception(vmdCheckRes[0])
            bNum = vmdCheckRes[1]["keyframeCount"]
            mNum = vmdCheckRes[2]["keyframeCount"]
            cNum = vmdCheckRes[3]["keyframeCount"]
            if bNum == mNum == cNum == 0:
                raise Exception(LC("Cannot load any supported keyframe from %s! Try other file.")%greenText(mmdd.lastVmdFilename))
            lsd.vmdCheckResults = {os.path.split(mmdd.lastVmdFilename)[1]: vmdCheckRes}
            lsd.selectedActors = {}
            wsSels = tuple(game.studio.treeNodeCtrl.selectNodes)
            for f in game.scene_get_all_females():
                fa = f.as_actor
                lsd.selectedActors[fa] = fa.treeNodeObject in wsSels
            for m in game.scene_get_all_males():
                ma = m.as_actor
                lsd.selectedActors[ma] = ma.treeNodeObject in wsSels
            if len(lsd.selectedActors) == 0 and (bNum > 0 or mNum > 0) and cNum == 0:
                raise Exception(LC("Selected file %s is a motion/morph vmd but no charactor found in scene to import data.")%greenText(mmdd.lastVmdFilename))
            if len(lsd.selectedActors) == 1:
                lsd.selectedActors.values()[0] = True
            lsd.frameCounts = (bNum, mNum, cNum)
            lsd.loadTargets = [bNum > 0, mNum > 0, cNum > 0]
            lsd.cameraTargetActor = None
            lsd.boneDefine = None
            lsd.scrollPos = Vector2.zero
            lsd.vmdLoadError = None
            lsd.loadStep = "targetfile"
        except Exception, e:
            lsd.vmdLoadError = str(e)
            lsd.loadStep = "error"
    def clearImport():
        del sd.tempImportVmdNormalGuiData
    def searchRelativeFiles(baseFolder, baseStrip, exclude, wavSet, vmdDict):
        allfile = os.listdir(baseFolder)
        for fn in allfile:
            f = os.path.join(baseFolder, fn)
            if len(wavSet) + len(vmdDict) > relativeFileLimit:
                return False
            elif f in exclude:
                continue
            elif os.path.isdir(f):
                # search subfolder
                if not searchRelativeFiles(f, baseStrip, exclude, wavSet, vmdDict):
                    return False
            elif os.path.isfile(f) and f.lower().endswith(".wav"):
                wavSet.add(f[baseStrip:])
                #print " - add wav:", f[baseStrip:]
            elif os.path.isfile(f) and f.lower().endswith(".vmd"):
                checkRes = VmdLib.checkVmd(f)
                if len(checkRes) > 1:
                    vmdDict[f[baseStrip:]] = checkRes
                    #print " - add vmd:", f[baseStrip:]
        return True
    # job fucntion
    def initVmdLoadJob():
        fn = os.path.split(mmdd.lastVmdFilename)[1]
        frameTgt = set()
        if lsd.loadTargets[0]:
            frameTgt.add("motion")
        if lsd.loadTargets[1]:
            frameTgt.add("morph")
        if lsd.loadTargets[2]:
            frameTgt.add("camera")
        bindTargets = [actor for actor in lsd.selectedActors.keys() if lsd.selectedActors[actor]]
        if lsd.loadTargets[0]:
            motionBindTgt = set(bindTargets)
        else:
            motionBindTgt = set()
        if lsd.loadTargets[1]:
            morphBindTgt = set(bindTargets)
        else:
            morphBindTgt = set()
        if lsd.loadTargets[2]:
            cameraTarget = lsd.cameraTargetActor
        else:
            cameraTarget = None
        lsd.importJobs = {fn: [frameTgt, motionBindTgt, morphBindTgt, cameraTarget]}
        lsd.sortedImportJobName = [fn]
        lsd.importJobsBackup = {fn: [frameTgt, motionBindTgt, morphBindTgt, cameraTarget]}
        lsd.sortedImportJobNameBackup = [fn]
        # check relative files
        if mmddConfig.scanRelatives and checkRelative():
            lsd.loadStep = "relativesetup"
        else:
            startLoadJobs()
    def chooseBoneDefine():
        from vmdlib import MotionController
        lsd.boneDefineTemp = lsd.boneDefine
        # add default
        lsd.boneDefineSelector = [(None, greenText(LC("Default bone define")))]
        # add extend define list
        validfile = MotionController.ListExtendDefineFile("chara")
        for pn in sorted(validfile.keys()):
            fn = os.path.split(pn)[1]
            desp = validfile[pn]
            lsd.boneDefineSelector.append((pn, greenText(fn) + ": " + desp))
        lsd.loadStep = "choosebonedefine"
    def checkRelative():
        baseFolder = os.path.split(mmdd.lastVmdFilename)[0]
        baseStrip = len(baseFolder) + 1
        exclude = {mmdd.lastVmdFilename}
        wavSet = set()
        vmdDict = {}
        if not searchRelativeFiles(baseFolder, baseStrip, exclude, wavSet, vmdDict):
            return False
        # check motion
        relMotionFiles = [f for f in vmdDict if vmdDict[f][1]["keyframeCount"] > 0]
        relMotionFiles.sort()
        relMotion = len(relMotionFiles) > 0
        if relMotion and len(mmdd.motionControllers) == len(lsd.selectedActors):
            relMotion = False
            for mc in mmdd.motionControllers.values():
                if mc.FileName not in relMotionFiles:
                    relMotion = True
                    break
        # check morph
        relMorphFiles = [f for f in vmdDict if vmdDict[f][2]["keyframeCount"] > 0]
        relMorphFiles.sort()
        relMorph = len(relMorphFiles) > 0
        if relMorph and len(mmdd.morphControllers) == len(lsd.selectedActors):
            relMorph = False
            for mp in mmdd.morphControllers.values():
                if mp.FileName not in relMorphFiles:
                    relMorph = True
                    break
        # check camera
        relCameraFiles = [f for f in vmdDict if vmdDict[f][3]["keyframeCount"] > 0]
        relCameraFiles.sort()
        relCamera = len(relCameraFiles) > 0
        if mmdd.cameraController and mmdd.cameraController.FileName in relCameraFiles:
            relCamera = False
        if "camera" in lsd.importJobs.values()[0][0]:
            relCamera = False
        if relCamera and len(lsd.importJobs.values()[0][1]) == 1:
            for actor in lsd.importJobs.values()[0][1]:
                camTgtActor = actor
        else:
            camTgtActor = None
        # check wav
        relWavFiles = list(wavSet)
        relWavFiles.sort()
        relWav = len(wavSet) > 0
        if mmdd.soundController and mmdd.soundController.FileName in wavSet:
            relWav = False
        #print "check relative:", relMotion, relMorph, relCamera, relWav
        anyRel = relMotion or relMorph or relCamera or relWav
        if anyRel:
            lsd.relativeFiles = (
                relMotionFiles if relMotion else None,
                relMorphFiles if relMorph else None,
                relCameraFiles if relCamera else None,
                relWavFiles if relWav else None
            )
            lsd.relativeFileSelect = [
                [[False, set()] for f in relMotionFiles],
                [[False, set()] for f in relMorphFiles],
                [0, camTgtActor],
                0,
            ]
            lsd.vmdCheckResults.update(vmdDict)
        return anyRel
    def appendLoadJobs():
        # motion
        if lsd.relativeFiles[0]:
            for i in range(len(lsd.relativeFiles[0])):
                flg, bindTarget = lsd.relativeFileSelect[0][i]
                if not flg or len(bindTarget) == 0: continue
                fn = lsd.relativeFiles[0][i]
                if fn in lsd.importJobs:
                    lsd.importJobs[fn][0].add("motion")
                    lsd.importJobs[fn][1].update(bindTarget)
                else:
                    tgt = set()
                    tgt.add("motion")
                    lsd.importJobs[fn] = [tgt, bindTarget.copy(), set(), None]
        # morph
        if lsd.relativeFiles[1]:
            for i in range(len(lsd.relativeFiles[1])):
                flg, bindTarget = lsd.relativeFileSelect[1][i]
                if not flg or len(bindTarget) == 0: continue
                fn = lsd.relativeFiles[1][i]
                if fn in lsd.importJobs:
                    lsd.importJobs[fn][0].add("morph")
                    lsd.importJobs[fn][2].update(bindTarget)
                else:
                    tgt = set()
                    tgt.add("morph")
                    lsd.importJobs[fn] = [tgt, set(), bindTarget.copy(), None]
        # camera
        if lsd.relativeFiles[2] and 0 <= lsd.relativeFileSelect[2][0] < len(lsd.relativeFiles[2]):
            i, alignTarget = lsd.relativeFileSelect[2]
            fn = lsd.relativeFiles[2][i]
            if fn in lsd.importJobs:
                lsd.importJobs[fn][0].add("camera")
                lsd.importJobs[fn][3] = alignTarget
            else:
                tgt = set()
                tgt.add("camera")
                lsd.importJobs[fn] = [tgt, set(), set(), alignTarget]
        # wav
        if lsd.relativeFiles[3] and 0 <= lsd.relativeFileSelect[3] < len(lsd.relativeFiles[3]):
            i = lsd.relativeFileSelect[3]
            fn = lsd.relativeFiles[3][i]
            tgt = set()
            tgt.add("wav")
            lsd.importJobs[fn] = [tgt, set(), set(), None]
        confirmLoadJobs()
    def confirmLoadJobs():
        lsd.sortedImportJobName = []
        for fn in lsd.importJobs:
            if fn not in lsd.sortedImportJobName and "motion" in lsd.importJobs[fn][0]:
                lsd.sortedImportJobName.append(fn)
        for fn in lsd.importJobs:
            if fn not in lsd.sortedImportJobName and "morph" in lsd.importJobs[fn][0]:
                lsd.sortedImportJobName.append(fn)
        for fn in lsd.importJobs:
            if fn not in lsd.sortedImportJobName and "camera" in lsd.importJobs[fn][0]:
                lsd.sortedImportJobName.append(fn)
        for fn in lsd.importJobs:
            if fn not in lsd.sortedImportJobName and "wav" in lsd.importJobs[fn][0]:
                lsd.sortedImportJobName.append(fn)
        lsd.loadStep = "confirmloadjobs"
    def reselectRelativeFiles():
        fn = lsd.sortedImportJobNameBackup[0]
        lsd.importJobs = {fn: [copy.copy(lsd.importJobsBackup[fn][0]), copy.copy(lsd.importJobsBackup[fn][1]), copy.copy(lsd.importJobsBackup[fn][2]), lsd.importJobsBackup[fn][3]]}
        lsd.sortedImportJobName = [fn]
        lsd.loadStep = "relativesetup"
    def startLoadJobs():
        # load on thread
        lsd.onLoadingJobIndex = 0
        lsd.loadingFlag = True
        lsd.loadStep = "loadingwait"
        import thread
        thread.start_new_thread(doLoadJobs, ())
    def doLoadJobs():
        def getMC(actor):
            if actor in gottedMC:
                return gottedMC[actor], "replace"
            else:
                mc = mmdd.createMotionController(actor, lsd.boneDefine)
                gottedMC[actor] = mc
                return mc, "renew"
        def getMP(actor):
            if actor in gottedMP:
                return gottedMP[actor], "replace"
            else:
                mp = mmdd.createMorphController(actor)
                gottedMP[actor] = mp
                return mp, "renew"
        allError = []
        try:
            # prepare
            print "==== start batch loading job ===="
            baseFolder = os.path.split(mmdd.lastVmdFilename)[0]
            gottedMC = {}
            gottedMP = {}
            # do
            for i in range(len(lsd.sortedImportJobName)):
                lsd.onLoadingJobIndex = i
                job = lsd.sortedImportJobName[i]
                tgtFile = os.path.join(baseFolder, job)
                frameTgt, mcBindTgt, mpBindTgt, cameraTgt = lsd.importJobs[job]
                print " --- start load job (%d/%d): %s"%(i+1, len(lsd.sortedImportJobName), job)
                if "wav" in frameTgt:
                    # load wav
                    err = mmdd.loadWavFile(tgtFile)
                    if err:
                        allError.append(err)
                else:
                    # load vmd
                    vmdResult = VmdLib.loadVmd(tgtFile)
                    if len(vmdResult) == 1:
                        allError.append(vmdResult[0])
                        continue
                    # motion
                    if "motion" in frameTgt:
                        for actor in mcBindTgt:
                            mc, mode = getMC(actor)
                            mc.importBoneKeyframes(tgtFile, vmdResult[1], mode)
                    if "morph" in frameTgt:
                        for actor in mpBindTgt:
                            mp, mode = getMP(actor)
                            mp.importMorphKeyframes(tgtFile, vmdResult[2], mode)
                    if "camera" in frameTgt:
                        mmdd.loadCameraKeyframes(tgtFile, vmdResult[3])
                        mmdd.cameraController.switchMinFramePeriod = mmddConfig.switchMinFramePeriod
                        mmdd.cameraController.switchMaxFramePeriod = mmddConfig.switchMaxFramePeriod
                        mmdd.cameraController.switchProbability = mmddConfig.switchProbability
                        if cameraTgt:
                            mmdd.cameraController.offPos = cameraTgt.pos
                            mmdd.cameraController.offRot = cameraTgt.rot
            # after
            if len(gottedMC):
                for actor in gottedMC.keys():
                    mmdd.postProcessForMotionImport(actor)
                #game.updFuncParam = set(gottedMC.values())
                #game.updFunc = enableAndCheck
            mmdd.refreshTotalFrame()
            mmdd.verify()
            mmdd.update()
        except Exception, e:
            allError.append(str(e))
            traceback.print_exc()
        print "==== batch loading job finished ===="
        # report
        if len(allError):
            lsd.vmdLoadError = "Some error occured in loading:\n"
            for err in allError:
                lsd.vmdLoadError += "\n" + err
        else:
            lsd.vmdLoadError = greenText(LC("All files loaded successfully!"))
        # end flag
        lsd.loadingFlag = False
    def enableAndCheck(param):
        mcs = param
        for mc in mcs:
            if not mc.Enable:
                mc.setEnable(True)
            else:
                mcs.remove(mc)
        if len(mcs) > 0:
            game.updFuncParam = mcs
            game.updFunc = enableAndCheck

    # init switch
    if mmddGuiSwitch(game, guiImportVmdData_normal):
        initImport()
        return
    if not hasattr(lsd, "loadStep"):
        return

    labelStyle = GUIStyle("label")
    labelStyle.richText = True
    labelStyle.fontSize = 16
    labelStyle.wordWrap = True
    toggleStyle = GUIStyle("toggle")
    toggleStyle.fontSize = 16
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    fullw = mmddConfig.guiWidth - 20
    fullh = mmddConfig.guiHeight - 20

    # main UI by step
    enableCancel = True
    if lsd.loadStep == "targetfile":
        # header
        cameraOnly = lsd.frameCounts[0] == lsd.frameCounts[1] == 0
        GUILayout.Label("VMD File: <color=#00ff00>%s</color>, base model: <color=#00ff00>%s</color>"%(lsd.vmdCheckResults.keys()[0], lsd.vmdCheckResults.values()[0][0]), labelStyle)
        # load target
        GUILayout.BeginHorizontal()
        GUILayout.Space(4)
        GUILayout.Label(LC("Contents keyframes: "), labelStyle)
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        GUILayout.Space(4)
        if lsd.frameCounts[0]:
            lsd.loadTargets[0] = GUILayout.Toggle(lsd.loadTargets[0], "<color=#00ff00>%d</color> motion keyframes"%(lsd.frameCounts[0]), toggleStyle)
            GUILayout.Space(10)
        if lsd.frameCounts[1]:
            lsd.loadTargets[1] = GUILayout.Toggle(lsd.loadTargets[1], "<color=#00ff00>%d</color> morph keyframes"%(lsd.frameCounts[1]), toggleStyle)
            GUILayout.Space(10)
        if lsd.frameCounts[2]:
            if cameraOnly:
                GUILayout.Label("<color=#00ff00>%d</color> camera keyframes"%(lsd.frameCounts[2]), labelStyle)
            else:
                lsd.loadTargets[2] = GUILayout.Toggle(lsd.loadTargets[2], "<color=#00ff00>%d</color> camera keyframes"%(lsd.frameCounts[2]), toggleStyle)
        GUILayout.FlexibleSpace()
        GUILayout.EndHorizontal()
        # char target
        GUILayout.Space(10)
        selected = 0
        if lsd.loadTargets[0] or lsd.loadTargets[1]:
            GUILayout.Label(LC("Apply motion/morph keyframes to charactor:"), labelStyle)
            lsd.scrollPos = GUILayout.BeginScrollView(lsd.scrollPos, GUI.skin.box, GUILayout.Width(fullw))
            for actor in lsd.selectedActors.keys():
                lsd.selectedActors[actor] = GUILayout.Toggle(lsd.selectedActors[actor], actor.text_name, toggleStyle)
                if lsd.selectedActors[actor]:
                    selected = selected + 1 
            GUILayout.EndScrollView()
            # char target select helper
            GUILayout.BeginHorizontal()
            if GUILayout.Button(LC("Select All")):
                for actor in lsd.selectedActors:
                    lsd.selectedActors[actor] = True
                selected = len(lsd.selectedActors)
            if GUILayout.Button(LC("select from studio workspace")):
                selected = 0
                wsSels = tuple(game.studio.treeNodeCtrl.selectNodes)
                for actor in lsd.selectedActors:
                    lsd.selectedActors[actor] = actor.treeNodeObject in wsSels
                    selected += 1 if lsd.selectedActors[actor] else 0
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
        # camera target
        if lsd.loadTargets[2] and len(lsd.selectedActors):
            # auto align
            GUILayout.Label(LC("Auto align camera to charactor:"), labelStyle)
            lsd.scrollPos = GUILayout.BeginScrollView(lsd.scrollPos, GUI.skin.box, GUILayout.Width(fullw))
            for actor in lsd.selectedActors.keys():
                newChoice = GUILayout.Toggle(actor == lsd.cameraTargetActor, actor.text_name, toggleStyle)
                if newChoice and actor != lsd.cameraTargetActor:
                    lsd.cameraTargetActor = actor
                if not newChoice and actor == lsd.cameraTargetActor:
                    lsd.cameraTargetActor = None
            GUILayout.EndScrollView()
            # camera target select helper
            GUILayout.BeginHorizontal()
            if GUILayout.Button(LC("select from studio workspace")):
                wsSels = tuple(game.studio.treeNodeCtrl.selectNodes)
                for actor in lsd.selectedActors:
                    if actor.treeNodeObject in wsSels:
                        lsd.cameraTargetActor = actor
                        break
            GUILayout.FlexibleSpace()
            GUILayout.EndHorizontal()
    elif lsd.loadStep == "choosebonedefine":
        # Header
        GUILayout.Label(LC("Select a bone define file:"))
        lsd.scrollPos = GUILayout.BeginScrollView(lsd.scrollPos, GUI.skin.box, GUILayout.Width(fullw))
        for bdi in lsd.boneDefineSelector:
            sel = GUILayout.Toggle(lsd.boneDefineTemp == bdi[0], bdi[1])
            if sel:
                lsd.boneDefineTemp = bdi[0]
        GUILayout.EndScrollView()
        enableCancel = False
    elif lsd.loadStep == "relativesetup":
        # header
        GUILayout.Label(LC("Find relative files in the folder/subfolder of target file '%s', check now!")%greenText(lsd.sortedImportJobName[0]), labelStyle)
        lsd.scrollPos = GUILayout.BeginScrollView(lsd.scrollPos, GUI.skin.box, GUILayout.Width(fullw))
        # motion
        if lsd.relativeFiles[0]:
            GUILayout.Label(yellowText(LC("Motion Data:")), labelStyle)
            for i in range(len(lsd.relativeFiles[0])):
                fn = lsd.relativeFiles[0][i]
                desp = fn + " (" + greenText(str(lsd.vmdCheckResults[fn][1]["keyframeCount"])) + " keyframes)"
                lsd.relativeFileSelect[0][i][0] = GUILayout.Toggle(lsd.relativeFileSelect[0][i][0], desp)
                if lsd.relativeFileSelect[0][i][0]:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(30)
                    GUILayout.Label(LC("Import to: "))
                    for actor in lsd.selectedActors.keys():
                        sel = GUILayout.Toggle(actor in lsd.relativeFileSelect[0][i][1], actor.text_name)
                        if sel:
                            lsd.relativeFileSelect[0][i][1].add(actor)
                        else:
                            lsd.relativeFileSelect[0][i][1].discard(actor)
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
            GUILayout.Space(10)
        # morph
        if lsd.relativeFiles[1]:
            GUILayout.Label(yellowText(LC("Morph Data:")), labelStyle)
            for i in range(len(lsd.relativeFiles[1])):
                fn = lsd.relativeFiles[1][i]
                desp = fn + " (" + greenText(str(lsd.vmdCheckResults[fn][2]["keyframeCount"])) + " keyframes)"
                lsd.relativeFileSelect[1][i][0] = GUILayout.Toggle(lsd.relativeFileSelect[1][i][0], desp)
                if lsd.relativeFileSelect[1][i][0]:
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(30)
                    GUILayout.Label(LC("Import to: "))
                    for actor in lsd.selectedActors.keys():
                        sel = GUILayout.Toggle(actor in lsd.relativeFileSelect[1][i][1], actor.text_name)
                        if sel:
                            lsd.relativeFileSelect[1][i][1].add(actor)
                        else:
                            lsd.relativeFileSelect[1][i][1].discard(actor)
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
            GUILayout.Space(10)
        # camera
        if lsd.relativeFiles[2]:
            GUILayout.Label(yellowText(LC("Camera Data:")), labelStyle)
            for i in range(len(lsd.relativeFiles[2])):
                fn = lsd.relativeFiles[2][i]
                desp = fn + " (" + greenText(str(lsd.vmdCheckResults[fn][3]["keyframeCount"])) + " keyframes)"
                sel = GUILayout.Toggle(i == lsd.relativeFileSelect[2][0], desp)
                if sel:
                    lsd.relativeFileSelect[2][0] = i
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(30)
                    GUILayout.Label(LC("Align to: "))
                    for actor in lsd.selectedActors.keys():
                        sela = GUILayout.Toggle(actor == lsd.relativeFileSelect[2][1], actor.text_name)
                        if sela:
                            lsd.relativeFileSelect[2][1] = actor
                        else:
                            if lsd.relativeFileSelect[2][1] == actor:
                                lsd.relativeFileSelect[2][1] = None
                    GUILayout.FlexibleSpace()
                    GUILayout.EndHorizontal()
                else:
                    if lsd.relativeFileSelect[2][0] == i:
                        lsd.relativeFileSelect[2][0] = -1
            GUILayout.Space(10)
        # morph
        if lsd.relativeFiles[3]:
            GUILayout.Label(yellowText(LC("Music Data:")), labelStyle)
            for i in range(len(lsd.relativeFiles[3])):
                fn = lsd.relativeFiles[3][i]
                sel = GUILayout.Toggle(i == lsd.relativeFileSelect[3], fn)
                if sel:
                    lsd.relativeFileSelect[3] = i
                else:
                    if lsd.relativeFileSelect[3] == i:
                        lsd.relativeFileSelect[3] = -1
        GUILayout.EndScrollView()
    elif lsd.loadStep == "confirmloadjobs":
        # header
        GUILayout.Label(greenText(LC("Totally %d file(s) will be loaded:"))%len(lsd.importJobs), labelStyle)
        lsd.scrollPos = GUILayout.BeginScrollView(lsd.scrollPos, GUI.skin.box, GUILayout.Width(fullw))
        for fn in lsd.sortedImportJobName:
            GUILayout.Label(yellowText(fn), labelStyle)
            if "motion" in lsd.importJobs[fn][0]:
                GUILayout.BeginHorizontal()
                GUILayout.Space(30)
                GUILayout.Label(LC("Import <color=#00ff00>%d</color> motion keyframes to: ")%lsd.vmdCheckResults[fn][1]["keyframeCount"])
                for actor in lsd.importJobs[fn][1]:
                    GUILayout.Label(cyanText(actor.text_name))
                    GUILayout.Space(10)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            if "morph" in lsd.importJobs[fn][0]:
                GUILayout.BeginHorizontal()
                GUILayout.Space(30)
                GUILayout.Label(LC("Import <color=#00ff00>%d</color> morph keyframes to: ")%lsd.vmdCheckResults[fn][2]["keyframeCount"])
                for actor in lsd.importJobs[fn][2]:
                    GUILayout.Label(cyanText(actor.text_name))
                    GUILayout.Space(10)
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            if "camera" in lsd.importJobs[fn][0]:
                GUILayout.BeginHorizontal()
                GUILayout.Space(30)
                GUILayout.Label(LC("Import <color=#00ff00>%d</color> camera keyframes,")%lsd.vmdCheckResults[fn][3]["keyframeCount"])
                if lsd.importJobs[fn][3]:
                    actor = lsd.importJobs[fn][3]
                    GUILayout.Label(LC("align to ") + cyanText(actor.text_name))
                else:
                    GUILayout.Label(yellowText(LC("align to (0,0,0)")))
                GUILayout.FlexibleSpace()
                GUILayout.EndHorizontal()
            if "wav" in lsd.importJobs[fn][0]:
                GUILayout.BeginHorizontal()
                GUILayout.Space(30)
                GUILayout.Label(LC("Music file"))
                GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
    elif lsd.loadStep.startswith("loadingwait"):
        GUILayout.FlexibleSpace()
        if lsd.loadingFlag:
            fn = lsd.sortedImportJobName[lsd.onLoadingJobIndex]
            GUILayout.Label(LC("Loading <color=#00ff00>%d</color> of <color=#00ff00>%d</color> file(s):")%(lsd.onLoadingJobIndex+1, len(lsd.importJobs)), labelStyle)
            GUILayout.Label(greenText(fn), labelStyle)
            if fn in lsd.vmdCheckResults:
                mcf = lsd.vmdCheckResults[fn][1]["keyframeCount"]
                mpf = lsd.vmdCheckResults[fn][2]["keyframeCount"]
                ccf = lsd.vmdCheckResults[fn][3]["keyframeCount"]
                GUILayout.Space(10)
                GUILayout.Label(LC("Contents <color=#00ff00>%d</color> motion, <color=#00ff00>%d</color> morph, <color=#00ff00>%d</color> camera keyframes")%(mcf, mpf, ccf), labelStyle)
            GUILayout.Space(10)
            GUILayout.Label(LC("Please wait a while..."), labelStyle)
        else:
            GUILayout.Label(lsd.vmdLoadError, labelStyle)
        GUILayout.FlexibleSpace()
        enableCancel = False
    else:
        # loadStep == "error" or "report"
        GUILayout.FlexibleSpace()
        GUILayout.Label(lsd.vmdLoadError, labelStyle)
        GUILayout.FlexibleSpace()
        enableCancel = False

    # Control buttons by step
    cbwidth = (fullw - 3*4) / 3
    GUILayout.FlexibleSpace()
    GUILayout.BeginHorizontal()
    # two function buttons
    if lsd.loadStep == "targetfile":
        mmddConfig.scanRelatives = GUILayout.Toggle(mmddConfig.scanRelatives, LC("Search for relative files automatically"), toggleStyle)
        GUILayout.EndHorizontal()
        GUILayout.BeginHorizontal()
        # target file
        if cameraOnly:
            if GUILayout.Button("Import Camera", btnstyle, GUILayout.Width(cbwidth)):
                initVmdLoadJob()
            GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth))
        else:
            if GUILayout.Button("To Selected <color=#00ff00>%d</color>"%selected if selected else " ", btnstyle, GUILayout.Width(cbwidth)) and selected:
                initVmdLoadJob()
            if GUILayout.Button(LC("Choose bone define"), btnstyle, GUILayout.Width(cbwidth)):
                chooseBoneDefine()
    elif lsd.loadStep == "choosebonedefine":
        # select bone define
        if GUILayout.Button("OK", btnstyle, GUILayout.Width(cbwidth)):
            lsd.boneDefine = lsd.boneDefineTemp
            lsd.loadStep = "targetfile" 
        GUILayout.Button(" ", btnstyle, GUILayout.Width(cbwidth))
        if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
            lsd.loadStep = "targetfile"        
    elif lsd.loadStep == "relativesetup":
        # select relative file
        if GUILayout.Button(LC("Load selected files"), btnstyle, GUILayout.Width(cbwidth)):
            appendLoadJobs()
        if GUILayout.Button(LC("No bother, skip"), btnstyle, GUILayout.Width(cbwidth)):
            startLoadJobs()
    elif lsd.loadStep == "confirmloadjobs":
        # confirm load jobs
        if GUILayout.Button(LC("OK"), btnstyle, GUILayout.Width(cbwidth)):
            startLoadJobs()
        if GUILayout.Button(LC("Re-select"), btnstyle, GUILayout.Width(cbwidth)):
            reselectRelativeFiles()
    elif lsd.loadStep == "loadingwait":
        if not lsd.loadingFlag:
            GUILayout.FlexibleSpace()
            if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
                clearImport()
                mmddGuiMain(game)
            GUILayout.FlexibleSpace()
    else:
        # error
        GUILayout.FlexibleSpace()
        if GUILayout.Button("Back", btnstyle, GUILayout.Width(cbwidth)):
            clearImport()
            mmddGuiMain(game)
        GUILayout.FlexibleSpace()
    # cancel button
    if enableCancel:
        if GUILayout.Button("Cancel", btnstyle, GUILayout.Width(cbwidth)):
            clearImport()
            mmddGuiMain(game)
    GUILayout.EndHorizontal()

def guiImportVmdData_advanced(game):
    mmdd = game.gdata.mmdd
    mmddConfig = game.gdata.mmdd_config
    sd = game.scenedata
    if mmddGuiSwitch(game, guiImportVmdData_advanced):
        sd.bindTarget = "selecttarget"
        sd.tempScrollPos = Vector2.zero
        return

    labelStyle = GUIStyle("label")
    labelStyle.richText = True
    labelStyle.fontSize = 16
    labelStyle.wordWrap = True
    toggleStyle = GUIStyle("toggle")
    toggleStyle.fontSize = 16
    btnstyle = GUIStyle("button")
    btnstyle.fontSize = 16
    fullw = mmddConfig.guiWidth - 20
    cbwidth2 = (fullw - 3*2) / 2
    cbwidth3 = (fullw - 4*2) / 3
    cbwidth4 = (fullw - 5*2) / 4

    def getCurrentSelectInfo():
        if len(game.studio.treeNodeCtrl.selectNodes) == 0:
            return None, redText("None, Please select object from WORKSPACE"), None
        if len(game.studio.treeNodeCtrl.selectNodes) > 1:
            return None, redText("Multi, Please select only one object"), None
        tgtObj = HSNeoOCI.create_from_treenode(game.studio.treeNodeCtrl.selectNodes[0])
        if isinstance(tgtObj, HSNeoOCIChar):
            tgtDesp = greenText(tgtObj.text_name) + " (" + greenText(LC("Charactor")) + ")"
            tgtType = "chara"
        elif isinstance(tgtObj, HSNeoOCIItem):
            tgtDesp = greenText(tgtObj.text_name) + " (" + yellowText(LC("Item")) + ")"
            tgtType = "item"
        elif isinstance(tgtObj, HSNeoOCIFolder):
            tgtDesp = greenText(tgtObj.text_name) + " (" + cyanText(LC("Folder")) + ")" + redText(" Not Supported yet...")
            tgtType = "folder"
            tgtObj = None
        else:
            tgtDesp = greenText(tgtObj.text_name) + " (" + redText(LC("Unsupported type!!")) + ")"
            tgtObj = None
            tgtType = None
        return tgtObj, tgtDesp, tgtType
    def checkSelTarget():
        oci = sd.tempAdvTarget.objctrl
        emc = emp = None
        for mc in mmdd.motionControllers.values():
            if mc.ociChar == oci:
                emc = mc
                break
        for mp in mmdd.morphControllers.values():
            if mp.actor.objctrl == oci:
                emp = mp
                break
        return emc, emp
    def listValidBoneDefine(tgtType):
        if tgtType == "chara":
            validfile = [(None, greenText(LC("Default bone define")))]
        else:
            validfile = []
        from vmdlib import MotionController
        extBds = MotionController.ListExtendDefineFile(tgtType)
        for pathname in sorted(extBds.keys()):
            filename = os.path.split(pathname)[1]
            desp = greenText(filename) + ": " + extBds[pathname]
            validfile.append((pathname, desp))
        return validfile
    def selectKeys(mode, tgtDict, tgtGroup):
        for gkey in tgtDict.keys():
            if tgtGroup != None and tgtGroup != gkey:
                continue
            if gkey == "unsupported":
                continue
            for bi in tgtDict[gkey].values():
                if mode == "all":
                    bi[0] = True
                elif mode == "none":
                    bi[0] = False
                elif mode == "auto":
                    bi[0] = bi[2] > 1
                else:
                    print "invalid select mode:", mode
                    return
    def onLoadVmd(files):
        if not files or len(files) == 0:
            sd.tempAdvMessage = LC("No vmd file selected.")
            sd.bindTarget = "redo"
            return
        # prepare
        mmdd.lastVmdFilename = files[0]
        sd.bindTarget = "vmdloadingwait"
        # load
        from vmdlib import VmdLib
        sd.vmdResult = VmdLib.loadVmd(mmdd.lastVmdFilename)
        # check result
        if len(sd.vmdResult) == 1:
            sd.tempAdvMessage = LC("Load vmd file error: %s"%redText(sd.vmdResult[0]))
            sd.bindTarget = "redo"
            return
        # check contents
        boneKeyframes = sd.vmdResult[1]
        morphKeyframes = sd.vmdResult[2]
        cameraKeyframes = sd.vmdResult[3]
        if len(boneKeyframes) > 0 or len(morphKeyframes) > 0:
            sd.tempScrollPos = Vector2.zero
            # get vmd bones
            sd.tempAdvExpend = [False, False]
            sd.tempAdvBoneSelection = {}
            sd.tempAdvMorphSelection = {}
            for bkf in boneKeyframes:
                if sd.tempAdvBoneSelection.has_key(bkf.name):
                    sd.tempAdvBoneSelection[bkf.name][2] += 1
                else:
                    sd.tempAdvBoneSelection[bkf.name] = [True, bkf.name, 1]
            for mkf in morphKeyframes:
                if sd.tempAdvMorphSelection.has_key(mkf.name):
                    sd.tempAdvMorphSelection[mkf.name][2] += 1
                else:
                    sd.tempAdvMorphSelection[mkf.name] = [True, mkf.name, 1]
            # group up and check
            anyMarch = False
            mc = sd.tempAdvTgtMotionCtrl
            sd.tempAdvBoneSelectionGrouped = {}
            sd.tempAdvExpendBone = {}
            for vmdbone in sd.tempAdvBoneSelection.keys():
                group = None
                for gname in mc.boneGroupSet.keys():
                    if vmdbone in mc.boneGroupSet[gname]:
                        group = gname
                        break
                if group == None:
                    if mc.vmd2BoneMap.has_key(vmdbone):
                        group = "ungrouped"
                    else:
                        group = "unsupported"
                        sd.tempAdvBoneSelection[vmdbone][0] = False
                anyMarch |= group != "unsupported"
                if group in sd.tempAdvBoneSelectionGrouped:
                    sd.tempAdvBoneSelectionGrouped[group][vmdbone] = sd.tempAdvBoneSelection[vmdbone]
                else:
                    sd.tempAdvBoneSelectionGrouped[group] = {vmdbone: sd.tempAdvBoneSelection[vmdbone]}
                    sd.tempAdvExpendBone[group] = False
            sd.tempAdvMorphSelectionGrouped = {}
            sd.tempAdvExpendMorph = {}
            for vmdmorph in sd.tempAdvMorphSelection.keys():
                group = None
                for gname in mc.morphGroupSet.keys():
                    if vmdmorph in mc.morphGroupSet[gname]:
                        group = gname
                        break
                if group == None:
                    if vmdmorph in mc.vmdMorphs:
                        group = "ungrouped"
                    else:
                        group = "unsupported"
                        sd.tempAdvMorphSelection[vmdmorph][0] = False
                anyMarch |= group != "unsupported"
                if group in sd.tempAdvMorphSelectionGrouped:
                    sd.tempAdvMorphSelectionGrouped[group][vmdmorph] = sd.tempAdvMorphSelection[vmdmorph]
                else:
                    sd.tempAdvMorphSelectionGrouped[group] = {vmdmorph: sd.tempAdvMorphSelection[vmdmorph]}
                    sd.tempAdvExpendMorph[group] = False
            # done
            if anyMarch:
                sd.bindTarget = "selectframes"
            else:
                sd.tempAdvMessage = LC("No keyframe in vmd file matches bone define! Nothing can be imported.")
                sd.bindTarget = "redo"
        elif len(cameraKeyframes) > 0:
            # to camera import gui
            sd.tempAdvMessage = LC("This is a camera vmd, load it with normal vmd import.")
            sd.bindTarget = "redo"
        else:
            sd.tempAdvMessage = LC("No bone/morph/camera keyframes found in this vmd file.")
            sd.bindTarget = "redo"
    def doAdvImport():
        from vmdlib import MorphController
        if isinstance(sd.tempAdvTarget, HSNeoOCIChar):
            actor = sd.tempAdvTarget.as_actor
            aid = mmdd.autoRegistActor(actor)
        else:
            actor = sd.tempAdvTarget.as_prop
            aid = mmdd.autoRegistProp(actor)
        boneImportedList = boneIgnoredList = morphImportedList = morphIgnoredList = None
        try:
            def enableAndCheck(param):
                mc = param
                if not mc.Enable:
                    mc.setEnable(True)
                    game.updFuncParam = param
                    game.updFunc = enableAndCheck
            # build motion controller
            needActive = False
            mc = sd.tempAdvTgtMotionCtrl
            if mc.ociChar == None:
                mc.bindChara(actor)
            if sd.tempAdvFrameTgt[0]:
                ignoreList = [bi[1] for bi in sd.tempAdvBoneSelection.values() if not bi[0]]
                if len(ignoreList) and False:
                    print "Ignored:"
                    for igb in ignoreList:
                        print " ", igb
                boneImportedList, boneIgnoredList = mc.importBoneKeyframes(mmdd.lastVmdFilename, boneKeyframes, sd.tempAdvImportMode, ignoreList)
                if hasattr(sd, "MotionKF_KeyframeViews") and sd.MotionKF_KeyframeViews.has_key(mc):
                    del sd.MotionKF_KeyframeViews[mc]
                mmdd.postProcessForMotionImport(actor)
                motionOK = len(boneImportedList) > 0
                needActive = (not mc.Enable) and motionOK
            else:
                motionOK = True
            # build morph controller
            mp = None
            if sd.tempAdvFrameTgt[1]:
                if aid in mmdd.morphControllers:
                    mp = mmdd.getMorphController(aid)
                else:
                    mp = MorphController(mc)
                    mp.bindChara(actor)
                ignoreList = [mi[1] for mi in sd.tempAdvMorphSelection.values() if not mi[0]]
                morphImportedList, morphIgnoredList = mp.importMorphKeyframes(mmdd.lastVmdFilename, morphKeyframes, sd.tempAdvImportMode, ignoreList)
                if hasattr(sd, "MorphKF_KeyframeViews") and sd.MorphKF_KeyframeViews.has_key(mp):
                    del sd.MorphKF_KeyframeViews[mp]
                morphOK = len(morphImportedList) > 0
                needActive = needActive or mp.NeedActiveMotion
            else:
                morphOK = True
            # check loaded
            if motionOK and morphOK:
                # active mc
                if needActive:
                    game.updFuncParam = mc
                    game.updFunc = enableAndCheck
                # regist ctrl
                if sd.tempAdvFrameTgt[0] and aid not in mmdd.motionControllers:
                    mmdd.registerMotionController(aid, mc)
                if sd.tempAdvFrameTgt[1] and aid not in mmdd.morphControllers:
                    mmdd.morphControllers[aid] = mp
                # refresh 
                mmdd.refreshTotalFrame()
                mmdd.update()
                error = None
            elif motionOK:
                error = "No morph keyframe loaded. Bone setting and vmd file mismatch?"
            elif morphOK:
                error = "No motion keyframe loaded. Bone setting and vmd file mismatch?"
            else:
                error = "No motion/morph keyframe loaded. Bone setting and vmd file mismatch?"      
        except Exception, e:
            traceback.print_exc()
            error = str(e)
        # build report
        txt = []
        txt.append(LC("Load VMD Advanced:"))
        txt.append(LC(" - From: ") + greenText(os.path.split(mmdd.lastVmdFilename)[1]))
        txt.append(LC(" - Import: ") + (LC("<color=#00ff00>%d</color> motion ")%len(boneKeyframes) if sd.tempAdvFrameTgt[0] else "") + (LC("<color=#00ff00>%d</color> morph ")%len(morphKeyframes) if sd.tempAdvFrameTgt[1] else "") + LC("keyframes"))
        txt.append(LC(" - Apply to: ") + greenText(sd.tempAdvTarget.text_name) + " [" + cyanText(aid) + "]")
        txt.append(LC(" - With bone define: ") + (greenText(os.path.split(sd.tempAdvBoneDefine)[1]) if sd.tempAdvBoneDefine else greenText(LC("Default bone define"))))
        txt.append("")
        if error:
            txt.append(LC("Result: ") + redText(LC("FAILED")))
            txt.append("Message: " + redText(error))
        else:
            txt.append(LC("Result: ") + greenText(LC("COMPLETED")))
            txt.append("")
            txt.append(LC("Detail: "))
            if sd.tempAdvFrameTgt[0]:
                txt.append([False, LC(" - Motion: %d bone imported")%len(boneImportedList), ["imported <color=#00ff00>%s</color> with %d keyframes"%(m[0], m[1]) for m in boneImportedList]])
                if len(boneIgnoredList):
                    txt.append([False, LC(" - Motion: %d bone ingored")%len(boneIgnoredList), ["ignored <color=#ff0000>%s</color> with %s keyframes"%(m[0], str(m[1]) if m[1] <= 2 else redText(str(m[1]))) for m in boneIgnoredList]])
                else:
                    txt.append(LC("All bone imported."))
            if sd.tempAdvFrameTgt[1]:
                txt.append([False, LC(" - Morph: %d morph imported")%len(morphImportedList), ["imported <color=#00ff00>%s</color> with %d keyframes"%(m[0], m[1]) for m in morphImportedList]])
                if len(morphIgnoredList):
                    txt.append([False, LC(" - Morph: %d morph ingored")%len(morphIgnoredList), ["ignored <color=#ff0000>%s</color> with %s keyframes"%(m[0], str(m[1]) if m[1] <= 2 else redText(str(m[1]))) for m in morphIgnoredList]])
                else:
                    txt.append(LC("All morph imported."))
        return txt

    # GUI by step
    if sd.bindTarget == "selecttarget":
        sd.tempAdvTarget, tgtDesp, tgtType = getCurrentSelectInfo()
        GUILayout.Label(LC("Step 1:"), labelStyle)
        GUILayout.Label(LC("Select an actor or object from <color=#00ff00>WORKSPACE in STUDIO</color> to import keyframes"), labelStyle)
        GUILayout.Space(20)
        GUILayout.Label(LC("Selected: %s")%tgtDesp, labelStyle)
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if sd.tempAdvTarget:
            if GUILayout.Button(LC("Next >>"), btnstyle, GUILayout.Width(cbwidth2)):
                emc, emp = checkSelTarget()
                if emc or emp:
                    # motion or morph ctrl exist, need to select import mode
                    sd.tempAdvTgtMotionCtrl = emc if emc else emp.motionCtrl
                    sd.tempAdvBoneDefine = emc.boneDefineFilename
                    sd.bindTarget = "selectimportmode"
                else:
                    # new ctrl, need to select bone define
                    sd.tempAdvTgtMotionCtrl = None
                    sd.tempAdvValidBoneDefines = listValidBoneDefine(tgtType)
                    if len(sd.tempAdvValidBoneDefines) > 0:
                        sd.tempAdvBoneDefine = sd.tempAdvValidBoneDefines[0][0]
                        sd.bindTarget = "selectbonedefine"
                    else:
                        sd.tempAdvMessage = LC("No valid bone define file found for selected object: ") + tgtDesp
                        sd.bindTarget = "message"
        else:
            GUILayout.Button("", btnstyle, GUILayout.Width(cbwidth2))
        if GUILayout.Button(LC("Cancel"), btnstyle, GUILayout.Width(cbwidth2)):
            mmddGuiMain(game)
        GUILayout.EndHorizontal()
    elif sd.bindTarget == "selectbonedefine":
        GUILayout.Label(LC("Step 2:"), labelStyle)
        GUILayout.Label(LC("Select a bone define file"), labelStyle)
        GUILayout.Space(10)
        sd.tempScrollPos = GUILayout.BeginScrollView(sd.tempScrollPos, GUILayout.Width(fullw))
        for fi in sd.tempAdvValidBoneDefines:
            sel = GUILayout.Toggle(sd.tempAdvBoneDefine == fi[0], fi[1])
            if sel:
                sd.tempAdvBoneDefine = fi[0]
        GUILayout.EndScrollView()
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if GUILayout.Button(LC("Next >>"), btnstyle, GUILayout.Width(cbwidth2)):
            from vmdlib import MotionController
            try:
                sd.tempAdvTgtMotionCtrl = MotionController(sd.tempAdvBoneDefine)
                sd.tempAdvImportMode = "renew"
                sd.bindTarget = "selectvmdfile"
            except Exception, e:
                sd.tempAdvMessage = LC("Load bone define file error:\n") + redText(str(e))
                sd.bindTarget = "message"
        if GUILayout.Button(LC("Cancel"), btnstyle, GUILayout.Width(cbwidth2)):
            mmddGuiMain(game)
        GUILayout.EndHorizontal()
    elif sd.bindTarget == "selectimportmode":
        GUILayout.Label(LC("Step 2:"), labelStyle)
        GUILayout.Label(LC("Motion/morph controller existed for %s, choose import mode for new keyframes")%sd.tempAdvTgtMotionCtrl.CharName, labelStyle)
        GUILayout.Space(10)
        GUILayout.Label(LC("For example: if keyframes for bone A, B and C existed in current motion/morph controller, and plan to import new keyframes for bone C and D:"))
        GUILayout.Label(greenText(LC("Renew")) + LC(": discard all old keyframes, result will be new C and D."))
        GUILayout.Label(greenText(LC("Replace")) + LC(": leave A and B untouched, replace old C with new C, and import D."))
        GUILayout.Label(greenText(LC("Append")) + LC(": leave old A, B and C untouched, only import D."))
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if GUILayout.Button(LC("Renew"), btnstyle, GUILayout.Width(cbwidth4)):
            sd.tempAdvImportMode = "renew"
            sd.bindTarget = "selectvmdfile"
        if GUILayout.Button(LC("Replace"), btnstyle, GUILayout.Width(cbwidth4)):
            sd.tempAdvImportMode = "replace"
            sd.bindTarget = "selectvmdfile"
        if GUILayout.Button(LC("Append"), btnstyle, GUILayout.Width(cbwidth4)):
            sd.tempAdvImportMode = "append"
            sd.bindTarget = "selectvmdfile"
        if GUILayout.Button(LC("Cancel"), btnstyle, GUILayout.Width(cbwidth4)):
            mmddGuiMain(game)
        GUILayout.EndHorizontal()
    elif sd.bindTarget == "selectvmdfile":
        guiOpenFileDialog(onLoadVmd, mmdd.lastVmdFilename if len(mmdd.lastVmdFilename) else mmddConfig.vmdHomeDir, True, "VMD")
        sd.bindTarget = "selectvmdwait"
    elif sd.bindTarget == "selectvmdwait":
        try:
            GUILayout.Label(LC("Step 3:"), labelStyle)
            GUILayout.Label(LC("Select a motion/morph vmd file..."), labelStyle)
        except:
            pass
    elif sd.bindTarget == "vmdloadingwait":
        GUILayout.FlexibleSpace()
        GUILayout.Label(LC("Loading %s...")%greenText(mmdd.lastVmdFilename), labelStyle)
        GUILayout.FlexibleSpace()
    elif sd.bindTarget == "selectframes":
        selBoneCnt = len([a for a in sd.tempAdvBoneSelection.values() if a[0]])
        selBoneTxt = greenText(str(selBoneCnt)) if selBoneCnt > 0 else redText(str(selBoneCnt))
        selMorphCnt = len([a for a in sd.tempAdvMorphSelection.values() if a[0]])
        selMorphTxt = greenText(str(selMorphCnt)) if selMorphCnt > 0 else redText(str(selMorphCnt))
        GUILayout.Label(LC("Step 4:"), labelStyle)
        GUILayout.Label(LC("Select the keyframes you want to import: (%s bones and %s morphs selected)")%(selBoneTxt, selMorphTxt), labelStyle)
        GUILayout.Space(10)
        GUILayout.Label(LC("VMD data base model: <color=#00ff00>%s</color>")%sd.vmdResult[0], labelStyle)
        sd.tempScrollPos = GUILayout.BeginScrollView(sd.tempScrollPos, GUILayout.Width(fullw))
        mc = sd.tempAdvTgtMotionCtrl
        boneKeyframes = sd.vmdResult[1]
        if len(boneKeyframes) > 0:
            GUILayout.BeginHorizontal()
            if GUILayout.Button("-" if sd.tempAdvExpend[0] else "+", GUILayout.Width(20)):
                sd.tempAdvExpend[0] = not sd.tempAdvExpend[0]
            GUILayout.Label(LC("Motion: Totally <color=#00ff00>%d</color> bones, <color=#00ff00>%d</color> selected")%(len(sd.tempAdvBoneSelection), selBoneCnt), labelStyle)
            GUILayout.FlexibleSpace()
            if GUILayout.Button("All"):
                selectKeys("all", sd.tempAdvBoneSelectionGrouped, None)
            if GUILayout.Button("None"):
                selectKeys("none", sd.tempAdvBoneSelectionGrouped, None)
            if GUILayout.Button("Auto"):
                selectKeys("auto", sd.tempAdvBoneSelectionGrouped, None)
            GUILayout.EndHorizontal()
            if sd.tempAdvExpend[0]:
                for group in mc.boneGroupList:
                    if group not in sd.tempAdvBoneSelectionGrouped.keys():
                        continue
                    cntInGroup = len(sd.tempAdvBoneSelectionGrouped[group])
                    cntSelected = len([a for a in sd.tempAdvBoneSelectionGrouped[group].values() if a[0]])
                    groupSelTxt = " (<color=#00ff00>%d</color>/<color=#00ff00>%d</color> selected)"%(cntSelected, cntInGroup)
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(30)
                    if GUILayout.Button("-" if sd.tempAdvExpendBone[group] else "+", GUILayout.Width(20)):
                        sd.tempAdvExpendBone[group] = not sd.tempAdvExpendBone[group]
                    if group != "unsupported":
                        GUILayout.Label(mc.boneGroupName[group] + groupSelTxt, labelStyle)
                        GUILayout.FlexibleSpace()
                        if GUILayout.Button("All"):
                            selectKeys("all", sd.tempAdvBoneSelectionGrouped, group)
                        if GUILayout.Button("None"):
                            selectKeys("none", sd.tempAdvBoneSelectionGrouped, group)
                        if GUILayout.Button("Auto"):
                            selectKeys("auto", sd.tempAdvBoneSelectionGrouped, group)
                    else:
                        GUILayout.Label(mc.boneGroupName[group], labelStyle)
                    GUILayout.EndHorizontal()
                    if sd.tempAdvExpendBone[group]:
                        if group in mc.boneGroupSet.keys():
                            for vmdbone in mc.boneGroupSet[group]:
                                if vmdbone not in sd.tempAdvBoneSelectionGrouped[group].keys():
                                    continue
                                bi = sd.tempAdvBoneSelectionGrouped[group][vmdbone]
                                GUILayout.BeginHorizontal()
                                GUILayout.Space(60)
                                bi[0] = GUILayout.Toggle(bi[0], LC("%s: <color=#00ffff>%d</color> keyframes")%(greenText(bi[1]), bi[2]))
                                GUILayout.EndHorizontal()
                        elif group == "ungrouped":
                            for vmdbone in sorted(sd.tempAdvBoneSelectionGrouped[group].keys()):
                                bi = sd.tempAdvBoneSelectionGrouped[group][vmdbone]
                                GUILayout.BeginHorizontal()
                                GUILayout.Space(60)
                                bi[0] = GUILayout.Toggle(bi[0], LC("%s: <color=#00ffff>%d</color> keyframes")%(greenText(bi[1]), bi[2]))
                                GUILayout.EndHorizontal()
                        else: # unsupported
                            for vmdbone in sorted(sd.tempAdvBoneSelectionGrouped[group].keys()):
                                bi = sd.tempAdvBoneSelectionGrouped[group][vmdbone]
                                GUILayout.BeginHorizontal()
                                GUILayout.Space(60)
                                if bi[2] > 1:
                                    GUILayout.Label(LC("%s: <color=#ff0000>%d</color> keyframes")%(redText(bi[1]), bi[2]))
                                else:
                                    GUILayout.Label(LC("%s: <color=#00ffff>%d</color> keyframes")%(greenText(bi[1]), bi[2]))
                                GUILayout.EndHorizontal()
        morphKeyframes = sd.vmdResult[2]
        if len(morphKeyframes) > 0:
            GUILayout.BeginHorizontal()
            if GUILayout.Button("-" if sd.tempAdvExpend[1] else "+", GUILayout.Width(20)):
                sd.tempAdvExpend[1] = not sd.tempAdvExpend[1]
            GUILayout.Label(LC("Morph: Totally <color=#00ff00>%d</color> morphs, <color=#00ff00>%d</color> selected")%(len(sd.tempAdvMorphSelection), selMorphCnt), labelStyle)
            GUILayout.FlexibleSpace()
            if GUILayout.Button("All"):
                selectKeys("all", sd.tempAdvMorphSelectionGrouped, None)
            if GUILayout.Button("None"):
                selectKeys("none", sd.tempAdvMorphSelectionGrouped, None)
            if GUILayout.Button("Auto"):
                selectKeys("auto", sd.tempAdvMorphSelectionGrouped, None)
            GUILayout.EndHorizontal()
            if sd.tempAdvExpend[1]:
                for group in mc.morphGroupList:
                    if group not in sd.tempAdvMorphSelectionGrouped.keys():
                        continue
                    cntInGroup = len(sd.tempAdvMorphSelectionGrouped[group])
                    cntSelected = len([a for a in sd.tempAdvMorphSelectionGrouped[group].values() if a[0]])
                    groupSelTxt = " (<color=#00ff00>%d</color>/<color=#00ff00>%d</color> selected)"%(cntSelected, cntInGroup)
                    GUILayout.BeginHorizontal()
                    GUILayout.Space(30)
                    if GUILayout.Button("-" if sd.tempAdvExpendMorph[group] else "+", GUILayout.Width(20)):
                        sd.tempAdvExpendMorph[group] = not sd.tempAdvExpendMorph[group]
                    if group != "unsupported":
                        GUILayout.Label(mc.morphGroupName[group] + groupSelTxt, labelStyle)
                        GUILayout.FlexibleSpace()
                        if GUILayout.Button("All"):
                            selectKeys("all", sd.tempAdvMorphSelectionGrouped, group)
                        if GUILayout.Button("None"):
                            selectKeys("none", sd.tempAdvMorphSelectionGrouped, group)
                        if GUILayout.Button("Auto"):
                            selectKeys("auto", sd.tempAdvMorphSelectionGrouped, group)
                    else:
                        GUILayout.Label(mc.morphGroupName[group], labelStyle)
                    GUILayout.EndHorizontal()
                    if sd.tempAdvExpendMorph[group]:
                        if group in mc.morphGroupSet.keys():
                            for vmdbone in mc.morphGroupSet[group]:
                                if vmdbone not in sd.tempAdvMorphSelectionGrouped[group].keys():
                                    continue
                                bi = sd.tempAdvMorphSelectionGrouped[group][vmdbone]
                                GUILayout.BeginHorizontal()
                                GUILayout.Space(60)
                                bi[0] = GUILayout.Toggle(bi[0], LC("%s: <color=#00ffff>%d</color> keyframes")%(greenText(bi[1]), bi[2]))
                                GUILayout.EndHorizontal()
                        elif group == "ungrouped":
                            for vmdbone in sorted(sd.tempAdvMorphSelectionGrouped[group].keys()):
                                bi = sd.tempAdvMorphSelectionGrouped[group][vmdbone]
                                GUILayout.BeginHorizontal()
                                GUILayout.Space(60)
                                bi[0] = GUILayout.Toggle(bi[0], LC("%s: <color=#00ffff>%d</color> keyframes")%(greenText(bi[1]), bi[2]))
                                GUILayout.EndHorizontal()
                        else: # unsupported
                            for vmdbone in sorted(sd.tempAdvMorphSelectionGrouped[group].keys()):
                                bi = sd.tempAdvMorphSelectionGrouped[group][vmdbone]
                                GUILayout.BeginHorizontal()
                                GUILayout.Space(60)
                                if bi[2] > 1:
                                    GUILayout.Label(LC("%s: <color=#ff0000>%d</color> keyframes")%(redText(bi[1]), bi[2]))
                                else:
                                    GUILayout.Label(LC("%s: <color=#00ffff>%d</color> keyframes")%(greenText(bi[1]), bi[2]))
                                GUILayout.EndHorizontal()
        GUILayout.EndScrollView()
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        sd.tempAdvFrameTgt = (selBoneCnt > 0, selMorphCnt > 0)
        if sd.tempAdvFrameTgt[0] or sd.tempAdvFrameTgt[1]:
            if GUILayout.Button(LC("Next >>"), btnstyle, GUILayout.Width(cbwidth2)):
                sd.tempAdvMessage = doAdvImport()
                sd.bindTarget = "message"
        else:
            GUILayout.Button("", btnstyle, GUILayout.Width(cbwidth2))
        if GUILayout.Button(LC("Cancel"), btnstyle, GUILayout.Width(cbwidth2)):
            mmddGuiMain(game)
        GUILayout.EndHorizontal()
    elif sd.bindTarget == "redo":
        GUILayout.FlexibleSpace()
        GUILayout.Label(sd.tempAdvMessage, labelStyle)
        GUILayout.FlexibleSpace()
        GUILayout.BeginHorizontal()
        if GUILayout.Button(LC("Re-select VMD"), btnstyle, GUILayout.Width(cbwidth3)):
            sd.bindTarget = "selectvmdfile"
        if GUILayout.Button(LC("Re-select Target"), btnstyle, GUILayout.Width(cbwidth3)):
            sd.bindTarget = "selecttarget"
        if GUILayout.Button(LC("Cancel"), btnstyle, GUILayout.Width(cbwidth3)):
            mmddGuiMain(game)
        GUILayout.EndHorizontal()
    else:
        if isinstance(sd.tempAdvMessage, list):
            sd.tempScrollPos = GUILayout.BeginScrollView(sd.tempScrollPos, GUILayout.Width(fullw))
            for ml in sd.tempAdvMessage:
                if isinstance(ml, list):
                    GUILayout.BeginHorizontal()
                    if GUILayout.Button("-" if ml[0] else "+", GUILayout.Width(20)):
                        ml[0] = not ml[0]
                    GUILayout.Label(ml[1], labelStyle)
                    GUILayout.EndHorizontal()
                    if ml[0]:
                        for stxt in ml[2]:
                            GUILayout.Label("       " + stxt)
                else:
                    GUILayout.Label(ml, labelStyle)
            GUILayout.EndScrollView()
            GUILayout.FlexibleSpace()
        else:
            GUILayout.FlexibleSpace()
            GUILayout.Label(sd.tempAdvMessage, labelStyle)
            GUILayout.FlexibleSpace()
        if GUILayout.Button("OK", btnstyle):
            mmddGuiMain(game)

def guiExportVmdData(game, iniController):
    mmdd = game.gdata.mmdd
    game.scenedata.exporttemp = GData()
    et = game.scenedata.exporttemp
    et.expMC = None
    et.expMP = None
    et.expCC = None
    et.newfile = ""

    def expMotion(g):
        et.expMC = iniController
        selectFile()
    def expMotionMorph(g):
        et.expMC = iniController
        for mp in mmdd.morphControllers.values():
            if mp.motionCtrl == iniController:
                et.expMP = mp
                break
        selectFile()
    def expMorph(g):
        et.expMP = iniController
        selectFile()
    def expMorphMotion(g):
        et.expMP = iniController
        et.expMC = iniController.motionCtrl
        selectFile()
    def doExpData(files):
        if files and len(files) > 0:
            et.newfile = files[0]
            if not et.newfile.lower().endswith(".vmd"):
                et.newfile += ".vmd"
            guiWaitDialog(game, LC("Exporting keyframes to %s...")%greenText(et.newfile))

            tgtStr = ""
            if et.expMC:
                tgtStr += "motion"
                boneKeyframes = et.expMC.KeyframesForExport
            else:
                boneKeyframes = None
            if et.expMP:
                if len(tgtStr): tgtStr += "+"
                tgtStr += "morph"
                morphKeyframes = et.expMP.KeyframesForExport
            else:
                morphKeyframes = None
            if et.expCC:
                if len(tgtStr): tgtStr += "+"
                tgtStr += "camera"
                cameraKeyframes = et.expCC.KeyframesForExport
            else:
                cameraKeyframes = None

            from vmdlib import VmdLib
            res, msg = VmdLib.saveVmd(et.newfile, boneKeyframes, morphKeyframes, cameraKeyframes, None)
            if res:
                msg = LC("Export %s keyframes DONE!")%tgtStr + "\n" + greenText(msg)
                msg += "\n\n" + LC("Re-direct controllers to new file and delete keyframe undo buffer?")
                guiSelectDialog(game, (msg, (LC("OK! Do it"), doRedirect), (LC("No, thanks"), backToCaller)))
            else:
                msg = LC("Export %s keyframes ERROR!")%tgtStr + "\n" + redText(msg)
                guiPromptDialog(game, msg, backToCaller)
    def doRedirect(game):
        if et.expMC:
            et.expMC.redirectBoneKeyframes(et.newfile)
        if et.expMP:
            et.expMP.redirectMorphKeyframes(et.newfile)
        if et.expCC:
            et.expCC.redirectCameraKeyframes(et.newfile)
        backToCaller(game)
    def backToCaller(game):
        if et.expMC == iniController:
            callerFunc = mmddGuiMotionKeyframe
            callerParam = iniController
        elif et.expMP == iniController:
            callerFunc = mmddGuiMorphKeyframe
            callerParam = iniController
        elif et.expCC == iniController:
            callerFunc = mmddGuiCameraKeyframe
            callerParam = None
        else:
            print "Unexpected initialize controller!?"
            callerFunc = mmddGuiMain
            callerParam = None
        del game.scenedata.exporttemp
        mmddGuiSwitch(game, callerFunc, callerParam)
    def selectFile():
        guiOpenFileDialog(doExpData, iniController.vmdFilePath, False, "VMD")

    from vmdlib import MotionController, MorphController, CameraController
    if isinstance(iniController, MotionController):
        linkedMp = None
        for mp in mmdd.morphControllers.values():
            if mp.motionCtrl == iniController:
                linkedMp = mp
                break
        if linkedMp:
            guiSelectDialog(game, (LC("Export motion keyframes only or with morph keyframes?"), ("Motion Only", expMotion), ("With Morph", expMotionMorph), ("Cancel", None)))
        else:
            expMotion(game)
    elif isinstance(iniController, MorphController):
        guiSelectDialog(game, (LC("Export morph keyframes only or with motion keyframes?"), ("Morph Only", expMorph), ("With Motion", expMorphMotion), ("Cancel", None)))
    elif isinstance(iniController, CameraController):
        et.expCC = iniController
        selectFile()
    else:
        raise Exception("Unexported initialize controller: " + str(iniController))

# ====================================  help function ====================================
def colorText(text, color = "ffffff"):
    return "<color=#" + color + ">" + text + "</color>"
def redText(text):
    return colorText(text, "ff0000")
def greenText(text):
    return colorText(text, "00ff00")
def blueText(text):
    return colorText(text, "0000ff")
def yellowText(text):
    return colorText(text, "ffff00")
def orangeText(text):
    return colorText(text, "ffa500")
def cyanText(text):
    return colorText(text, "00ffff")
def magentaText(text):
    return colorText(text, "ff00ff")
def greyText(text):
    return colorText(text, "555555")

# find and set a folder under parent folder.
# if only need get a folder, set newContent = None
# if only create new folder, set toFind = None
def getsetFolder(parent, toFind, newContent):
    # search for old folder
    foundTno = None
    if toFind:
        # setup search range
        if parent:
            searchRange = parent.treeNodeObject.child
        else:
            from Studio import Studio
            searchRange = Studio.Instance.treeNodeCtrl.m_TreeNodeObject
        for tno in searchRange:
            if tno.textName.startswith(toFind):
                foundTno = tno
                break
    # create or update
    if newContent:
        if foundTno:
            # update
            fold = HSNeoOCIFolder.create_from_treenode(foundTno)
            if fold.name != newContent:
                fold.name = newContent
            return fold
        else:
            # create
            fold = HSNeoOCIFolder.add(newContent)
            if parent:
                fold.set_parent(parent)
            return fold
    else:
        if foundTno:
            # found
            fold = HSNeoOCIFolder.create_from_treenode(foundTno)
            return fold
        else:
            return None

# save a struct to folder/sub-folder
# start with a dict and a name
# if member of dict is dict, repeat same operation
# if member of dict is list or tuple, clear its child and re-create all child, child must be string!
# if member of dict is str, save as a single setting
def saveToFolder(parent, name, setting):
    if isinstance(setting, dict):
        sfolder = getsetFolder(parent, "-" + name, "-" + name)
        sfolder.delete_all_children()
        for skey in setting.keys():
            saveToFolder(sfolder, skey, setting[skey])
    elif isinstance(setting, list) or isinstance(setting, tuple):
        sfolder = getsetFolder(parent, "-" + name + ":", "-" + name + ":")
        sfolder.delete_all_children()
        for ss in setting:
            getsetFolder(sfolder, None, str(ss))
    elif isinstance(setting, str):
        getsetFolder(parent, name + ":", name + ":" + setting)
    else:
        raise Exception("Unknown setting type: %s"%str(setting))

# load a struct from folder/sub-folder to a dict
def loadFromFolder(parent, setting):
    if parent.name.startswith("-"):
        if parent.name.endswith(":"):
            # list
            pname = parent.name[1:-1]
            setting[pname] = []
            for subtno in parent.treeNodeObject.child:
                setting[pname].append(subtno.textName)
        else:
            # dict
            pname = parent.name[1:]
            setting[pname] = {}
            for subtno in parent.treeNodeObject.child:
                subfld = HSNeoOCIFolder.create_from_treenode(subtno)
                loadFromFolder(subfld, setting[pname])
    else:
        # string
        pname, pvalue = parent.name.split(":", 1)
        setting[pname] = pvalue

# change c# dictionary to python dict
def dict_cs2py(orgData):
    from System.Collections.Generic import Dictionary
    if isinstance(orgData, Dictionary[object, object]):
        newData = dict(orgData)
        for key in newData.keys():
            newData[key] = dict_cs2py(newData[key])
        return newData
    return orgData

# save data to PNG file
def saveToPNG(dataSet, dataKey, data, version=1):
    # save extended data
    from extplugins import ExtensibleSaveFormat
    esfPlugin = ExtensibleSaveFormat()
    if esfPlugin.isDetected:
        esfPlugin.loadExtendSaveData(dataSet)
        esfPlugin.setExtendSaveData(dataKey, data)
        esfPlugin.setExtendSaveVersion(version)
        esfPlugin.saveExtendSaveData(dataSet)
        return True
    else:
        return False

# load data from PNG file
# check version if setted
def loadFromPNG(dataSet, dataKey, version=None):
    # load extended data
    from extplugins import ExtensibleSaveFormat
    esfPlugin = ExtensibleSaveFormat()
    if esfPlugin.isDetected:
        esfPlugin.loadExtendSaveData(dataSet)
        if version != None and esfPlugin.getExtendSaveVersion() != version:
            return None
        data = esfPlugin.getExtendSaveData(dataKey)
        return dict_cs2py(data)
    else:
        return None

# clear data set from PNG file
def clearFromPNG(dataSet):
    # save extended data
    from extplugins import ExtensibleSaveFormat
    esfPlugin = ExtensibleSaveFormat()
    if esfPlugin.isDetected:
        esfPlugin.loadExtendSaveData(dataSet)
        esfPlugin.newExtendSaveData("")
        esfPlugin.saveExtendSaveData(dataSet)
        return True
    else:
        return False

# pre-load all setting data from folder
def preloadMultiSceneData_v1():
    # base folder
    loaded = []
    drootName = "-mmddirector"
    droot = getsetFolder(None, drootName, None)        
    if droot:
        for subtno in droot.treeNodeObject.child:
            sroot = HSNeoOCIFolder.create_from_treenode(subtno)
            setting = {}
            loadFromFolder(sroot, setting)
            sname = setting.keys()[0]
            ssetting = setting[sname]
            if ssetting.has_key("general") and ssetting["general"].has_key("time"):
                loaded.append((sname, ssetting["general"]["time"], ssetting, 1))
    return loaded

# pre-load all setting data
def preloadMultiSceneData():
    # load data from png
    from extplugins import ExtensibleSaveFormat
    esfPlugin = ExtensibleSaveFormat()
    if esfPlugin.isDetected:
        sData = esfPlugin.loadExtendSaveData("mmddirectorData")
        if sData.version >= 2:
            loaded = []
            for mode in sData.data.Keys:
                setting = dict_cs2py(esfPlugin.getExtendSaveData(mode))
                loaded.append((mode, setting["general"]["time"], setting, sData.version))
            return loaded
    return preloadMultiSceneData_v1()

# check auto load target data name, may be "save" or "backup" or None
def checkAutoLoadTarget():
    allData = preloadMultiSceneData()
    setname = [ad[0] for ad in allData]
    if "save" in setname:
        return "save"
    elif "backup" in setname:
        return "backup"
    else:
        return None

# check miss file in all saved data
def checkSaveData():
    missFiles = {}
    def addMissFile(fn, dataname, useas):
        if fn in missFiles:
            mfinfo = missFiles[fn]
            if dataname not in mfinfo[0]:
                mfinfo[0].append(dataname)
            if useas not in mfinfo[1]:
                mfinfo[1].append(useas)
        else:
            missFiles[fn] = [[dataname,], [useas,], None]
    allData = preloadMultiSceneData()
    for data in allData:
        dataName = data[0]
        setting = data[2]
        for aid in setting["motion"]:   # motion file
            fn = setting["motion"][aid]["filename"]
            if not os.path.isfile(fn) and not setting.has_key("keyframes"):
                addMissFile(fn, dataName, LC("motion"))
        for aid in setting["morph"]:    # morph file
            fn = setting["morph"][aid]["filename"]
            if not os.path.isfile(fn) and not setting.has_key("keyframes"):
                addMissFile(fn, dataName, LC("morph"))
        if "filename" in setting["camera"]:
            fn = setting["camera"]["filename"]
            if not os.path.isfile(fn) and not setting.has_key("keyframes"):
                addMissFile(fn, dataName, LC("camera"))
        if "filename" in setting["sound"]:
            fn = setting["sound"]["filename"]
            if not os.path.isfile(fn) and not setting.has_key("keyframes"):
                addMissFile(fn, dataName, LC("sound"))
    return missFiles

# correct miss file in all saved data
def correctSaveData(replaceFiles):
    def replaceFile_v1(path, newFilename):
        dtgt = getsetFolder(None, "-mmddirector", None)
        for pitem in path:
            dtgt = getsetFolder(dtgt, pitem, None)
            if not dtgt:
                return
        getsetFolder(dtgt, "filename:", "filename:" + newFilename)

    allData = preloadMultiSceneData()
    for data in allData:
        resaveV2 = False
        dataName = "-" + data[0]
        setting = data[2]
        version = data[3]
        for aid in setting["motion"]:   # motion file
            fn = setting["motion"][aid]["filename"]
            if fn in replaceFiles:
                if version == 1:
                    replaceFile_v1((dataName, "-motion", "-" + aid), replaceFiles[fn])
                else:
                    setting["motion"][aid]["filename"] = replaceFiles[fn]
                    resaveV2 = True
        for aid in setting["morph"]:    # morph file
            fn = setting["morph"][aid]["filename"]
            if fn in replaceFiles:
                if version == 1:
                    replaceFile_v1((dataName, "-morph", "-" + aid), replaceFiles[fn])
                else:
                    setting["morph"][aid]["filename"] = replaceFiles[fn]
                    resaveV2 = True
        if "filename" in setting["camera"]:
            fn = setting["camera"]["filename"]
            if fn in replaceFiles:
                if version == 1:
                    replaceFile_v1((dataName, "-camera"), replaceFiles[fn])
                else:
                    setting["camera"]["filename"] = replaceFiles[fn]
                    resaveV2 = True
        if "filename" in setting["sound"]:
            fn = setting["sound"]["filename"]
            if fn in replaceFiles:
                if version == 1:
                    replaceFile_v1((dataName, "-sound"), replaceFiles[fn])
                else:
                    setting["sound"]["filename"] = replaceFiles[fn]
                    resaveV2 = True
        if resaveV2:
            saveToPNG("mmddirectorData", data[0], setting, 2)

# ======================================  wrap class =====================================
class PluginMotionController():
    def __init__(self, actor, filename, length, controller):
        self.actor = actor
        self.filename = filename
        self.length = length
        self.controller = controller
        self.enable = True
        self.delay = 0.0

    @property
    def maxFrameNo(self):
        return int(self.length * 30)

    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.maxFrameNo

class PluginCameraController():
    def __init__(self, filename, length):
        self.filename = filename
        self.length = length
        self.enable = True
        self.delay = 0.0

    @property
    def maxFrameNo(self):
        return int(self.length * 30)

    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.maxFrameNo

class PluginSoundController():
    def __init__(self, filename, length):
        self.filename = filename
        self.length = length
        self.enable = True
        self.delay = 0.0

    @property
    def maxFrameNo(self):
        return int(self.length * 30)

    @property
    def TotalFrameLength(self):
        return int(self.delay * 30.0) + self.maxFrameNo


# ======================================  main class =====================================
class MMDDirector():
    @property
    def MaxFrameNo(self):
        mf = 0
        # motion
        for mc in self.motionControllers.values():
            if mc.TotalFrameLength > mf: mf = mc.TotalFrameLength
        # morph
        for mc in self.morphControllers.values():
            if mc.TotalFrameLength > mf: mf = mc.TotalFrameLength
        # camera
        if self.cameraController:
            if self.cameraController.TotalFrameLength > mf: mf = self.cameraController.TotalFrameLength
        # sound
        if self.soundController:
            if self.soundController.TotalFrameLength > mf: mf = self.soundController.TotalFrameLength
        # clip
        for cc in self.clipControllers:
            if cc.TotalFrameLength > mf: mf = cc.TotalFrameLength
        # vmd plugin
        for vc in self.vmdControllers.values():
            if vc.TotalFrameLength > mf: mf = vc.TotalFrameLength
        if self.vmdCameraCtrl:
            if self.vmdCameraCtrl.TotalFrameLength > mf: mf = self.vmdCameraCtrl.TotalFrameLength
        if self.vmdSoundCtrl:
            if self.vmdSoundCtrl.TotalFrameLength > mf: mf = self.vmdSoundCtrl.TotalFrameLength
        return mf

    @property
    def StartFrame(self):
        if self.customStartFrame == -1:
            return 0
        else:
            return self.customStartFrame

    def setStartFrame(self, frameNo):
        frameNo = int(frameNo)
        if 0 <= frameNo <= self.EndFrame:
            self.customStartFrame = frameNo
        else:
            self.customStartFrame = -1

    @property
    def EndFrame(self):
        if self.customEndFrame == -1:
            return self.totalFrame
        else:
            return self.customEndFrame

    def setEndFrame(self, frameNo):
        frameNo = int(frameNo)
        if self.StartFrame <= frameNo <= self.totalFrame:
            self.customEndFrame = frameNo
        else:
            self.customEndFrame = -1

    @property
    def isPlaying(self):
        return self.tid != -1

    def __init__(self, game):
        # runtime
        self.game = game
        """:type :vngameengine.VNNeoController"""

        # general setting
        self.lastVmdFilename = ""
        self.lastWavFilename = ""
        self.playTimer = "record"
        self.customStartFrame = -1
        self.customEndFrame = -1
        self.playFPS = 30.0

        # controllers
        self.motionControllers = {}
        self.morphControllers = {}
        self.cameraController = None
        self.soundController = None
        self.clipControllers = []
        self.vmdControllers = {}
        self.vmdSoundCtrl = None
        self.vmdCameraCtrl = None
        self.bindControllers = []
        self.parentControllers = []

        # runtime
        self.motionSequence = []
        self.curFrame = 0
        self.totalFrame = 0
        self.tid = -1
        self.onStartFunc = None
        self.onStopFunc = None
        self.loop = False

        # ext plugins
        import extplugins
        # load CharaAnime plugin
        self.charaAnimePlugin = extplugins.CharaAnime()
        if not self.charaAnimePlugin.isDetected:
            self.charaAnimePlugin = None
        # load VMD plugin
        if self.game.isNEOV2:
            # try load HS2 plugin
            self.vmdPlugin = extplugins.HS2VMDPlayPlugin()
            if not self.vmdPlugin.isDetected:
                # try load AI plugin
                self.vmdPlugin = extplugins.AIVMDPlugin()
                if not self.vmdPlugin.isDetected:
                    self.vmdPlugin = None
        elif self.game.isCharaStudio:
            # try load KK plugin
            self.vmdPlugin = extplugins.KKVMDPlayPlugin()
            if not self.vmdPlugin.isDetected:
                self.vmdPlugin = None
        else:
            raise Exception("MMDDirector run in an unsupported studio <%s>!?"%self.game.engine_name)
    
    def exportSetting(self):
        def saveto(dic, key, setvalue):
            dic[key] = setvalue
        # export target
        setting = {}
        # general
        setting["general"] = {}
        saveto(setting["general"], "version", "1")
        saveto(setting["general"], "time", time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()))
        saveto(setting["general"], "lastVmdFile", self.lastVmdFilename)
        saveto(setting["general"], "lastWavFile", self.lastWavFilename)
        saveto(setting["general"], "customStartFrame", str(self.customStartFrame))
        saveto(setting["general"], "customEndFrame", str(self.customEndFrame))
        saveto(setting["general"], "playTimer", self.playTimer)
        saveto(setting["general"], "loop", str(self.loop))

        # motion
        setting["motion"] = {}
        for aid in self.motionSequence:
            mc = self.motionControllers[aid]
            setting["motion"][aid] = mc.exportSetting()
        saveto(setting["general"], "motion_order", script2string(self.motionSequence))
        # morph
        setting["morph"] = {}
        for aid in self.morphControllers.keys():
            mc = self.morphControllers[aid]
            setting["morph"][aid] = mc.exportSetting()
        # camera
        if self.cameraController:
            setting["camera"] = self.cameraController.exportSetting()
        else:
            setting["camera"] = {}
        # sound
        if self.soundController:
            setting["sound"] = self.soundController.exportSetting()
        else:
            setting["sound"] = {}
        # clip
        setting["clip"] = []
        for cc in self.clipControllers:
            setting["clip"].append(cc.exportSetting())
        # plugin
        setting["plugin"] = {}
        # bind
        setting["bind"] = []
        for bc in self.bindControllers:
            setting["bind"].append(bc.exportSetting())
        # parent
        setting["parent"] = {}
        for pc in self.parentControllers:
            setting["parent"][pc.Key] = pc.exportSetting()
        parent_order = script2string([pc.Key for pc in self.parentControllers])
        saveto(setting["general"], "parent_order", parent_order)

        # Done
        return setting

    def importSetting(self, setting):
        def getActorProp(aid):
            actor = self.game.scenef_get_actor(aid)
            if actor == None:
                actor = self.game.scenef_get_propf(aid)
            return actor
        def tryload(dic, key, defvalue):
            try:
                if dic.has_key(key):
                    if isinstance(defvalue, bool):
                        return dic[key].lower() == "true" or dic[key] == "1"
                    elif isinstance(defvalue, int):
                        return int(dic[key])
                    elif isinstance(defvalue, float):
                        return float(dic[key])
                    elif isinstance(defvalue, str):
                        return dic[key]
                    elif isinstance(defvalue, dict) or isinstance(defvalue, list) or isinstance(defvalue, tuple):
                        return eval(dic[key])
                    else:
                        raise Exception("Unsupported data type '%s'"%type(defvalue))
                else:
                    raise Exception("key [%s] not found in source dict."%key)
            except Exception, e:
                print "Fail to load [%s]: %s"%(key, str(e)) 
                return defvalue
        def enableAndCheck(param):
            mcs = param
            for mc in mcs:
                if not mc.Enable:
                    mc.setEnable(True)
                else:
                    mcs.remove(mc)
            if len(mcs) > 0:
                self.game.updFuncParam = mcs
                self.game.updFunc = enableAndCheck
        try:
            # general
            version = tryload(setting["general"], "version", "1")
            self.lastVmdFilename = tryload(setting["general"], "lastVmdFile", self.lastVmdFilename)
            self.lastWavFilename = tryload(setting["general"], "lastWavFile", self.lastWavFilename)
            self.customStartFrame = tryload(setting["general"], "customStartFrame", self.customStartFrame)
            self.customEndFrame = tryload(setting["general"], "customEndFrame", self.customEndFrame)
            self.playTimer = tryload(setting["general"], "playTimer", self.playTimer)
            self.loop = tryload(setting["general"], "loop", self.loop)

            from vmdlib import MotionController, MorphController, CameraController, SoundController, ClipController, BindController, ParentController
            vmdFrameBuffer = {}
            toActiveMC = set()
            # motion with order
            for aid in setting["motion"]:
                actor = getActorProp(aid)
                if actor == None:
                    raise Exception("Can't get actor id = %s"%aid)
                # restore motion controller
                mc = MotionController.parseFromSetting(setting["motion"][aid], actor, vmdFrameBuffer)
                self.postProcessForMotionImport(actor)
                self.registerMotionController(aid, mc)
                if mc.NeedActiveMotion:
                    toActiveMC.add(mc)
            self.motionSequence = tryload(setting["general"], "motion_order", self.motionSequence)
            # morph
            for aid in setting["morph"]:
                actor = getActorProp(aid)
                if actor == None:
                    raise Exception("Can't get actor id = %s"%aid)
                # restore morph controller
                if aid in self.motionControllers:
                    param = self.motionControllers[aid]
                else:
                    param = actor
                mc = MorphController.parseFromSetting(setting["morph"][aid], param, vmdFrameBuffer)
                self.morphControllers[aid] = mc
                if mc.NeedActiveMotion:
                    toActiveMC.add(mc.motionCtrl)
            # camera
            if len(setting["camera"]) > 0:
                cc = CameraController()
                cc.importSetting(setting["camera"])
                self.cameraController = cc
            # sound
            if len(setting["sound"]) > 0:
                sc = SoundController()
                sc.importSetting(setting["sound"])
                self.soundController = sc
            # clip
            for ccs in setting["clip"]:
                cc = ClipController()
                if isinstance(ccs, str):
                    ccs = eval(ccs)
                else:
                    ccs = dict_cs2py(ccs)
                cc.importSetting(ccs, self.game)
                if not cc.clip:
                    raise Exception("Fail to import and bind clip %s"%cc.Id)
                for ecc in self.clipControllers:
                    if ecc.Id == cc.Id:
                        raise Exception("ClipController Id duplicated! %s"%cc.Id)
                self.clipControllers.append(cc)
            # bind
            if setting.has_key("bind"):
                for bcs in setting["bind"]:
                    bc = BindController()
                    if isinstance(bcs, str):
                        bcs = eval(bcs)
                    else:
                        bcs = dict_cs2py(bcs)
                    bc.importSetting(bcs)
                    if not bc.Valid:
                        print "Fail to import bind setting: %s"%bcs
                        continue
                    self.bindControllers.append(bc)
            # parent with order
            temp_pcs = []
            if setting.has_key("parent"):
                order = []
                for pKey in setting["parent"]:
                    pc = ParentController.parseFromSetting(setting["parent"][pKey], self.game, self.motionControllers)
                    if not pc.Valid:
                        raise Exception("Fail to import external parent setting")
                    temp_pcs.append(pc)
            parent_order = tryload(setting["general"], "parent_order", [])
            if len(temp_pcs) and len(parent_order) == len(temp_pcs):
                for pKey in parent_order:
                    for pc in temp_pcs:
                        if pc.Key == pKey:
                            self.parentControllers.append(pc)
                            break
                if len(self.parentControllers) != len(parent_order):
                    raise Exception("Parent controllers setting not match with parent order setting")
            else:
                if len(temp_pcs): print "WARNING: parent control order missed"
                self.parentControllers = temp_pcs
            # all green
            if len(toActiveMC) > 0:
                self.game.updFuncParam = toActiveMC
                self.game.updFunc = enableAndCheck
            err = None
        except Exception, e:
            traceback.print_exc()
            err = "Fail to import setting: " + str(e)
            print err
        self.refreshTotalFrame()
        self.curFrame = 0
        self.update()
        return err

    def saveToScene_v1(self, mode):
        # base folder
        drootName = "-mmddirector"
        droot = getsetFolder(None, drootName, drootName)
        # save setting
        setting = self.exportSetting()
        saveToFolder(droot, mode, setting)
        self.saveExtData(mode)

    def saveToScene(self, mode):
        # save to PNG
        setting = self.exportSetting()
        if not saveToPNG("mmddirectorData", mode, setting, 2):
            raise Exception("Fail to save data!")

    def saveExtData(self, mode):
        # save extended data
        from extplugins import ExtensibleSaveFormat
        esfPlugin = ExtensibleSaveFormat()
        if esfPlugin.isDetected:
            esfPlugin.loadExtendSaveData("mmddirectorExtData")
            for aid in self.motionSequence:
                mc = self.motionControllers[aid]
                bkey = "%s:motion:%s:%s:baked"%(mode, aid, mc.FileName)
                if mc.Baked:
                    bdata = mc.exportBakedData()
                    esfPlugin.setExtendSaveData(bkey, bdata)
                else:
                    esfPlugin.delExtendSaveData(bkey)
            esfPlugin.setExtendSaveVersion(1)
            esfPlugin.saveExtendSaveData("mmddirectorExtData")

    def loadFromScene_v1(self, mode):
        # base folder
        drootName = "-mmddirector"
        droot = getsetFolder(None, drootName, None)
        if droot:
            sroot = getsetFolder(droot, "-" + mode, None)
            if sroot:
                setting = {}
                loadFromFolder(sroot, setting)
                err = self.importSetting(setting[mode])
                if not err:
                    self.loadExtData_v1(mode)
                return err
            else:
                return "Selected save folder '-mmddirector/%s' not found!"%mode
        else:
            return "MMD Director base save folder '-mmddirector' not found!"

    def loadExtData_v1(self, mode):
        # load extended data
        for aid in self.motionSequence:
            mc = self.motionControllers[aid]
            bkey = "%s:motion:%s:%s:baked"%(mode, aid, mc.FileName)
            bdata = loadFromPNG("mmddirectorExtData", bkey)
            if bdata:
                mc.importBakedData(bdata)
        self.update()

    def loadFromScene(self, mode):
        # load data from png
        setting = loadFromPNG("mmddirectorData", mode)
        if setting:
            err = self.importSetting(setting)
        else:
            # try load from folder
            print "Try load from folder (MMDD v1 save data)"
            err = self.loadFromScene_v1(mode)
        return err

    def getAllControllerList(self):
        return self.getVmdLibControllerList() + self.getPluginControllerList()

    def getVmdLibControllerList(self):
        cl = []
        # motion and paired morph
        for aid in self.motionSequence:
            mc = self.motionControllers[aid]
            cl.append(mc)
            if self.morphControllers.has_key(aid):
                mc = self.morphControllers[aid]
                cl.append(mc)
        # unpaired morph
        for aid in self.morphControllers.keys():
            mc = self.morphControllers[aid]
            if mc not in cl:
                cl.append(mc)
        # camera
        if self.cameraController:
            cl.append(self.cameraController)
        # sound
        if self.soundController:
            cl.append(self.soundController)
        # clip
        for cc in self.clipControllers:
            cl.append(cc)
        return cl

    def getPluginControllerList(self):
        cl = []
        # plugin vmd motion and morph
        for actor in self.vmdControllers.keys():
            mc = self.vmdControllers[actor]
            cl.append(mc)
        # plugin vmd camera
        if self.vmdCameraCtrl:
            cl.append(self.vmdCameraCtrl)
        # plugin vmd sound
        if self.vmdSoundCtrl:
            cl.append(self.vmdSoundCtrl)
        return cl

    def loadVmdPluginAsset(self):
        def getFilename(filePath):
            if filePath != None and len(filePath) > 0:
                from os import path
                return path.split(filePath)[1]
            else:
                return ""

        if self.vmdPlugin:
            # build a list of current actors
            allActor = []
            for f in self.game.scene_get_all_females():
                allActor.append(f.as_actor)
            for m in self.game.scene_get_all_males():
                allActor.append(m.as_actor)            
            # vmd ctrls
            for vc in self.vmdPlugin.MMDGetAllVmdController():
                info = self.vmdPlugin.MMDGetVmdInfo(vc)
                if not info["Loaded"]:
                    continue
                for actor in allActor:
                    if info["ChaControl"] == actor.objctrl.charInfo:
                        self.vmdControllers[actor] = PluginMotionController(actor, getFilename(info["FilePath"]), info["Length"], vc)
                        break
            # camera
            info = self.vmdPlugin.MMDGetCameraInfo()
            if info and info["Loaded"]:
                self.vmdCameraCtrl = PluginCameraController(getFilename(info["FilePath"]), info["Length"])
            # sound
            info = self.vmdPlugin.MMDGetSoundInfo()
            if info and info["Loaded"]:
                self.vmdSoundCtrl = PluginSoundController(getFilename(info["FilePath"]), info["Length"])
        self.refreshTotalFrame()

    def vmdPluginAction(self, action):
        if not self.vmdPlugin:
            return
        curTime = self.curFrame / self.playFPS
        for vc in self.vmdControllers.values():
            if vc.enable:
                if action == "sync":
                    syncTime = curTime - vc.delay
                    if syncTime > 0:
                        self.vmdPlugin.MMDSyncVmd(vc.controller, syncTime)
                    else:
                        self.vmdPlugin.MMDSyncVmd(vc.controller, 0)
                elif action == "play":
                    syncTime = curTime - vc.delay
                    if syncTime >= 0:
                        self.vmdPlugin.MMDPlayVmd(vc.controller)
                elif action == "pause":
                    self.vmdPlugin.MMDPauseVmd(vc.controller)
                elif action == "stop":
                    self.vmdPlugin.MMDStopVmd(vc.controller)
                else:
                    raise Exception("Unknown action [%s]"%action)
        if self.vmdCameraCtrl and self.vmdCameraCtrl.enable:
            if action == "sync":
                syncTime = curTime - self.vmdCameraCtrl.delay
                if syncTime > 0:
                    self.vmdPlugin.MMDSyncCamera(syncTime)
                else:
                    self.vmdPlugin.MMDSyncCamera(0)
            elif action == "play":
                syncTime = curTime - self.vmdCameraCtrl.delay
                if syncTime >= 0:
                    self.vmdPlugin.MMDPlayCamera()
            elif action == "pause":
                self.vmdPlugin.MMDPauseCamera()
            elif action == "stop":
                self.vmdPlugin.MMDStopCamera()
            else:
                raise Exception("Unknown action [%s]"%action)
        if self.vmdSoundCtrl and self.vmdSoundCtrl.enable:
            if action == "sync":
                syncTime = curTime - self.vmdSoundCtrl.delay
                if syncTime > 0:
                    self.vmdPlugin.MMDSyncSound(syncTime)
                else:
                    self.vmdPlugin.MMDSyncSound(0)
            elif action == "play":
                syncTime = curTime - self.vmdSoundCtrl.delay
                if syncTime >= 0:
                    self.vmdPlugin.MMDPlaySound()
            elif action == "pause":
                self.vmdPlugin.MMDPauseSound()
            elif action == "stop":
                self.vmdPlugin.MMDStopSound()
            else:
                raise Exception("Unknown action [%s]"%action)

    def refreshTotalFrame(self):
        self.totalFrame = self.MaxFrameNo
        if self.curFrame > self.totalFrame:
            self.curFrame = self.totalFrame

    def autoRegist(self, actor):
        from vnactor import Actor, Prop
        if isinstance(actor, Actor):
            return self.autoRegistActor(actor)
        elif isinstance(actor, Prop):
            return self.autoRegistProp(actor)
        else:
            raise Exception("actor <%s> is not an Actor nor a Prop"%actor)

    def autoRegistActor(self, actor):
        for aid in self.game.scenef_get_all_actors():
            if actor.objctrl == self.game.scenef_get_actor(aid).objctrl:
                # already registed actor
                return aid
        # create a new id
        i = 0
        aid = "act" + str(i)
        while aid in self.game.scenef_get_all_actors():
            i += 1
            aid = "act" + str(i)
        # create tag folder
        tagfld = HSNeoOCIFolder.add("-actor:"+aid)
        tagfld.set_parent_treenodeobject(actor.treeNodeObject.child[0].child[0])
        # regist new one
        self.game.scenef_register_actorsprops()
        print "Actor %s auto registed as %s"%(actor.text_name, aid)
        return aid

    def autoRegistProp(self, actor):
        for aid in self.game.scenef_get_all_props():
            if actor.objctrl == self.game.scenef_get_propf(aid).objctrl:
                # already registed prop
                return aid
        # create a new id
        i = 0
        aid = "prp" + str(i)
        while aid in self.game.scenef_get_all_props():
            i += 1
            aid = "prp" + str(i)
        # create tag folder
        tagfld = HSNeoOCIFolder.add("-prop:"+aid)
        tagfld.set_parent_treenodeobject(actor.treeNodeObject)
        # regist new one
        self.game.scenef_register_actorsprops()
        print "Prop %s auto registed as %s"%(actor.text_name, aid)
        return aid

    def createMotionController(self, actor, boneDefine = None):
        def enableAndCheck(param):
            mc = param
            if not mc.Enable:
                mc.setEnable(True)
                self.game.updFuncParam = param
                self.game.updFunc = enableAndCheck
        try:
            # get old controller and delete it if exist
            omc = self.getMotionController(actor)
            if omc:
                print "MMDD WARNING: Auto delete existed motion controller to create new one"
                self.deleteMotionController(omc)
            # creating
            from vmdlib import MotionController
            aid = self.autoRegist(actor)
            mc = MotionController(boneDefine)
            mc.bindChara(actor)
            mc.setEnable(True)
            self.registerMotionController(aid, mc)
            self.game.updFuncParam = mc
            self.game.updFunc = enableAndCheck
            return mc
        except Exception, e:
            err = "Fail to create motion controller for %s: %s"%(actor, str(e))
            raise Exception(err)
            return None

    def registerMotionController(self, aid, mctrl):
        if aid in self.motionControllers or aid in self.motionSequence:
            raise Exception("Motion controller for [%s] is already existed!?"%aid)
        else:
            self.motionControllers[aid] = mctrl
            self.motionSequence.append(aid)

    def getMotionController(self, tgt):
        # motion controller can be found by id or actor
        if tgt in self.motionControllers:
            return self.motionControllers[tgt]
        elif isinstance(tgt, str):
            return None # can not go on if tgt is str
        else:
            # search as a actor/prop object
            actor = tgt
            for aid in self.motionControllers.keys():
                if aid in self.game.scenef_get_all_actors() and actor.objctrl == self.game.scenef_get_actor(aid).objctrl:
                    return self.motionControllers[aid]
                if aid in self.game.scenef_get_all_props() and actor.objctrl == self.game.scenef_get_propf(aid).objctrl:
                    return self.motionControllers[aid]
            return None

    def deleteMotionController(self, tgt):
        if tgt in self.motionControllers:
            tgtid = tgt
        else:
            tgtid = None
            try:
                for aid in self.motionSequence:
                    if self.motionControllers[aid] == tgt:
                        tgtid = aid
                        break
            except:
                traceback.print_exc()
        if tgtid:
            mc = self.motionControllers[tgtid]
            mc.cleanup()
            del self.motionControllers[tgtid]
            self.motionSequence.remove(tgtid)
            # delete linked morph controller 
            if tgtid in self.morphControllers:
                self.deleteMorphController(tgtid)
            else:
                self.refreshTotalFrame()
        else:
            print "Motion controller %s not found!"%(str(tgt))

    def createMorphController(self, actor):
        try:
            # get and delete old one
            omp = self.getMorphController(actor)
            if omp:
                print "MMDD WARNING: Auto delete existed morph controller to create new one"
                self.deleteMorphController(omp)
            # create if not found, auto regist if not yet
            from vmdlib import MorphController
            aid = self.autoRegist(actor)
            if aid in self.motionControllers:
                lmc = self.motionControllers[aid]
            else:
                lmc = None
            mc = MorphController(lmc)
            mc.bindChara(actor)
            self.morphControllers[aid] = mc
            return mc
        except Exception, e:
            err = "Fail to create morph controller for %s: %s"%(actor, str(e))
            raise Exception(err)
            return None

    def getMorphController(self, tgt):
        # morph controller can be found by id or actor
        if tgt in self.morphControllers:
            return self.morphControllers[tgt]
        elif isinstance(tgt, str):
            return None # can not go on if tgt is str
        else:
            # search as a actor
            actor = tgt
            for aid in self.morphControllers.keys():
                if aid in self.game.scenef_get_all_actors() and actor.objctrl == self.game.scenef_get_actor(aid).objctrl:
                    return self.morphControllers[aid]
                if aid in self.game.scenef_get_all_props() and actor.objctrl == self.game.scenef_get_propf(aid).objctrl:
                    return self.morphControllers[aid]
            return None

    def deleteMorphController(self, tgt):
        if tgt in self.morphControllers:
            tgtid = tgt
        else:
            tgtid = None
            try:
                for aid in self.morphControllers.keys():
                    if self.morphControllers[aid] == tgt:
                        tgtid = aid
                        break
            except:
                pass
        if tgtid:
            mc = self.morphControllers[tgtid]
            mc.cleanup()
            del self.morphControllers[tgtid]
            self.refreshTotalFrame()
        else:
            print "Morph controller %s not found!"%(str(tgt))

    def loadCameraKeyframes(self, filepath, cameraKeyframes):
        from vmdlib import CameraController
        self.cameraController = CameraController(filepath, cameraKeyframes)

    def loadWavFile(self, filepath):
        if self.soundController:
            self.soundController.cleanup()
        from vmdlib import SoundController
        self.soundController = SoundController()
        err = self.soundController.loadWavFile(filepath)
        if err == None:
            self.lastWavFilename = filepath
        return err

    def deleteClipController(self, tgt):
        for i in range(len(self.clipControllers)):
            if self.clipControllers[i].Id == tgt.Id:
                cc = self.clipControllers.pop(i)
                self.refreshTotalFrame()
                return
        print "Clip controller %s@%d not found!"%(tgt.clipName, clip.linkFrame)

    def deletePluginMotionController(self, tgt):
        for aid in self.vmdControllers.keys():
            if self.vmdControllers[aid] == tgt:
                del self.vmdControllers[aid]
                self.refreshTotalFrame()
                return
        print "VMD plugin motion controller %s not found!"%(str(tgt))

    def deleteBindController(self, tgt):
        if tgt in self.bindControllers:
            tgt.cleanup()
            self.bindControllers.remove(tgt)

    def deleteParentController(self, tgt):
        if tgt in self.parentControllers:
            tgt.cleanup()
            self.parentControllers.remove(tgt)

    def postProcessForMotionImport(self, actor):
        from vnactor import Actor
        if isinstance(actor, str):
            actor = self.game.scenef_get_actor(actor)
        if not isinstance(actor, Actor):
            #print "Motion Import Post Process SKIPPED: Input is not an actor or actor's id"
            return

        # for HS2/AI
        if self.game.isNEOV2:
            def enableDynBone(boneKind):
                tb = actor.objctrl.charInfo.GetDynamicBoneBustAndHip(boneKind)
                tb.enabled = False
                tb.enabled = True
            from AIChara import ChaControlDefine
            enableDynBone(ChaControlDefine.DynamicBoneKind.BreastL)
            enableDynBone(ChaControlDefine.DynamicBoneKind.BreastR)
            enableDynBone(ChaControlDefine.DynamicBoneKind.HipL)
            enableDynBone(ChaControlDefine.DynamicBoneKind.HipR)

        #print "Motion Import Post Process for %s: DONE!"%(actor.text_name)

    def refreshBind(self):
        for bc in self.bindControllers:
            bc.setFrame(self.curFrame)

    def clearBoneGizmos(self):
        if self.charaAnimePlugin:
            self.charaAnimePlugin.GizmosMgr.ClearGizmos()

    def setPlayFPS(self, playFPS):
        self.playFPS = playFPS
        if self.soundController:
            self.soundController.setSpeed(playFPS / 30.0)

    def verify(self):
        for mc in self.motionControllers.values():
            mc.verify()
        for mp in self.morphControllers.values():
            mp.verify()
        for bc in self.bindControllers:
            if not bc.verify():
                #self.bindControllers.remove(bc)
                return False
        for pc in self.parentControllers:
            if not pc.verify(self.game, self.motionControllers):
                #self.parentControllers.remove(pc)
                return False
        return True

    def cleanup(self, excludeBindClip = False):
        # stop if playing
        self.stop()
        # delete controllers
        # bind
        if not excludeBindClip:
            for bc in self.bindControllers:
                bc.cleanup()
            self.bindControllers = []
        # parent
        if not excludeBindClip:
            for pc in self.parentControllers:
                pc.cleanup()
            self.parentControllers = []
        # morph
        for mc in self.morphControllers.values():
            mc.cleanup()
        self.morphControllers = {}
        # motion
        for mc in self.motionControllers.values():
            mc.cleanup()
        self.motionControllers = {}
        self.motionSequence = []
        # camera
        self.cameraController = None
        # sound
        if self.soundController:
            self.soundController.cleanup()
            self.soundController = None
        # clip
        if not excludeBindClip:
            self.clipControllers = []
        # ext plugin
        self.vmdControllers = {}
        self.vmdSoundCtrl = None
        self.vmdCameraCtrl = None        
        # all done
        self.refreshTotalFrame()

    def gotoFrame(self, frameNo):
        # this func only works when mmdd is stopped
        if self.isPlaying: return
        if frameNo < 0: frameNo = 0
        if frameNo > self.totalFrame: frameNo = self.totalFrame
        if frameNo != self.curFrame:
            self.curFrame = frameNo
            self.update()

    def gotoStartFrame(self):
        self.gotoFrame(self.StartFrame)

    def gotoEndFrame(self):
        self.gotoFrame(self.EndFrame)

    def gotoPrevFrame(self):
        self.gotoFrame(int(self.curFrame) - 1)

    def gotoNextFrame(self):
        self.gotoFrame(int(self.curFrame) + 1)

    def gotoPrevSecond(self):
        self.gotoFrame(int(self.curFrame) - 30)

    def gotoNextSecond(self):
        self.gotoFrame(int(self.curFrame) + 30)

    def startStop(self):
        if self.isPlaying:
            self.stop()
        else:
            self.start()

    def start(self):
        # check
        if self.isPlaying:
            print "!!! Animation has already started"
            return
        self.refreshTotalFrame()
        if self.totalFrame == 0 or self.StartFrame == self.EndFrame:
            print "!!! No frame to play! load mmd asset first."
            return
        if not self.verify():
            print "!!! Fail to verify controllers"
            return
        if self.curFrame < self.StartFrame or self.curFrame >= self.EndFrame:
            self.curFrame = self.StartFrame     # auto rewind
        self.timeOffset = self.curFrame / self.playFPS
        totalTime = (self.EndFrame - self.curFrame) / self.playFPS
        # sync
        self.update()
        # delay check
        self.delayStartNeeded = False
        for mc in self.getPluginControllerList():
            if mc.delay > self.timeOffset:
                self.delayStartNeeded = True
                break
        # setup multi target
        if self.cameraController and self.cameraController.enableMultiTarget:
            multiTgt = []
            for mcid in self.motionSequence:
                actor = self.game.scenef_get_actor(mcid)
                if actor:
                    multiTgt.append(actor)
            self.cameraController.setupMultiTarget(multiTgt)
        # start timer
        self.tid = self.game.set_timer(totalTime, self.playTimerEnd, self.playTimerUpdate)
        if self.tid == -1:
            raise Exception("!!! Fail to start anime timer.")
        else:
            if self.soundController:
                self.delayStartNeeded = self.delayStartNeeded or not self.soundController.setPlay(self.timeOffset)
            self.vmdPluginAction("play")    # for non-delay controller
            # fps counter
            self.startTime = time.clock()
            self.startTime2 = Time.time
            self.startTime3 = Time.unscaledTime
            self.updateTrack = 0
            # end flag
            self.stopByTimer = True
            # call start func
            if self.onStartFunc:
                self.onStartFunc(self.game)
            print "*** Animation started from frame #%d (%.2fs), total time %.2f"%(self.curFrame, self.timeOffset, totalTime)

    def stop(self):
        if self.isPlaying:
            self.stopByTimer = False
            self.game.clear_timer(self.tid, True)  # playTimerEnd will be called

    def playTimerUpdate(self, game, dt, ttime, duration):
        if self.playTimer == "record":
            ptime = ttime + self.timeOffset
        else:
            ptime = time.clock() - self.startTime + self.timeOffset
        fr = ptime * self.playFPS
        if fr > self.totalFrame : fr = self.totalFrame
        if fr >= 0 and self.curFrame != fr:
            self.curFrame = fr
            self.update()
        if self.delayStartNeeded:
            self.delayStartNeeded = False   # recheck delay start
            if self.soundController:
                self.delayStartNeeded = self.delayStartNeeded or not self.soundController.setPlay(ptime)
            self.vmdPluginAction("play")    # for delay
            for mc in self.getPluginControllerList():
                if mc.delay > ptime:
                    self.delayStartNeeded = True
                    break
        # fps counter
        self.updateTrack += 1
    
    def playTimerEnd(self, game):
        self.tid = -1
        if self.soundController:
            self.soundController.pause()
        self.vmdPluginAction("pause")
        # clear multi target
        if self.cameraController:
            self.cameraController.clearMultiTarget()
        # fps counter
        playedTime = time.clock() - self.startTime
        playedTime2 = Time.time - self.startTime2
        playedTime3 = Time.unscaledTime - self.startTime3
        # call stop func
        if self.onStopFunc:
            self.onStopFunc(self.game, self.stopByTimer)
        avgFps = float(self.updateTrack) / playedTime
        print "*** Animation Stopped! played %.3f, %.3f, %.3fsec, Avg FPS %.3f"%(playedTime, playedTime2, playedTime3, avgFps)
        # loop
        if self.stopByTimer and self.loop:
            self.start()

    def update(self):
        # parent control
        for pc in self.parentControllers:
            pc.setFrame(self.curFrame)
        # motion control
        for mcid in self.motionSequence:
            self.motionControllers[mcid].setFrame(self.curFrame)
        # morph control
        for mc in self.morphControllers.values():
            mc.setFrame(self.curFrame)
        # camera control
        if self.cameraController:
            self.cameraController.setFrame(self.curFrame)
        # sound control
        if not self.isPlaying and self.soundController:
            self.soundController.setFrame(self.curFrame)
        # clip control
        for cc in self.clipControllers:
            cc.setFrame(self.curFrame)
        # sync vmd plugin if not playing
        if not self.isPlaying:
            self.vmdPluginAction("sync")
        # bind control
        for bc in self.bindControllers:
            bc.setFrame(self.curFrame)
