diff --git a/Packages/icosa-api-client-unity/Editor/AssetBrowser/AssetBrowserWindow.cs b/Packages/icosa-api-client-unity/Editor/AssetBrowser/AssetBrowserWindow.cs index 49dc87e..a1e85c4 100644 --- a/Packages/icosa-api-client-unity/Editor/AssetBrowser/AssetBrowserWindow.cs +++ b/Packages/icosa-api-client-unity/Editor/AssetBrowser/AssetBrowserWindow.cs @@ -1,1159 +1,1159 @@ -// Copyright 2017 Google Inc. All rights reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -using System.Collections.Generic; -using System.IO; -using UnityEditor; -using UnityEngine; -using PolyToolkit; -using PolyToolkitInternal; -using System; -using System.Text.RegularExpressions; - -namespace PolyToolkitEditor { -/// -/// Window that allows the user to browse and import Poly models. -/// -/// Note that EditorWindow objects are created and deleted when the window is opened or closed, so this -/// object can't be responsible for any background logic like importing assets, etc. It should only deal -/// with UI. For all background logic and and real work, we rely on AssetBrowserManager. -/// -public class AssetBrowserWindow : EditorWindow { - /// - /// Title of the window (shown in the Unity UI). - /// - private const string WINDOW_TITLE = "Poly Toolkit"; - - /// - /// URL of the user's profile page. - /// - private const string USER_PROFILE_URL = "https://icosa.gallery/user"; - - /// - /// Width and height of each asset thumbnail image in the grid. - /// - private const int THUMBNAIL_WIDTH = 128; - private const int THUMBNAIL_HEIGHT = 96; - - /// - /// Width of each grid cell in the assets grid. - /// - private const int CELL_WIDTH = THUMBNAIL_WIDTH; - - /// - /// Spacing between grid cells in the assets grid. - /// - private const int CELL_SPACING = 5; - - /// - /// Height of the title bar at the top of the window. - /// - private const int TITLE_BAR_HEIGHT = 64; - - /// - /// Height of the bar that contains the back button. - /// - private const int BACK_BUTTON_BAR_HEIGHT = 28; - - /// - /// Height of the title image. - /// - private const int TITLE_IMAGE_HEIGHT = 40; - - /// - /// Padding around title image. - /// - private const int TITLE_IMAGE_PADDING = 12; - - /// - /// Margin from the top where the UI begins. - /// This does not account for the back button bar. - /// - private const int TOP_MARGIN_BASE = TITLE_BAR_HEIGHT + 10; - - /// - /// Padding around the window. - /// - private const int PADDING = 5; - - /// - /// Size of the user's profile picture. - /// - private const int PROFILE_PICTURE_SIZE = 40; - - /// - /// Size of the left column (labels), in pixels. - /// - private const int LEFT_COL_WIDTH = 140; - - /// - /// Height of the "successfully imported" box. - /// - private const int IMPORT_SUCCESS_BOX_HEIGHT = 120; - - /// - /// Texture to use for the title bar. - /// - private const string TITLE_TEX = "Editor/Textures/IcosaToolkitTitle.png"; - - /// - /// Texture to use for the back button (back arrow) if the skin is Unity pro. - /// - private const string BACK_ARROW_LIGHT_TEX = "Editor/Textures/BackArrow.png"; - - /// - /// Texture to use for the back button (back arrow) if the skin is Unity personal. - /// - private const string BACK_ARROW_DARK_TEX = "Editor/Textures/BackArrowDark.png"; - - /// - /// Texture to use for the back button bar background if the skin is Unity pro. - /// - private const string DARK_GREY_TEX = "Editor/Textures/DarkGrey.png"; - - /// - /// Texture to use for the back button bar background if the skin is Unity personal. - /// - private const string LIGHT_GREY_TEX = "Editor/Textures/LightGrey.png"; - - /// - /// Category key corresponding to selecting the "FEATURED" section. - /// - private const string KEY_FEATURED = "_featured"; - - /// - /// Category key corresponding to selecting the "Your Uploads" section. - /// - private const string KEY_YOUR_UPLOADS = "_your_uploads"; - - /// - /// Category key corresponding to selecting the "Your Uploads" section. - /// - private const string KEY_YOUR_LIKES = "_your_likes"; - - /// - /// Index of the "FEATURED" category below. - /// - private const int CATEGORY_FEATURED = 0; - - /// - /// Categories that the user can choose to browse. - /// - private static readonly CategoryInfo[] CATEGORIES = { - new CategoryInfo(KEY_FEATURED, "Featured", PolyCategory.UNSPECIFIED), - new CategoryInfo(KEY_YOUR_UPLOADS, "Your Uploads", PolyCategory.UNSPECIFIED), - new CategoryInfo(KEY_YOUR_LIKES, "Your Likes", PolyCategory.UNSPECIFIED), - new CategoryInfo("animals", "Animals and Creatures", PolyCategory.ANIMALS), - new CategoryInfo("architecture", "Architecture", PolyCategory.ARCHITECTURE), - new CategoryInfo("art", "Art", PolyCategory.ART), - new CategoryInfo("food", "Food and Drink", PolyCategory.FOOD), - new CategoryInfo("nature", "Nature", PolyCategory.NATURE), - new CategoryInfo("objects", "Objects", PolyCategory.OBJECTS), - new CategoryInfo("people", "People and Characters", PolyCategory.PEOPLE), - new CategoryInfo("places", "Places and Scenes", PolyCategory.PLACES), - new CategoryInfo("tech", "Technology", PolyCategory.TECH), - new CategoryInfo("transport", "Transport", PolyCategory.TRANSPORT), - }; - - /// - /// Represent the several modes that the UI can be in. - /// - private enum UiMode { - // Browsing assets by category/type (default mode). - BROWSE, - // Searching for assets by keyword (search box open). - SEARCH, - // Viewing the details of a particular asset. - DETAILS, - }; - - /// - /// Current UI mode. - /// - private UiMode mode = UiMode.BROWSE; - - /// - /// The previous UI mode. - /// - private UiMode previousMode = UiMode.BROWSE; - - /// - /// Index of the category that is currently selected (in CATEGORIES[]). - /// - private int selectedCategory = 0; - - /// - /// The search terms the user has typed in the search box. - /// - private string searchTerms = ""; - - /// - /// The texture to use in place of a thumbnail when loading the thumbnail. - /// - private Texture2D loadingTex = null; - - /// - /// The texture to use for the back button (back arrow). - /// - private Texture2D backArrowTex = null; - - /// - /// Texture used for back button bar background. - /// - private Texture2D backBarBackgroundTex = null; - - /// - /// Reference to the AssetBrowserManager. - /// - private AssetBrowserManager manager; - - /// - /// Current scrolling position of the scroll view with the list of assets. - /// - private Vector2 assetListScrollPos; - - /// - /// Current scrolling position of the details window. - /// - private Vector2 detailsScrollPos; - - /// - /// If non-null, we're showing the details page for the given asset. If null, - /// we are showing the grid screen that shows all assets. - /// - private PolyAsset selectedAsset = null; - - /// - /// Indicates whether the query we're currently showing requires authentication. - /// - private bool queryRequiresAuth = false; - - /// - /// The selected asset path in the "Import" section of the details page. - /// - private string ptAssetLocalPath = ""; - - /// - /// Current asset type filter (indicates the type of asset that the user wishes to see). - /// - private PolyFormatFilter? assetTypeFilter = null; - - /// - /// Texture for the title bar. - /// - private Texture2D titleTex; - - /// - /// The style we use for the asset title in the details page. - /// - private GUIStyle detailsTitleStyle; - - /// - /// GUI helper that keeps track of our open layouts. - /// - private GUIHelper guiHelper = new GUIHelper(); - - /// - /// Currently selected import options. - /// - private EditTimeImportOptions importOptions; - - /// - /// If true, the currently displayed asset was just imported successfully. - /// - private bool justImported; - - /// - /// Shows the browser window. - /// - [MenuItem("Icosa/Browse Assets...")] - public static void BrowsePolyAssets() { - GetWindow(WINDOW_TITLE, /* focus */ true); - PtAnalytics.SendEvent(PtAnalytics.Action.MENU_BROWSE_ASSETS); - } - - /// - /// Shows the browser window. - /// - [MenuItem("Window/Poly: Browse Assets...")] - public static void BrowsePolyAssets2() { - BrowsePolyAssets(); - } - - /// - /// Notifies this window that the given asset was just imported successfully - /// (so we can show this state in the UI). - /// - /// The ID of the asset that was just imported. - public void HandleAssetImported(string assetId) { - if (selectedAsset != null && assetId == selectedAsset.assetId) { - justImported = true; - } - } - - /// - /// Performs one-time initialization. - /// - private void Initialize() { - PtDebug.Log("ABW: initializing."); - if (manager == null) { - manager = new AssetBrowserManager(); - manager.SetRefreshCallback(UpdateUi); - } - loadingTex = new Texture2D(1,1); - titleTex = PtUtils.LoadTexture2DFromRelativePath(TITLE_TEX); - backArrowTex = PtUtils.LoadTexture2DFromRelativePath( - EditorGUIUtility.isProSkin ? BACK_ARROW_LIGHT_TEX : BACK_ARROW_DARK_TEX); - backBarBackgroundTex = PtUtils.LoadTexture2DFromRelativePath( - EditorGUIUtility.isProSkin ? DARK_GREY_TEX : LIGHT_GREY_TEX); - - detailsTitleStyle = new GUIStyle(EditorStyles.wordWrappedLabel); - detailsTitleStyle.fontSize = 15; - detailsTitleStyle.fontStyle = FontStyle.Bold; - } - - private void SetUiMode(UiMode newMode) { - if (newMode != mode) { - previousMode = mode; - mode = newMode; - } - PtDebug.Log("ABW: changed UI mode to " + newMode); - } - - /// - /// Renders the window GUI (invoked by Unity). - /// - public void OnGUI() { - if (Application.isPlaying) { - DrawTitleBar(/* withSignInUi */ false); - GUILayout.Space(TOP_MARGIN_BASE); - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Label("(This window doesn't work in Play mode)", EditorStyles.wordWrappedLabel); - GUILayout.FlexibleSpace(); - guiHelper.EndHorizontal(); - return; - } - - if (manager == null) { - // Initialize, if we haven't yet. We delay this to OnGUI instead of earlier because we need - // the Unity GUI system to be completely initialized (so we can create styles, for instance), which - // only happens on OnGUI(). - PolyRequest request = BuildRequest(); - manager = new AssetBrowserManager(request); - manager.SetRefreshCallback(UpdateUi); - Initialize(); - } - - // We have to check if Poly is ready every time (it's cheap to check). This is because Poly can be - // unloaded and wiped every time we enter or exit play mode. - manager.EnsurePolyIsReady(); - - int topMargin; - if (!DrawHeader(out topMargin)) { - // Abort rendering because a button was pressed (for example, the back button). - return; - } - - guiHelper.BeginArea(new Rect(PADDING, PADDING, position.width - 2 * PADDING, position.height - 2 * PADDING)); - GUILayout.Space(topMargin); - - switch (mode) { - case UiMode.BROWSE: - DrawBrowseUi(); - break; - case UiMode.SEARCH: - DrawSearchUi(); - break; - case UiMode.DETAILS: - DrawDetailsUi(); - break; - default: - throw new System.Exception("Invalid UI mode: " + mode); - } - - guiHelper.EndArea(); - guiHelper.FinishAndCheck(); - } - - /// - /// Draws the header and handles header-related events (like the back button). - /// - /// (Out param). The top margin at which the rest of the content - /// should be rendered. Only valid if this method returns true. - /// True if the header was successfully drawn and rendering should continue. - /// False if it was aborted due to a back button press. - private bool DrawHeader(out int topMargin) { - DrawTitleBar(withSignInUi: true); - bool hasBackButtonBar = HasBackButtonBar(); - topMargin = TOP_MARGIN_BASE; - if (hasBackButtonBar) { - bool backButtonClicked = DrawBackButtonBar(); - if (backButtonClicked) { - HandleBackButton(); - return false; - } - // Increase the top margin of the content to account for the back button bar. - topMargin += BACK_BUTTON_BAR_HEIGHT; - } - return true; - } - - /// - /// Draws the title bar at the top of the window. - /// The title bar includes the user's profile picture and the Sign In/Out UI. - /// If true, also include the sign in/sign out UI. - /// - private void DrawTitleBar(bool withSignInUi) { - GUI.DrawTexture(new Rect(0, 0, position.width, TITLE_BAR_HEIGHT), Texture2D.whiteTexture); - - GUIStyle titleStyle = new GUIStyle (GUI.skin.label); - titleStyle.margin = new RectOffset(TITLE_IMAGE_PADDING, TITLE_IMAGE_PADDING, TITLE_IMAGE_PADDING, - TITLE_IMAGE_PADDING); - if (GUILayout.Button(titleTex, titleStyle, - GUILayout.Width(titleTex.width * TITLE_IMAGE_HEIGHT / titleTex.height), - GUILayout.Height(TITLE_IMAGE_HEIGHT))) { - // Clicked title image. Return to the featured assets page. - SetUiMode(UiMode.BROWSE); - selectedCategory = CATEGORY_FEATURED; - StartRequest(); - } - - if (!withSignInUi) return; - - guiHelper.BeginArea(new Rect(TITLE_IMAGE_PADDING, TITLE_IMAGE_PADDING, - position.width - 2 * TITLE_IMAGE_PADDING, TITLE_BAR_HEIGHT)); - - if (PolyApi.IsAuthenticated) { - // User is authenticated, so show the profie picture. - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - Texture2D userTex = PolyApi.UserIcon != null && PolyApi.UserIcon.texture != null ? - PolyApi.UserIcon.texture : loadingTex; - if (GUILayout.Button(new GUIContent(userTex, /* tooltip */ PolyApi.UserName), - GUIStyle.none, GUILayout.Width(PROFILE_PICTURE_SIZE), GUILayout.Height(PROFILE_PICTURE_SIZE))) { - // Clicked profile picture. Show the dropdown menu. - ShowProfileDropdownMenu(); - } - guiHelper.EndHorizontal(); - } else if (PolyApi.IsAuthenticating) { - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Label("Signing in... Please wait."); - guiHelper.EndHorizontal(); - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - bool cancelSignInClicked = GUILayout.Button("Cancel", EditorStyles.miniButton); - guiHelper.EndHorizontal(); - if (cancelSignInClicked) { - PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_IN_CANCEL); - manager.CancelSignIn(); - } - } else { - // Not signed in. Show "Sign In" button. - GUILayout.Space(30); - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Sign in")) { - PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_IN_START); - manager.LaunchSignInFlow(); - } - guiHelper.EndHorizontal(); - } - guiHelper.EndArea(); - } - - /// - /// Draws the "browse" UI. The main UI allows the user to select a category to browse and set filters, - /// and allows them to browse the assets grid. When they click on an asset, they go to the details UI. - /// - private void DrawBrowseUi() { - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - bool searchClicked = GUILayout.Button("Search..."); - guiHelper.EndHorizontal(); - - if (searchClicked) { - SetUiMode(UiMode.SEARCH); - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_SEARCH_CLICKED); - manager.ClearRequest(); - return; - } - - guiHelper.BeginHorizontal(); - - // Draw the category dropdowns. - GUILayout.Label("Show:", GUILayout.Width(LEFT_COL_WIDTH)); - if (EditorGUILayout.DropdownButton(new GUIContent(CATEGORIES[selectedCategory].title), - FocusType.Keyboard)) { - GenericMenu menu = new GenericMenu(); - for (int i = 0; i < CATEGORIES.Length; i++) { - if (i == 3) menu.AddSeparator(""); - menu.AddItem(new GUIContent(CATEGORIES[i].title), i == selectedCategory, DropdownMenuCallback, i); - } - menu.ShowAsContext(); - } - guiHelper.EndHorizontal(); - - // Draw the "Asset type" toggles. - bool showAssetTypeFilter = (CATEGORIES[selectedCategory].key != KEY_YOUR_LIKES); - - if (showAssetTypeFilter) { - guiHelper.BeginHorizontal(); - GUILayout.Label("Asset type:", GUILayout.Width(LEFT_COL_WIDTH)); - bool blocksToggle = GUILayout.Toggle(assetTypeFilter == PolyFormatFilter.BLOCKS, "Blocks", "Button"); - bool tiltBrushToggle = GUILayout.Toggle(assetTypeFilter == PolyFormatFilter.TILT, "Open Brush", "Button"); - bool allToggle = GUILayout.Toggle(assetTypeFilter == null, "All", "Button"); - guiHelper.EndHorizontal(); - GUILayout.Space(10); - if (blocksToggle && assetTypeFilter != PolyFormatFilter.BLOCKS) { - assetTypeFilter = PolyFormatFilter.BLOCKS; - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_TYPE_SELECTED, assetTypeFilter.ToString()); - StartRequest(); - return; - } else if (tiltBrushToggle && assetTypeFilter != PolyFormatFilter.TILT) { - assetTypeFilter = PolyFormatFilter.TILT; - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_TYPE_SELECTED, assetTypeFilter.ToString()); - StartRequest(); - return; - } else if (allToggle && assetTypeFilter != null) { - assetTypeFilter = null; - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_TYPE_SELECTED, assetTypeFilter.ToString()); - StartRequest(); - return; - } - } - - DrawResultsGrid(); - } - - private void HandleBackButton() { - switch (mode) { - case UiMode.SEARCH: - SetUiMode(UiMode.BROWSE); - StartRequest(); - return; - case UiMode.DETAILS: - selectedAsset = null; - justImported = false; - manager.ClearCurrentAssetResult(); - SetUiMode(previousMode); - return; - default: - throw new Exception("Invalid UI mode for back button: " + mode); - } - } - - private void DrawSearchUi() { - bool searchClicked = false; - - // Pressing ENTER in the search terms box is the same as clicking "Search". We have to check - // this BEFORE we draw the text field, otherwise it will consume the event and we won't see it. - if (Event.current != null && Event.current.type == EventType.KeyDown && - Event.current.keyCode == KeyCode.Return && GUI.GetNameOfFocusedControl() == "searchTerms") { - searchClicked = true; - } - - GUILayout.Space(10); - GUILayout.Label("Search", EditorStyles.boldLabel); - GUILayout.Label("Enter search terms (or a Poly URL) below:", EditorStyles.wordWrappedLabel); - GUI.SetNextControlName("searchTerms"); - searchTerms = EditorGUILayout.TextField(searchTerms, EditorStyles.textArea); - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - searchClicked = GUILayout.Button("Search") || searchClicked; - guiHelper.EndHorizontal(); - - if (searchClicked && searchTerms.Trim().Length > 0) { - // Note: for privacy reasons we don't log the search terms, just the fact that a search was made. - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_SEARCHED); - - string assetId; - if (SearchTermIsAssetPage(searchTerms, out assetId)) { - manager.StartRequestForSpecificAsset(assetId); - SetUiMode(UiMode.DETAILS); - return; - } - - StartRequest(); - } - DrawResultsGrid(); - } - - /// - /// Returns true if the url was a valid asset url, false if not. - /// - /// The url of the asset to get. - /// The id of the asset to get from the url, null if the url is invalid. - private bool SearchTermIsAssetPage(string url, out string assetId) { - Regex regex = new Regex("^(http[s]?:\\/\\/)?.*\\/view\\/([^?]+)"); - Match match = regex.Match(url); - if (match.Success) { - assetId = match.Groups[2].ToString(); - assetId = assetId.Trim(); - return true; - } - - assetId = null; - return false; - } - - /// - /// Draws the query results grid, using the current query results as reported by - /// the AssetsBrowserManager. - /// - private void DrawResultsGrid() { - if (manager.IsQuerying) { - GUILayout.Space(30); - GUILayout.Label("Fetching assets. Please wait..."); - return; - } - - if (manager.CurrentResult == null) { - return; - } - - if (manager.CurrentResult != null && !manager.CurrentResult.Status.ok) { - GUILayout.Space(30); - GUILayout.Label("There was a problem with that request! Please try again later.", - EditorStyles.wordWrappedLabel); - - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Retry")) { - // Retry the previous request. - manager.ClearCaches(); - StartRequest(); - } - GUILayout.FlexibleSpace(); - guiHelper.EndHorizontal(); - - return; - } - - if (manager.CurrentResult.Value.assets == null || - manager.CurrentResult.Value.assets.Count == 0) { - GUILayout.Space(30); - GUILayout.Label("No results."); - return; - } - - PolyListAssetsResult result = manager.CurrentResult.Value; - if (!result.status.ok) { - GUILayout.Space(30); - GUILayout.Label("*** ERROR fetching results. Try again.."); - return; - } - - guiHelper.BeginHorizontal(); - string resultCountLabel; - resultCountLabel = string.Format("{0} assets found.", result.totalSize); - if (result.assets.Count < result.totalSize) { - resultCountLabel += string.Format(" (showing 1-{0}).", result.assets.Count); - } - GUILayout.Label(resultCountLabel, EditorStyles.miniLabel); - GUILayout.FlexibleSpace(); - bool refreshClicked = GUILayout.Button("Refresh"); - guiHelper.EndHorizontal(); - GUILayout.Space(5); - - if (refreshClicked) { - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_REFRESH_CLICKED); - manager.ClearCaches(); - StartRequest(); - return; - } - - // Calculate how many assets we want to show on each row, based on the window width. - // This allows the user to dynamically resize the window and have the assets grid - // automatically reflow. - int assetsPerRow = Mathf.Clamp( - Mathf.FloorToInt((position.width - 40) / (CELL_WIDTH + CELL_SPACING)), 1, 8); - int assetsThisRow = 0; - - // The assets grid is in a scroll view. - assetListScrollPos = guiHelper.BeginScrollView(assetListScrollPos); - GUILayout.Space(5); - guiHelper.BeginVertical(); - guiHelper.BeginHorizontal(); - - PolyAsset clickedAsset = null; - foreach (PolyAsset asset in result.assets) { - if (assetsThisRow >= assetsPerRow) { - // Begin new row. - guiHelper.EndHorizontal(); - GUILayout.Space(20); - guiHelper.BeginHorizontal(); - assetsThisRow = 0; - } - - if (assetsThisRow > 0) GUILayout.Space(CELL_SPACING); - guiHelper.BeginVertical(); - if (GUILayout.Button(asset.thumbnailTexture ?? loadingTex, - GUILayout.Width(THUMBNAIL_WIDTH), GUILayout.Height(THUMBNAIL_HEIGHT))) { - clickedAsset = asset; - } - GUILayout.Label(asset.displayName, EditorStyles.boldLabel, GUILayout.Width(CELL_WIDTH)); - GUILayout.Label(asset.authorName, GUILayout.Width(CELL_WIDTH)); - guiHelper.EndVertical(); - assetsThisRow++; - } - - // Complete last row with dummy cells. - if (assetsThisRow > 0) { - while (assetsThisRow < assetsPerRow) { - guiHelper.BeginVertical(); - GUILayout.Label("", GUILayout.Width(CELL_WIDTH), GUILayout.Height(THUMBNAIL_HEIGHT)); - GUILayout.Label("", EditorStyles.boldLabel, GUILayout.Width(CELL_WIDTH)); - GUILayout.Label("", GUILayout.Width(CELL_WIDTH)); - guiHelper.EndVertical(); - assetsThisRow++; - } - } - - guiHelper.EndHorizontal(); - GUILayout.Space(10); - - bool loadMoreClicked = false; - if (manager.resultHasMorePages) { - // If the current response has at least another page of results left, show the load more button. - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - loadMoreClicked = GUILayout.Button("Load more"); - GUILayout.FlexibleSpace(); - guiHelper.EndHorizontal(); - } - GUILayout.Space(5); - - guiHelper.EndVertical(); - guiHelper.EndScrollView(); - - if (loadMoreClicked) { - manager.GetNextPageRequest(); - return; - } - - if (clickedAsset != null) { - PrepareDetailsUi(clickedAsset); - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_DETAILS_CLICKED, - GetAssetFormatDescription(selectedAsset)); - SetUiMode(UiMode.DETAILS); - } - } - - /// - /// Called as a callback by AssetBrowserManager whenever something interesting changes which - /// requires us to update our UI. - /// - private void UpdateUi() { - // Tell Unity to repaint this window, which will invoke OnGUI(). - Repaint(); - } - - /// - /// Returns whether or not the given category requires authentication. - /// - private bool CategoryRequiresAuth(int category) { - string key = CATEGORIES[category].key; - return (key == KEY_YOUR_UPLOADS || key == KEY_YOUR_LIKES); - } - - /// - /// Shows the profile dropdown menu (the dropdown menu that appears when the user clicks their profile - /// picture). - /// - private void ShowProfileDropdownMenu() { - GenericMenu menu = new GenericMenu(); - menu.AddItem(new GUIContent("My Profile (web)"), /* on */ false, () => { - PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_VIEW_PROFILE); - Application.OpenURL(USER_PROFILE_URL); - }); - menu.AddSeparator(""); - menu.AddItem(new GUIContent("Sign Out"), /* on */ false, () => { - PolyApi.SignOut(); - PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_OUT); - // If the user was viewing a category that requires sign in, reset back to the home page. - if (queryRequiresAuth) { - selectedCategory = CATEGORY_FEATURED; - StartRequest(); - } - }); - menu.ShowAsContext(); - } - - /// - /// Callback invoked when the user picks a new category from the category dropdown. - /// - /// The index of the selected category. - private void DropdownMenuCallback(object userData) { - int selection = (int)userData; - if (selection == selectedCategory) return; - - // If the user picked a category that requires authentication, and they are not authenticated, - // tell them why they can't view it. - if (CategoryRequiresAuth(selection) && !PolyApi.IsAuthenticated) { - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_MISSING_AUTH); - EditorUtility.DisplayDialog("Sign in required", - "To view your uploads or likes, you must sign in first.", "OK"); - return; - } - - selectedCategory = (int)userData; - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_CATEGORY_SELECTED, CATEGORIES[selectedCategory].key); - - StartRequest(); - } - - /// - /// Builds and returns a PolyRequest from the current state of the AssetBrowswerWindow variables. - /// - private PolyRequest BuildRequest() { - CategoryInfo info = CATEGORIES[selectedCategory]; - - if (info.key == KEY_YOUR_UPLOADS) { - PolyListUserAssetsRequest listUserAssetsRequest = PolyListUserAssetsRequest.MyNewest(); - listUserAssetsRequest.formatFilter = assetTypeFilter; - return listUserAssetsRequest; - } - - if (info.key == KEY_YOUR_LIKES) { - PolyListLikedAssetsRequest listLikedAssetsRequest = PolyListLikedAssetsRequest.MyLiked(); - return listLikedAssetsRequest; - } - - PolyListAssetsRequest listAssetsRequest; - if (info.key == KEY_FEATURED) { - listAssetsRequest = PolyListAssetsRequest.Featured(); - } else { - listAssetsRequest = new PolyListAssetsRequest(); - listAssetsRequest.category = info.polyCategory; - } - - listAssetsRequest.formatFilter = assetTypeFilter; - // Only show curated results. - listAssetsRequest.curated = true; - return listAssetsRequest; - } - - /// - /// Sends a list assets request to the assets service according to the parameters currently selected in the UI. - /// When the request is done, AssetBrowserManager will inform us through the callback. - /// - private void StartRequest() { - if (mode == UiMode.BROWSE) { - PolyRequest request = BuildRequest(); - queryRequiresAuth = CategoryRequiresAuth(selectedCategory); - if (!queryRequiresAuth || PolyApi.IsAuthenticated) { - manager.StartRequest(request); - } - } else if (mode == UiMode.SEARCH) { - PolyListAssetsRequest request = new PolyListAssetsRequest(); - request.keywords = searchTerms; - manager.StartRequest(request); - } else { - throw new System.Exception("Unexpected UI mode for StartQuery: " + mode); - } - // Reset scroll bar position. - assetListScrollPos = Vector2.zero; - } - - /// - /// Set the variables of the details ui page for the newly selected asset. - /// - private void PrepareDetailsUi(PolyAsset newSelectedAsset) { - selectedAsset = newSelectedAsset; - ptAssetLocalPath = PtUtils.GetDefaultPtAssetPath(selectedAsset); - detailsScrollPos = Vector2.zero; - importOptions = PtSettings.Instance.defaultImportOptions; - } - - /// - /// Draws the asset details page. This is the page that shows the details about the selected asset - /// and allows the user to click to import it. - /// - private void DrawDetailsUi() { - if (manager.IsQuerying) { - // Check if manager is querying for a specific asset. - GUILayout.Space(30); - GUILayout.Label("Fetching asset. Please wait..."); - selectedAsset = null; - return; - } - - // If we just got the specific asset result, set the details ui import options to the relevant - // variables at first. - if (manager.CurrentAssetResult != null && selectedAsset == null) { - PrepareDetailsUi(manager.CurrentAssetResult); - } - - // Check if the user selected something in the PtAsset picker. - if (Event.current != null && Event.current.commandName == "ObjectSelectorUpdated") { - UnityEngine.Object picked = EditorGUIUtility.GetObjectPickerObject(); - ptAssetLocalPath = (picked != null && picked is PtAsset) ? - AssetDatabase.GetAssetPath(picked) : - PtUtils.GetDefaultPtAssetPath(selectedAsset); - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_LOCATION_CHANGED); - } - - if (selectedAsset == null) { - manager.ClearCurrentAssetResult(); - GUILayout.Space(20); - GUILayout.Label("Asset not found."); - return; - } - - detailsScrollPos = guiHelper.BeginScrollView(detailsScrollPos); - const float width = 150; - - guiHelper.BeginHorizontal(); - GUILayout.Label(selectedAsset.displayName, detailsTitleStyle); - guiHelper.EndHorizontal(); - - guiHelper.BeginHorizontal(); - GUILayout.Label(selectedAsset.authorName, EditorStyles.wordWrappedLabel); - - GUILayout.FlexibleSpace(); - if (GUILayout.Button("View on Web", GUILayout.MaxWidth(100))) { - PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_VIEW_ON_WEB); - Application.OpenURL(selectedAsset.Url); - } - GUILayout.Space(5); - guiHelper.EndHorizontal(); - - GUILayout.Space(10); - - Texture2D image = selectedAsset.thumbnailTexture ?? loadingTex; - float displayWidth = position.width - 40; - float displayHeight = image.height * displayWidth / image.width; - displayHeight = Mathf.Min(image.height, displayHeight); - displayWidth = Mathf.Min(image.width, displayWidth); - GUILayout.Label(image, GUILayout.Width(displayWidth), GUILayout.Height(displayHeight)); - - if (manager.IsDownloadingAsset(selectedAsset)) { - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - GUILayout.Label("Downloading asset. Please wait..."); - GUILayout.FlexibleSpace(); - guiHelper.EndHorizontal(); - guiHelper.EndScrollView(); - return; - } else if (justImported) { - Rect lastRect = GUILayoutUtility.GetLastRect(); - Rect boxRect = new Rect(PADDING, lastRect.yMax + PADDING, position.width - 2 * PADDING, - IMPORT_SUCCESS_BOX_HEIGHT); - GUI.DrawTexture(boxRect, backBarBackgroundTex); - GUILayout.Space(20); - guiHelper.BeginHorizontal(); - GUILayout.FlexibleSpace(); - guiHelper.BeginVertical(); - GUILayout.Label("Asset successfully imported."); - GUILayout.Label("Saved to: " + PtSettings.Instance.assetObjectsPath); - GUILayout.Space(10); - if (GUILayout.Button("OK")) { - // Dismiss. - justImported = false; - } - guiHelper.EndVertical(); - GUILayout.FlexibleSpace(); - guiHelper.EndHorizontal(); - guiHelper.EndScrollView(); - return; - } - - GUILayout.Space(5); - GUILayout.Label("Import Options", EditorStyles.boldLabel); - GUILayout.Space(5); - - guiHelper.BeginHorizontal(); - GUILayout.Space(12); - GUILayout.Label("Import Location", GUILayout.Width(width)); - GUILayout.FlexibleSpace(); - GUILayout.Label(ptAssetLocalPath, EditorStyles.wordWrappedLabel); - GUILayout.Space(5); - guiHelper.EndHorizontal(); - - guiHelper.BeginHorizontal(); - GUILayout.Space(width - 10); - GUILayout.FlexibleSpace(); - if (GUILayout.Button("Replace existing...")) { - PtAsset current = AssetDatabase.LoadAssetAtPath(ptAssetLocalPath); - EditorGUIUtility.ShowObjectPicker(current, /* allowSceneObjects */ false, PtAsset.FilterString, 0); - } - GUILayout.Space(5); - guiHelper.EndHorizontal(); - - EditTimeImportOptions oldOptions = importOptions; - importOptions = ImportOptionsGui.ImportOptionsField(importOptions); - if (oldOptions.alsoInstantiate != importOptions.alsoInstantiate) { - // Persist 'always instantiate' option if it was changed. - PtSettings.Instance.defaultImportOptions.alsoInstantiate = importOptions.alsoInstantiate; - } - SendImportOptionMutationAnalytics(oldOptions, importOptions); - - GUILayout.Space(10); - GUILayout.Box("", GUILayout.ExpandWidth(true), GUILayout.Height(1)); - GUILayout.Space(10); - - guiHelper.BeginHorizontal(); - GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); - buttonStyle.fontStyle = FontStyle.Bold; - buttonStyle.padding = new RectOffset(10, 10, 8, 8); - bool importButtonClicked = GUILayout.Button("Import into Project", buttonStyle); - GUILayout.FlexibleSpace(); - guiHelper.EndHorizontal(); - GUILayout.Space(5); - - if (!File.Exists(PtUtils.ToAbsolutePath(ptAssetLocalPath))) { - GUILayout.Label(PolyInternalUtils.ATTRIBUTION_NOTICE, EditorStyles.wordWrappedLabel); - } else { - GUILayout.Label("WARNING: The indicated asset already exists and will be OVERWRITTEN by " + - "the new asset. Existing instances of the old asset will be automatically updated " + - "to the new asset.", - EditorStyles.wordWrappedLabel); - } - guiHelper.EndScrollView(); - - if (importButtonClicked) { - if (!ptAssetLocalPath.StartsWith("Assets/") || !ptAssetLocalPath.EndsWith(".asset")) { - EditorUtility.DisplayDialog("Invalid import path", - "The import path must begin with Assets/ and have the .asset extension.", "OK"); - return; - } - string errorString; - if (!ValidateImportOptions(importOptions.baseOptions, out errorString)) { - EditorUtility.DisplayDialog("Invalid import options", errorString, "OK"); - return; - } - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_STARTED, GetAssetFormatDescription(selectedAsset)); - if (previousMode == UiMode.SEARCH) { - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_STARTED_FROM_SEARCH, - GetAssetFormatDescription(selectedAsset)); - } - - manager.StartDownloadAndImport(selectedAsset, ptAssetLocalPath, importOptions); - } - } - - /// - /// Returns whether the import options are valid and, if not, a relevant error message to display. - /// - private bool ValidateImportOptions(PolyImportOptions options, out string errorString) { - switch (options.rescalingMode) { - case PolyImportOptions.RescalingMode.CONVERT: - if (options.scaleFactor == 0.0f) { - errorString = "Scale factor must not be 0."; - return false; - } - break; - case PolyImportOptions.RescalingMode.FIT: - if (options.desiredSize == 0.0f) { - errorString = "Desired size must not be 0."; - return false; - } - break; - default: - throw new System.Exception("Import options must hvae a valid rescaling mode"); - } - errorString = null; - return true; - } - - /// - /// Returns true if the current UI mode has a back button bar. - /// - /// True if the current UI mode has a back button bar, false otherwise. - private bool HasBackButtonBar() { - return mode == UiMode.DETAILS || mode == UiMode.SEARCH; - } - - /// - /// Draws the "Back" button bar. - /// - /// True if the back button was clicked, false otherwise. - private bool DrawBackButtonBar() { - guiHelper.BeginArea(new Rect(0, TITLE_BAR_HEIGHT, position.width, BACK_BUTTON_BAR_HEIGHT)); - GUI.DrawTexture(new Rect(0, 0, position.width, BACK_BUTTON_BAR_HEIGHT), backBarBackgroundTex); - guiHelper.BeginHorizontal(); - GUILayout.Space(PADDING); - bool backButtonClicked = GUILayout.Button(new GUIContent("Back", backArrowTex), EditorStyles.miniLabel); - GUILayout.FlexibleSpace(); - guiHelper.EndHorizontal(); - guiHelper.EndArea(); - return backButtonClicked; - } - - /// - /// Returns a string with all the asset formats of the given asset. - /// - /// The asset. - /// A string with all the comma-separated asset formats of the given asset. - private string GetAssetFormatDescription(PolyAsset asset) { - List formatTypes = new List(); - if (asset.formats != null) { - foreach (PolyFormat format in asset.formats) { - formatTypes.Add(format.formatType.ToString()); - } - } else { - // Shouldn't happen [tm]. - formatTypes.Add("NULL"); - } - formatTypes.Sort(); - return string.Join(",", formatTypes.ToArray()); - } - - /// - /// Sends Analytics events for mutations in the given import options. - /// - /// The options before the mutation. - /// The options after the mutation. - private void SendImportOptionMutationAnalytics(EditTimeImportOptions before, EditTimeImportOptions after) { - if (before.alsoInstantiate != after.alsoInstantiate) { - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_INSTANTIATE_TOGGLED, after.alsoInstantiate.ToString()); - } - if (before.baseOptions.desiredSize != after.baseOptions.desiredSize) { - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_DESIRED_SIZE_SET, - after.baseOptions.desiredSize.ToString()); - } - if (before.baseOptions.recenter != after.baseOptions.recenter) { - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_RECENTER_TOGGLED, - after.baseOptions.recenter.ToString()); - } - if (before.baseOptions.rescalingMode != after.baseOptions.rescalingMode) { - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_SCALE_MODE_CHANGED, - after.baseOptions.rescalingMode.ToString()); - } - if (before.baseOptions.scaleFactor != after.baseOptions.scaleFactor) { - PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_SCALE_FACTOR_CHANGED, - after.baseOptions.scaleFactor.ToString()); - } - } - - private void OnDestroy() { - PtDebug.Log("ABW: destroying."); - manager.SetRefreshCallback(null); - } - - private struct CategoryInfo { - public string key; - public string title; - public PolyCategory polyCategory; - public CategoryInfo(string key, string title, PolyCategory polyCategory = PolyCategory.UNSPECIFIED) { - this.key = key; - this.title = title; - this.polyCategory = polyCategory; - } - } -} - -} +// Copyright 2017 Google Inc. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using System.Collections.Generic; +using System.IO; +using UnityEditor; +using UnityEngine; +using PolyToolkit; +using PolyToolkitInternal; +using System; +using System.Text.RegularExpressions; + +namespace PolyToolkitEditor { +/// +/// Window that allows the user to browse and import Poly models. +/// +/// Note that EditorWindow objects are created and deleted when the window is opened or closed, so this +/// object can't be responsible for any background logic like importing assets, etc. It should only deal +/// with UI. For all background logic and and real work, we rely on AssetBrowserManager. +/// +public class AssetBrowserWindow : EditorWindow { + /// + /// Title of the window (shown in the Unity UI). + /// + private const string WINDOW_TITLE = "Poly Toolkit"; + + /// + /// URL of the user's profile page. + /// + private const string USER_PROFILE_URL = "https://icosa.gallery/user"; + + /// + /// Width and height of each asset thumbnail image in the grid. + /// + private const int THUMBNAIL_WIDTH = 128; + private const int THUMBNAIL_HEIGHT = 96; + + /// + /// Width of each grid cell in the assets grid. + /// + private const int CELL_WIDTH = THUMBNAIL_WIDTH; + + /// + /// Spacing between grid cells in the assets grid. + /// + private const int CELL_SPACING = 5; + + /// + /// Height of the title bar at the top of the window. + /// + private const int TITLE_BAR_HEIGHT = 64; + + /// + /// Height of the bar that contains the back button. + /// + private const int BACK_BUTTON_BAR_HEIGHT = 28; + + /// + /// Height of the title image. + /// + private const int TITLE_IMAGE_HEIGHT = 40; + + /// + /// Padding around title image. + /// + private const int TITLE_IMAGE_PADDING = 12; + + /// + /// Margin from the top where the UI begins. + /// This does not account for the back button bar. + /// + private const int TOP_MARGIN_BASE = TITLE_BAR_HEIGHT + 10; + + /// + /// Padding around the window. + /// + private const int PADDING = 5; + + /// + /// Size of the user's profile picture. + /// + private const int PROFILE_PICTURE_SIZE = 40; + + /// + /// Size of the left column (labels), in pixels. + /// + private const int LEFT_COL_WIDTH = 140; + + /// + /// Height of the "successfully imported" box. + /// + private const int IMPORT_SUCCESS_BOX_HEIGHT = 120; + + /// + /// Texture to use for the title bar. + /// + private const string TITLE_TEX = "Editor/Textures/IcosaToolkitTitle.png"; + + /// + /// Texture to use for the back button (back arrow) if the skin is Unity pro. + /// + private const string BACK_ARROW_LIGHT_TEX = "Editor/Textures/BackArrow.png"; + + /// + /// Texture to use for the back button (back arrow) if the skin is Unity personal. + /// + private const string BACK_ARROW_DARK_TEX = "Editor/Textures/BackArrowDark.png"; + + /// + /// Texture to use for the back button bar background if the skin is Unity pro. + /// + private const string DARK_GREY_TEX = "Editor/Textures/DarkGrey.png"; + + /// + /// Texture to use for the back button bar background if the skin is Unity personal. + /// + private const string LIGHT_GREY_TEX = "Editor/Textures/LightGrey.png"; + + /// + /// Category key corresponding to selecting the "FEATURED" section. + /// + private const string KEY_FEATURED = "_featured"; + + /// + /// Category key corresponding to selecting the "Your Uploads" section. + /// + private const string KEY_YOUR_UPLOADS = "_your_uploads"; + + /// + /// Category key corresponding to selecting the "Your Uploads" section. + /// + private const string KEY_YOUR_LIKES = "_your_likes"; + + /// + /// Index of the "FEATURED" category below. + /// + private const int CATEGORY_FEATURED = 0; + + /// + /// Categories that the user can choose to browse. + /// + private static readonly CategoryInfo[] CATEGORIES = { + new CategoryInfo(KEY_FEATURED, "Featured", PolyCategory.UNSPECIFIED), + new CategoryInfo(KEY_YOUR_UPLOADS, "Your Uploads", PolyCategory.UNSPECIFIED), + new CategoryInfo(KEY_YOUR_LIKES, "Your Likes", PolyCategory.UNSPECIFIED), + new CategoryInfo("animals", "Animals and Creatures", PolyCategory.ANIMALS), + new CategoryInfo("architecture", "Architecture", PolyCategory.ARCHITECTURE), + new CategoryInfo("art", "Art", PolyCategory.ART), + new CategoryInfo("food", "Food and Drink", PolyCategory.FOOD), + new CategoryInfo("nature", "Nature", PolyCategory.NATURE), + new CategoryInfo("objects", "Objects", PolyCategory.OBJECTS), + new CategoryInfo("people", "People and Characters", PolyCategory.PEOPLE), + new CategoryInfo("places", "Places and Scenes", PolyCategory.PLACES), + new CategoryInfo("tech", "Technology", PolyCategory.TECH), + new CategoryInfo("transport", "Transport", PolyCategory.TRANSPORT), + }; + + /// + /// Represent the several modes that the UI can be in. + /// + private enum UiMode { + // Browsing assets by category/type (default mode). + BROWSE, + // Searching for assets by keyword (search box open). + SEARCH, + // Viewing the details of a particular asset. + DETAILS, + }; + + /// + /// Current UI mode. + /// + private UiMode mode = UiMode.BROWSE; + + /// + /// The previous UI mode. + /// + private UiMode previousMode = UiMode.BROWSE; + + /// + /// Index of the category that is currently selected (in CATEGORIES[]). + /// + private int selectedCategory = 0; + + /// + /// The search terms the user has typed in the search box. + /// + private string searchTerms = ""; + + /// + /// The texture to use in place of a thumbnail when loading the thumbnail. + /// + private Texture2D loadingTex = null; + + /// + /// The texture to use for the back button (back arrow). + /// + private Texture2D backArrowTex = null; + + /// + /// Texture used for back button bar background. + /// + private Texture2D backBarBackgroundTex = null; + + /// + /// Reference to the AssetBrowserManager. + /// + private AssetBrowserManager manager; + + /// + /// Current scrolling position of the scroll view with the list of assets. + /// + private Vector2 assetListScrollPos; + + /// + /// Current scrolling position of the details window. + /// + private Vector2 detailsScrollPos; + + /// + /// If non-null, we're showing the details page for the given asset. If null, + /// we are showing the grid screen that shows all assets. + /// + private PolyAsset selectedAsset = null; + + /// + /// Indicates whether the query we're currently showing requires authentication. + /// + private bool queryRequiresAuth = false; + + /// + /// The selected asset path in the "Import" section of the details page. + /// + private string ptAssetLocalPath = ""; + + /// + /// Current asset type filter (indicates the type of asset that the user wishes to see). + /// + private PolyFormatFilter? assetTypeFilter = null; + + /// + /// Texture for the title bar. + /// + private Texture2D titleTex; + + /// + /// The style we use for the asset title in the details page. + /// + private GUIStyle detailsTitleStyle; + + /// + /// GUI helper that keeps track of our open layouts. + /// + private GUIHelper guiHelper = new GUIHelper(); + + /// + /// Currently selected import options. + /// + private EditTimeImportOptions importOptions; + + /// + /// If true, the currently displayed asset was just imported successfully. + /// + private bool justImported; + + /// + /// Shows the browser window. + /// + [MenuItem("Icosa/Browse Assets...")] + public static void BrowsePolyAssets() { + GetWindow(WINDOW_TITLE, /* focus */ true); + PtAnalytics.SendEvent(PtAnalytics.Action.MENU_BROWSE_ASSETS); + } + + /// + /// Shows the browser window. + /// + [MenuItem("Window/Poly: Browse Assets...")] + public static void BrowsePolyAssets2() { + BrowsePolyAssets(); + } + + /// + /// Notifies this window that the given asset was just imported successfully + /// (so we can show this state in the UI). + /// + /// The ID of the asset that was just imported. + public void HandleAssetImported(string assetId) { + if (selectedAsset != null && assetId == selectedAsset.assetId) { + justImported = true; + } + } + + /// + /// Performs one-time initialization. + /// + private void Initialize() { + PtDebug.Log("ABW: initializing."); + if (manager == null) { + manager = new AssetBrowserManager(); + manager.SetRefreshCallback(UpdateUi); + } + loadingTex = new Texture2D(1,1); + titleTex = PtUtils.LoadTexture2DFromRelativePath(TITLE_TEX); + backArrowTex = PtUtils.LoadTexture2DFromRelativePath( + EditorGUIUtility.isProSkin ? BACK_ARROW_LIGHT_TEX : BACK_ARROW_DARK_TEX); + backBarBackgroundTex = PtUtils.LoadTexture2DFromRelativePath( + EditorGUIUtility.isProSkin ? DARK_GREY_TEX : LIGHT_GREY_TEX); + + detailsTitleStyle = new GUIStyle(EditorStyles.wordWrappedLabel); + detailsTitleStyle.fontSize = 15; + detailsTitleStyle.fontStyle = FontStyle.Bold; + } + + private void SetUiMode(UiMode newMode) { + if (newMode != mode) { + previousMode = mode; + mode = newMode; + } + PtDebug.Log("ABW: changed UI mode to " + newMode); + } + + /// + /// Renders the window GUI (invoked by Unity). + /// + public void OnGUI() { + if (Application.isPlaying) { + DrawTitleBar(/* withSignInUi */ false); + GUILayout.Space(TOP_MARGIN_BASE); + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Label("(This window doesn't work in Play mode)", EditorStyles.wordWrappedLabel); + GUILayout.FlexibleSpace(); + guiHelper.EndHorizontal(); + return; + } + + if (manager == null) { + // Initialize, if we haven't yet. We delay this to OnGUI instead of earlier because we need + // the Unity GUI system to be completely initialized (so we can create styles, for instance), which + // only happens on OnGUI(). + PolyRequest request = BuildRequest(); + manager = new AssetBrowserManager(request); + manager.SetRefreshCallback(UpdateUi); + Initialize(); + } + + // We have to check if Poly is ready every time (it's cheap to check). This is because Poly can be + // unloaded and wiped every time we enter or exit play mode. + manager.EnsurePolyIsReady(); + + int topMargin; + if (!DrawHeader(out topMargin)) { + // Abort rendering because a button was pressed (for example, the back button). + return; + } + + guiHelper.BeginArea(new Rect(PADDING, PADDING, position.width - 2 * PADDING, position.height - 2 * PADDING)); + GUILayout.Space(topMargin); + + switch (mode) { + case UiMode.BROWSE: + DrawBrowseUi(); + break; + case UiMode.SEARCH: + DrawSearchUi(); + break; + case UiMode.DETAILS: + DrawDetailsUi(); + break; + default: + throw new System.Exception("Invalid UI mode: " + mode); + } + + guiHelper.EndArea(); + guiHelper.FinishAndCheck(); + } + + /// + /// Draws the header and handles header-related events (like the back button). + /// + /// (Out param). The top margin at which the rest of the content + /// should be rendered. Only valid if this method returns true. + /// True if the header was successfully drawn and rendering should continue. + /// False if it was aborted due to a back button press. + private bool DrawHeader(out int topMargin) { + DrawTitleBar(withSignInUi: false); // TODO Update signin for Icosa device code flow + bool hasBackButtonBar = HasBackButtonBar(); + topMargin = TOP_MARGIN_BASE; + if (hasBackButtonBar) { + bool backButtonClicked = DrawBackButtonBar(); + if (backButtonClicked) { + HandleBackButton(); + return false; + } + // Increase the top margin of the content to account for the back button bar. + topMargin += BACK_BUTTON_BAR_HEIGHT; + } + return true; + } + + /// + /// Draws the title bar at the top of the window. + /// The title bar includes the user's profile picture and the Sign In/Out UI. + /// If true, also include the sign in/sign out UI. + /// + private void DrawTitleBar(bool withSignInUi) { + GUI.DrawTexture(new Rect(0, 0, position.width, TITLE_BAR_HEIGHT), Texture2D.whiteTexture); + + GUIStyle titleStyle = new GUIStyle (GUI.skin.label); + titleStyle.margin = new RectOffset(TITLE_IMAGE_PADDING, TITLE_IMAGE_PADDING, TITLE_IMAGE_PADDING, + TITLE_IMAGE_PADDING); + if (GUILayout.Button(titleTex, titleStyle, + GUILayout.Width(titleTex.width * TITLE_IMAGE_HEIGHT / titleTex.height), + GUILayout.Height(TITLE_IMAGE_HEIGHT))) { + // Clicked title image. Return to the featured assets page. + SetUiMode(UiMode.BROWSE); + selectedCategory = CATEGORY_FEATURED; + StartRequest(); + } + + if (!withSignInUi) return; + + guiHelper.BeginArea(new Rect(TITLE_IMAGE_PADDING, TITLE_IMAGE_PADDING, + position.width - 2 * TITLE_IMAGE_PADDING, TITLE_BAR_HEIGHT)); + + if (PolyApi.IsAuthenticated) { + // User is authenticated, so show the profie picture. + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + Texture2D userTex = PolyApi.UserIcon != null && PolyApi.UserIcon.texture != null ? + PolyApi.UserIcon.texture : loadingTex; + if (GUILayout.Button(new GUIContent(userTex, /* tooltip */ PolyApi.UserName), + GUIStyle.none, GUILayout.Width(PROFILE_PICTURE_SIZE), GUILayout.Height(PROFILE_PICTURE_SIZE))) { + // Clicked profile picture. Show the dropdown menu. + ShowProfileDropdownMenu(); + } + guiHelper.EndHorizontal(); + } else if (PolyApi.IsAuthenticating) { + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Label("Signing in... Please wait."); + guiHelper.EndHorizontal(); + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + bool cancelSignInClicked = GUILayout.Button("Cancel", EditorStyles.miniButton); + guiHelper.EndHorizontal(); + if (cancelSignInClicked) { + PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_IN_CANCEL); + manager.CancelSignIn(); + } + } else { + // Not signed in. Show "Sign In" button. + GUILayout.Space(30); + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Sign in")) { + PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_IN_START); + manager.LaunchSignInFlow(); + } + guiHelper.EndHorizontal(); + } + guiHelper.EndArea(); + } + + /// + /// Draws the "browse" UI. The main UI allows the user to select a category to browse and set filters, + /// and allows them to browse the assets grid. When they click on an asset, they go to the details UI. + /// + private void DrawBrowseUi() { + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + bool searchClicked = GUILayout.Button("Search..."); + guiHelper.EndHorizontal(); + + if (searchClicked) { + SetUiMode(UiMode.SEARCH); + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_SEARCH_CLICKED); + manager.ClearRequest(); + return; + } + + guiHelper.BeginHorizontal(); + + // Draw the category dropdowns. + GUILayout.Label("Show:", GUILayout.Width(LEFT_COL_WIDTH)); + if (EditorGUILayout.DropdownButton(new GUIContent(CATEGORIES[selectedCategory].title), + FocusType.Keyboard)) { + GenericMenu menu = new GenericMenu(); + for (int i = 0; i < CATEGORIES.Length; i++) { + if (i == 3) menu.AddSeparator(""); + menu.AddItem(new GUIContent(CATEGORIES[i].title), i == selectedCategory, DropdownMenuCallback, i); + } + menu.ShowAsContext(); + } + guiHelper.EndHorizontal(); + + // Draw the "Asset type" toggles. + bool showAssetTypeFilter = (CATEGORIES[selectedCategory].key != KEY_YOUR_LIKES); + + if (showAssetTypeFilter) { + guiHelper.BeginHorizontal(); + GUILayout.Label("Asset type:", GUILayout.Width(LEFT_COL_WIDTH)); + bool blocksToggle = GUILayout.Toggle(assetTypeFilter == PolyFormatFilter.BLOCKS, "Blocks", "Button"); + bool tiltBrushToggle = GUILayout.Toggle(assetTypeFilter == PolyFormatFilter.TILT, "Open Brush", "Button"); + bool allToggle = GUILayout.Toggle(assetTypeFilter == null, "All", "Button"); + guiHelper.EndHorizontal(); + GUILayout.Space(10); + if (blocksToggle && assetTypeFilter != PolyFormatFilter.BLOCKS) { + assetTypeFilter = PolyFormatFilter.BLOCKS; + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_TYPE_SELECTED, assetTypeFilter.ToString()); + StartRequest(); + return; + } else if (tiltBrushToggle && assetTypeFilter != PolyFormatFilter.TILT) { + assetTypeFilter = PolyFormatFilter.TILT; + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_TYPE_SELECTED, assetTypeFilter.ToString()); + StartRequest(); + return; + } else if (allToggle && assetTypeFilter != null) { + assetTypeFilter = null; + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_TYPE_SELECTED, assetTypeFilter.ToString()); + StartRequest(); + return; + } + } + + DrawResultsGrid(); + } + + private void HandleBackButton() { + switch (mode) { + case UiMode.SEARCH: + SetUiMode(UiMode.BROWSE); + StartRequest(); + return; + case UiMode.DETAILS: + selectedAsset = null; + justImported = false; + manager.ClearCurrentAssetResult(); + SetUiMode(previousMode); + return; + default: + throw new Exception("Invalid UI mode for back button: " + mode); + } + } + + private void DrawSearchUi() { + bool searchClicked = false; + + // Pressing ENTER in the search terms box is the same as clicking "Search". We have to check + // this BEFORE we draw the text field, otherwise it will consume the event and we won't see it. + if (Event.current != null && Event.current.type == EventType.KeyDown && + Event.current.keyCode == KeyCode.Return && GUI.GetNameOfFocusedControl() == "searchTerms") { + searchClicked = true; + } + + GUILayout.Space(10); + GUILayout.Label("Search", EditorStyles.boldLabel); + GUILayout.Label("Enter search terms (or a Poly URL) below:", EditorStyles.wordWrappedLabel); + GUI.SetNextControlName("searchTerms"); + searchTerms = EditorGUILayout.TextField(searchTerms, EditorStyles.textArea); + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + searchClicked = GUILayout.Button("Search") || searchClicked; + guiHelper.EndHorizontal(); + + if (searchClicked && searchTerms.Trim().Length > 0) { + // Note: for privacy reasons we don't log the search terms, just the fact that a search was made. + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_SEARCHED); + + string assetId; + if (SearchTermIsAssetPage(searchTerms, out assetId)) { + manager.StartRequestForSpecificAsset(assetId); + SetUiMode(UiMode.DETAILS); + return; + } + + StartRequest(); + } + DrawResultsGrid(); + } + + /// + /// Returns true if the url was a valid asset url, false if not. + /// + /// The url of the asset to get. + /// The id of the asset to get from the url, null if the url is invalid. + private bool SearchTermIsAssetPage(string url, out string assetId) { + Regex regex = new Regex("^(http[s]?:\\/\\/)?.*\\/view\\/([^?]+)"); + Match match = regex.Match(url); + if (match.Success) { + assetId = match.Groups[2].ToString(); + assetId = assetId.Trim(); + return true; + } + + assetId = null; + return false; + } + + /// + /// Draws the query results grid, using the current query results as reported by + /// the AssetsBrowserManager. + /// + private void DrawResultsGrid() { + if (manager.IsQuerying) { + GUILayout.Space(30); + GUILayout.Label("Fetching assets. Please wait..."); + return; + } + + if (manager.CurrentResult == null) { + return; + } + + if (manager.CurrentResult != null && !manager.CurrentResult.Status.ok) { + GUILayout.Space(30); + GUILayout.Label("There was a problem with that request! Please try again later.", + EditorStyles.wordWrappedLabel); + + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Retry")) { + // Retry the previous request. + manager.ClearCaches(); + StartRequest(); + } + GUILayout.FlexibleSpace(); + guiHelper.EndHorizontal(); + + return; + } + + if (manager.CurrentResult.Value.assets == null || + manager.CurrentResult.Value.assets.Count == 0) { + GUILayout.Space(30); + GUILayout.Label("No results."); + return; + } + + PolyListAssetsResult result = manager.CurrentResult.Value; + if (!result.status.ok) { + GUILayout.Space(30); + GUILayout.Label("*** ERROR fetching results. Try again.."); + return; + } + + guiHelper.BeginHorizontal(); + string resultCountLabel; + resultCountLabel = string.Format("{0} assets found.", result.totalSize); + if (result.assets.Count < result.totalSize) { + resultCountLabel += string.Format(" (showing 1-{0}).", result.assets.Count); + } + GUILayout.Label(resultCountLabel, EditorStyles.miniLabel); + GUILayout.FlexibleSpace(); + bool refreshClicked = GUILayout.Button("Refresh"); + guiHelper.EndHorizontal(); + GUILayout.Space(5); + + if (refreshClicked) { + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_REFRESH_CLICKED); + manager.ClearCaches(); + StartRequest(); + return; + } + + // Calculate how many assets we want to show on each row, based on the window width. + // This allows the user to dynamically resize the window and have the assets grid + // automatically reflow. + int assetsPerRow = Mathf.Clamp( + Mathf.FloorToInt((position.width - 40) / (CELL_WIDTH + CELL_SPACING)), 1, 8); + int assetsThisRow = 0; + + // The assets grid is in a scroll view. + assetListScrollPos = guiHelper.BeginScrollView(assetListScrollPos); + GUILayout.Space(5); + guiHelper.BeginVertical(); + guiHelper.BeginHorizontal(); + + PolyAsset clickedAsset = null; + foreach (PolyAsset asset in result.assets) { + if (assetsThisRow >= assetsPerRow) { + // Begin new row. + guiHelper.EndHorizontal(); + GUILayout.Space(20); + guiHelper.BeginHorizontal(); + assetsThisRow = 0; + } + + if (assetsThisRow > 0) GUILayout.Space(CELL_SPACING); + guiHelper.BeginVertical(); + if (GUILayout.Button(asset.thumbnailTexture ?? loadingTex, + GUILayout.Width(THUMBNAIL_WIDTH), GUILayout.Height(THUMBNAIL_HEIGHT))) { + clickedAsset = asset; + } + GUILayout.Label(asset.displayName, EditorStyles.boldLabel, GUILayout.Width(CELL_WIDTH)); + GUILayout.Label(asset.authorName, GUILayout.Width(CELL_WIDTH)); + guiHelper.EndVertical(); + assetsThisRow++; + } + + // Complete last row with dummy cells. + if (assetsThisRow > 0) { + while (assetsThisRow < assetsPerRow) { + guiHelper.BeginVertical(); + GUILayout.Label("", GUILayout.Width(CELL_WIDTH), GUILayout.Height(THUMBNAIL_HEIGHT)); + GUILayout.Label("", EditorStyles.boldLabel, GUILayout.Width(CELL_WIDTH)); + GUILayout.Label("", GUILayout.Width(CELL_WIDTH)); + guiHelper.EndVertical(); + assetsThisRow++; + } + } + + guiHelper.EndHorizontal(); + GUILayout.Space(10); + + bool loadMoreClicked = false; + if (manager.resultHasMorePages) { + // If the current response has at least another page of results left, show the load more button. + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + loadMoreClicked = GUILayout.Button("Load more"); + GUILayout.FlexibleSpace(); + guiHelper.EndHorizontal(); + } + GUILayout.Space(5); + + guiHelper.EndVertical(); + guiHelper.EndScrollView(); + + if (loadMoreClicked) { + manager.GetNextPageRequest(); + return; + } + + if (clickedAsset != null) { + PrepareDetailsUi(clickedAsset); + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_ASSET_DETAILS_CLICKED, + GetAssetFormatDescription(selectedAsset)); + SetUiMode(UiMode.DETAILS); + } + } + + /// + /// Called as a callback by AssetBrowserManager whenever something interesting changes which + /// requires us to update our UI. + /// + private void UpdateUi() { + // Tell Unity to repaint this window, which will invoke OnGUI(). + Repaint(); + } + + /// + /// Returns whether or not the given category requires authentication. + /// + private bool CategoryRequiresAuth(int category) { + string key = CATEGORIES[category].key; + return (key == KEY_YOUR_UPLOADS || key == KEY_YOUR_LIKES); + } + + /// + /// Shows the profile dropdown menu (the dropdown menu that appears when the user clicks their profile + /// picture). + /// + private void ShowProfileDropdownMenu() { + GenericMenu menu = new GenericMenu(); + menu.AddItem(new GUIContent("My Profile (web)"), /* on */ false, () => { + PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_VIEW_PROFILE); + Application.OpenURL(USER_PROFILE_URL); + }); + menu.AddSeparator(""); + menu.AddItem(new GUIContent("Sign Out"), /* on */ false, () => { + PolyApi.SignOut(); + PtAnalytics.SendEvent(PtAnalytics.Action.ACCOUNT_SIGN_OUT); + // If the user was viewing a category that requires sign in, reset back to the home page. + if (queryRequiresAuth) { + selectedCategory = CATEGORY_FEATURED; + StartRequest(); + } + }); + menu.ShowAsContext(); + } + + /// + /// Callback invoked when the user picks a new category from the category dropdown. + /// + /// The index of the selected category. + private void DropdownMenuCallback(object userData) { + int selection = (int)userData; + if (selection == selectedCategory) return; + + // If the user picked a category that requires authentication, and they are not authenticated, + // tell them why they can't view it. + if (CategoryRequiresAuth(selection) && !PolyApi.IsAuthenticated) { + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_MISSING_AUTH); + EditorUtility.DisplayDialog("Sign in required", + "To view your uploads or likes, you must sign in first.", "OK"); + return; + } + + selectedCategory = (int)userData; + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_CATEGORY_SELECTED, CATEGORIES[selectedCategory].key); + + StartRequest(); + } + + /// + /// Builds and returns a PolyRequest from the current state of the AssetBrowswerWindow variables. + /// + private PolyRequest BuildRequest() { + CategoryInfo info = CATEGORIES[selectedCategory]; + + if (info.key == KEY_YOUR_UPLOADS) { + PolyListUserAssetsRequest listUserAssetsRequest = PolyListUserAssetsRequest.MyNewest(); + listUserAssetsRequest.formatFilter = assetTypeFilter; + return listUserAssetsRequest; + } + + if (info.key == KEY_YOUR_LIKES) { + PolyListLikedAssetsRequest listLikedAssetsRequest = PolyListLikedAssetsRequest.MyLiked(); + return listLikedAssetsRequest; + } + + PolyListAssetsRequest listAssetsRequest; + if (info.key == KEY_FEATURED) { + listAssetsRequest = PolyListAssetsRequest.Featured(); + } else { + listAssetsRequest = new PolyListAssetsRequest(); + listAssetsRequest.category = info.polyCategory; + } + + listAssetsRequest.formatFilter = assetTypeFilter; + // Only show curated results. + listAssetsRequest.curated = true; + return listAssetsRequest; + } + + /// + /// Sends a list assets request to the assets service according to the parameters currently selected in the UI. + /// When the request is done, AssetBrowserManager will inform us through the callback. + /// + private void StartRequest() { + if (mode == UiMode.BROWSE) { + PolyRequest request = BuildRequest(); + queryRequiresAuth = CategoryRequiresAuth(selectedCategory); + if (!queryRequiresAuth || PolyApi.IsAuthenticated) { + manager.StartRequest(request); + } + } else if (mode == UiMode.SEARCH) { + PolyListAssetsRequest request = new PolyListAssetsRequest(); + request.keywords = searchTerms; + manager.StartRequest(request); + } else { + throw new System.Exception("Unexpected UI mode for StartQuery: " + mode); + } + // Reset scroll bar position. + assetListScrollPos = Vector2.zero; + } + + /// + /// Set the variables of the details ui page for the newly selected asset. + /// + private void PrepareDetailsUi(PolyAsset newSelectedAsset) { + selectedAsset = newSelectedAsset; + ptAssetLocalPath = PtUtils.GetDefaultPtAssetPath(selectedAsset); + detailsScrollPos = Vector2.zero; + importOptions = PtSettings.Instance.defaultImportOptions; + } + + /// + /// Draws the asset details page. This is the page that shows the details about the selected asset + /// and allows the user to click to import it. + /// + private void DrawDetailsUi() { + if (manager.IsQuerying) { + // Check if manager is querying for a specific asset. + GUILayout.Space(30); + GUILayout.Label("Fetching asset. Please wait..."); + selectedAsset = null; + return; + } + + // If we just got the specific asset result, set the details ui import options to the relevant + // variables at first. + if (manager.CurrentAssetResult != null && selectedAsset == null) { + PrepareDetailsUi(manager.CurrentAssetResult); + } + + // Check if the user selected something in the PtAsset picker. + if (Event.current != null && Event.current.commandName == "ObjectSelectorUpdated") { + UnityEngine.Object picked = EditorGUIUtility.GetObjectPickerObject(); + ptAssetLocalPath = (picked != null && picked is PtAsset) ? + AssetDatabase.GetAssetPath(picked) : + PtUtils.GetDefaultPtAssetPath(selectedAsset); + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_LOCATION_CHANGED); + } + + if (selectedAsset == null) { + manager.ClearCurrentAssetResult(); + GUILayout.Space(20); + GUILayout.Label("Asset not found."); + return; + } + + detailsScrollPos = guiHelper.BeginScrollView(detailsScrollPos); + const float width = 150; + + guiHelper.BeginHorizontal(); + GUILayout.Label(selectedAsset.displayName, detailsTitleStyle); + guiHelper.EndHorizontal(); + + guiHelper.BeginHorizontal(); + GUILayout.Label(selectedAsset.authorName, EditorStyles.wordWrappedLabel); + + GUILayout.FlexibleSpace(); + if (GUILayout.Button("View on Web", GUILayout.MaxWidth(100))) { + PtAnalytics.SendEvent(PtAnalytics.Action.BROWSE_VIEW_ON_WEB); + Application.OpenURL(selectedAsset.Url); + } + GUILayout.Space(5); + guiHelper.EndHorizontal(); + + GUILayout.Space(10); + + Texture2D image = selectedAsset.thumbnailTexture ?? loadingTex; + float displayWidth = position.width - 40; + float displayHeight = image.height * displayWidth / image.width; + displayHeight = Mathf.Min(image.height, displayHeight); + displayWidth = Mathf.Min(image.width, displayWidth); + GUILayout.Label(image, GUILayout.Width(displayWidth), GUILayout.Height(displayHeight)); + + if (manager.IsDownloadingAsset(selectedAsset)) { + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + GUILayout.Label("Downloading asset. Please wait..."); + GUILayout.FlexibleSpace(); + guiHelper.EndHorizontal(); + guiHelper.EndScrollView(); + return; + } else if (justImported) { + Rect lastRect = GUILayoutUtility.GetLastRect(); + Rect boxRect = new Rect(PADDING, lastRect.yMax + PADDING, position.width - 2 * PADDING, + IMPORT_SUCCESS_BOX_HEIGHT); + GUI.DrawTexture(boxRect, backBarBackgroundTex); + GUILayout.Space(20); + guiHelper.BeginHorizontal(); + GUILayout.FlexibleSpace(); + guiHelper.BeginVertical(); + GUILayout.Label("Asset successfully imported."); + GUILayout.Label("Saved to: " + PtSettings.Instance.assetObjectsPath); + GUILayout.Space(10); + if (GUILayout.Button("OK")) { + // Dismiss. + justImported = false; + } + guiHelper.EndVertical(); + GUILayout.FlexibleSpace(); + guiHelper.EndHorizontal(); + guiHelper.EndScrollView(); + return; + } + + GUILayout.Space(5); + GUILayout.Label("Import Options", EditorStyles.boldLabel); + GUILayout.Space(5); + + guiHelper.BeginHorizontal(); + GUILayout.Space(12); + GUILayout.Label("Import Location", GUILayout.Width(width)); + GUILayout.FlexibleSpace(); + GUILayout.Label(ptAssetLocalPath, EditorStyles.wordWrappedLabel); + GUILayout.Space(5); + guiHelper.EndHorizontal(); + + guiHelper.BeginHorizontal(); + GUILayout.Space(width - 10); + GUILayout.FlexibleSpace(); + if (GUILayout.Button("Replace existing...")) { + PtAsset current = AssetDatabase.LoadAssetAtPath(ptAssetLocalPath); + EditorGUIUtility.ShowObjectPicker(current, /* allowSceneObjects */ false, PtAsset.FilterString, 0); + } + GUILayout.Space(5); + guiHelper.EndHorizontal(); + + EditTimeImportOptions oldOptions = importOptions; + importOptions = ImportOptionsGui.ImportOptionsField(importOptions); + if (oldOptions.alsoInstantiate != importOptions.alsoInstantiate) { + // Persist 'always instantiate' option if it was changed. + PtSettings.Instance.defaultImportOptions.alsoInstantiate = importOptions.alsoInstantiate; + } + SendImportOptionMutationAnalytics(oldOptions, importOptions); + + GUILayout.Space(10); + GUILayout.Box("", GUILayout.ExpandWidth(true), GUILayout.Height(1)); + GUILayout.Space(10); + + guiHelper.BeginHorizontal(); + GUIStyle buttonStyle = new GUIStyle(GUI.skin.button); + buttonStyle.fontStyle = FontStyle.Bold; + buttonStyle.padding = new RectOffset(10, 10, 8, 8); + bool importButtonClicked = GUILayout.Button("Import into Project", buttonStyle); + GUILayout.FlexibleSpace(); + guiHelper.EndHorizontal(); + GUILayout.Space(5); + + if (!File.Exists(PtUtils.ToAbsolutePath(ptAssetLocalPath))) { + GUILayout.Label(PolyInternalUtils.ATTRIBUTION_NOTICE, EditorStyles.wordWrappedLabel); + } else { + GUILayout.Label("WARNING: The indicated asset already exists and will be OVERWRITTEN by " + + "the new asset. Existing instances of the old asset will be automatically updated " + + "to the new asset.", + EditorStyles.wordWrappedLabel); + } + guiHelper.EndScrollView(); + + if (importButtonClicked) { + if (!ptAssetLocalPath.StartsWith("Assets/") || !ptAssetLocalPath.EndsWith(".asset")) { + EditorUtility.DisplayDialog("Invalid import path", + "The import path must begin with Assets/ and have the .asset extension.", "OK"); + return; + } + string errorString; + if (!ValidateImportOptions(importOptions.baseOptions, out errorString)) { + EditorUtility.DisplayDialog("Invalid import options", errorString, "OK"); + return; + } + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_STARTED, GetAssetFormatDescription(selectedAsset)); + if (previousMode == UiMode.SEARCH) { + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_STARTED_FROM_SEARCH, + GetAssetFormatDescription(selectedAsset)); + } + + manager.StartDownloadAndImport(selectedAsset, ptAssetLocalPath, importOptions); + } + } + + /// + /// Returns whether the import options are valid and, if not, a relevant error message to display. + /// + private bool ValidateImportOptions(PolyImportOptions options, out string errorString) { + switch (options.rescalingMode) { + case PolyImportOptions.RescalingMode.CONVERT: + if (options.scaleFactor == 0.0f) { + errorString = "Scale factor must not be 0."; + return false; + } + break; + case PolyImportOptions.RescalingMode.FIT: + if (options.desiredSize == 0.0f) { + errorString = "Desired size must not be 0."; + return false; + } + break; + default: + throw new System.Exception("Import options must hvae a valid rescaling mode"); + } + errorString = null; + return true; + } + + /// + /// Returns true if the current UI mode has a back button bar. + /// + /// True if the current UI mode has a back button bar, false otherwise. + private bool HasBackButtonBar() { + return mode == UiMode.DETAILS || mode == UiMode.SEARCH; + } + + /// + /// Draws the "Back" button bar. + /// + /// True if the back button was clicked, false otherwise. + private bool DrawBackButtonBar() { + guiHelper.BeginArea(new Rect(0, TITLE_BAR_HEIGHT, position.width, BACK_BUTTON_BAR_HEIGHT)); + GUI.DrawTexture(new Rect(0, 0, position.width, BACK_BUTTON_BAR_HEIGHT), backBarBackgroundTex); + guiHelper.BeginHorizontal(); + GUILayout.Space(PADDING); + bool backButtonClicked = GUILayout.Button(new GUIContent("Back", backArrowTex), EditorStyles.miniLabel); + GUILayout.FlexibleSpace(); + guiHelper.EndHorizontal(); + guiHelper.EndArea(); + return backButtonClicked; + } + + /// + /// Returns a string with all the asset formats of the given asset. + /// + /// The asset. + /// A string with all the comma-separated asset formats of the given asset. + private string GetAssetFormatDescription(PolyAsset asset) { + List formatTypes = new List(); + if (asset.formats != null) { + foreach (PolyFormat format in asset.formats) { + formatTypes.Add(format.formatType.ToString()); + } + } else { + // Shouldn't happen [tm]. + formatTypes.Add("NULL"); + } + formatTypes.Sort(); + return string.Join(",", formatTypes.ToArray()); + } + + /// + /// Sends Analytics events for mutations in the given import options. + /// + /// The options before the mutation. + /// The options after the mutation. + private void SendImportOptionMutationAnalytics(EditTimeImportOptions before, EditTimeImportOptions after) { + if (before.alsoInstantiate != after.alsoInstantiate) { + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_INSTANTIATE_TOGGLED, after.alsoInstantiate.ToString()); + } + if (before.baseOptions.desiredSize != after.baseOptions.desiredSize) { + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_DESIRED_SIZE_SET, + after.baseOptions.desiredSize.ToString()); + } + if (before.baseOptions.recenter != after.baseOptions.recenter) { + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_RECENTER_TOGGLED, + after.baseOptions.recenter.ToString()); + } + if (before.baseOptions.rescalingMode != after.baseOptions.rescalingMode) { + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_SCALE_MODE_CHANGED, + after.baseOptions.rescalingMode.ToString()); + } + if (before.baseOptions.scaleFactor != after.baseOptions.scaleFactor) { + PtAnalytics.SendEvent(PtAnalytics.Action.IMPORT_SCALE_FACTOR_CHANGED, + after.baseOptions.scaleFactor.ToString()); + } + } + + private void OnDestroy() { + PtDebug.Log("ABW: destroying."); + manager.SetRefreshCallback(null); + } + + private struct CategoryInfo { + public string key; + public string title; + public PolyCategory polyCategory; + public CategoryInfo(string key, string title, PolyCategory polyCategory = PolyCategory.UNSPECIFIED) { + this.key = key; + this.title = title; + this.polyCategory = polyCategory; + } + } +} + +}