mirror of
https://github.com/RaindropViewer/RaindropViewer.git
synced 2026-07-29 19:03:10 +00:00
573 lines
24 KiB
C#
573 lines
24 KiB
C#
//#define NOTCH_SOLUTION_DEBUG_TRANSITIONS
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditor.Build;
|
|
using UnityEditor.Build.Reporting;
|
|
using UnityEditor.Callbacks;
|
|
using UnityEditor.Experimental.SceneManagement;
|
|
using UnityEditor.SceneManagement;
|
|
using UnityEditor.ShortcutManagement;
|
|
using UnityEngine;
|
|
|
|
//For bugfix hack
|
|
//For bugfix hack
|
|
|
|
namespace E7.NotchSolution.Editor
|
|
{
|
|
/// <summary>
|
|
/// Notch Solution components can receive simulated device values in editor from this
|
|
/// instead of the usual <see cref="Screen"/> API. Also the mockup overlay is provided by
|
|
/// an invisible full screen canvas game object with <see cref="HideFlags.HideAndDontSave"/>.
|
|
/// </summary>
|
|
internal class NotchSimulator : EditorWindow, IHasCustomMenu, IPreprocessBuildWithReport //For bugfix hack
|
|
{
|
|
private const string prefix = "NoSo";
|
|
private const string mockupCanvasName = prefix + "-MockupCanvas";
|
|
private const HideFlags overlayCanvasFlag = HideFlags.HideAndDontSave;
|
|
private static NotchSimulator win;
|
|
private static Vector2 gameviewResolution;
|
|
|
|
private static MockupCanvas mockupCanvas;
|
|
private static MockupCanvas prefabMockupCanvas;
|
|
|
|
private static bool eventAdded;
|
|
|
|
private Vector2 scrollPos;
|
|
|
|
internal static bool IsOpen => win;
|
|
|
|
/// <summary>
|
|
/// This need to return both from normal scene and prefab environment scene.
|
|
/// </summary>
|
|
internal static IEnumerable<MockupCanvas> AllMockupCanvases
|
|
{
|
|
get
|
|
{
|
|
if (mockupCanvas == null)
|
|
{
|
|
mockupCanvas = GameObject.Find(mockupCanvasName)?.GetComponent<MockupCanvas>();
|
|
}
|
|
|
|
if (mockupCanvas != null)
|
|
{
|
|
yield return mockupCanvas;
|
|
}
|
|
|
|
if (prefabMockupCanvas == null)
|
|
{
|
|
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
if (prefabStage != null)
|
|
{
|
|
prefabMockupCanvas = prefabStage.stageHandle.FindComponentOfType<MockupCanvas>();
|
|
}
|
|
}
|
|
|
|
if (prefabMockupCanvas != null)
|
|
{
|
|
yield return prefabMockupCanvas;
|
|
}
|
|
}
|
|
}
|
|
|
|
[ExecuteInEditMode]
|
|
private void OnEnable()
|
|
{
|
|
SimulationDatabase.Refresh();
|
|
EditorApplication.update += RespawnMockup;
|
|
}
|
|
|
|
[ExecuteInEditMode] private void OnDisable()
|
|
{
|
|
EditorApplication.update -= RespawnMockup;
|
|
}
|
|
|
|
/// <summary>
|
|
/// It is currently active only when Notch Simulator tab is present.
|
|
/// </summary>
|
|
private void OnGUI()
|
|
{
|
|
win = this;
|
|
|
|
//Sometimes even with flag I can see it in hierarchy until I move a mouse over it??
|
|
EditorApplication.RepaintHierarchyWindow();
|
|
|
|
scrollPos = EditorGUILayout.BeginScrollView(scrollPos);
|
|
EditorGUI.BeginChangeCheck();
|
|
|
|
var settings = Settings.Instance;
|
|
|
|
var switchConfigShortcut = ShortcutManager.instance
|
|
.GetShortcutBinding(NotchSolutionShortcuts.switchConfigurationShortcut).ToString();
|
|
if (string.IsNullOrEmpty(switchConfigShortcut))
|
|
{
|
|
switchConfigShortcut = "None";
|
|
}
|
|
|
|
var simulateShortcut = ShortcutManager.instance
|
|
.GetShortcutBinding(NotchSolutionShortcuts.toggleSimulationShortcut).ToString();
|
|
if (string.IsNullOrEmpty(simulateShortcut))
|
|
{
|
|
simulateShortcut = "None";
|
|
}
|
|
|
|
settings.EnableSimulation =
|
|
EditorGUILayout.BeginToggleGroup($"Simulate ({simulateShortcut})", settings.EnableSimulation);
|
|
|
|
var previousIndex = settings.ActiveConfiguration.DeviceIndex;
|
|
var currentIndex = Mathf.Clamp(settings.ActiveConfiguration.DeviceIndex, 0,
|
|
SimulationDatabase.KeyList.Length - 1);
|
|
|
|
var selectedIndex = EditorGUILayout.Popup(currentIndex, SimulationDatabase.KeyList);
|
|
settings.ActiveConfiguration.Orientation =
|
|
(PreviewOrientation) EditorGUILayout.EnumPopup("Orientation", settings.ActiveConfiguration.Orientation);
|
|
settings.ActiveConfiguration.GameViewSize =
|
|
(GameViewSizePolicy) EditorGUILayout.EnumPopup("Game View Size",
|
|
settings.ActiveConfiguration.GameViewSize);
|
|
|
|
if (GUILayout.Button($"{settings.ActiveConfiguration.ConfigurationName} ({switchConfigShortcut})",
|
|
EditorStyles.helpBox))
|
|
{
|
|
NotchSolutionShortcuts.SwitchConfiguration();
|
|
}
|
|
|
|
EditorGUILayout.EndToggleGroup();
|
|
|
|
settings.ActiveConfiguration.DeviceIndex = selectedIndex;
|
|
|
|
var simulationDevice = SimulationDatabase.Get(SimulationDatabase.KeyList[selectedIndex]);
|
|
|
|
//Draw warning about wrong aspect ratio
|
|
if (settings.EnableSimulation && simulationDevice != null)
|
|
{
|
|
if (NotchSolutionUtilityEditor.UnityDeviceSimulatorActive)
|
|
{
|
|
EditorGUILayout.HelpBox(
|
|
"Device preview won't resize automatically because Unity Device Simulator window is open.",
|
|
MessageType.Warning);
|
|
}
|
|
|
|
var gameViewOrientation = NotchSimulatorUtility.GetGameViewOrientation();
|
|
|
|
var gameViewSize = NotchSimulatorUtility.GetMainGameViewSize();
|
|
if (gameViewOrientation == ScreenOrientation.Landscape)
|
|
{
|
|
var flip = gameViewSize.x;
|
|
gameViewSize.x = gameViewSize.y;
|
|
gameViewSize.y = flip;
|
|
}
|
|
|
|
var screen = simulationDevice.Screens.FirstOrDefault();
|
|
if (screen != null)
|
|
{
|
|
var simAspect = NotchSolutionUtilityEditor.ScreenRatio(new Vector2(screen.width, screen.height));
|
|
var gameViewAspect = NotchSolutionUtilityEditor.ScreenRatio(gameViewSize);
|
|
var aspectDiff = Math.Abs(simAspect.x / simAspect.y - gameViewAspect.x / gameViewAspect.y);
|
|
if (aspectDiff > 0.01f)
|
|
{
|
|
EditorGUILayout.HelpBox(
|
|
"The selected simulation device has an aspect ratio of " +
|
|
$"{simAspect.y}:{simAspect.x} ({screen.height}x{screen.width}) but your game view is currently " +
|
|
$"in aspect {gameViewAspect.y}:{gameViewAspect.x} ({gameViewSize.y}x{gameViewSize.x}). " +
|
|
"The overlay mockup won't be displayed correctly.",
|
|
MessageType.Warning);
|
|
}
|
|
}
|
|
}
|
|
|
|
var changed = EditorGUI.EndChangeCheck();
|
|
|
|
if (changed)
|
|
{
|
|
UpdateAllMockups();
|
|
settings.Save();
|
|
}
|
|
|
|
EditorGUILayout.EndScrollView();
|
|
|
|
UpdateSimulatorTargets();
|
|
}
|
|
|
|
//IHasCustomMenu
|
|
public void AddItemsToMenu(GenericMenu menu)
|
|
{
|
|
menu.AddItem(new GUIContent("Reset Settings"), false, ResetSettings);
|
|
menu.AddItem(new GUIContent("Refresh Device List"), false, SimulationDatabase.Refresh);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Part of the IPreprocessBuildWithReport
|
|
/// </summary>
|
|
public int callbackOrder => 0;
|
|
|
|
/// <summary>
|
|
/// Bugfix hack
|
|
/// https://github.com/5argon/NotchSolution/issues/11
|
|
/// https://fogbugz.unity3d.com/default.asp?1157422_sfvtcfi1jmvc3702
|
|
/// https://fogbugz.unity3d.com/default.asp?1167068_4884utp26ji27ro0
|
|
/// </summary>
|
|
public void OnPreprocessBuild(BuildReport report)
|
|
{
|
|
//Unity has a bug that any scene change not ordered by you (building, or execute a test)
|
|
//could not destroy a DontSave game object (Hide or not doesn't matter) and instead logs some errors.
|
|
//This is a problem on building since any console error will fail the build.
|
|
//We will hack it to destroy it ourselve first before the scene could change.
|
|
|
|
//However this destroy we are doing is also subjected to the same error, and will fail the build as this
|
|
//preprocess build callback is called when you are already in a build. To counter this, we set a hide flag
|
|
//to None (risk of being baked in the scene? I think not as long as we didn't save it) to not cause the bug
|
|
//then quickly destroy it without errors.
|
|
|
|
//The error when you run a Test Runner is still there though, because Unity didn't provide a callback
|
|
//when entering test for me to hack my way into it. At least you can build the game while NoSo is running.
|
|
|
|
//I have submitted the bug since 2019.1 release, now 2019.2 is here but it is still not fixed.
|
|
|
|
if (mockupCanvas != null)
|
|
{
|
|
mockupCanvas.gameObject.hideFlags = HideFlags.None;
|
|
DestroyImmediate(mockupCanvas.gameObject);
|
|
mockupCanvas = null;
|
|
}
|
|
|
|
if (prefabMockupCanvas != null)
|
|
{
|
|
prefabMockupCanvas.gameObject.hideFlags = HideFlags.None;
|
|
DestroyImmediate(prefabMockupCanvas.gameObject);
|
|
prefabMockupCanvas = null;
|
|
}
|
|
}
|
|
|
|
[MenuItem("Window/General/Notch Simulator")]
|
|
public static void ShowWindow()
|
|
{
|
|
win = (NotchSimulator) GetWindow(typeof(NotchSimulator));
|
|
win.titleContent = new GUIContent("Notch Simulator");
|
|
}
|
|
|
|
private void ResetSettings()
|
|
{
|
|
Settings.Instance.Reset();
|
|
Debug.Log("Reset all Notch Solution settings.");
|
|
}
|
|
|
|
internal static void RespawnMockup()
|
|
{
|
|
//When the game view is changed, the mockup sometimes disappears or isn't scaled correctly
|
|
if (gameviewResolution != NotchSimulatorUtility.GetMainGameViewSize())
|
|
{
|
|
DestroyHiddenCanvas(); //So we delete the old canvas
|
|
UpdateAllMockups(); //And we respawn it
|
|
UpdateSimulatorTargets();
|
|
gameviewResolution = NotchSimulatorUtility.GetMainGameViewSize(); //Update the saved game view
|
|
}
|
|
}
|
|
|
|
internal static void Redraw()
|
|
{
|
|
win?.Repaint();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all <see cref="INotchSimulatorTarget"/> and update them.
|
|
/// </summary>
|
|
internal static void UpdateSimulatorTargets()
|
|
{
|
|
var enableSimulation = Settings.Instance.EnableSimulation;
|
|
var selectedDevice = SimulationDatabase.ByIndex(Settings.Instance.ActiveConfiguration.DeviceIndex);
|
|
|
|
var simulatedRectRelative = enableSimulation && selectedDevice != null
|
|
? NotchSimulatorUtility.CalculateSimulatorSafeAreaRelative(selectedDevice)
|
|
: NotchSolutionUtility.defaultSafeArea;
|
|
var simulatedCutoutsRelative = enableSimulation && selectedDevice != null
|
|
? NotchSimulatorUtility.CalculateSimulatorCutoutsRelative(selectedDevice)
|
|
: NotchSolutionUtility.defaultCutouts;
|
|
|
|
var normalSceneSimTargets = FindObjectsOfType<MonoBehaviour>().OfType<INotchSimulatorTarget>();
|
|
foreach (var nst in normalSceneSimTargets)
|
|
{
|
|
nst.SimulatorUpdate(simulatedRectRelative, simulatedCutoutsRelative);
|
|
}
|
|
|
|
//Now find one in the prefab mode scene as well
|
|
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
if (prefabStage != null)
|
|
{
|
|
var prefabSceneSimTargets = prefabStage.stageHandle.FindComponentsOfType<MonoBehaviour>()
|
|
.OfType<INotchSimulatorTarget>();
|
|
foreach (var nst in prefabSceneSimTargets)
|
|
{
|
|
nst.SimulatorUpdate(simulatedRectRelative, simulatedCutoutsRelative);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// We lose all events on entering play mode, use this to register the event and also make a canvas again
|
|
/// after it was destroyed by the event (that now disappeared)
|
|
/// </summary>
|
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
|
|
private static void AddOverlayInPlayMode()
|
|
{
|
|
UpdateAllMockups();
|
|
}
|
|
|
|
/// <summary>
|
|
/// This is called even if Notch Simulator tab is not present on the screen.
|
|
/// Also have to handle if we reload scripts while in prefab mode.
|
|
/// </summary>
|
|
[DidReloadScripts]
|
|
private static void OnScriptsReloaded()
|
|
{
|
|
//DebugTransitions($"Script reloaded PLAY {EditorApplication.isPlaying} PLAY or WILL CHANGE {EditorApplication.isPlayingOrWillChangePlaymode}");
|
|
|
|
//Avoid script reload due to entering playmode
|
|
if (EditorApplication.isPlayingOrWillChangePlaymode == false)
|
|
{
|
|
UpdateAllMockups();
|
|
}
|
|
}
|
|
|
|
private static void DestroyHiddenCanvas()
|
|
{
|
|
foreach (var mockup in AllMockupCanvases)
|
|
{
|
|
if (EditorApplication.isPlaying)
|
|
{
|
|
Destroy(mockup.gameObject);
|
|
}
|
|
else
|
|
{
|
|
DestroyImmediate(mockup.gameObject, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
internal static void UpdateAllMockups()
|
|
{
|
|
//When building, the scene may open-close multiple times and brought back the mockup canvas,
|
|
//which combined with bugs mentioned at https://github.com/5argon/NotchSolution/issues/11,
|
|
//will fail the build. This `if` prevents mockup refresh while building.
|
|
if (BuildPipeline.isBuildingPlayer)
|
|
{
|
|
return;
|
|
}
|
|
|
|
EnsureCanvasAndEventSetup();
|
|
|
|
//Make the editing environment contains an another copy of mockup canvas.
|
|
var prefabStage = PrefabStageUtility.GetCurrentPrefabStage();
|
|
if (prefabStage != null)
|
|
{
|
|
EnsureCanvasAndEventSetup(prefabStage);
|
|
}
|
|
|
|
var settings = Settings.Instance;
|
|
var enableSimulation = settings.EnableSimulation;
|
|
var selectedDevice = SimulationDatabase.ByIndex(Settings.Instance.ActiveConfiguration.DeviceIndex);
|
|
var landscape = Settings.Instance.ActiveConfiguration.Orientation == PreviewOrientation.LandscapeLeft ||
|
|
Settings.Instance.ActiveConfiguration.Orientation == PreviewOrientation.LandscapeRight;
|
|
|
|
if (enableSimulation && selectedDevice != null)
|
|
{
|
|
var screen = selectedDevice.Screens.FirstOrDefault();
|
|
GameViewResolution.SetResolution(landscape ? screen.height : screen.width,
|
|
landscape ? screen.width : screen.height,
|
|
Settings.Instance.ActiveConfiguration.GameViewSize == GameViewSizePolicy.MatchAspectRatio);
|
|
|
|
var name = selectedDevice.Meta.overlay;
|
|
Sprite mockupSprite = null;
|
|
if (!string.IsNullOrEmpty(name))
|
|
{
|
|
var filePath = Path.Combine(NotchSimulatorUtility.DevicesFolder, name);
|
|
mockupSprite = AssetDatabase.LoadAssetAtPath<Sprite>(filePath);
|
|
if (mockupSprite == null)
|
|
{
|
|
if (File.Exists(filePath))
|
|
{
|
|
var tex = new Texture2D(1, 1);
|
|
tex.LoadImage(File.ReadAllBytes(filePath));
|
|
mockupSprite = Sprite.Create(tex, new Rect(0.0f, 0.0f, tex.width, tex.height),
|
|
new Vector2(0.5f, 0.5f), 100.0f);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning(
|
|
$"No mockup image named {name} in {NotchSimulatorUtility.DevicesFolder} folder!");
|
|
}
|
|
}
|
|
}
|
|
|
|
foreach (var mockup in AllMockupCanvases)
|
|
{
|
|
mockup.UpdateMockupSprite(
|
|
mockupSprite,
|
|
NotchSimulatorUtility.GetGameViewOrientation(),
|
|
enableSimulation,
|
|
settings.FlipOrientation,
|
|
Settings.Instance.PrefabModeOverlayColor
|
|
);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DestroyHiddenCanvas();
|
|
GameViewResolution.ClearResolution();
|
|
}
|
|
}
|
|
|
|
private static void DebugTransitions(string s)
|
|
{
|
|
#if NOTCH_SOLUTION_DEBUG_TRANSITIONS
|
|
Debug.Log(s);
|
|
#endif
|
|
}
|
|
|
|
|
|
/// <param name="prefabStage">
|
|
/// If not <c>null</c>, look for the mockup canvas on environment scene
|
|
/// for editing a prefab <b>instead</b> of normal scenes.
|
|
/// </param>
|
|
private static void EnsureCanvasAndEventSetup(PrefabStage prefabStage = null)
|
|
{
|
|
//Create the hidden canvas if not already.
|
|
var prefabMode = prefabStage != null;
|
|
var selectedMockupCanvas = prefabMode ? prefabMockupCanvas : mockupCanvas;
|
|
|
|
if (selectedMockupCanvas == null)
|
|
{
|
|
//Find existing in the case of assembly reload
|
|
//For some reason GameObject.FindObjectOfType could not get the canvas on main scene, it is active also, but by name works...
|
|
var canvasObject = prefabMode
|
|
? prefabStage.stageHandle.FindComponentOfType<MockupCanvas>()
|
|
: GameObject.Find(mockupCanvasName)?.GetComponent<MockupCanvas>();
|
|
if (canvasObject != null)
|
|
{
|
|
DebugTransitions($"[Notch Solution] Found existing (Prefab mode {prefabMode})");
|
|
}
|
|
else
|
|
{
|
|
var prefabGuids = AssetDatabase.FindAssets(mockupCanvasName);
|
|
if (prefabGuids.Length == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
DebugTransitions($"[Notch Solution] Creating canvas (Prefab mode {prefabMode})");
|
|
var mockupCanvasPrefab =
|
|
AssetDatabase.LoadAssetAtPath<GameObject>(AssetDatabase.GUIDToAssetPath(prefabGuids.First()));
|
|
|
|
var instantiated =
|
|
prefabMode
|
|
? (GameObject) PrefabUtility.InstantiatePrefab(mockupCanvasPrefab, prefabStage.scene)
|
|
: (GameObject) PrefabUtility.InstantiatePrefab(mockupCanvasPrefab);
|
|
|
|
//It sometimes instantiated into null on script reloading when starting Unity?
|
|
if (instantiated != null)
|
|
{
|
|
canvasObject = instantiated.GetComponent<MockupCanvas>();
|
|
instantiated.hideFlags = overlayCanvasFlag;
|
|
|
|
if (Application.isPlaying)
|
|
{
|
|
DontDestroyOnLoad(canvasObject);
|
|
}
|
|
|
|
canvasObject.PrefabStage = prefabMode;
|
|
if (prefabMode)
|
|
{
|
|
prefabMockupCanvas = canvasObject;
|
|
}
|
|
else
|
|
{
|
|
mockupCanvas = canvasObject;
|
|
}
|
|
}
|
|
|
|
if (eventAdded == false)
|
|
{
|
|
eventAdded = true;
|
|
|
|
//Add clean up event.
|
|
EditorApplication.playModeStateChanged += PlayModeStateChangeAction;
|
|
// EditorSceneManager.sceneClosing += (a, b) =>
|
|
// {
|
|
// DebugTransitions($"Scene closing {a} {b}");
|
|
// };
|
|
// EditorSceneManager.sceneClosed += (a) =>
|
|
// {
|
|
// DebugTransitions($"Scene closed {a}");
|
|
// };
|
|
// EditorSceneManager.sceneLoaded += (a, b) =>
|
|
// {
|
|
// DebugTransitions($"Scene loaded {a} {b}");
|
|
// };
|
|
// EditorSceneManager.sceneUnloaded += (a) =>
|
|
// {
|
|
// DebugTransitions($"Scene unloaded {a}");
|
|
// };
|
|
PrefabStage.prefabStageOpened += ps =>
|
|
{
|
|
DebugTransitions(
|
|
$"Prefab opening {ps.scene.GetRootGameObjects().First().name} {ps.prefabContentsRoot.name}");
|
|
|
|
//On open prefab, the "dont save" objects on the main scene will disappear too.
|
|
//So that we could still see it in the game view WHILE editing a prefab, we make it back.
|
|
//Along with this the prefab mode canvas will also be updated.
|
|
UpdateAllMockups();
|
|
|
|
//On entering prefab mode, the Notch Simulator panel did not get OnGUI().
|
|
UpdateSimulatorTargets();
|
|
};
|
|
|
|
PrefabStage.prefabStageClosing += ps =>
|
|
{
|
|
DebugTransitions(
|
|
$"Prefab closing {ps.scene.GetRootGameObjects().First().name} {ps.prefabContentsRoot.name}");
|
|
//There is no problem on closing prefab stage, no need to restore the outer mockup.
|
|
};
|
|
|
|
EditorSceneManager.sceneOpening += (a, b) =>
|
|
{
|
|
DebugTransitions($"Scene opening {a} {b}");
|
|
DestroyHiddenCanvas();
|
|
};
|
|
|
|
EditorSceneManager.sceneOpened += (a, b) =>
|
|
{
|
|
DebugTransitions($"Scene opened {a} {b}");
|
|
UpdateAllMockups();
|
|
};
|
|
|
|
void PlayModeStateChangeAction(PlayModeStateChange state)
|
|
{
|
|
DebugTransitions(
|
|
$"Changed state PLAY {EditorApplication.isPlaying} PLAY or WILL CHANGE {EditorApplication.isPlayingOrWillChangePlaymode}");
|
|
switch (state)
|
|
{
|
|
case PlayModeStateChange.EnteredEditMode:
|
|
DebugTransitions($"Entered Edit {canvasObject}");
|
|
AddOverlayInPlayMode(); //For when coming back from play mode.
|
|
break;
|
|
case PlayModeStateChange.EnteredPlayMode:
|
|
DebugTransitions($"Entered Play {canvasObject}");
|
|
break;
|
|
case PlayModeStateChange.ExitingEditMode:
|
|
DebugTransitions($"Exiting Edit {canvasObject}");
|
|
DestroyHiddenCanvas(); //Clean up the DontSave canvas we made in edit mode.
|
|
break;
|
|
case PlayModeStateChange.ExitingPlayMode:
|
|
DebugTransitions($"Exiting Play {canvasObject}");
|
|
DestroyHiddenCanvas(); //Clean up the DontSave canvas we made in play mode.
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} |