A game about forced loneliness, made by TACStudios
1using System;
2using Unity.Multiplayer.Center.Analytics;
3using UnityEditor;
4using UnityEngine;
5using UnityEngine.UIElements;
6
7namespace Unity.Multiplayer.Center.Window
8{
9 internal class MultiplayerCenterWindow : EditorWindow, ISerializationCallbackReceiver
10 {
11 const string k_PathInPackage = "Packages/com.unity.multiplayer.center/Editor/MultiplayerCenterWindow";
12 const string k_SpinnerClassName = "processing";
13 const string k_SessionStateDomainReloadKey = "MultiplayerCenter.InDomainReload";
14
15 VisualElement m_SpinningIcon;
16
17 /// <summary>
18 /// Nest the main container in a VisualElement to allow for easy enabling/disabling of the entire window but
19 /// without the spinning icon.
20 /// </summary>
21 VisualElement m_MainContainer;
22
23 Vector2 m_WindowSize = new(350, 300);
24
25 public int CurrentTab => m_TabGroup.CurrentTab;
26
27 // Testing purposes only. We don't want to set CurrentTab from window
28 internal int CurrentTabTest
29 {
30 get => m_TabGroup.CurrentTab;
31 set => m_TabGroup.SetSelected(value);
32 }
33
34 [SerializeField]
35 bool m_RequestGettingStartedTabAfterDomainReload = false;
36
37 [SerializeField]
38 TabGroup m_TabGroup;
39
40 /// <summary>
41 /// This is the reference Multiplayer Center analytics implementation. This class owns it.
42 /// </summary>
43 IMultiplayerCenterAnalytics m_MultiplayerCenterAnalytics;
44
45 IMultiplayerCenterAnalytics MultiplayerCenterAnalytics => m_MultiplayerCenterAnalytics ??= MultiplayerCenterAnalyticsFactory.Create();
46
47 [MenuItem("Window/Multiplayer/Multiplayer Center")]
48 public static void OpenWindow()
49 {
50 var showUtility = false; // TODO: figure out if it would be a good idea to have a utility window (always on top, cannot be tabbed)
51 GetWindow<MultiplayerCenterWindow>(showUtility, "Multiplayer Center", true);
52 }
53
54 void OnEnable()
55 {
56 // Adjust window size based on dpi scaling
57 var dpiScale = EditorGUIUtility.pixelsPerPoint;
58 minSize = new Vector2(m_WindowSize.x * dpiScale, m_WindowSize.y * dpiScale);
59
60 AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeDomainReload;
61 AssemblyReloadEvents.afterAssemblyReload -= OnAfterDomainReload;
62 AssemblyReloadEvents.beforeAssemblyReload += OnBeforeDomainReload;
63 AssemblyReloadEvents.afterAssemblyReload += OnAfterDomainReload;
64 }
65
66 void OnDisable()
67 {
68 AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeDomainReload;
69 AssemblyReloadEvents.afterAssemblyReload -= OnAfterDomainReload;
70 }
71
72 /// <summary>
73 /// Changes Tab from Recommendation to the Quickstart tab.
74 /// </summary>
75 public void RequestShowGettingStartedTabAfterDomainReload()
76 {
77 m_RequestGettingStartedTabAfterDomainReload = true;
78
79 // If no domain reload is necessary, this will be called.
80 // If domain reload is necessary, the delay call will be forgotten, but CreateGUI will be called like after any domain reload
81 // An extra delay is added to make sure that the visibility conditions of the Quickstart tab have been
82 // fully evaluated. This solves MTT-8939.
83 EditorApplication.delayCall += () =>
84 {
85 rootVisualElement.schedule.Execute(CallCreateGuiWithQuickstartRequest).ExecuteLater(300);
86 };
87 }
88
89 internal void DisableUiForInstallation()
90 {
91 SetSpinnerIconRotating();
92 m_MainContainer.SetEnabled(false);
93 }
94
95 internal void ReenableUiAfterInstallation()
96 {
97 RemoveSpinnerIconRotating();
98 m_MainContainer.SetEnabled(true);
99 }
100
101 void Update()
102 {
103 // Restore the GUI if it was cleared in OnBeforeSerialize.
104 if (m_TabGroup == null || m_TabGroup.ViewCount < 1)
105 {
106 CreateGUI();
107 }
108 }
109
110 void CreateGUI()
111 {
112 rootVisualElement.name = "root";
113 m_MainContainer ??= new VisualElement();
114 m_MainContainer.name = "recommendation-tab-container";
115 m_MainContainer.Clear();
116 rootVisualElement.Add(m_MainContainer);
117 m_SpinningIcon = new VisualElement();
118 var theme = EditorGUIUtility.isProSkin ? "dark" : "light";
119 rootVisualElement.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>($"{k_PathInPackage}/UI/{theme}.uss"));
120 rootVisualElement.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>($"{k_PathInPackage}/UI/MultiplayerCenterWindow.uss"));
121
122 if (m_TabGroup == null || m_TabGroup.ViewCount < 1 || !m_TabGroup.TabsAreValid())
123 m_TabGroup = new TabGroup(MultiplayerCenterAnalytics, new ITabView[] {new RecommendationTabView(), new GettingStartedTabView()});
124 else // since we are not serializing the analytics provider, we need to set it again
125 m_TabGroup.MultiplayerCenterAnalytics = MultiplayerCenterAnalytics;
126
127 m_TabGroup.CreateTabs();
128 m_MainContainer.Add(m_TabGroup.Root);
129
130 var installationInProgress = !PackageManagement.IsInstallationFinished();
131 SetWindowContentEnabled(installationInProgress, m_RequestGettingStartedTabAfterDomainReload);
132 ShowAppropriateTab(installationInProgress);
133 }
134
135 void ShowAppropriateTab(bool installationInProgress)
136 {
137 if (installationInProgress)
138 {
139 PackageManagement.RegisterToExistingInstaller(b => RequestShowGettingStartedTabAfterDomainReload());
140 m_TabGroup.SetSelected(0, force: true);
141 return;
142 }
143
144 if (m_RequestGettingStartedTabAfterDomainReload)
145 {
146 m_RequestGettingStartedTabAfterDomainReload = false;
147 m_TabGroup.SetSelected(1, force: true);
148 }
149 else
150 {
151 m_TabGroup.SetSelected(m_TabGroup.CurrentTab, force: true);
152 }
153 }
154
155 void SetWindowContentEnabled(bool installationInProgress, bool quickstartRequested)
156 {
157 m_MainContainer.SetEnabled(!installationInProgress || quickstartRequested);
158
159 // if we are current already processing an installation, show the spinning icon
160 if (installationInProgress)
161 {
162 // Wait a bit because the animation does not trigger when we call this in CreateGUI
163 EditorApplication.delayCall += SetSpinnerIconRotating;
164 }
165
166 rootVisualElement.Add(m_SpinningIcon);
167 }
168
169 void CallCreateGuiWithQuickstartRequest()
170 {
171 // Interestingly, setting this before registering the delay call sometimes results in the value
172 // being false when CreateGUI starts, so we set it again here.
173 m_RequestGettingStartedTabAfterDomainReload = true;
174 CreateGUI();
175 }
176
177 void SetSpinnerIconRotating()
178 {
179 m_SpinningIcon.AddToClassList(k_SpinnerClassName);
180 }
181
182 void RemoveSpinnerIconRotating()
183 {
184 m_SpinningIcon?.RemoveFromClassList(k_SpinnerClassName);
185 }
186
187 void ClearTabs()
188 {
189 m_TabGroup?.Clear();
190 m_TabGroup = null;
191 }
192
193 // This will not get called when the Editor is closed.
194 void OnDestroy()
195 {
196 ClearTabs();
197 }
198
199 static void OnBeforeDomainReload()
200 {
201 SessionState.SetBool(k_SessionStateDomainReloadKey, true);
202 }
203
204 static void OnAfterDomainReload()
205 {
206 SessionState.SetBool(k_SessionStateDomainReloadKey, false);
207 }
208
209 public void OnBeforeSerialize()
210 {
211 // ClearTabs if the Window gets serialized, but we are not in DomainReload
212 // This happens when the Editor closes or the WindowLayout is saved by the user.
213 // This ensures that the State of the Tabs is not serialized into the WindowLayout of the User.
214 if (SessionState.GetBool(k_SessionStateDomainReloadKey, false) == false)
215 {
216 ClearTabs();
217 }
218 }
219
220 public void OnAfterDeserialize()
221 {
222 // Empty on purpose.
223 }
224 }
225}