Skip to content

Commit

Permalink
Added locale and localized string explorer windows; updated localized…
Browse files Browse the repository at this point in the history
… string search window
  • Loading branch information
Danae Dekker committed Apr 3, 2024
1 parent 080b188 commit 00f60eb
Show file tree
Hide file tree
Showing 10 changed files with 756 additions and 63 deletions.
182 changes: 182 additions & 0 deletions Editor/Windows/LocaleExplorerTreeView.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using Audune.Utils.UnityEditor.Editor;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
using UnityEngine;

namespace Audune.Localization.Editor
{
// Class that defines a tree view for the locale explorer window
public class LocaleExplorerTreeView : ItemsTreeView<string>
{
// Default strings
private const string _missingDisplayName = "Strings with missing values";
private const string _stringsDisplayName = "Strings";


// Default options for the tree view
private static readonly Options _options = new Options {
displayNameSelector = key => key.Split('.')[^1],
iconSelector = key => EditorIcons.text,
groupIconSelector = (path, expanded) => {
if (path.Length > 1)
return expanded ? EditorIcons.folderOpened : EditorIcons.folder;
else if (path[0] == _missingDisplayName)
return EditorIcons.errorMark;
else if (path[0] == _stringsDisplayName)
return EditorIcons.font;
else
return null;
},
};


// The locales used in the tree view
private List<Locale> _locales;
private Dictionary<string, List<string>> _values;


// Constructor
public LocaleExplorerTreeView(IEnumerable<Locale> locales) : base(LocalesToKeys(locales), _options)
{
_locales = new List<Locale>(locales ?? Enumerable.Empty<Locale>());
_values = LocalesToKeys(_locales).ToDictionary(key => key, key => _locales.Select(locale => locale.strings.Find(key)).Where(value => value != null).ToList());

multiColumnHeader = new MultiColumnHeader(new MultiColumnHeaderState(Enumerable
.Repeat(new MultiColumnHeaderState.Column() { headerContent = new GUIContent("Key"), width = 275, canSort = false, allowToggleVisibility = false }, 1)
.Concat(LocalesToColumms(locales))
.ToArray()));
}


// Build the items of the tree view
protected override void Build(ref TreeViewItem rootItem, ref int id)
{
// Create root items for different types of items
var missingRootItem = CreateGroupItem(id++, new[] { _missingDisplayName });
var stringsRootItem = CreateGroupItem(id++, new[] { _stringsDisplayName });

// Iterate over the items and create data items for them
var stringsPathItems = new Dictionary<string, GroupItem>();
foreach (var item in items)
{
// Create the data item
var dataItem = CreateDataItem(id++, item);
var hasMissingValues = _locales.ContainsMissingString(item);
if (hasMissingValues)
dataItem.icon = EditorIcons.errorMark;

// Create a separate data item if the localized string of the item contains a missing value
if (hasMissingValues)
missingRootItem.AddChild(CreateDataItem(id++, item, displayName: item, icon: EditorIcons.errorMark));

// Create the path items for the string item
var path = item.Split('.');
var pathItem = stringsRootItem;
for (int i = 0; i < path.Length - 1; i++)
{
var joinedPath = path[..(i + 1)];
var joinedPathString = string.Join("/", path[..(i + 1)]);
if (!stringsPathItems.TryGetValue(joinedPathString, out var newPathItem))
{
newPathItem = CreateGroupItem(id++, new[] { _stringsDisplayName }.Concat(joinedPath).ToArray(), path[i]);
pathItem.AddChild(newPathItem);
stringsPathItems.Add(joinedPathString, newPathItem);
}
pathItem = newPathItem;
}

// Add the data item to the correct path item
pathItem.AddChild(dataItem);
}

// Add root items
if (missingRootItem.hasChildren)
rootItem.AddChild(missingRootItem);
rootItem.AddChild(stringsRootItem);
}

// Draw a cell that represents a data item in the tree view
protected override void OnDataCellGUI(DataItem item, int columnIndex, Rect columnRect)
{
if (columnIndex == 0)
{
// Key column
if (!isSearching)
columnRect = columnRect.ContractLeft(GetContentIndent(item));

var hasMissingValues = !string.IsNullOrEmpty(item.data) && _locales.ContainsMissingString(item.data);
EditorGUI.LabelField(columnRect, HighlightSearchString(new GUIContent(isSearching ? item.data : item.displayName, hasMissingValues ? EditorIcons.errorMark : item.icon)), label);
}
else if (!string.IsNullOrEmpty(item.data))
{
// Locale column
EditorGUI.LabelField(columnRect, HighlightSearchString(_locales[columnIndex - 1].strings.TryFind(item.data, out var value) ? value.Replace("\n", " ") : "<color=red><Undefined></color>"), label);
}
}

// Draw a cell that represents a group item in the tree view
protected override void OnGroupCellGUI(GroupItem item, int columnIndex, Rect columnRect)
{
if (columnIndex == 0)
{
// Key column
if (!isSearching)
{
columnRect = columnRect.ContractLeft(GetContentIndent(item));
EditorGUI.LabelField(columnRect, item, boldLabel);
}
}
}

// Handler for when an item is double clicked
protected override void OnDoubleClicked(DataItem item)
{
LocalizedStringExplorerWindow.ShowWindow(searchString: item.data);
}

// Return a context menu for a data item
protected override GenericMenu GetDataItemContextMenu(DataItem item)
{
var menu = new GenericMenu();

menu.AddItem(new GUIContent("Find References"), false, () => LocalizedStringExplorerWindow.ShowWindow(searchString: item.data));

menu.AddSeparator("");

menu.AddItem(new GUIContent("Expand All"), false, () => ExpandAll());
menu.AddItem(new GUIContent("Collapse All"), false, () => CollapseAll());

return menu;
}

// Return if an item matches the specified search query
protected override bool Matches(string data, string search)
{
if (string.IsNullOrEmpty(data))
return false;
if (string.IsNullOrEmpty(search))
return true;

return data.Contains(search, StringComparison.InvariantCultureIgnoreCase)
|| (_values.TryGetValue(data, out var values) && values.Any(value => value.Contains(search, StringComparison.InvariantCultureIgnoreCase)));
}


#region Convert locales to keys and columns
// Convert a list of locales to keys
private static IEnumerable<string> LocalesToKeys(IEnumerable<Locale> locales)
{
return locales?.SelectMany(locale => locale.strings.Keys).Distinct() ?? Enumerable.Empty<string>();
}

// Convert a list of locales to tree view columns
private static IEnumerable<MultiColumnHeaderState.Column> LocalesToColumms(IEnumerable<Locale> locales)
{
return locales?.Select(locale => new MultiColumnHeaderState.Column() { headerContent = new GUIContent($"{locale.englishName} Value"), width = 150, canSort = false }) ?? Enumerable.Empty<MultiColumnHeaderState.Column>();
}
#endregion
}
}
11 changes: 11 additions & 0 deletions Editor/Windows/LocaleExplorerTreeView.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

