476 lines
19 KiB
TypeScript
476 lines
19 KiB
TypeScript
import { FairyGUI, FairyEditor, System } from 'csharp';
|
||
import { $generic, $typeof } from 'puerts';
|
||
import { IConfig, IComponent, EComponent, EMode, IInspector } from './index';
|
||
|
||
const App = FairyEditor.App;
|
||
// 动效设置(参考 LanguageSettings 的存储方式)
|
||
const TweenSettings_1 = require("./TweenSettings");
|
||
|
||
class CustomAttributer extends FairyEditor.View.PluginInspector {
|
||
private list: FairyGUI.GList;
|
||
private components: IComponent[] = [];
|
||
private pattern: string = "*";
|
||
private parent: boolean = false;
|
||
private textMode: FairyGUI.GTextField;
|
||
private mode: EMode = EMode.WRITE;
|
||
private modeCtr: FairyGUI.Controller;
|
||
// private btn_save: FairyGUI.GButton;
|
||
// private btn_reset: FairyGUI.GButton;
|
||
private customData: string = "";
|
||
private customDataObj: {} = {};
|
||
|
||
public constructor(data: IInspector) {
|
||
super();
|
||
|
||
this.panel = FairyGUI.UIPackage.CreateObject("TweenAttributer", "Main").asCom;
|
||
// this.panel.GetChild("btn_save").asButton.visible = false;
|
||
// this.panel.GetChild("btn_reset").asButton.visible = false;
|
||
// this.panel.GetChild("btn_copy").asButton.visible = false;
|
||
let { components, mode, pattern, parent } = data;
|
||
this.components = components;
|
||
this.pattern = pattern;
|
||
this.parent = parent;
|
||
this.list = this.panel.GetChild("list_components").asList;
|
||
//this.textMode = this.panel.GetChild("text_mode").asTextField;
|
||
this.mode = mode || EMode.WRITE; // todo
|
||
if (this.components.length > 0) {
|
||
this.list.numItems = 0;
|
||
}
|
||
this.modeCtr = this.panel.GetController("op");
|
||
|
||
// this.btn_save = this.panel.GetChild("btn_save").asButton;
|
||
// this.btn_save.onClick.Add(() => {
|
||
// this.setCustomData();
|
||
// })
|
||
|
||
// this.btn_reset = this.panel.GetChild("btn_reset").asButton;
|
||
// this.btn_reset.onClick.Add(() => {
|
||
// this.showList(true);
|
||
// })
|
||
|
||
this.updateAction = () => { return this.updateUI(); };
|
||
}
|
||
|
||
private lastSelectedComponent: string = "";
|
||
private lastData: string = "";
|
||
private updateUI(): boolean {
|
||
let curDoc = App.activeDoc;
|
||
let { inspectingTarget } = curDoc;
|
||
let id = this.parent ? curDoc.docURL : inspectingTarget.id;
|
||
// 实时获取自定义数据
|
||
let propName = this.parent ? "remark" : "customData";
|
||
this.customData = inspectingTarget.GetProperty(propName);
|
||
try {
|
||
this.customDataObj = JSON.parse(this.customData) || {};
|
||
} catch (e) {
|
||
// console.log("自定义数据异常或没有发现自定义数据,无法渲染列表");
|
||
this.customDataObj = {};
|
||
}
|
||
|
||
// 根据匹配规则验证是否显示inspector
|
||
// 正则 & 字符串 通配符
|
||
let name = this.parent ? curDoc.displayTitle : inspectingTarget.name;
|
||
let pattern = isMatch(name, this.pattern);
|
||
if ((pattern && this.lastSelectedComponent != id) || this.customData !== this.lastData) { // 判断是否满足条件的组件以及是上一次选中的组件或者数据是否被修改
|
||
this.showList();
|
||
}
|
||
|
||
this.lastData = this.customData;
|
||
this.lastSelectedComponent = id;
|
||
return pattern;
|
||
}
|
||
|
||
private showList(reset: boolean = false) {
|
||
this.list.numItems = 0;
|
||
// todo
|
||
// if (this.mode == EMode.WRITE) {
|
||
// this.textMode.SetVar("mode", "设置").FlushVars();
|
||
// this.modeCtr.SetSelectedPage("write");
|
||
// } else {
|
||
// this.textMode.SetVar("mode", "读取").FlushVars();
|
||
// this.modeCtr.SetSelectedPage("read");
|
||
// }
|
||
|
||
// 根据自定义属性和配置文件混合比较【以自定义属性为主】渲染列表数据
|
||
for (let item of this.components) {
|
||
let { type, name, id, key } = item;
|
||
let com = getComponent(type);
|
||
if (!key) {
|
||
console.log("未定义唯一keyID:", id);
|
||
return
|
||
}
|
||
if (!com) {
|
||
console.log("发现未定义扩展组件,ID:", id);
|
||
}
|
||
|
||
(<FairyGUI.GButton>com).title = name || key;
|
||
const component = com.GetChild("component");
|
||
com.name = key;
|
||
this.renderItem(component, item, reset);
|
||
|
||
// 监听组件变更并保存到 json(参考 LanguageCustomInspector 的做法)
|
||
this.bindPersistEvents(component, item);
|
||
|
||
this.list.AddChild(com);
|
||
}
|
||
this.list.ResizeToFit();
|
||
|
||
// 更新关联关系
|
||
for (let item of this.components) {
|
||
let { associate, key } = item;
|
||
// 找到关联组件
|
||
let associateCom,curCom;
|
||
for(let i = 0;i<this.list.numChildren;i++){
|
||
let com = this.list.GetChildAt(i);
|
||
if(com.name == associate){
|
||
associateCom = com;
|
||
}
|
||
if(com.name == key){
|
||
curCom = com;
|
||
}
|
||
}
|
||
if(associateCom && !associateCom.GetChild("component").selected && curCom){
|
||
this.list.RemoveChild(curCom);
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
/**
|
||
* 给渲染出来的控件绑定事件:
|
||
* -(已暂时停用)更新自定义属性(SetProperty)
|
||
* - 将动效设置保存到项目设置目录下的 json(whootTween/<package>.json)
|
||
*/
|
||
private bindPersistEvents(component: FairyGUI.GObject, item: IComponent) {
|
||
const persist = () => {
|
||
// 暂时不写回到自定义数据,仅保存到 json
|
||
// this.setCustomData();
|
||
|
||
// 将与动效相关的设置保存到 json
|
||
this.trySaveTweenSetting(item, component);
|
||
};
|
||
|
||
// 根据控件类型选择合适的触发时机
|
||
if (component instanceof FairyGUI.GComboBox) {
|
||
component.onChanged.Add(persist);
|
||
} else if (component instanceof FairyEditor.Component.NumericInput) {
|
||
// 数字输入使用失焦触发
|
||
(component as FairyEditor.Component.NumericInput).onFocusOut.Add(persist);
|
||
} else if (component instanceof FairyGUI.GSlider) {
|
||
component.onChanged.Add(persist);
|
||
} else if (component instanceof FairyGUI.GButton) { // SWITCH / RADIOBOX
|
||
component.onChanged.Add(persist);
|
||
} else if (component instanceof FairyEditor.Component.ColorInput) {
|
||
component.onChanged.Add(persist);
|
||
} else if (component instanceof FairyGUI.GLabel) { // 文本输入类:失焦时保存
|
||
(component as FairyGUI.GLabel).onFocusOut.Add(persist);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 将与动效相关的设置保存到 json 文件。
|
||
* 规则:
|
||
* - 仅对下拉框(ComboBox)做持久化(其 value 代表动效 key)
|
||
* - 当选中值为 "null" 视为未启用(useable=false),否则为启用
|
||
*/
|
||
private trySaveTweenSetting(item: IComponent, component: any) {
|
||
try {
|
||
if (!(component instanceof FairyGUI.GComboBox)) return;
|
||
|
||
// 读取当前对象与文档信息
|
||
let sels = App.activeDoc.inspectingTargets;
|
||
let obj = sels.get_Item(0);
|
||
let activeDoc = App.activeDoc;
|
||
let packageName = activeDoc.packageItem.owner.name;
|
||
let docUrl = activeDoc.docURL;
|
||
let objId = obj.id;
|
||
// 父级组件通常没有有效的 id,这里为父级使用文档级占位 id
|
||
if ((!objId || objId === "") && this.parent) {
|
||
objId = "__root__";
|
||
}
|
||
|
||
// 当前选择的动效 key
|
||
const selectedIndex = component.selectedIndex;
|
||
const selectedValue = component.values?.get_Item(selectedIndex);
|
||
|
||
// 条件:无(值为"null")或组件ID为空(非父级场景),不记录
|
||
if ((!objId || objId === "") && !this.parent) return;
|
||
if (selectedValue == null) return;
|
||
if (selectedValue === "null") {
|
||
// 选择“无”时,删除已存在的记录
|
||
TweenSettings_1.default.remove(packageName, docUrl, objId);
|
||
return;
|
||
}
|
||
|
||
const useable = true; // 只有有效选择才记录,直接标记为可用
|
||
// 保存到 whootTween/<package>.json
|
||
TweenSettings_1.default.update(packageName, docUrl, objId, selectedValue, useable);
|
||
|
||
// 保存后做一次清理:把已删除的组件从 json 中移除
|
||
this.cleanupDeletedComponents(packageName, docUrl);
|
||
} catch (e) {
|
||
console.error("保存动效设置到 json 失败:", e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 收集当前文档中仍然存在的组件ID(递归)并触发清理
|
||
*/
|
||
private cleanupDeletedComponents(packageName: string, docUrl: string) {
|
||
try {
|
||
const ids: string[] = [];
|
||
const visited: { [k: string]: boolean } = {};
|
||
|
||
const collect = (node: FairyEditor.FObject) => {
|
||
if (!node) return;
|
||
const nid = (node as any).id as string;
|
||
if (nid && !visited[nid]) {
|
||
visited[nid] = true;
|
||
ids.push(nid);
|
||
}
|
||
if (node instanceof FairyEditor.FComponent) {
|
||
const cnt = (node as FairyEditor.FComponent).numChildren;
|
||
for (let i = 0; i < cnt; i++) {
|
||
const child = (node as FairyEditor.FComponent).GetChildAt(i);
|
||
collect(child);
|
||
}
|
||
}
|
||
}
|
||
|
||
const activeDoc = App.activeDoc;
|
||
if (activeDoc && activeDoc.content) {
|
||
collect(activeDoc.content);
|
||
}
|
||
|
||
// 仅当有 id 列表时尝试清理(保留 __root__)
|
||
TweenSettings_1.default.cleanupDoc(packageName, docUrl, ids);
|
||
} catch (err) {
|
||
console.error('清理已删除组件记录失败', err);
|
||
}
|
||
}
|
||
|
||
private renderItem(component: FairyGUI.GObject, item: IComponent, reset: boolean) {
|
||
let { value, key } = item;
|
||
if (!reset) {
|
||
let defaultVal = this.getValueByName(key);
|
||
value = defaultVal != undefined ? defaultVal : value;
|
||
}
|
||
// 下拉框
|
||
if (component instanceof FairyGUI.GComboBox && item.type == EComponent.COMBOBOX) {
|
||
let data = item.data;
|
||
let valueArr = System.Array.CreateInstance($typeof(System.String), data.values.length) as System.Array$1<string>;
|
||
for (let i = 0; i < data.values.length; i++) {
|
||
let v = data.values[i];
|
||
valueArr.set_Item(i, v);
|
||
}
|
||
|
||
let itemArr = System.Array.CreateInstance($typeof(System.String), data.items.length) as System.Array$1<string>;
|
||
for (let i = 0; i < data.items.length; i++) {
|
||
let v = data.items[i];
|
||
itemArr.set_Item(i, v);
|
||
}
|
||
|
||
component.items = itemArr;
|
||
component.values = valueArr;
|
||
|
||
// 优先使用外部配置(TweenSettings)进行回填
|
||
const extVal = this.getExternalTweenKey();
|
||
if (extVal && (data.values as string[]).indexOf(extVal) >= 0) {
|
||
component.value = extVal;
|
||
} else {
|
||
// 兼容:customData 可能保存的是值字符串或索引
|
||
if (typeof value === 'string' && (data.values as string[]).indexOf(value) >= 0) {
|
||
component.value = value as string;
|
||
} else {
|
||
const idx = Number(value);
|
||
const finalIdx = isNaN(idx) ? 0 : idx;
|
||
component.value = data.values[finalIdx] ?? data.values[0];
|
||
}
|
||
}
|
||
|
||
} else if (component instanceof FairyGUI.GLabel &&
|
||
(
|
||
item.type == EComponent.TEXTINPUT ||
|
||
item.type == EComponent.TEXTAREA ||
|
||
item.type == EComponent.RESOURCEINPUT
|
||
)) { // 文本输入框
|
||
component.title = value + "" || "";
|
||
} else if (item.type == EComponent.COLORINPUT && component instanceof FairyEditor.Component.ColorInput) { // 颜色输入框
|
||
let colorValue = value + "" || "#000000";
|
||
component.colorValue = FairyEditor.ColorUtil.FromHexString(colorValue);
|
||
} else if (component instanceof FairyGUI.GSlider && item.type == EComponent.SLIDER) { // 滑动块
|
||
let data = item.data;
|
||
component.min = +data.min || 0;
|
||
component.max = +data.max || 100;
|
||
component.value = +value || 0;
|
||
} else if (component instanceof FairyEditor.Component.NumericInput && item.type == EComponent.NUMBERINPUT) { // 数字输入框
|
||
let data = item.data;
|
||
component.min = +data.min || 0;
|
||
component.max = +data.max || 100;
|
||
component.step = +data.step || 0;
|
||
component.value = +value || 0;
|
||
} else if (component instanceof FairyGUI.GButton && item.type == EComponent.SWITCH) { // 切换器
|
||
component.selected = Boolean(value);
|
||
} else if (component instanceof FairyGUI.GButton && item.type == EComponent.RADIOBOX) { // 单选框
|
||
let data = item.data;
|
||
component.GetChildAt(0).text = data.items[0];
|
||
component.GetChildAt(1).text = data.items[1];
|
||
component.selected = Boolean(value);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取当前对象在 TweenSettings 中的外部配置 key
|
||
*/
|
||
private getExternalTweenKey(): string | null {
|
||
try {
|
||
let sels = App.activeDoc.inspectingTargets;
|
||
// puerts 映射的 IList$1 没有 Count 属性,这里直接尝试取第 0 项
|
||
if (!sels) return null;
|
||
let obj = sels.get_Item(0);
|
||
let activeDoc = App.activeDoc;
|
||
let packageName = activeDoc.packageItem.owner.name;
|
||
let docUrl = activeDoc.docURL;
|
||
let objId = obj.id;
|
||
if ((!objId || objId === "") && this.parent) {
|
||
objId = "__root__";
|
||
}
|
||
let data = TweenSettings_1.default.get(packageName, docUrl, objId);
|
||
if (data && data.useable == 1 && data.key) {
|
||
return data.key as string;
|
||
}
|
||
} catch (e) {
|
||
console.error('读取 TweenSettings 外部配置失败', e);
|
||
}
|
||
return null;
|
||
}
|
||
|
||
private getListItemVal(): string {
|
||
for (let i = 0; i < this.list.numChildren; i++) {
|
||
let item = this.list.GetChildAt(i) as FairyGUI.GComponent;
|
||
let component = item.GetChild("component") as any;
|
||
|
||
let value = component.title;
|
||
if (component instanceof FairyEditor.Component.ColorInput) {
|
||
value = FairyEditor.ColorUtil.ToHexString(component.colorValue);
|
||
} else if (component instanceof FairyGUI.GComboBox) {
|
||
// value = component.selectedIndex;
|
||
value = component.values.get_Item(component.selectedIndex);
|
||
} else if (component instanceof FairyEditor.Component.NumericInput) {
|
||
value = component.value;
|
||
} else if (this.components[i].type == EComponent.SWITCH) {
|
||
value = (component as FairyGUI.GButton).selected;
|
||
} else if (this.components[i].type == EComponent.RADIOBOX) {
|
||
value = (component as FairyGUI.GButton).selected ? 1 : 0;
|
||
} else if (this.components[i].type == EComponent.SLIDER) {
|
||
value = (component as FairyGUI.GSlider).value;
|
||
}
|
||
|
||
let key = this.components[i].key;
|
||
if (this.customDataObj) {
|
||
this.customDataObj[key] = value;
|
||
}
|
||
|
||
}
|
||
return JSON.stringify(this.customDataObj) || "";
|
||
}
|
||
|
||
private getValueByName(name: string): string {
|
||
// let value = "";
|
||
// if (this.customDataObj?.[name]) {
|
||
// value = this.customDataObj[name];
|
||
// }
|
||
// return value;
|
||
return this.customDataObj[name];
|
||
}
|
||
|
||
private setCustomData() {
|
||
let propName = this.parent ? "remark" : "customData";
|
||
let data = this.getListItemVal();
|
||
App.activeDoc.inspectingTarget.docElement.SetProperty(propName, data);
|
||
}
|
||
}
|
||
|
||
let isCharacterMatch = (s: string, p: string): boolean => {
|
||
let dp = [];
|
||
for (let i = 0; i <= s.length; i++) {
|
||
let child = [];
|
||
for (let j = 0; j <= p.length; j++) {
|
||
child.push(false);
|
||
}
|
||
dp.push(child);
|
||
}
|
||
dp[s.length][p.length] = true;
|
||
|
||
for (let i = p.length - 1; i >= 0; i--) {
|
||
if (p[i] != "*") break;
|
||
else dp[s.length][i] = true;
|
||
}
|
||
|
||
for (let i = s.length - 1; i >= 0; i--) {
|
||
for (let j = p.length - 1; j >= 0; j--) {
|
||
if (s[i] == p[j] || p[j] == "?") {
|
||
dp[i][j] = dp[i + 1][j + 1];
|
||
} else if (p[j] == "*") {
|
||
dp[i][j] = dp[i + 1][j] || dp[i][j + 1];
|
||
} else {
|
||
dp[i][j] = false;
|
||
}
|
||
}
|
||
}
|
||
return dp[0][0];
|
||
};
|
||
|
||
let isRegMatch = (source: string, pattern: string): boolean => {
|
||
const patt = new RegExp(pattern);
|
||
return patt.test(source);
|
||
}
|
||
|
||
let isMatch = (source: string, pattern: string): boolean => {
|
||
if (pattern.includes("*") || pattern.includes("?")) {
|
||
return isCharacterMatch(source, pattern);
|
||
} else if (pattern.includes("/")) {
|
||
return isRegMatch(source, pattern);
|
||
} else {
|
||
return source.includes(pattern);
|
||
}
|
||
}
|
||
|
||
let getComponent = (componentType: EComponent): FairyGUI.GComponent => {
|
||
let component: FairyGUI.GComponent;
|
||
switch (componentType) {
|
||
case EComponent.TEXTINPUT:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.TEXTINPUT).asCom;
|
||
break;
|
||
case EComponent.TEXTAREA:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.TEXTAREA).asCom;
|
||
break;
|
||
case EComponent.COMBOBOX:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.COMBOBOX).asCom;
|
||
break;
|
||
case EComponent.COLORINPUT:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.COLORINPUT).asCom;
|
||
break;
|
||
case EComponent.NUMBERINPUT:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.NUMBERINPUT).asCom;
|
||
break;
|
||
case EComponent.RESOURCEINPUT:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.RESOURCEINPUT).asCom;
|
||
break;
|
||
case EComponent.SLIDER:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.SLIDER).asCom;
|
||
break;
|
||
case EComponent.RADIOBOX:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.RADIOBOX).asCom;
|
||
break;
|
||
case EComponent.SWITCH:
|
||
component = FairyGUI.UIPackage.CreateObject("TweenAttributer", EComponent.SWITCH).asCom;
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
return component;
|
||
}
|
||
|
||
export { CustomAttributer };
|