HotReloadWindow.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. 
  2. using System;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Runtime.CompilerServices;
  7. using System.Text.RegularExpressions;
  8. using System.Threading;
  9. using SingularityGroup.HotReload.DTO;
  10. using SingularityGroup.HotReload.Editor.Cli;
  11. using SingularityGroup.HotReload.Editor.Semver;
  12. using UnityEditor;
  13. using UnityEditor.Compilation;
  14. using UnityEngine;
  15. [assembly: InternalsVisibleTo("SingularityGroup.HotReload.EditorSamples")]
  16. namespace SingularityGroup.HotReload.Editor {
  17. class HotReloadWindow : EditorWindow {
  18. public static HotReloadWindow Current { get; private set; }
  19. List<HotReloadTabBase> tabs;
  20. List<HotReloadTabBase> Tabs => tabs ?? (tabs = new List<HotReloadTabBase> {
  21. RunTab,
  22. SettingsTab,
  23. AboutTab,
  24. });
  25. int selectedTab;
  26. internal static Vector2 scrollPos;
  27. static Timer timer;
  28. HotReloadRunTab runTab;
  29. internal HotReloadRunTab RunTab => runTab ?? (runTab = new HotReloadRunTab(this));
  30. HotReloadSettingsTab settingsTab;
  31. internal HotReloadSettingsTab SettingsTab => settingsTab ?? (settingsTab = new HotReloadSettingsTab(this));
  32. HotReloadAboutTab aboutTab;
  33. internal HotReloadAboutTab AboutTab => aboutTab ?? (aboutTab = new HotReloadAboutTab(this));
  34. static ShowOnStartupEnum _showOnStartupOption;
  35. /// <summary>
  36. /// This token is cancelled when the EditorWindow is disabled.
  37. /// </summary>
  38. /// <remarks>
  39. /// Use it for all tasks.
  40. /// When token is cancelled, scripts are about to be recompiled and this will cause tasks to fail for weird reasons.
  41. /// </remarks>
  42. public CancellationToken cancelToken;
  43. CancellationTokenSource cancelTokenSource;
  44. static readonly PackageUpdateChecker packageUpdateChecker = new PackageUpdateChecker();
  45. [MenuItem("Window/Hot Reload/Open &#H")]
  46. internal static void Open() {
  47. // opening the window on CI systems was keeping Unity open indefinitely
  48. if (EditorWindowHelper.IsHumanControllingUs()) {
  49. if (Current) {
  50. Current.Show();
  51. Current.Focus();
  52. } else {
  53. Current = GetWindow<HotReloadWindow>();
  54. }
  55. }
  56. }
  57. [MenuItem("Window/Hot Reload/Recompile")]
  58. internal static void Recompile() {
  59. HotReloadRunTab.Recompile();
  60. }
  61. void OnInterval(object o) {
  62. HotReloadRunTab.RepaintInstant();
  63. }
  64. void OnEnable() {
  65. if (timer == null) {
  66. timer = new Timer(OnInterval, null, 20 * 1000, 20 * 1000);
  67. }
  68. Current = this;
  69. if (cancelTokenSource != null) {
  70. cancelTokenSource.Cancel();
  71. }
  72. // Set min size initially so that full UI is visible
  73. if (!HotReloadPrefs.OpenedWindowAtLeastOnce) {
  74. this.minSize = new Vector2(Constants.RecompileButtonTextHideWidth + 1, Constants.EventsListHideHeight + 70);
  75. HotReloadPrefs.OpenedWindowAtLeastOnce = true;
  76. }
  77. cancelTokenSource = new CancellationTokenSource();
  78. cancelToken = cancelTokenSource.Token;
  79. this.titleContent = new GUIContent(" Hot Reload", GUIHelper.GetInvertibleIcon(InvertibleIcon.Logo));
  80. _showOnStartupOption = HotReloadPrefs.ShowOnStartup;
  81. packageUpdateChecker.StartCheckingForNewVersion();
  82. }
  83. void Update() {
  84. foreach (var tab in Tabs) {
  85. tab.Update();
  86. }
  87. }
  88. void OnDisable() {
  89. if (cancelTokenSource != null) {
  90. cancelTokenSource.Cancel();
  91. cancelTokenSource = null;
  92. }
  93. if (Current == this) {
  94. Current = null;
  95. }
  96. timer.Dispose();
  97. timer = null;
  98. }
  99. internal void SelectTab(Type tabType) {
  100. selectedTab = Tabs.FindIndex(x => x.GetType() == tabType);
  101. }
  102. public HotReloadRunTabState RunTabState { get; private set; }
  103. void OnGUI() {
  104. // TabState ensures rendering is consistent between Layout and Repaint calls
  105. // Without it errors like this happen:
  106. // ArgumentException: Getting control 2's position in a group with only 2 controls when doing repaint
  107. // See thread for more context: https://answers.unity.com/questions/17718/argumentexception-getting-control-2s-position-in-a.html
  108. if (Event.current.type == EventType.Layout) {
  109. RunTabState = HotReloadRunTabState.Current;
  110. }
  111. using(var scope = new EditorGUILayout.ScrollViewScope(scrollPos, false, false)) {
  112. scrollPos = scope.scrollPosition;
  113. // RenderDebug();
  114. RenderTabs();
  115. }
  116. GUILayout.FlexibleSpace(); // GUI below will be rendered on the bottom
  117. if (HotReloadWindowStyles.windowScreenHeight > 90)
  118. RenderBottomBar();
  119. }
  120. void RenderDebug() {
  121. if (GUILayout.Button("RESET WINDOW")) {
  122. OnDisable();
  123. RequestHelper.RequestLogin("test", "test", 1).Forget();
  124. HotReloadPrefs.LicenseEmail = null;
  125. HotReloadPrefs.ExposeServerToLocalNetwork = true;
  126. HotReloadPrefs.LicensePassword = null;
  127. HotReloadPrefs.LoggedBurstHint = false;
  128. HotReloadPrefs.DontShowPromptForDownload = false;
  129. HotReloadPrefs.RateAppShown = false;
  130. HotReloadPrefs.ActiveDays = string.Empty;
  131. HotReloadPrefs.LaunchOnEditorStart = false;
  132. HotReloadPrefs.ShowUnsupportedChanges = true;
  133. HotReloadPrefs.RedeemLicenseEmail = null;
  134. HotReloadPrefs.RedeemLicenseInvoice = null;
  135. OnEnable();
  136. File.Delete(EditorCodePatcher.serverDownloader.GetExecutablePath(HotReloadCli.controller));
  137. InstallUtility.DebugClearInstallState();
  138. InstallUtility.CheckForNewInstall();
  139. EditorPrefs.DeleteKey(Attribution.LastLoginKey);
  140. File.Delete(RedeemLicenseHelper.registerOutcomePath);
  141. CompileMethodDetourer.Reset();
  142. AssetDatabase.Refresh();
  143. }
  144. }
  145. internal static void RenderLogo(int width = 243) {
  146. var isDarkMode = HotReloadWindowStyles.IsDarkMode;
  147. var tex = Resources.Load<Texture>(isDarkMode ? "Logo_HotReload_DarkMode" : "Logo_HotReload_LightMode");
  148. //Can happen during player builds where Editor Resources are unavailable
  149. if(tex == null) {
  150. return;
  151. }
  152. var targetWidth = width;
  153. var targetHeight = 44;
  154. GUILayout.Space(4f);
  155. // background padding top and bottom
  156. float padding = 5f;
  157. // reserve layout space for the texture
  158. var backgroundRect = GUILayoutUtility.GetRect(targetWidth + padding, targetHeight + padding, HotReloadWindowStyles.LogoStyle);
  159. // draw the texture into that reserved space. First the bg then the logo.
  160. if (isDarkMode) {
  161. GUI.DrawTexture(backgroundRect, EditorTextures.DarkGray17, ScaleMode.StretchToFill);
  162. } else {
  163. GUI.DrawTexture(backgroundRect, EditorTextures.LightGray238, ScaleMode.StretchToFill);
  164. }
  165. var foregroundRect = backgroundRect;
  166. foregroundRect.yMin += padding;
  167. foregroundRect.yMax -= padding;
  168. // during player build (EditorWindow still visible), Resources.Load returns null
  169. if (tex) {
  170. GUI.DrawTexture(foregroundRect, tex, ScaleMode.ScaleToFit);
  171. }
  172. }
  173. int? collapsedTab;
  174. void RenderTabs() {
  175. using(new EditorGUILayout.VerticalScope(HotReloadWindowStyles.BoxStyle)) {
  176. if (HotReloadWindowStyles.windowScreenHeight > 210 && HotReloadWindowStyles.windowScreenWidth > 375) {
  177. selectedTab = GUILayout.Toolbar(
  178. selectedTab,
  179. Tabs.Select(t =>
  180. new GUIContent(t.Title.StartsWith(" ", StringComparison.Ordinal) ? t.Title : " " + t.Title,
  181. t.Icon, t.Tooltip)).ToArray(),
  182. GUILayout.Height(22f) // required, otherwise largest icon height determines toolbar height
  183. );
  184. if (collapsedTab != null) {
  185. selectedTab = collapsedTab.Value;
  186. collapsedTab = null;
  187. }
  188. } else {
  189. if (collapsedTab == null) {
  190. collapsedTab = selectedTab;
  191. }
  192. // When window is super small, we pretty much can only show run tab
  193. SelectTab(typeof(HotReloadRunTab));
  194. }
  195. if (HotReloadWindowStyles.windowScreenHeight > 250 && HotReloadWindowStyles.windowScreenWidth > 275) {
  196. RenderLogo();
  197. }
  198. Tabs[selectedTab].OnGUI();
  199. }
  200. }
  201. void RenderBottomBar() {
  202. SemVersion newVersion;
  203. var updateAvailable = packageUpdateChecker.TryGetNewVersion(out newVersion);
  204. if (HotReloadWindowStyles.windowScreenWidth > Constants.RateAppHideWidth
  205. && HotReloadWindowStyles.windowScreenHeight > Constants.RateAppHideHeight
  206. ) {
  207. RenderRateApp();
  208. }
  209. if (updateAvailable) {
  210. RenderUpdateButton(newVersion);
  211. }
  212. using(new EditorGUILayout.HorizontalScope("ProjectBrowserBottomBarBg", GUILayout.ExpandWidth(true), GUILayout.Height(25f))) {
  213. RenderBottomBarCore();
  214. }
  215. }
  216. static GUIStyle _renderAppBoxStyle;
  217. static GUIStyle renderAppBoxStyle => _renderAppBoxStyle ?? (_renderAppBoxStyle = new GUIStyle(GUI.skin.box) {
  218. padding = new RectOffset(10, 10, 0, 0)
  219. });
  220. static GUILayoutOption[] _nonExpandable;
  221. public static GUILayoutOption[] NonExpandableLayout => _nonExpandable ?? (_nonExpandable = new [] {GUILayout.ExpandWidth(false), GUILayout.ExpandHeight(true)});
  222. internal static void RenderRateApp() {
  223. if (!ShouldShowRateApp()) {
  224. return;
  225. }
  226. using (new EditorGUILayout.VerticalScope(renderAppBoxStyle)) {
  227. using (new EditorGUILayout.HorizontalScope()) {
  228. HotReloadGUIHelper.HelpBox("Are you enjoying using Hot Reload?", MessageType.Info, 11);
  229. if (GUILayout.Button("Hide", NonExpandableLayout)) {
  230. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), new EditorExtraData { { "dismissed", true } }).Forget();
  231. HotReloadPrefs.RateAppShown = true;
  232. }
  233. }
  234. using (new EditorGUILayout.HorizontalScope()) {
  235. if (GUILayout.Button("Yes")) {
  236. var openedUrl = PackageConst.IsAssetStoreBuild && EditorUtility.DisplayDialog("Rate Hot Reload", "Thank you for using Hot Reload!\n\nPlease consider leaving a review on the Asset Store to support us.", "Open in browser", "Cancel");
  237. if (openedUrl) {
  238. Application.OpenURL(Constants.UnityStoreRateAppURL);
  239. }
  240. HotReloadPrefs.RateAppShown = true;
  241. var data = new EditorExtraData();
  242. if (PackageConst.IsAssetStoreBuild) {
  243. data.Add("opened_url", openedUrl);
  244. }
  245. data.Add("enjoy_app", true);
  246. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), data).Forget();
  247. }
  248. if (GUILayout.Button("No")) {
  249. HotReloadPrefs.RateAppShown = true;
  250. var data = new EditorExtraData();
  251. data.Add("enjoy_app", false);
  252. RequestHelper.RequestEditorEventWithRetry(new Stat(StatSource.Client, StatLevel.Debug, StatFeature.RateApp), data).Forget();
  253. }
  254. }
  255. }
  256. }
  257. internal static bool ShouldShowRateApp() {
  258. if (HotReloadPrefs.RateAppShown) {
  259. return false;
  260. }
  261. var activeDays = EditorCodePatcher.GetActiveDaysForRateApp();
  262. if (activeDays.Count < Constants.DaysToRateApp) {
  263. return false;
  264. }
  265. return true;
  266. }
  267. void RenderUpdateButton(SemVersion newVersion) {
  268. if (GUILayout.Button($"Update To v{newVersion}", HotReloadWindowStyles.UpgradeButtonStyle)) {
  269. packageUpdateChecker.UpdatePackageAsync(newVersion).Forget(CancellationToken.None);
  270. }
  271. }
  272. internal static void RenderShowOnStartup() {
  273. var prevLabelWidth = EditorGUIUtility.labelWidth;
  274. try {
  275. EditorGUIUtility.labelWidth = 105f;
  276. using (new GUILayout.VerticalScope()) {
  277. using (new GUILayout.HorizontalScope()) {
  278. GUILayout.Label("Show On Startup");
  279. Rect buttonRect = GUILayoutUtility.GetLastRect();
  280. if (EditorGUILayout.DropdownButton(new GUIContent(Regex.Replace(_showOnStartupOption.ToString(), "([a-z])([A-Z])", "$1 $2")), FocusType.Passive, GUILayout.Width(110f))) {
  281. GenericMenu menu = new GenericMenu();
  282. foreach (ShowOnStartupEnum option in Enum.GetValues(typeof(ShowOnStartupEnum))) {
  283. menu.AddItem(new GUIContent(Regex.Replace(option.ToString(), "([a-z])([A-Z])", "$1 $2")), false, () => {
  284. if (_showOnStartupOption != option) {
  285. _showOnStartupOption = option;
  286. HotReloadPrefs.ShowOnStartup = _showOnStartupOption;
  287. }
  288. });
  289. }
  290. menu.DropDown(new Rect(buttonRect.x, buttonRect.y, 100, 0));
  291. }
  292. }
  293. }
  294. } finally {
  295. EditorGUIUtility.labelWidth = prevLabelWidth;
  296. }
  297. }
  298. internal static readonly OpenURLButton autoRefreshTroubleshootingBtn = new OpenURLButton("Troubleshooting", Constants.TroubleshootingURL);
  299. void RenderBottomBarCore() {
  300. bool troubleshootingShown = EditorCodePatcher.Started && HotReloadWindowStyles.windowScreenWidth >= 400;
  301. bool alertsShown = EditorCodePatcher.Started && HotReloadWindowStyles.windowScreenWidth > Constants.EventFiltersShownHideWidth;
  302. using (new EditorGUILayout.VerticalScope()) {
  303. using (new EditorGUILayout.HorizontalScope(HotReloadWindowStyles.FooterStyle)) {
  304. if (!troubleshootingShown) {
  305. GUILayout.FlexibleSpace();
  306. if (alertsShown) {
  307. GUILayout.Space(-20);
  308. }
  309. } else {
  310. GUILayout.Space(21);
  311. }
  312. GUILayout.Space(0);
  313. var lastRect = GUILayoutUtility.GetLastRect();
  314. // show events button when scrolls are hidden
  315. if (!HotReloadRunTab.CanRenderBars(RunTabState) && !RunTabState.starting) {
  316. using (new EditorGUILayout.VerticalScope()) {
  317. GUILayout.FlexibleSpace();
  318. var icon = HotReloadState.ShowingRedDot ? InvertibleIcon.EventsNew : InvertibleIcon.Events;
  319. if (GUILayout.Button(new GUIContent("", GUIHelper.GetInvertibleIcon(icon)))) {
  320. PopupWindow.Show(new Rect(lastRect.x, lastRect.y, 0, 0), HotReloadEventPopup.I);
  321. }
  322. GUILayout.FlexibleSpace();
  323. }
  324. GUILayout.Space(3f);
  325. }
  326. if (alertsShown) {
  327. using (new EditorGUILayout.VerticalScope()) {
  328. GUILayout.FlexibleSpace();
  329. HotReloadTimelineHelper.RenderAlertFilters();
  330. GUILayout.FlexibleSpace();
  331. }
  332. }
  333. GUILayout.FlexibleSpace();
  334. if (troubleshootingShown) {
  335. using (new EditorGUILayout.VerticalScope()) {
  336. GUILayout.FlexibleSpace();
  337. autoRefreshTroubleshootingBtn.OnGUI();
  338. GUILayout.FlexibleSpace();
  339. }
  340. GUILayout.Space(21);
  341. }
  342. }
  343. }
  344. }
  345. }
  346. }