using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using UnityEditor;
using UnityEditor.AnimatedValues;
using UnityEngine;
using UnityEngine.UI;
using VketCloudGUITools.Runtime;
using VketCloudGUITools.Serialization;
using VketCloudGUITools.Utilities;
namespace VketCloudGUITools.Editor
{
public class VCEditorCanvasExporter : EditorWindow
{
///
/// CanvasのExport前のCheck時のエラーor警告メッセージ、およびメッセージ発生対象
///
public class CheckMessage
{
///
/// メッセージ
///
public string message;
///
/// メッセージ発生対象
///
public UnityEngine.Object obj;
public Action onDrawAutoFixButton = null;
}
public class AutoApplyAutoFixSettings
{
public bool activate = false;
public bool safe = false;
public bool major = false;
public bool removeComponent = false;
public bool hierarchical = false;
public bool animation = false;
public bool generateAutoLayer = false;
}
private const string prefix = "VketCloudGUIToolsExporter_";
private const string helliodorDataPathKey = prefix + "HeliodorDataPath";
private const string unityDataPathKey = prefix + "UnityDataPath";
string heliodorDataPath = "";
string unityDataPath = "";
bool zFlip = true;
Canvas targetCanvas = null;
List criticalMessages = new List();
List errorMessages = new List();
List warningMessages = new List();
VCLayerList autoGeneratedLayerList = null;
bool delayClearMessages = false;
bool autoFixResolved = false;
string delayMessage = string.Empty;
double updateTime = 0;
private AutoFixErrorHelper _autoFixErrorHelper = null;
private AnimBool _autoApplyAutoFixExpand = null;
private AutoApplyAutoFixSettings _autoApplyAutoFix = new AutoApplyAutoFixSettings();
private bool advancedOptions = false;
public AutoFixErrorHelper AutoFixError
{
get => _autoFixErrorHelper;
set => _autoFixErrorHelper = value;
}
Vector2 scrollPosition;
readonly Type[] allowedGUIComponents = new Type[]
{
typeof(RectTransform),
typeof(CanvasRenderer),
typeof(Button),
typeof(Image),
typeof(Text),
typeof(Slider),
typeof(VCTransform),
typeof(VCGUIContent),
typeof(VCButton),
typeof(VCImage),
typeof(VCText),
typeof(VCSlider),
typeof(VCComment),
};
readonly Type[] allowedCanvasComponents = new Type[]
{
typeof(RectTransform),
typeof(Canvas),
typeof(VCCanvas),
typeof(CanvasScaler)
};
readonly Type[] allowedLayerComponents = new Type[]
{
typeof(RectTransform),
typeof(VCLayerList)
};
private void OnEnable()
{
heliodorDataPath = EditorPrefs.GetString(helliodorDataPathKey);
unityDataPath = EditorPrefs.GetString(unityDataPathKey, "Assets");
EditorApplication.hierarchyChanged += OnExternalChange;
Undo.undoRedoPerformed += OnExternalChange;
_autoFixErrorHelper = new AutoFixErrorHelper(this);
_autoApplyAutoFixExpand = new AnimBool();
_autoApplyAutoFixExpand.valueChanged.AddListener(Repaint);
}
private void OnDisable()
{
Undo.undoRedoPerformed -= OnExternalChange;
EditorApplication.hierarchyChanged -= OnExternalChange;
EditorPrefs.SetString(helliodorDataPathKey, heliodorDataPath);
EditorPrefs.SetString(unityDataPathKey, unityDataPath);
_autoApplyAutoFixExpand.valueChanged.RemoveListener(Repaint);
}
private void OnInspectorUpdate()
{
Repaint();
}
private void OnExternalChange()
{
Repaint();
}
[MenuItem("VketCloudGUITools/GUI Exporter")]
private static void Create()
{
GetWindow("VketCloud GUI Exporter");
}
private void OnGUI()
{
using (new GUILayout.HorizontalScope())
{
heliodorDataPath = EditorGUILayout.TextField("Export Canvas Path", heliodorDataPath);
if (GUILayout.Button("...", GUILayout.Width(EditorGUIUtility.singleLineHeight)))
{
var path = EditorUtility.SaveFolderPanel("Select Export Folder", heliodorDataPath, "");
if (!string.IsNullOrEmpty(path))
heliodorDataPath = path;
}
}
targetCanvas = EditorGUILayout.ObjectField("Target Canvas", targetCanvas, typeof(Canvas), true) as Canvas;
advancedOptions = EditorGUILayout.BeginFoldoutHeaderGroup(advancedOptions, "Advanced Options");
if (advancedOptions)
{
EditorGUI.indentLevel++;
using (new GUILayout.HorizontalScope())
{
unityDataPath = EditorGUILayout.TextField("Assets Output Path", unityDataPath);
if (GUILayout.Button("...", GUILayout.Width(EditorGUIUtility.singleLineHeight)))
{
var path = EditorUtility.SaveFolderPanel("Select Assets Output Folder", unityDataPath, "");
if (IsDataPath(path))
{
path = "Assets" + StlipDataPath(path);
}
if (!string.IsNullOrEmpty(path))
unityDataPath = path;
}
}
autoGeneratedLayerList = EditorGUILayout.ObjectField("Auto Generated Layer", autoGeneratedLayerList, typeof(VCLayerList), true) as VCLayerList;
// Error check (Auto cycle)
if (EditorApplication.timeSinceStartup - updateTime >= 1.0 && Event.current.type == EventType.Layout)
{
ClearMessages();
if (targetCanvas != null)
{
DoErrorCheck();
updateTime = EditorApplication.timeSinceStartup;
}
}
EditorGUI.indentLevel--;
}
// Draw Warnings & Auto Fix
DrawErrorWarningList();
// Export Button
EditorGUI.BeginDisabledGroup(criticalMessages.Count > 0);
if (targetCanvas != null && GUILayout.Button("Export .json"))
{
DoExport(targetCanvas);
}
EditorGUI.EndDisabledGroup();
}
private void DoErrorCheck()
{
var vcCanvas = targetCanvas.GetComponent();
bool ok = false;
if (vcCanvas == null)
{
ok = EditorUtility.DisplayDialog("注意", "対象はVCGUICanvasではありません。\n変換処理の可能性があるため、対象はコピーされます。", "OK", "キャンセル");
if (ok)
{
ConvertCancasToVCGUICanvas();
}
else
{
targetCanvas = null;
}
}
else if (vcCanvas != null && targetCanvas != null)
{
// Canvas自体のエラーチェック
CanvasErrorCheck(targetCanvas);
// Canvasの子要素のエラーチェック
CanvasChildErrorCheck();
// 名前の重複を確認
CheckNameUniquness();
}
}
private void ConvertCancasToVCGUICanvas()
{
var sourceTargetCanvas = targetCanvas;
targetCanvas = Instantiate(targetCanvas);
// Instantiateすると名前に(Clone)が勝手につくので戻す
targetCanvas.name = sourceTargetCanvas.name;
targetCanvas.transform.SetParent(sourceTargetCanvas.transform.parent);
targetCanvas.transform.localPosition = sourceTargetCanvas.transform.localPosition;
targetCanvas.transform.localRotation = sourceTargetCanvas.transform.localRotation;
targetCanvas.transform.localScale = sourceTargetCanvas.transform.localScale;
var vcCanvas2 = targetCanvas.gameObject.AddComponent();
vcCanvas2.CanvasType = CanvasType.LandScape;
sourceTargetCanvas.gameObject.SetActive(false);
Selection.activeObject = targetCanvas;
}
private void CanvasChildErrorCheck()
{
for (int i = 0; i < targetCanvas.transform.childCount; ++i)
{
Transform child = targetCanvas.transform.GetChild(i);
var canvasRenderer = child.GetComponent();
var slider = child.GetComponent();
var childRectTransform = child.GetComponent();
if (childRectTransform != null && (slider != null || canvasRenderer != null))
{
AddCriticalMessages("Canvasの直下にGUIコンテンツが存在しています。\nGUIコンテンツはレイヤーが必要です。\n手動でレイヤーに移動するか、[Auto Fix]からレイヤーを自動生成して移動してください。\n[Auto Fix]では、GUIコンテンツの座標などがズレてしまう場合があるため、手動調整を忘れないでください。", child, AutoFixError.Canvas.MoveChildToAutoGeneratedLayer(child, targetCanvas, autoGeneratedLayerList, l => autoGeneratedLayerList = l));
if (childRectTransform != null)
ContentErrorCheck(childRectTransform);
}
else
{
var childRect = child.GetComponent();
if (childRect == null)
{
AddWarningMessage($"{child.name}[Canvas]直下に、RectTransformではないものがあります。UIではないため、出力時には無視されます。", child, AutoFixError.Misc.RemoveOrConvertToRectTransform(child));
}
else if (childRect != null)
{
LayerErrorCheck(childRect);
}
}
}
}
private void CheckNameUniquness()
{
Dictionary layerListNameCount = new Dictionary();
foreach (Transform decendant in targetCanvas.transform.GetAllDecendants())
{
var path = AnimationUtility.CalculateTransformPath(decendant, targetCanvas.transform);
if (!layerListNameCount.ContainsKey(path))
{
layerListNameCount[path] = 0;
}
layerListNameCount[path]++;
}
foreach (var kv in layerListNameCount)
{
var name = kv.Key;
var count = kv.Value;
if (count > 1)
{
AddWarningMessage($"{name}[Content]名前が重複しています。\nPath : {name}\nは同じ階層に{count}個あります。\n予想できない動作を引き起こす可能性があるため、避けてください。", null);
}
}
}
///
/// エラー&警告メッセージの一覧を表示
///
private void DrawErrorWarningList()
{
using (var scrollView = new EditorGUILayout.ScrollViewScope(scrollPosition))
{
// Undo注意喚起
if (criticalMessages.Count > 0 || warningMessages.Count > 0 || errorMessages.Count > 0)
{
EditorGUILayout.HelpBox("間違えて[Auto Fix]を押した場合、あわてずにUndoをしてください。Undoに対応済みです。", MessageType.Info);
}
else if (targetCanvas != null)
{
EditorGUILayout.HelpBox("エラーメッセージはありません。", MessageType.None);
}
_autoApplyAutoFixExpand.target = EditorGUILayout.Foldout(_autoApplyAutoFixExpand.target, "Auto Fix Settings");
if (EditorGUILayout.BeginFadeGroup(_autoApplyAutoFixExpand.faded))
{
GUILayout.BeginVertical(GUI.skin.box);
_autoApplyAutoFix.activate = EditorGUILayout.Toggle("Acticate", _autoApplyAutoFix.activate);
var defaultLabelWidth = EditorGUIUtility.labelWidth;
EditorGUIUtility.labelWidth = 180f;
EditorGUILayout.LabelField("Auto Apply Auto Fix", "Rules");
_autoApplyAutoFix.safe = EditorGUILayout.Toggle("Allow: Safe", _autoApplyAutoFix.safe);
_autoApplyAutoFix.major = EditorGUILayout.Toggle("Allow: Major Change", _autoApplyAutoFix.major);
_autoApplyAutoFix.hierarchical = EditorGUILayout.Toggle("Allow: Hierarchical", _autoApplyAutoFix.hierarchical);
_autoApplyAutoFix.removeComponent = EditorGUILayout.Toggle("Allow: Remove Component", _autoApplyAutoFix.removeComponent);
_autoApplyAutoFix.animation = EditorGUILayout.Toggle("Allow: Animation", _autoApplyAutoFix.animation);
_autoApplyAutoFix.generateAutoLayer = EditorGUILayout.Toggle("Allow: Generate Auto Layer", _autoApplyAutoFix.generateAutoLayer);
EditorGUIUtility.labelWidth = defaultLabelWidth;
GUILayout.EndVertical();
}
EditorGUILayout.EndFadeGroup();
// 致命的なエラーメッセージ
using (var criticalScope = new EditorGUILayout.VerticalScope())
{
if (Event.current.type == EventType.Repaint)
{
EditorGUI.DrawRect(criticalScope.rect, new Color(1f, 0f, 0f, 0.25f));
}
if (criticalMessages.Count > 0)
{
EditorGUILayout.HelpBox("この色のメッセージは、修正が必要ですが、その順番によって結果が変わります。\n解決するまでExportが実行できません。", MessageType.Error);
}
foreach (var message in criticalMessages)
{
using (new EditorGUILayout.VerticalScope(GUI.skin.box))
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.HelpBox(message.message, MessageType.Error);
if (message.onDrawAutoFixButton != null)
{
message.onDrawAutoFixButton();
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Select", GUILayout.ExpandWidth(false)))
{
Selection.activeObject = message.obj;
}
EditorGUILayout.ObjectField(message.obj, typeof(UnityEngine.Object), allowSceneObjects: true);
}
}
PostDrawMessage(message);
EditorGUILayout.Space();
}
}
// 警告メッセージ
if (warningMessages.Count > 0)
{
EditorGUILayout.HelpBox("この色のメッセージは、無視しても自動修正されてHeliodorで再現可能に出力されます。\n[Auto Fix]を押すか、再インポート時に修正済みの値になります。", MessageType.Warning);
}
foreach (var message in warningMessages)
{
using (new EditorGUILayout.VerticalScope(GUI.skin.box))
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.HelpBox(message.message, MessageType.Warning);
if (message.onDrawAutoFixButton != null)
{
message.onDrawAutoFixButton();
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Select", GUILayout.ExpandWidth(false)))
{
Selection.activeObject = message.obj;
}
EditorGUILayout.ObjectField(message.obj, typeof(UnityEngine.Object), allowSceneObjects: true);
}
}
PostDrawMessage(message);
EditorGUILayout.Space();
}
// エラーメッセージ
if (errorMessages.Count > 0)
{
EditorGUILayout.HelpBox("この色のメッセージは、Heliodorで正確に再現できない設定を警告しています。\n[Auto Fix]を押すとHeliodorで可能な動きの設定になります。\n自動修正でレイアウトが変わる可能性があるため、必ず再確認してください。", MessageType.Error);
}
foreach (var message in errorMessages)
{
using (new EditorGUILayout.VerticalScope(GUI.skin.box))
{
using (new EditorGUILayout.HorizontalScope())
{
EditorGUILayout.HelpBox(message.message, MessageType.Error);
if (message.onDrawAutoFixButton != null)
{
message.onDrawAutoFixButton();
}
}
using (new EditorGUILayout.HorizontalScope())
{
if (GUILayout.Button("Select", GUILayout.ExpandWidth(false)))
{
Selection.activeObject = message.obj;
}
EditorGUILayout.ObjectField(message.obj, typeof(UnityEngine.Object), allowSceneObjects: true);
}
}
EditorGUILayout.Space();
PostDrawMessage(message);
}
scrollPosition = scrollView.scrollPosition;
}
if (Event.current.type == EventType.Repaint)
{
if (delayClearMessages)
{
delayClearMessages = false;
ClearMessages();
AddWarningMessage(delayMessage, null);
}
}
}
public void DoExport(Canvas targetCanvas)
{
// 出力対象が選んでない
if (targetCanvas == null)
{
EditorUtility.DisplayDialog("VketCloud GUI Exporter", "Canvasを指定してください", "OK");
return;
}
// 出力対象にエラーが残っている
if (criticalMessages.Count > 0)
{
EditorUtility.DisplayDialog("VketCloud GUI Exporter", "「致命的なエラー」が残っています。赤背景の警告に、すべて対応してください。", "OK");
return;
}
// Canvasを解析してJson元データのCanvasDefを組み立てる
Canvas canvasGameObject;
VCCanvasDef canvasDef;
VCCanvas vCanvas;
ExportCanvas(targetCanvas, out canvasGameObject, out canvasDef, out vCanvas);
// 保存先および関連パラメタを確定
// CanvasTypeはCanvasJsonファイル内ではなく、ファイル名/フォルダ名で確定される
DestroyImmediate(canvasGameObject.gameObject);
var path = EditorUtility.SaveFilePanel("Save Canvas JSON", heliodorDataPath, targetCanvas.name + ".json", "json");
// 出力先が選ばれなかったので中断
if (string.IsNullOrEmpty(path))
return;
// CanvasTypeを出力先ディレクトリから確定する
var canvasType = Runtime.CanvasType.None;
var directoryNames = path.Replace('\\', '/').Split('/').Reverse();
for (int i = 0; i < directoryNames.Count(); ++i)
{
var name = directoryNames.ElementAt(i);
if (Regex.IsMatch(name, "landscape", RegexOptions.IgnoreCase))
{
canvasType = Runtime.CanvasType.LandScape;
break;
}
else if (Regex.IsMatch(name, "portrait", RegexOptions.IgnoreCase))
{
canvasType = Runtime.CanvasType.Portrait;
break;
}
}
// Portrait設定と出力ディレクトリの不一致がないか確認する
var landscapeCheck = vCanvas.CanvasType == CanvasType.LandScape && canvasType != CanvasType.LandScape;
var portraitCheck = vCanvas.CanvasType == CanvasType.Portrait && canvasType != CanvasType.Portrait;
var folderCheck = landscapeCheck || portraitCheck;
// 出力ディレクトリがPortrait設定と不一致のとき、安全のためユーザーに確認する
if (folderCheck && !EditorUtility.DisplayDialog("確認", $"{vCanvas.CanvasType}用Canvasを{canvasType}用のディレクトリに出力します。", "OK", "キャンセル"))
return;
// Json出力
WriteJsonToFile(canvasDef, path);
EditorUtility.DisplayDialog("VketCloud GUI Exporter", "JSONの出力が完了しました。", "OK");
}
private static void WriteJsonToFile(VCCanvasDef canvasDef, string path)
{
// Json出力
JsonSerializer serializer = new JsonSerializer()
{
Formatting = Formatting.Indented,
};
serializer.Converters.Add(new SingleLineJsonConverter(typeof(int[]), typeof(float[])));
serializer.FloatFormatHandling = FloatFormatHandling.String;
using (var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
using (var streamWriter = new StreamWriter(fileStream))
using (var writer = new JsonTextWriter(streamWriter))
{
serializer.Serialize(writer, canvasDef);
}
// エンコーディング統一
if (File.Exists(path))
{
var str = File.ReadAllText(path);
str = str.Replace("\\/", "/");
using (var sw = new StreamWriter(path, false, new UTF8Encoding(false)))
sw.Write(str);
}
}
private void ExportCanvas(Canvas targetCanvas, out Canvas canvasGameObject, out VCCanvasDef canvasDef, out VCCanvas vCanvas)
{
canvasGameObject = Instantiate(targetCanvas);
canvasGameObject.name = "OutputCanvas";
//JSON出力
var zMpy = zFlip ? -1 : 1;
canvasDef = new VCCanvasDef();
canvasDef.Z = (int)(canvasGameObject.transform.localPosition.z * zMpy);
vCanvas = canvasGameObject.GetComponent();
if (vCanvas != null)
{
canvasDef.Version = vCanvas.Version;
canvasDef.Z = vCanvas.Z;
}
canvasDef.Layout = new List();
foreach (Transform layer in canvasGameObject.transform)
{
ExportContents(zMpy, canvasDef, layer);
}
var scripts = new List();
foreach (var textAsset in vCanvas.Scripts)
{
var assetPath = AssetDatabase.GetAssetPath(textAsset);
// Assets/*/Canvas/~ Canvasまでをスキップ
assetPath = assetPath.Substring(assetPath.IndexOf("/Canvas/",StringComparison.CurrentCulture) + 1);
scripts.Add(assetPath);
CopyAssets(textAsset);
}
canvasDef.Scripts = scripts.Distinct().ToList();
}
private void ExportContents(int zMpy, VCCanvasDef canvasDef, Transform layer)
{
var rectTransform = layer.GetComponent();
var vLayoutList = layer.GetComponent();
if (rectTransform != null)
{
var layerDef = new VCLayerDef();
layerDef.Name = layer.name;
layerDef.Show = layer.gameObject.activeSelf;
layerDef.Z = (int)(rectTransform.localPosition.z * zMpy);
layerDef.SpreadMode = vLayoutList.SpreadMode;
layerDef.AutoLoading = vLayoutList.AutoLoading;
layerDef.Components = vLayoutList.Components;
layerDef.Mask = ExportLayerMask(vLayoutList);
layerDef.Gui = new List();
foreach (Transform child in layer.transform)
{
var childRect = child.gameObject.GetComponent();
if (childRect == null)
continue;
// 関係しうる全てのコンポーネントをチェック
var vcTransform = child.gameObject.GetComponent();
var button = child.GetComponent