using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using UnityEditor.Localization.Plugins.Google.Columns; using UnityEditor.Localization.Reporting; using UnityEditor.Localization.UI; using UnityEngine; namespace UnityEditor.Localization.Plugins.Google { class GoogleSheetsExtensionPropertyDrawerData : PropertyDrawerExtendedData { public SerializedProperty m_Collection; public SerializedProperty m_SheetsServiceProvider; public SerializedProperty m_SpreadSheetId; public SerializedProperty m_SheetId; public SerializedProperty m_Columns; public SerializedProperty m_RemoveMissingPulledKeys; public string m_NewSheetName; public ReorderableListExtended columnsList; public Task pushTask; public SheetsServiceProvider Provider => m_SheetsServiceProvider.objectReferenceValue as SheetsServiceProvider; public bool UsingApiKey => Provider?.Authentication != AuthenticationType.OAuth; } [CustomPropertyDrawer(typeof(GoogleSheetsExtension))] class GoogleSheetsExtensionPropertyDrawer : PropertyDrawerExtended { class Styles { public static readonly GUIContent addDefaultColumns = EditorGUIUtility.TrTextContent("Add Default Columns"); public static readonly GUIContent addLocalesInSheet = EditorGUIUtility.TrTextContent("Add Project Locales Found In Sheet"); public static readonly GUIContent addSheet = EditorGUIUtility.TrTextContent("Add Sheet"); public static readonly GUIContent createNewSpredsheet = EditorGUIUtility.TrTextContent("Create New Spreadsheet"); public static readonly GUIContent extractColumns = EditorGUIUtility.TrTextContent("Extract Columns From Sheet"); public static readonly GUIContent header = EditorGUIUtility.TrTextContent("Google Sheets", EditorIcons.GoogleSheets); public static readonly GUIContent newSheetName = EditorGUIUtility.TrTextContent("Sheet Name"); public static readonly GUIContent noSheetsFound = EditorGUIUtility.TrTextContent("No Sheets Could Be Found"); public static readonly GUIContent mappedColumns = EditorGUIUtility.TrTextContent("Mapped Columns"); public static readonly GUIContent openSpreadsheet = EditorGUIUtility.TrTextContent("Open", "Opens the sheet in an external browser"); public static readonly GUIContent push = EditorGUIUtility.TrTextContent("Push"); public static readonly GUIContent pull = EditorGUIUtility.TrTextContent("Pull"); public static readonly GUIContent pushSelected = EditorGUIUtility.TrTextContent("Push Selected"); public static readonly GUIContent pullSelected = EditorGUIUtility.TrTextContent("Pull Selected"); public static readonly GUIContent selectSheet = EditorGUIUtility.TrTextContent("Select Sheet"); public static readonly GUIContent sheetId = EditorGUIUtility.TrTextContent("Sheet Id", "The Sheet Id from your Google Spreadsheet. In the Spreadsheet’s Google URL, this is at the end of the URL: https://docs.google.com/spreadsheets/d/SpreadhsheetId/edit#gid=sheetId"); public static readonly GUIContent spreadSheetId = EditorGUIUtility.TrTextContent("Spreadsheet Id", "The Spreadsheet Id from your Google Spreadsheet. In the Spreadsheet’s Google URL, this is in the middle of the URL: https://docs.google.com/spreadsheets/d/SpreadhsheetId/edit#gid=sheetId"); } /// /// Keep track of any Async push requests in case the property drawer is closed/reopened. /// static List<(LocalizationTableCollection collection, Task pushTask)> s_PushRequests = new List<(LocalizationTableCollection, Task)>(); public override GoogleSheetsExtensionPropertyDrawerData CreatePropertyData(SerializedProperty property) { var data = new GoogleSheetsExtensionPropertyDrawerData { m_Collection = property.FindPropertyRelative("m_Collection"), m_SheetsServiceProvider = property.FindPropertyRelative("m_SheetsServiceProvider"), m_SpreadSheetId = property.FindPropertyRelative("m_SpreadsheetId"), m_SheetId = property.FindPropertyRelative("m_SheetId"), m_Columns = property.FindPropertyRelative("m_Columns"), m_RemoveMissingPulledKeys = property.FindPropertyRelative("m_RemoveMissingPulledKeys") }; var ltc = data.m_Collection.objectReferenceValue as LocalizationTableCollection; data.m_NewSheetName = ltc.TableCollectionName; data.columnsList = new ReorderableListExtended(property.serializedObject, data.m_Columns); data.columnsList.Header = Styles.mappedColumns; data.columnsList.AddMenuType = typeof(SheetColumn); var currentPushRequest = s_PushRequests.FirstOrDefault(tc => ReferenceEquals(tc.collection, ltc)); if (currentPushRequest != default) { if (currentPushRequest.pushTask.IsCompleted) s_PushRequests.Remove(currentPushRequest); else data.pushTask = currentPushRequest.pushTask; } data.columnsList.AddMenuItems = menu => { menu.AddSeparator(string.Empty); menu.AddItem(Styles.addDefaultColumns, false, () => { var columns = ColumnMapping.CreateDefaultMapping(); data.m_Columns.ClearArray(); foreach (var c in columns) { var colElement = data.m_Columns.AddArrayElement(); colElement.managedReferenceValue = c; } data.m_Columns.serializedObject.ApplyModifiedProperties(); }); // We can not extract the column data when using an if (!data.UsingApiKey) { if (string.IsNullOrEmpty(data.m_SpreadSheetId.stringValue)) { menu.AddDisabledItem(Styles.extractColumns); } else { menu.AddItem(Styles.extractColumns, false, () => { var target = property.GetActualObjectForSerializedProperty(fieldInfo); var google = GetGoogleSheets(data); var titles = google.GetColumnTitles(data.m_SheetId.intValue); List unused = new List(); var columns = ColumnMapping.CreateMappingsFromColumnNames(titles, unused); if (unused.Count > 0) { Debug.Log($"Could not map: {string.Join(", ", unused)}"); } data.m_Columns.ClearArray(); foreach (var c in columns) { var colElement = data.m_Columns.AddArrayElement(); colElement.managedReferenceValue = c; } data.m_Columns.serializedObject.ApplyModifiedProperties(); }); } } }; data.columnsList.CreateNewInstance = type => { // Try to extract the next free column var instance = Activator.CreateInstance(type) as SheetColumn; try { var target = property.GetActualObjectForSerializedProperty(fieldInfo); var usedColumns = target.Columns.Where(c => !string.IsNullOrEmpty(c.Column)).Select(c => c.Column); instance.Column = ColumnMapping.GetNextAvailableColumn(usedColumns.ToArray()); } catch (Exception e) { Debug.LogException(e); } return instance; }; return data; } public override void OnGUI(GoogleSheetsExtensionPropertyDrawerData data, Rect position, SerializedProperty property, GUIContent label) { position.yMin += EditorGUIUtility.standardVerticalSpacing; position.height = EditorGUIUtility.singleLineHeight; EditorGUI.LabelField(position, Styles.header, EditorStyles.boldLabel); position.MoveToNextLine(); EditorGUI.PropertyField(position, data.m_SheetsServiceProvider); position.MoveToNextLine(); EditorGUI.BeginDisabledGroup(data.m_SheetsServiceProvider.objectReferenceValue == null); using (new EditorGUI.DisabledGroupScope(data.m_SheetsServiceProvider.objectReferenceValue == null)) { EditorGUI.BeginDisabledGroup(data.UsingApiKey); { if (GUI.Button(position, Styles.createNewSpredsheet)) { var google = GetGoogleSheets(data); var results = google.CreateSpreadsheet(PlayerSettings.productName, data.m_NewSheetName, data.Provider.NewSheetProperties, new ProgressBarReporter { ReportTaskSummaryInConsole = true }); data.m_SpreadSheetId.stringValue = results.spreadSheetId; data.m_SheetId.intValue = results.sheetId; // Exit GUI to prevent erros due to GUI state changes. (LOC-698) data.m_SpreadSheetId.serializedObject.ApplyModifiedProperties(); GUIUtility.ExitGUI(); } position.MoveToNextLine(); } EditorGUI.EndDisabledGroup(); var spreadSheetPos = position.SplitHorizontalFixedWidthRight(50); EditorGUI.PropertyField(spreadSheetPos.left, data.m_SpreadSheetId, Styles.spreadSheetId); using (new EditorGUI.DisabledGroupScope(string.IsNullOrEmpty(data.m_SpreadSheetId.stringValue))) { if (GUI.Button(spreadSheetPos.right, Styles.openSpreadsheet)) { GoogleSheets.OpenSheetInBrowser(data.m_SpreadSheetId.stringValue, data.m_SheetId.intValue); } } position.MoveToNextLine(); DrawSheetField(data, ref position); DrawColumnsField(data, ref position); DrawSyncControls(data, property, ref position); } } void DrawSheetField(GoogleSheetsExtensionPropertyDrawerData data, ref Rect position) { EditorGUI.BeginDisabledGroup(string.IsNullOrEmpty(data.m_SpreadSheetId.stringValue)); // Add new sheet, select sheet EditorGUI.PropertyField(position, data.m_SheetId, Styles.sheetId); position.MoveToNextLine(); EditorGUI.BeginDisabledGroup(data.UsingApiKey); var sheetNamePos = position.SplitHorizontal(); var buttonPos = sheetNamePos.right.SplitHorizontal(); data.m_NewSheetName = EditorGUI.TextField(sheetNamePos.left, data.m_NewSheetName); if (GUI.Button(buttonPos.left, Styles.addSheet)) { try { EditorUtility.DisplayProgressBar("Add Sheet", string.Empty, 0); var google = GetGoogleSheets(data); data.m_SheetId.intValue = google.AddSheet(data.m_NewSheetName, data.Provider.NewSheetProperties); data.m_SheetId.serializedObject.ApplyModifiedProperties(); } catch (Exception e) { Debug.LogException(e); } finally { EditorUtility.ClearProgressBar(); // Exit GUI to prevent erros due to GUI state changes. (LOC-698) GUIUtility.ExitGUI(); } } EditorGUI.EndDisabledGroup(); if (EditorGUI.DropdownButton(buttonPos.right, Styles.selectSheet, FocusType.Passive)) { try { EditorUtility.DisplayProgressBar("Fetching Sheet", string.Empty, 0); var google = GetGoogleSheets(data); var sheets = google.GetSheets(); EditorUtility.ClearProgressBar(); var menu = new GenericMenu(); foreach (var s in sheets) { menu.AddItem(new GUIContent(s.name), data.m_SheetId.intValue == s.id, () => { data.m_SheetId.intValue = s.id; data.m_SheetId.serializedObject.ApplyModifiedProperties(); }); } if (menu.GetItemCount() == 0) menu.AddDisabledItem(Styles.noSheetsFound); menu.DropDown(buttonPos.right); } catch (Exception e) { Debug.LogException(e); } finally { EditorUtility.ClearProgressBar(); } } position.MoveToNextLine(); EditorGUI.EndDisabledGroup(); } void DrawColumnsField(GoogleSheetsExtensionPropertyDrawerData data, ref Rect position) { position.height = data.columnsList.GetHeight(); data.columnsList.DoList(position); position.MoveToNextLine(); position.height = EditorGUIUtility.singleLineHeight; position.MoveToNextLine(); } void DrawSyncControls(GoogleSheetsExtensionPropertyDrawerData data, SerializedProperty property, ref Rect position) { EditorGUI.PropertyField(position, data.m_RemoveMissingPulledKeys); position.MoveToNextLine(); // Disable if we have no destination sheet. var splitRow = position.SplitHorizontal(); position.MoveToNextLine(); if (data.pushTask != null && data.pushTask.IsCompleted) { var target = property.GetActualObjectForSerializedProperty(fieldInfo); var collection = target.TargetCollection as StringTableCollection; var currentPushRequest = s_PushRequests.FirstOrDefault(tc => ReferenceEquals(tc.collection, collection)); s_PushRequests.Remove(currentPushRequest); data.pushTask = null; } using (new EditorGUI.DisabledGroupScope(data.pushTask != null || string.IsNullOrEmpty(data.m_SpreadSheetId.stringValue) || data.columnsList.count == 0)) { using (new EditorGUI.DisabledGroupScope(data.columnsList.index < 0)) { if (GUI.Button(splitRow.left, Styles.pushSelected)) { var google = GetGoogleSheets(data); var target = property.GetActualObjectForSerializedProperty(fieldInfo); var selectedCollection = GetSelectedColumns(data.columnsList.index, property); var collection = target.TargetCollection as StringTableCollection; data.pushTask = google.PushStringTableCollectionAsync(data.m_SheetId.intValue, collection, selectedCollection, TaskReporter.CreateDefaultReporter()); s_PushRequests.Add((collection, data.pushTask)); // Exit GUI to prevent erros due to GUI state changes. (LOC-698) GUIUtility.ExitGUI(); } if (GUI.Button(splitRow.right, Styles.pullSelected)) { var google = GetGoogleSheets(data); var target = property.GetActualObjectForSerializedProperty(fieldInfo); var selectedCollection = GetSelectedColumns(data.columnsList.index, property); google.PullIntoStringTableCollection(data.m_SheetId.intValue, target.TargetCollection as StringTableCollection, selectedCollection, data.m_RemoveMissingPulledKeys.boolValue, TaskReporter.CreateDefaultReporter(), true); // Exit GUI to prevent erros due to GUI state changes. (LOC-698) GUIUtility.ExitGUI(); } } splitRow = position.SplitHorizontal(); position.MoveToNextLine(); if (GUI.Button(splitRow.left, Styles.push)) { var google = GetGoogleSheets(data); var target = property.GetActualObjectForSerializedProperty(fieldInfo); var collection = target.TargetCollection as StringTableCollection; data.pushTask = google.PushStringTableCollectionAsync(data.m_SheetId.intValue, collection, target.Columns, TaskReporter.CreateDefaultReporter()); s_PushRequests.Add((collection, data.pushTask)); // Exit GUI to prevent erros due to GUI state changes. (LOC-698) GUIUtility.ExitGUI(); } if (GUI.Button(splitRow.right, Styles.pull)) { var google = GetGoogleSheets(data); var target = property.GetActualObjectForSerializedProperty(fieldInfo); google.PullIntoStringTableCollection(data.m_SheetId.intValue, target.TargetCollection as StringTableCollection, target.Columns, data.m_RemoveMissingPulledKeys.boolValue, TaskReporter.CreateDefaultReporter(), true); // Exit GUI to prevent erros due to GUI state changes. (LOC-698) GUIUtility.ExitGUI(); } } } IList GetSelectedColumns(int selectedIndex, SerializedProperty property) { // Return the selected column and a key column var target = property.GetActualObjectForSerializedProperty(fieldInfo); var selectedCol = target.Columns[selectedIndex]; // If the selected column is the key column then thats all we need. if (selectedCol is IPullKeyColumn) return new SheetColumn[] { selectedCol }; // Find the Key column and add it. var keyCol = target.Columns.FirstOrDefault(c => c is IPullKeyColumn); if (keyCol == null) { Debug.LogError($"Missing Key Column. A Key Column is required in order to pull and push, please add one to '{Styles.mappedColumns.text}'"); return null; } return new SheetColumn[] { keyCol, selectedCol }; } GoogleSheets GetGoogleSheets(GoogleSheetsExtensionPropertyDrawerData data) { var google = new GoogleSheets(data.Provider); google.SpreadSheetId = data.m_SpreadSheetId.stringValue; return google; } public override float GetPropertyHeight(GoogleSheetsExtensionPropertyDrawerData data, SerializedProperty property, GUIContent label) { float height = EditorGUIUtility.standardVerticalSpacing; // top padding height += (EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing) * 10; height += data.columnsList.GetHeight() + EditorGUIUtility.standardVerticalSpacing; return height; } } }