114 changes: 114 additions & 0 deletions Editor/Windows/LocaleExplorerWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
using Audune.Utils.UnityEditor.Editor;
using UnityEditor;
using UnityEngine;

namespace Audune.Localization.Editor
{
// Class that defines an editor window for exploring locales in the project
[EditorWindowTitle(title = "Locale Explorer")]
public sealed class LocaleExplorerWindow : EditorWindow
{
// Show the window
public static void ShowWindow(string searchString = null, string selected = null)
{
var window = GetWindow<LocaleExplorerWindow>();
window.minSize = new Vector2(800, 600);
window.Refresh(searchString, selected);
}

// Show the window without context
[MenuItem("Window/Audune Localization/Locale Explorer _%#&L", secondaryPriority = 0)]
public static void ShowWindow()
{
ShowWindow(null, null);
}


// Tree view for displaying the localized strings
private LocaleExplorerTreeView _treeView;


// Refresh the window
private void Refresh(string searchString = null, string selected = null, bool forceRebuild = false)
{
Rebuild(forceRebuild);
_treeView.Reload();

if (!string.IsNullOrEmpty(searchString))
_treeView.searchString = searchString;
else if (selected != null)
_treeView.SetSelectionData(selected);
}

// Rebuild the tree view
private void Rebuild(bool forceRebuild = false)
{
if (forceRebuild || _treeView == null)
_treeView = new LocaleExplorerTreeView(Locale.GetAllLocaleAssets());
}


// OnGUI is called when the editor is drawn
private void OnGUI()
{
Rebuild();

// Draw the search box
GUILayout.BeginHorizontal(EditorStyles.toolbar);
OnToolbarGUI();
GUILayout.EndHorizontal();

// Draw the tree view
OnTreeViewGUI();
}


// OnToolbarGUI is called when the toolbar is drawn
private void OnToolbarGUI()
{
// Rescan project button
if (GUILayout.Button(new GUIContent("Rescan Project", EditorIcons.refresh, "Rescan the project for locales"), EditorStyles.toolbarButton, GUILayout.Width(111)))
Refresh(searchString: _treeView.searchString, selected: _treeView.GetSelectionData(), forceRebuild: true);

// Locales dropdown
if (GUILayout.Button(new GUIContent("Locales", EditorIcons.folderOpened, "Edit one of the locales in the project"), EditorStyles.toolbarDropDown, GUILayout.Width(80)))
{
var menu = new GenericMenu();
foreach (var locale in Locale.GetAllLocaleAssets())
{
menu.AddItem(new GUIContent($"{locale.englishName}/Show Asset"), false, () => {
Selection.SetActiveObjectWithContext(locale, locale);
EditorGUIUtility.PingObject(locale);
});
menu.AddItem(new GUIContent($"{locale.englishName}/Copy Path"), false, () => GUIUtility.systemCopyBuffer = AssetDatabase.GetAssetPath(locale));
menu.AddSeparator($"{locale.englishName}/");
menu.AddItem(new GUIContent($"{locale.englishName}/Edit Source"), false, () => AssetDatabase.OpenAsset(locale));
}

var rect = GUILayoutUtility.GetLastRect();
rect.x += 111;
rect.y += EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
menu.DropDown(rect);
}

GUILayout.FlexibleSpace();

// Search bar
_treeView.searchString = EditorGUILayout.TextField(_treeView.searchString, EditorStyles.toolbarSearchField, GUILayout.MaxWidth(300));

// Tree view buttons
if (GUILayout.Button(new GUIContent("Expand All", "Expand all tree view items"), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)))
_treeView.ExpandAll();
if (GUILayout.Button(new GUIContent("Collapse All", "Collapse all tree view items"), EditorStyles.toolbarButton, GUILayout.ExpandWidth(false)))
_treeView.CollapseAll();
}

// OnTreeViewGUI is called when the tree view is drawn
private void OnTreeViewGUI()
{
var rect = EditorGUILayout.GetControlRect(GUILayout.ExpandWidth(true), GUILayout.ExpandHeight(true));
_treeView.Reload();
_treeView.OnGUI(rect);
}
}
}
11 changes: 11 additions & 0 deletions Editor/Windows/LocaleExplorerWindow.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 00f60eb

Please sign in to comment.