2025-05-01 01:48:08 -07:00

300 lines
14 KiB
C#

#if ENABLE_PROPERTY_VARIANTS || PACKAGE_DOCS_GENERATION
using System.Linq;
using UnityEngine;
using UnityEngine.Localization;
using UnityEngine.Localization.PropertyVariants;
using UnityEngine.Localization.PropertyVariants.TrackedProperties;
using UnityEngine.Localization.Settings;
using UnityEngine.Localization.Tables;
namespace UnityEditor.Localization.PropertyVariants
{
[InitializeOnLoad]
static class ScenePropertyTracker
{
internal static bool ProcessingUndoData;
static ScenePropertyTracker()
{
Undo.postprocessModifications += PostProcessModifications;
ObjectChangeEvents.changesPublished += ChangesPublished; // 2020.2 feature
}
/// <summary>
/// Checks if a tracked component was removed, if it was then remove the object tracker and
/// create an Undo record so it can be undone with the object deletion.
/// </summary>
/// <param name="stream"></param>
static void ChangesPublished(ref ObjectChangeEventStream stream)
{
for (int i = 0; i < stream.length; ++i)
{
if (stream.GetEventType(i) != ObjectChangeKind.ChangeGameObjectStructure)
continue;
stream.GetChangeGameObjectStructureEvent(i, out var eventArgs);
if (!(EditorUtility.InstanceIDToObject(eventArgs.instanceId) is GameObject goAsset
&& goAsset.GetComponent<GameObjectLocalizer>() is {} objectLocalizer)) continue;
var removedComponents = objectLocalizer.TrackedObjects.Where(t => t.Target == null).ToList();
if (removedComponents.Count == 0)
continue;
Undo.RecordObject(objectLocalizer, "Remove Tracked Object");
foreach (var r in removedComponents)
{
objectLocalizer.TrackedObjects.Remove(r);
}
if (objectLocalizer.TrackedObjects.Count == 0)
{
Undo.DestroyObjectImmediate(objectLocalizer);
}
}
}
internal static UndoPropertyModification[] PostProcessModifications(UndoPropertyModification[] modifications)
{
if (ProcessingUndoData)
return modifications;
// If we detect a change to a LocalizedTable then we force a refresh to the editor preview.
foreach (var mod in modifications)
{
if (mod.currentValue?.target is LocalizationTable)
{
EditorApplication.delayCall += LocalizationEditorSettings.RefreshEditorPreview;
break;
}
}
if (Application.isPlaying || !LocalizationSettings.HasSettings || LocalizationSettings.SelectedLocale == null)
return modifications;
// We want to create variant data in our GameObjectLocalizer however we also need to create Undo records for these changes.
// We can not record Undo data inside of PostProcessModifications so we defer the task and use the group id so we can fold our
// changes into this Undo operation later.
EditorApplication.delayCall += () => DelayedPostProcessModifications(Undo.GetCurrentGroup(), Undo.GetCurrentGroupName(), modifications);
return modifications;
}
static void DelayedPostProcessModifications(int undoGroup, string groupName, UndoPropertyModification[] modifications)
{
// We ignore Create undo events as they sometimes occur when UGUI swaps the Transform for a RectTransform.
// When a new object is created the Undo groupName will sometimes bleed over into the next event and wont be updated until after PostProcessModifications,
// to handle this we compare the current name against the name at the time of the event.
if ((groupName.StartsWith("Create") && Undo.GetCurrentGroupName().StartsWith("Create")) || LocalizationSettings.SelectedLocale == null)
return;
var variant = LocalizationSettings.SelectedLocale.Identifier;
var shouldAddVariant = LocalizationProjectSettings.TrackChanges && LocalizationSettings.ProjectLocale != LocalizationSettings.SelectedLocale;
var hasChanges = false;
try
{
// We don't want any Undo events processed that we may have generated, such as when we update a Localized table.
ProcessingUndoData = true;
foreach (var mod in modifications)
{
// Ignore anything to do with GameObjectLocalizer.
if (mod.currentValue?.target == null || mod.currentValue.target is GameObjectLocalizer)
continue;
// Ignore anything with no previous value. (LOC-359)
if (mod.previousValue == null)
continue;
// Ignore values that have not actually changed. Some editors will change values even though the value is the same.
if (mod.currentValue.value == mod.previousValue.value && mod.currentValue.objectReference == mod.previousValue.objectReference)
continue;
// We may need to revert all the changes we make if we find the property can not be tracked
if (UpdateTrackedSceneProperty(mod, shouldAddVariant, variant))
hasChanges = true;
}
// Fold the changes into the undo operation that made the original change
if (hasChanges)
Undo.CollapseUndoOperations(undoGroup);
}
finally
{
ProcessingUndoData = false;
}
}
static bool UpdateTrackedSceneProperty(UndoPropertyModification propertyModification, bool addNewVariant, LocaleIdentifier localeIdentifier)
{
// We only support GameObject components. Maybe more in the future
var targetComponent = propertyModification.currentValue.target as Component;
if (targetComponent == null)
return false;
// Get/Add the GameObjectLocalizer
var localizerAdded = false;
var localizer = targetComponent.GetComponent<GameObjectLocalizer>();
if (localizer == null)
{
if (!addNewVariant)
return false;
localizerAdded = true;
// We add here so it supports
localizer = Undo.AddComponent<GameObjectLocalizer>(targetComponent.gameObject);
}
Undo.RecordObject(localizer, "Record property variant");
// Get/Add the tracked object
var trackedObject = localizer.GetTrackedObject(targetComponent);
if (trackedObject == null)
{
if (!addNewVariant)
return false;
trackedObject = TrackedObjectFactory.CreateTrackedObject(targetComponent);
if (trackedObject == null)
{
Debug.LogWarning($"Could not find a TrackedObject for {targetComponent.GetType()}.");
// We just destroy the object here because a reverting the Undo group would trigger the Undo callbacks and break things such that use drag operations to make changes (slider, transform etc).
if (localizerAdded)
Object.DestroyImmediate(localizer);
return false;
}
localizer.TrackedObjects.Add(trackedObject);
}
// Get/Add the tracked property
var propertyPath = propertyModification.currentValue.propertyPath;
var trackedProperty = trackedObject.GetTrackedProperty(propertyPath);
if (trackedProperty == null)
{
if (!addNewVariant || !trackedObject.CanTrackProperty(propertyPath))
{
// We just destroy the object here because reverting the Undo group would trigger the Undo callbacks and break things (dragging operations such as sliders, transform etc).
if (localizerAdded)
Object.DestroyImmediate(localizer);
return false;
}
trackedProperty = trackedObject.CreateCustomTrackedProperty(propertyPath) ?? TrackedObjectFactory.CreateTrackedProperty(targetComponent, propertyModification.currentValue.propertyPath);
if (trackedProperty == null)
{
// We just destroy the object here because reverting the Undo group would trigger the Undo callbacks and break things (dragging operations such as sliders, transform etc).
if (localizerAdded)
Object.DestroyImmediate(localizer);
return false;
}
// TODO: If the new property is an array size or if the array size has increased then we may want to capture the new array items.
// The Undo event will only include the new size and not the new values in the array item.
// We may want to just track all elements in an array when the size value is modified.
PrepareNewTrackedProperty(propertyModification);
trackedObject.AddTrackedProperty(trackedProperty);
if (LocalizationSettings.ProjectLocale != null)
UpdateTrackedProperty(LocalizationSettings.ProjectLocale.Identifier, propertyModification.previousValue, trackedProperty);
}
// Dont add new variant values to a property when we are not adding new variants. (LOC-380).
if (!trackedProperty.HasVariant(localeIdentifier) && !addNewVariant)
return false;
UpdateTrackedProperty(localeIdentifier, propertyModification.currentValue, trackedProperty);
return true;
}
static void PrepareNewTrackedProperty(UndoPropertyModification propertyModification)
{
// The first time a property is modified it will not have been marked as driven.
// This means that it will have been changed to the selected locale and the default/scene value will not have been preserved.
// We need to revert the value back to its original, mark the property as driven and then reapply the change or the scene
// value will be whatever locale happened to be selected when the first change was made and not the one set in LocalizationSettings.
// Revert the value
var so = new SerializedObject(propertyModification.currentValue.target);
var prop = so.FindProperty(propertyModification.currentValue.propertyPath);
prop.ApplyPropertyModification(propertyModification.previousValue);
so.ApplyModifiedProperties();
// If the object has just been added we can not revert.
if (propertyModification.currentValue.target == null) return;
// Mark the property driven and set it back to its new value
VariantsPropertyDriver.RegisterProperty(propertyModification.currentValue.target, propertyModification.currentValue.propertyPath);
prop.ApplyPropertyModification(propertyModification.currentValue);
so.ApplyModifiedProperties();
}
static void UpdateTrackedProperty(LocaleIdentifier variant, PropertyModification propertyModification, ITrackedProperty trackedProperty)
{
switch (trackedProperty)
{
case IStringProperty stringProperty:
stringProperty.SetValueFromString(variant, propertyModification.value);
break;
case UnityObjectProperty objectProperty:
objectProperty.SetValue(variant, propertyModification.objectReference);
break;
case LocalizedStringProperty localizedStringProperty:
UpdateLocalizedStringProperty(variant, propertyModification, localizedStringProperty);
break;
case LocalizedAssetProperty localizedAssetProperty:
UpdateLocalizedAssetProperty(variant, propertyModification, localizedAssetProperty);
break;
}
}
static void UpdateLocalizedStringProperty(LocaleIdentifier variant, PropertyModification propertyModification, LocalizedStringProperty localizedStringProperty)
{
var locString = localizedStringProperty.LocalizedString;
var collection = LocalizationEditorSettings.GetStringTableCollection(locString.TableReference);
if (collection == null)
return;
var entry = collection.SharedData.GetEntryFromReference(locString.TableEntryReference);
if (entry == null)
return;
var stringTable = collection.GetTable(variant) as StringTable;
if (stringTable == null)
return;
Undo.RecordObject(stringTable, "Set Localized Value");
stringTable.AddEntry(entry.Id, propertyModification.value);
EditorUtility.SetDirty(stringTable);
LocalizationEditorSettings.EditorEvents.RaiseCollectionModified(null, collection);
}
static void UpdateLocalizedAssetProperty(LocaleIdentifier variant, PropertyModification propertyModification, LocalizedAssetProperty localizedAssetProperty)
{
var locObject = localizedAssetProperty.LocalizedObject;
var collection = LocalizationEditorSettings.GetAssetTableCollection(locObject.TableReference);
if (collection == null)
return;
var entry = collection.SharedData.GetEntryFromReference(locObject.TableEntryReference);
if (entry == null)
return;
var assetTable = collection.GetTable(variant) as AssetTable;
if (assetTable == null)
return;
if (propertyModification.objectReference != null)
collection.AddAssetToTable(assetTable, locObject.TableEntryReference, propertyModification.objectReference, true);
else
collection.RemoveAssetFromTable(assetTable, locObject.TableEntryReference, true);
}
}
}
#endif