diff --git a/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs index 5565c4adf8..392fa2372f 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/Editor/ActorSelectorLogic.cs @@ -21,6 +21,22 @@ namespace OpenRA.Mods.Common.Widgets.Logic { public class ActorSelectorLogic : ChromeLogic { + class ActorSelectorActor + { + public readonly ActorInfo Actor; + public readonly string[] Categories; + public readonly string[] SearchTerms; + public readonly string Tooltip; + + public ActorSelectorActor(ActorInfo actor, string[] categories, string[] searchTerms, string tooltip) + { + Actor = actor; + Categories = categories; + SearchTerms = searchTerms; + Tooltip = tooltip; + } + } + readonly EditorViewportControllerWidget editor; readonly DropDownButtonWidget ownersDropDown; readonly ScrollPanelWidget panel; @@ -28,10 +44,14 @@ namespace OpenRA.Mods.Common.Widgets.Logic readonly Ruleset mapRules; readonly World world; readonly WorldRenderer worldRenderer; - readonly List allCategories; - readonly List selectedCategories = new List(); + readonly string[] allCategories; + readonly HashSet selectedCategories = new HashSet(); + readonly List filteredCategories = new List(); + + readonly ActorSelectorActor[] allActors; PlayerReference selectedOwner; + string searchFilter; [ObjectCreator.UseCtor] public ActorSelectorLogic(Widget widget, World world, WorldRenderer worldRenderer) @@ -77,25 +97,119 @@ namespace OpenRA.Mods.Common.Widgets.Logic ownersDropDown.Text = selectedOwner.Name; ownersDropDown.TextColor = selectedOwner.Color.RGB; - var actorCategorySelector = widget.Get("ACTOR_CATEGORY"); - var filtersPanel = Ui.LoadWidget("ACTOR_CATEGORY_FILTER_PANEL", null, new WidgetArgs()); - var categoryTemplate = filtersPanel.Get("CATEGORY_TEMPLATE"); var tileSetId = world.Map.Rules.TileSet.Id; - allCategories = mapRules.Actors.Where(a => !a.Value.Name.Contains('^')).Select(a => a.Value.TraitInfoOrDefault()) - .Where(i => i != null && i.Categories != null && - !(i.ExcludeTilesets != null && i.ExcludeTilesets.Contains(tileSetId)) && !(i.RequireTilesets != null && !i.RequireTilesets.Contains(tileSetId))) - .SelectMany(i => i.Categories).Distinct().OrderBy(i => i).ToList(); - selectedCategories.AddRange(allCategories); + var allActorsTemp = new List(); + foreach (var a in mapRules.Actors.Values) + { + // Partial templates are not allowed + if (a.Name.Contains('^')) + continue; - var selectButtons = filtersPanel.Get("SELECT_CATEGORIES_BUTTONS"); - filtersPanel.AddChild(selectButtons); - filtersPanel.Bounds.Height = Math.Min(allCategories.Count * categoryTemplate.Bounds.Height + 5 + selectButtons.Bounds.Height, panel.Bounds.Height); + // Actor must have a preview associated with it + if (!a.HasTraitInfo()) + continue; + + var editorData = a.TraitInfoOrDefault(); + + // Actor must be included in at least one category + if (editorData == null || editorData.Categories == null) + continue; + + // Excluded by tileset + if (editorData.ExcludeTilesets != null && editorData.ExcludeTilesets.Contains(tileSetId)) + continue; + + if (editorData.RequireTilesets != null && !editorData.RequireTilesets.Contains(tileSetId)) + continue; + + var tooltip = a.TraitInfos().FirstOrDefault(ti => ti.EnabledByDefault) as TooltipInfoBase + ?? a.TraitInfos().FirstOrDefault(ti => ti.EnabledByDefault); + + var searchTerms = new List() { a.Name }; + if (tooltip != null) + searchTerms.Add(tooltip.Name); + + var tooltipText = (tooltip == null ? "Type: " : tooltip.Name + "\nType: ") + a.Name; + allActorsTemp.Add(new ActorSelectorActor(a, editorData.Categories, searchTerms.ToArray(), tooltipText)); + } + + allActors = allActorsTemp.ToArray(); + + allCategories = allActors.SelectMany(ac => ac.Categories) + .Distinct() + .OrderBy(x => x) + .ToArray(); + + foreach (var c in allCategories) + { + selectedCategories.Add(c); + filteredCategories.Add(c); + } + + var searchTextField = widget.Get("SEARCH_TEXTFIELD"); + searchTextField.OnTextEdited = () => + { + searchFilter = searchTextField.Text.Trim(); + filteredCategories.Clear(); + + if (!string.IsNullOrEmpty(searchFilter)) + filteredCategories.AddRange( + allActors.Where(t => t.SearchTerms.Any( + s => s.IndexOf(searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)) + .SelectMany(t => t.Categories) + .Distinct() + .OrderBy(x => x)); + else + filteredCategories.AddRange(allCategories); + + InitializeActorPreviews(); + }; + + var actorCategorySelector = widget.Get("CATEGORIES_DROPDOWN"); + actorCategorySelector.GetText = () => + { + if (selectedCategories.Count == 0) + return "None"; + + if (!string.IsNullOrEmpty(searchFilter)) + return "Search Results"; + + if (selectedCategories.Count == 1) + return selectedCategories.First(); + + if (selectedCategories.Count == allCategories.Length) + return "All"; + + return "Multiple"; + }; + + actorCategorySelector.OnMouseDown = _ => + { + if (searchTextField != null) + searchTextField.YieldKeyboardFocus(); + + actorCategorySelector.RemovePanel(); + actorCategorySelector.AttachPanel(CreateCategoriesPanel()); + }; + + InitializeActorPreviews(); + } + + Widget CreateCategoriesPanel() + { + var categoriesPanel = Ui.LoadWidget("ACTOR_CATEGORY_FILTER_PANEL", null, new WidgetArgs()); + var categoryTemplate = categoriesPanel.Get("CATEGORY_TEMPLATE"); + + var selectButtons = categoriesPanel.Get("SELECT_CATEGORIES_BUTTONS"); + categoriesPanel.AddChild(selectButtons); var selectAll = selectButtons.Get("SELECT_ALL"); selectAll.OnClick = () => { selectedCategories.Clear(); - selectedCategories.AddRange(allCategories); + foreach (var c in allCategories) + selectedCategories.Add(c); + InitializeActorPreviews(); }; @@ -106,13 +220,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic InitializeActorPreviews(); }; - actorCategorySelector.OnMouseDown = _ => - { - actorCategorySelector.RemovePanel(); - actorCategorySelector.AttachPanel(filtersPanel); - }; - - foreach (var cat in allCategories) + var categoryHeight = 5 + selectButtons.Bounds.Height; + foreach (var cat in filteredCategories) { var category = (CheckboxWidget)categoryTemplate.Clone(); category.GetText = () => cat; @@ -120,18 +229,19 @@ namespace OpenRA.Mods.Common.Widgets.Logic category.IsVisible = () => true; category.OnClick = () => { - if (selectedCategories.Contains(cat)) - selectedCategories.Remove(cat); - else + if (!selectedCategories.Remove(cat)) selectedCategories.Add(cat); InitializeActorPreviews(); }; - filtersPanel.AddChild(category); + categoriesPanel.AddChild(category); + categoryHeight += categoryTemplate.Bounds.Height; } - InitializeActorPreviews(); + categoriesPanel.Bounds.Height = Math.Min(categoryHeight, panel.Bounds.Height); + + return categoriesPanel; } void InitializeActorPreviews() @@ -140,29 +250,15 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (!selectedCategories.Any()) return; - var actors = mapRules.Actors.Where(a => !a.Value.Name.Contains('^')) - .Select(a => a.Value); - var tileSetId = world.Map.Rules.TileSet.Id; - - foreach (var a in actors) + foreach (var a in allActors) { - var actor = a; - if (actor.HasTraitInfo()) // bridge layer takes care about that automatically + if (!selectedCategories.Overlaps(a.Categories)) continue; - if (!actor.HasTraitInfo()) - continue; - - var filter = actor.TraitInfoOrDefault(); - if (filter == null || filter.Categories == null || !filter.Categories.Intersect(selectedCategories).Any()) - continue; - - if (filter.ExcludeTilesets != null && filter.ExcludeTilesets.Contains(tileSetId)) - continue; - - if (filter.RequireTilesets != null && !filter.RequireTilesets.Contains(tileSetId)) + if (!string.IsNullOrEmpty(searchFilter) && !a.SearchTerms.Any(s => s.IndexOf(searchFilter, StringComparison.OrdinalIgnoreCase) >= 0)) continue; + var actor = a.Actor; var td = new TypeDictionary(); td.Add(new OwnerInit(selectedOwner.Name)); td.Add(new FactionInit(selectedOwner.Faction)); @@ -192,10 +288,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic item.Bounds.Height = preview.Bounds.Height + 2 * preview.Bounds.Y; item.IsVisible = () => true; - var tooltip = actor.TraitInfos().FirstOrDefault(Exts.IsTraitEnabled) as TooltipInfoBase - ?? actor.TraitInfos().FirstOrDefault(Exts.IsTraitEnabled); - - item.GetTooltipText = () => (tooltip == null ? "Type: " : tooltip.Name + "\nType: ") + actor.Name; + item.GetTooltipText = () => a.Tooltip; panel.AddChild(item); } diff --git a/mods/cnc/chrome/editor.yaml b/mods/cnc/chrome/editor.yaml index 1c2160df60..6933db0051 100644 --- a/mods/cnc/chrome/editor.yaml +++ b/mods/cnc/chrome/editor.yaml @@ -316,18 +316,49 @@ Container@EDITOR_WORLD_ROOT: Width: PARENT_RIGHT Height: PARENT_BOTTOM Children: - DropDownButton@OWNERS_DROPDOWN: - Width: PARENT_RIGHT + Background: + Width: 61 + Height: 75 + Background: panel-black + Children: + Label@SEARCH_LABEL: + Width: PARENT_RIGHT - 5 + Height: 25 + Text: Search: + Align: Right + Font: TinyBold + Label@CATEGORIES_LABEL: + Y: 24 + Width: PARENT_RIGHT - 5 + Height: 25 + Text: Filter: + Align: Right + Font: TinyBold + Label@OWNERS_LABEL: + Y: 48 + Width: PARENT_RIGHT - 5 + Height: 25 + Text: Owner: + Align: Right + Font: TinyBold + TextField@SEARCH_TEXTFIELD: + X: 60 + Width: PARENT_RIGHT - 60 + Height: 25 + DropDownButton@CATEGORIES_DROPDOWN: + X: 60 + Y: 24 + Width: PARENT_RIGHT - 60 Height: 25 Font: Bold - DropDownButton@ACTOR_CATEGORY: - Y: 25 - Width: PARENT_RIGHT + DropDownButton@OWNERS_DROPDOWN: + X: 60 + Y: 48 + Width: PARENT_RIGHT - 60 Height: 25 - Text: Categories Font: Bold ScrollPanel@ACTORTEMPLATE_LIST: - Y: 50 + Y: 72 Width: PARENT_RIGHT Height: PARENT_BOTTOM - 50 TopBottomSpacing: 4 @@ -417,7 +448,7 @@ Container@EDITOR_WORLD_ROOT: Contrast: true ScrollPanel@ACTOR_CATEGORY_FILTER_PANEL: - Width: 250 + Width: 190 Children: Container@SELECT_CATEGORIES_BUTTONS: Width: PARENT_RIGHT @@ -426,15 +457,15 @@ ScrollPanel@ACTOR_CATEGORY_FILTER_PANEL: Button@SELECT_ALL: X: 10 Y: 2 - Width: 100 + Width: 60 Height: 25 - Text: Select all + Text: All Button@SELECT_NONE: - X: 120 + X: PARENT_RIGHT - WIDTH - 34 Y: 2 - Width: 100 + Width: 60 Height: 25 - Text: Select none + Text: None Checkbox@CATEGORY_TEMPLATE: X: 5 Y: 5 diff --git a/mods/common/chrome/editor.yaml b/mods/common/chrome/editor.yaml index b8ba36d943..6ba38bde29 100644 --- a/mods/common/chrome/editor.yaml +++ b/mods/common/chrome/editor.yaml @@ -289,24 +289,49 @@ Container@EDITOR_WORLD_ROOT: Width: 240 Height: WINDOW_BOTTOM - 382 Children: - DropDownButton@OWNERS_DROPDOWN: - X: 10 + Label@SEARCH_LABEL: Y: 10 - Width: 220 + Width: 55 + Height: 25 + Text: Search: + Align: Right + Font: TinyBold + TextField@SEARCH_TEXTFIELD: + X: 60 + Y: 10 + Width: PARENT_RIGHT - 70 + Height: 25 + Label@CATEGORIES_LABEL: + Y: 34 + Width: 55 + Height: 25 + Text: Filter: + Align: Right + Font: TinyBold + DropDownButton@CATEGORIES_DROPDOWN: + X: 60 + Y: 34 + Width: PARENT_RIGHT - 70 Height: 25 Font: Bold - DropDownButton@ACTOR_CATEGORY: - X: 10 - Y: 35 - Width: 220 + Label@OWNERS_LABEL: + Y: 58 + Width: 55 + Height: 25 + Text: Owner: + Align: Right + Font: TinyBold + DropDownButton@OWNERS_DROPDOWN: + X: 60 + Y: 58 + Width: PARENT_RIGHT - 70 Height: 25 - Text: Categories Font: Bold ScrollPanel@ACTORTEMPLATE_LIST: X: 10 - Y: 60 - Width: 220 - Height: PARENT_BOTTOM - 70 + Y: 82 + Width: PARENT_RIGHT - 20 + Height: PARENT_BOTTOM - 92 TopBottomSpacing: 4 ItemSpacing: 4 Children: @@ -403,25 +428,25 @@ Container@EDITOR_WORLD_ROOT: Contrast: true ScrollPanel@ACTOR_CATEGORY_FILTER_PANEL: - Width: 220 + Width: 170 Children: Container@SELECT_CATEGORIES_BUTTONS: - Width: 220 + Width: PARENT_RIGHT Height: 29 Children: Button@SELECT_ALL: - X: 5 + X: 10 Y: 2 - Width: 90 + Width: 60 Height: 25 - Text: Select all + Text: All Font: Bold Button@SELECT_NONE: - X: 100 + X: PARENT_RIGHT - WIDTH - 34 Y: 2 - Width: 90 + Width: 60 Height: 25 - Text: Select none + Text: None Font: Bold Checkbox@CATEGORY_TEMPLATE: X: 5