306 lines
13 KiB
C#
306 lines
13 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
using VF.Builder.Exceptions;
|
|
using VF.Inspector;
|
|
using VRC.SDK3.Avatars.ScriptableObjects;
|
|
|
|
namespace VF.Builder {
|
|
|
|
public class MenuManager {
|
|
private readonly VRCExpressionsMenu rootMenu;
|
|
private readonly string tmpDir;
|
|
private readonly Func<int> currentMenuSortPosition;
|
|
private readonly Dictionary<VRCExpressionsMenu.Control, int> sortPositions
|
|
= new Dictionary<VRCExpressionsMenu.Control, int>();
|
|
|
|
public MenuManager(VRCExpressionsMenu menu, string tmpDir, Func<int> currentMenuSortPosition) {
|
|
rootMenu = menu;
|
|
this.tmpDir = tmpDir;
|
|
this.currentMenuSortPosition = currentMenuSortPosition;
|
|
}
|
|
|
|
public VRCExpressionsMenu GetRaw() {
|
|
return rootMenu;
|
|
}
|
|
|
|
private VRCExpressionsMenu.Control NewControl() {
|
|
var control = new VRCExpressionsMenu.Control();
|
|
sortPositions[control] = currentMenuSortPosition();
|
|
return control;
|
|
}
|
|
|
|
public static IList<string> SplitPath(string path) {
|
|
if (string.IsNullOrWhiteSpace(path))
|
|
return new string[] { };
|
|
return path
|
|
.Replace("\\/", "REALSLASH")
|
|
.Split('/')
|
|
.Select(s => s.Replace("REALSLASH", "/"))
|
|
.ToArray();
|
|
}
|
|
|
|
private VRCExpressionsMenu.Control NewMenuItem(string path) {
|
|
var split = SplitPath(path);
|
|
if (split.Count == 0) split = new[] { "" };
|
|
var control = NewControl();
|
|
control.name = split[split.Count-1];
|
|
var submenu = GetSubmenu(Slice(split, split.Count-1));
|
|
submenu.controls.Add(control);
|
|
return control;
|
|
}
|
|
|
|
public bool SetIconGuid(string path, string guid) {
|
|
var iconPath = AssetDatabase.GUIDToAssetPath(guid);
|
|
if (string.IsNullOrWhiteSpace(iconPath)) return false;
|
|
var icon = AssetDatabase.LoadAssetAtPath<Texture2D>(iconPath);
|
|
if (!icon) return false;
|
|
return SetIcon(path, icon);
|
|
}
|
|
|
|
public bool SetIcon(string path, Texture2D icon) {
|
|
GetSubmenuAndItem(path, false, out _, out _, out var controlName, out var parentMenu);
|
|
if (!parentMenu) return false;
|
|
|
|
var controls = parentMenu.controls.Where(c => c.name == controlName).ToList();
|
|
if (controls.Count == 0) return false;
|
|
|
|
foreach (var control in controls) {
|
|
control.icon = icon;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public bool Move(string from, string to) {
|
|
GetSubmenuAndItem(from, false, out var fromPath, out var fromPrefix, out var fromName, out var fromMenu);
|
|
if (!fromMenu) return false;
|
|
|
|
var fromControls = fromMenu.controls.Where(c => c.name == fromName).ToList();
|
|
if (fromControls.Count == 0) return false;
|
|
fromMenu.controls.RemoveAll(c => fromControls.Contains(c));
|
|
|
|
if (string.IsNullOrWhiteSpace(to)) {
|
|
// Just delete them!
|
|
return true;
|
|
}
|
|
|
|
GetSubmenuAndItem(to, true, out var toPath, out var toPrefix, out var toName, out var toMenu);
|
|
foreach (var control in fromControls) {
|
|
if (control.type == VRCExpressionsMenu.Control.ControlType.SubMenu) {
|
|
GetSubmenu(toPath, createFromControl: control);
|
|
MergeMenu(toPath, control.subMenu);
|
|
} else {
|
|
control.name = toName;
|
|
var tmpMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
|
tmpMenu.controls.Add(control);
|
|
MergeMenu(toPrefix, tmpMenu);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private void GetSubmenuAndItem(
|
|
string rawPath,
|
|
bool create,
|
|
out IList<string> path,
|
|
out IList<string> prefix,
|
|
out string name,
|
|
out VRCExpressionsMenu prefixMenu
|
|
) {
|
|
path = SplitPath(rawPath);
|
|
if (path.Count > 0) {
|
|
prefix = Slice(path, path.Count - 1);
|
|
name = path[path.Count - 1];
|
|
} else {
|
|
prefix = new string[]{};
|
|
name = "";
|
|
}
|
|
prefixMenu = GetSubmenu(prefix, createIfMissing: create);
|
|
}
|
|
|
|
/**
|
|
* Gets the VRC menu for the path specified, recursively creating if it doesn't exist.
|
|
* If createFromControl is set, we will use it as the basis if creating the folder control is needed.
|
|
*/
|
|
private VRCExpressionsMenu GetSubmenu(
|
|
IList<string> path,
|
|
bool createIfMissing = true,
|
|
VRCExpressionsMenu.Control createFromControl = null,
|
|
Func<string,string> rewriteParamName = null
|
|
) {
|
|
var current = GetRaw();
|
|
for (var i = 0; i < path.Count; i++) {
|
|
var folderName = path[i];
|
|
var dupIndex = folderName.IndexOf(".dup.");
|
|
var offset = 0;
|
|
if (dupIndex >= 0) {
|
|
offset = Int32.Parse(folderName.Substring(dupIndex + 5));
|
|
folderName = folderName.Substring(0, dupIndex);
|
|
}
|
|
var folderControls = current.controls.Where(
|
|
c => c.name == folderName && c.type == VRCExpressionsMenu.Control.ControlType.SubMenu)
|
|
.ToArray();
|
|
var folderControl = offset < folderControls.Length ? folderControls[offset] : null;
|
|
if (folderControl == null) {
|
|
if (!createIfMissing) return null;
|
|
if (createFromControl != null && i == path.Count - 1) {
|
|
folderControl = CloneControl(createFromControl, rewriteParamName);
|
|
} else {
|
|
folderControl = NewControl();
|
|
}
|
|
folderControl.name = folderName;
|
|
folderControl.type = VRCExpressionsMenu.Control.ControlType.SubMenu;
|
|
folderControl.subMenu = null;
|
|
current.controls.Add(folderControl);
|
|
}
|
|
var folder = folderControl.subMenu;
|
|
if (folder == null) {
|
|
if (!createIfMissing) return null;
|
|
var newFolderPath = Slice(path, i + 1);
|
|
folder = CreateNewMenu(newFolderPath);
|
|
folderControl.subMenu = folder;
|
|
}
|
|
current = folder;
|
|
}
|
|
return current;
|
|
}
|
|
public void NewMenuButton(string path, VFAParam param = null, float value = 1, Texture2D icon = null) {
|
|
var control = NewMenuItem(path);
|
|
control.type = VRCExpressionsMenu.Control.ControlType.Toggle;
|
|
control.parameter = new VRCExpressionsMenu.Control.Parameter {
|
|
name = param != null ? param.Name() : ""
|
|
};
|
|
control.value = value;
|
|
control.icon = icon;
|
|
}
|
|
public void NewMenuToggle(string path, VFAParam param, float value = 1, Texture2D icon = null) {
|
|
var control = NewMenuItem(path);
|
|
control.type = VRCExpressionsMenu.Control.ControlType.Toggle;
|
|
control.parameter = new VRCExpressionsMenu.Control.Parameter {
|
|
name = param.Name()
|
|
};
|
|
control.value = value;
|
|
control.icon = icon;
|
|
}
|
|
public void NewMenuSlider(string path, VFANumber param, Texture2D icon = null) {
|
|
var control = NewMenuItem(path);
|
|
control.type = VRCExpressionsMenu.Control.ControlType.RadialPuppet;
|
|
var menuParam = new VRCExpressionsMenu.Control.Parameter {
|
|
name = param.Name()
|
|
};
|
|
control.subParameters = new[]{menuParam};
|
|
control.icon = icon;
|
|
}
|
|
public void NewMenuPuppet(string path, VFANumber x, VFANumber y, Texture2D icon = null) {
|
|
var control = NewMenuItem(path);
|
|
control.type = VRCExpressionsMenu.Control.ControlType.TwoAxisPuppet;
|
|
var menuParamX = new VRCExpressionsMenu.Control.Parameter();
|
|
menuParamX.name = (x != null) ? x.Name() : "";
|
|
var menuParamY = new VRCExpressionsMenu.Control.Parameter();
|
|
menuParamY.name = (y != null) ? y.Name() : "";
|
|
control.subParameters = new[]{menuParamX, menuParamY};
|
|
control.icon = icon;
|
|
}
|
|
|
|
private VRCExpressionsMenu CreateNewMenu(IList<string> path) {
|
|
var cleanPath = path.Select(CleanTitleForFilename);
|
|
var newMenu = ScriptableObject.CreateInstance<VRCExpressionsMenu>();
|
|
newMenu.name = string.Join(" » ", cleanPath);
|
|
AssetDatabase.AddObjectToAsset(newMenu, rootMenu);
|
|
return newMenu;
|
|
}
|
|
private static string CleanTitleForFilename(string str) {
|
|
// strip html tags
|
|
str = Regex.Replace(str, "<.*?>", string.Empty);
|
|
// remove after newline
|
|
str = Regex.Replace(str, "\n.*", string.Empty);
|
|
// clean up extra spaces
|
|
str = Regex.Replace(str, " +", " ");
|
|
return str.Trim();
|
|
}
|
|
|
|
public void MergeMenu(VRCExpressionsMenu from, Func<string,string> rewriteParamName = null) {
|
|
MergeMenu(new string[]{}, from, rewriteParamName);
|
|
}
|
|
|
|
public void MergeMenu(
|
|
IList<string> prefix,
|
|
VRCExpressionsMenu from,
|
|
Func<string,string> rewriteParamName = null,
|
|
Dictionary<VRCExpressionsMenu, VRCExpressionsMenu> seen = null
|
|
) {
|
|
var to = GetSubmenu(prefix);
|
|
if (seen == null) {
|
|
seen = new Dictionary<VRCExpressionsMenu, VRCExpressionsMenu>();
|
|
}
|
|
seen.Add(from, to);
|
|
|
|
var submenuCount = new Dictionary<string,int>();
|
|
int GetNextSubmenuDupId(string name) {
|
|
return submenuCount[name] = submenuCount.TryGetValue(name, out var value) ? value + 1 : 0;
|
|
}
|
|
foreach (var fromControl in from.controls) {
|
|
if (fromControl.type == VRCExpressionsMenu.Control.ControlType.SubMenu && fromControl.subMenu != null) {
|
|
// Properly handle loops
|
|
if (seen.ContainsKey(fromControl.subMenu)) {
|
|
var toControl = CloneControl(fromControl, rewriteParamName);
|
|
toControl.subMenu = seen[fromControl.subMenu];
|
|
to.controls.Add(toControl);
|
|
} else {
|
|
var submenuDupId = GetNextSubmenuDupId(fromControl.name);
|
|
var prefix2 = new List<string>(prefix);
|
|
prefix2.Add(fromControl.name + (submenuDupId > 0 ? (".dup." + submenuDupId) : ""));
|
|
GetSubmenu(prefix2.ToArray(), createFromControl: fromControl, rewriteParamName: rewriteParamName);
|
|
MergeMenu(prefix2.ToArray(), fromControl.subMenu, rewriteParamName, seen);
|
|
}
|
|
} else {
|
|
to.controls.Add(CloneControl(fromControl, rewriteParamName));
|
|
}
|
|
}
|
|
}
|
|
|
|
private VRCExpressionsMenu.Control CloneControl(VRCExpressionsMenu.Control from, Func<string,string> rewriteParamName) {
|
|
var control = NewControl();
|
|
control.name = from.name;
|
|
control.icon = from.icon;
|
|
control.type = from.type;
|
|
control.parameter = CloneControlParam(from.parameter, rewriteParamName);
|
|
control.value = from.value;
|
|
control.style = from.style;
|
|
control.subMenu = from.subMenu;
|
|
control.labels = from.labels;
|
|
control.subParameters = from.subParameters == null
|
|
? null
|
|
: new List<VRCExpressionsMenu.Control.Parameter>(from.subParameters)
|
|
.Select(p => CloneControlParam(p, rewriteParamName))
|
|
.ToArray();
|
|
return control;
|
|
}
|
|
private VRCExpressionsMenu.Control.Parameter CloneControlParam(VRCExpressionsMenu.Control.Parameter from, Func<string,string> rewriteParamName) {
|
|
if (from == null) return null;
|
|
return new VRCExpressionsMenu.Control.Parameter {
|
|
name = rewriteParamName != null ? rewriteParamName(from.name) : from.name
|
|
};
|
|
}
|
|
|
|
public static IList<string> Slice(IEnumerable<string> arr, int count) {
|
|
return new ArraySegment<string>(arr.ToArray(), 0, count).ToArray();
|
|
}
|
|
|
|
public void SortMenu() {
|
|
MenuSplitter.ForEachMenu(rootMenu, (menu, path) => {
|
|
menu.controls.Sort((a, b) => {
|
|
sortPositions.TryGetValue(a, out var aPos);
|
|
sortPositions.TryGetValue(b, out var bPos);
|
|
return aPos - bPos;
|
|
});
|
|
});
|
|
}
|
|
|
|
}
|
|
|
|
}
|