#region Copyright & License Information /* * Copyright (c) The OpenRA Developers and Contributors * This file is part of OpenRA, which is free software. It is made * available to you under the terms of the GNU General Public License * as published by the Free Software Foundation, either version 3 of * the License, or (at your option) any later version. For more * information, see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using OpenRA.Graphics; using OpenRA.Mods.Common.Traits; using OpenRA.Network; using OpenRA.Primitives; using OpenRA.Traits; using OpenRA.Widgets; namespace OpenRA.Mods.Common.Widgets.Logic { public static class LobbyUtils { [TranslationReference] const string Open = "options-lobby-slot.open"; [TranslationReference] const string Closed = "options-lobby-slot.closed"; [TranslationReference] const string Bots = "options-lobby-slot.bots"; [TranslationReference] const string BotsDisabled = "options-lobby-slot.bots-disabled"; [TranslationReference] const string Slot = "options-lobby-slot.slot"; sealed class SlotDropDownOption { public readonly string Title; public readonly string Order; public readonly Func Selected; public SlotDropDownOption(string title, string order, Func selected) { Title = title; Order = order; Selected = selected; } } public static void ShowSlotDropDown(DropDownButtonWidget dropdown, Session.Slot slot, Session.Client client, OrderManager orderManager, MapPreview map, ModData modData) { var open = TranslationProvider.GetString(Open); var closed = TranslationProvider.GetString(Closed); var options = new Dictionary> { { TranslationProvider.GetString(Slot), new List { new(open, "slot_open " + slot.PlayerReference, () => !slot.Closed && client == null), new(closed, "slot_close " + slot.PlayerReference, () => slot.Closed) } } }; var bots = new List(); if (slot.AllowBots) { foreach (var b in map.PlayerActorInfo.TraitInfos()) { var botController = orderManager.LobbyInfo.Clients.FirstOrDefault(c => c.IsAdmin); bots.Add(new SlotDropDownOption(b.Name, $"slot_bot {slot.PlayerReference} {botController.Index} {b.Type}", () => client != null && client.Bot == b.Type)); } } options.Add(bots.Count > 0 ? TranslationProvider.GetString(Bots) : TranslationProvider.GetString(BotsDisabled), bots); ScrollItemWidget SetupItem(SlotDropDownOption o, ScrollItemWidget itemTemplate) { var item = ScrollItemWidget.Setup(itemTemplate, o.Selected, () => orderManager.IssueOrder(Order.Command(o.Order))); item.Get("LABEL").GetText = () => o.Title; return item; } dropdown.ShowDropDown("LABEL_DROPDOWN_TEMPLATE", 180, options, SetupItem); } public static void ShowPlayerActionDropDown(DropDownButtonWidget dropdown, Session.Client c, OrderManager orderManager, Widget lobby, Action before, Action after) { Action okPressed = tempBan => { orderManager.IssueOrder(Order.Command($"kick {c.Index} {tempBan}")); after(); }; var onClick = new Action(() => { before(); Game.LoadWidget(null, "KICK_CLIENT_DIALOG", lobby.Get("TOP_PANELS_ROOT"), new WidgetArgs { { "clientName", c.Name }, { "okPressed", okPressed }, { "cancelPressed", after } }); }); var options = new List { new() { Title = "Kick", OnClick = onClick }, }; if (orderManager.LobbyInfo.GlobalSettings.Dedicated) { options.Add(new DropDownOption { Title = "Transfer Admin", OnClick = () => orderManager.IssueOrder(Order.Command($"make_admin {c.Index}")) }); } if (!c.IsObserver && orderManager.LobbyInfo.GlobalSettings.AllowSpectators) { options.Add(new DropDownOption { Title = "Move to Spectator", OnClick = () => orderManager.IssueOrder(Order.Command($"make_spectator {c.Index}")) }); } ScrollItemWidget SetupItem(DropDownOption o, ScrollItemWidget itemTemplate) { var item = ScrollItemWidget.Setup(itemTemplate, o.IsSelected, o.OnClick); var labelWidget = item.Get("LABEL"); labelWidget.GetText = () => o.Title; return item; } dropdown.ShowDropDown("PLAYERACTION_DROPDOWN_TEMPLATE", 167, options, SetupItem); } public static void ShowTeamDropDown(DropDownButtonWidget dropdown, Session.Client client, OrderManager orderManager, int teamCount) { ScrollItemWidget SetupItem(int ii, ScrollItemWidget itemTemplate) { var item = ScrollItemWidget.Setup(itemTemplate, () => client.Team == ii, () => orderManager.IssueOrder(Order.Command($"team {client.Index} {ii}"))); item.Get("LABEL").GetText = () => ii == 0 ? "-" : ii.ToString(NumberFormatInfo.CurrentInfo); return item; } var options = Enumerable.Range(0, teamCount + 1); dropdown.ShowDropDown("TEAM_DROPDOWN_TEMPLATE", 150, options, SetupItem); } public static void ShowHandicapDropDown(DropDownButtonWidget dropdown, Session.Client client, OrderManager orderManager) { ScrollItemWidget SetupItem(int ii, ScrollItemWidget itemTemplate) { var item = ScrollItemWidget.Setup(itemTemplate, () => client.Handicap == ii, () => orderManager.IssueOrder(Order.Command($"handicap {client.Index} {ii}"))); var label = $"{ii}%"; item.Get("LABEL").GetText = () => label; return item; } // Handicaps may be set between 0 - 95% in steps of 5% var options = Enumerable.Range(0, 20).Select(i => 5 * i); dropdown.ShowDropDown("TEAM_DROPDOWN_TEMPLATE", 150, options, SetupItem); } public static void ShowSpawnDropDown(DropDownButtonWidget dropdown, Session.Client client, OrderManager orderManager, IEnumerable spawnPoints) { ScrollItemWidget SetupItem(int ii, ScrollItemWidget itemTemplate) { var item = ScrollItemWidget.Setup(itemTemplate, () => client.SpawnPoint == ii, () => SetSpawnPoint(orderManager, client, ii)); item.Get("LABEL").GetText = () => ii == 0 ? "-" : Convert.ToChar('A' - 1 + ii).ToString(); return item; } dropdown.ShowDropDown("SPAWN_DROPDOWN_TEMPLATE", 150, spawnPoints, SetupItem); } /// Splits a string into two parts on the first instance of a given token. public static (string First, string Second) SplitOnFirstToken(string input, string token = "\n") { if (string.IsNullOrEmpty(input)) return (null, null); var split = input.IndexOf(token, StringComparison.Ordinal); var first = split > 0 ? input[..split] : input; var second = split > 0 ? input[(split + token.Length)..] : null; return (first, second); } public static void ShowFactionDropDown(DropDownButtonWidget dropdown, Session.Client client, OrderManager orderManager, Dictionary factions) { ScrollItemWidget SetupItem(string factionId, ScrollItemWidget itemTemplate) { var item = ScrollItemWidget.Setup(itemTemplate, () => client.Faction == factionId, () => orderManager.IssueOrder(Order.Command($"faction {client.Index} {factionId}"))); var faction = factions[factionId]; var label = item.Get("LABEL"); var labelText = WidgetUtils.TruncateText(TranslationProvider.GetString(faction.Name), label.Bounds.Width, Game.Renderer.Fonts[label.Font]); label.GetText = () => labelText; var flag = item.Get("FLAG"); flag.GetImageCollection = () => "flags"; flag.GetImageName = () => factionId; var description = faction.Description != null ? TranslationProvider.GetString(faction.Description) : null; var (text, desc) = SplitOnFirstToken(description); item.GetTooltipText = () => text; item.GetTooltipDesc = () => desc; return item; } var options = factions.Where(f => f.Value.Selectable).GroupBy(f => f.Value.Side) .ToDictionary(g => g.Key != null ? TranslationProvider.GetString(g.Key) : "", g => g.Select(f => TranslationProvider.GetString(f.Key))); dropdown.ShowDropDown("FACTION_DROPDOWN_TEMPLATE", 154, options, SetupItem); } public static void SelectSpawnPoint(OrderManager orderManager, MapPreviewWidget mapPreview, MapPreview preview, MouseInput mi) { if (orderManager.LocalClient.State == Session.ClientState.Ready) return; if (mi.Button == MouseButton.Left) SelectPlayerSpawnPoint(orderManager, mapPreview, preview, mi); if (mi.Button == MouseButton.Right) ClearPlayerSpawnPoint(orderManager, mapPreview, preview, mi); } static void SelectPlayerSpawnPoint(OrderManager orderManager, MapPreviewWidget mapPreview, MapPreview preview, MouseInput mi) { var selectedSpawn = DetermineSelectedSpawnPoint(mapPreview, preview, mi); var locals = orderManager.LobbyInfo.Clients.Where(c => c.Index == orderManager.LocalClient.Index || (Game.IsHost && c.Bot != null)); var playerToMove = locals.FirstOrDefault(c => (selectedSpawn == 0) ^ (c.SpawnPoint == 0) && !c.IsObserver); SetSpawnPoint(orderManager, playerToMove, selectedSpawn); } static void ClearPlayerSpawnPoint(OrderManager orderManager, MapPreviewWidget mapPreview, MapPreview preview, MouseInput mi) { var selectedSpawn = DetermineSelectedSpawnPoint(mapPreview, preview, mi); if (Game.IsHost || orderManager.LobbyInfo.Clients.FirstOrDefault(cc => cc.SpawnPoint == selectedSpawn) == orderManager.LocalClient) orderManager.IssueOrder(Order.Command($"clear_spawn {selectedSpawn}")); } static int DetermineSelectedSpawnPoint(MapPreviewWidget mapPreview, MapPreview preview, MouseInput mi) { var spawnSize = ChromeProvider.GetImage("lobby-bits", "spawn-unclaimed").Size.XY; var selectedSpawn = preview.SpawnPoints .Select((sp, i) => (SpawnLocation: mapPreview.ConvertToPreview(sp, preview.GridType), Index: i)) .Where(a => ((a.SpawnLocation - mi.Location).ToFloat2() / spawnSize * 2).LengthSquared <= 1) .Select(a => a.Index + 1) .FirstOrDefault(); return selectedSpawn; } static void SetSpawnPoint(OrderManager orderManager, Session.Client playerToMove, int selectedSpawnPoint) { var owned = orderManager.LobbyInfo.Clients.Any(c => c.SpawnPoint == selectedSpawnPoint) || orderManager.LobbyInfo.DisabledSpawnPoints.Contains(selectedSpawnPoint); if (selectedSpawnPoint == 0 || !owned) orderManager.IssueOrder(Order.Command($"spawn {(playerToMove ?? orderManager.LocalClient).Index} {selectedSpawnPoint}")); } public static List AvailableSpawnPoints(int spawnPoints, Session lobbyInfo) { return Enumerable.Range(1, spawnPoints).Except(lobbyInfo.DisabledSpawnPoints).ToList(); } public static bool InsufficientEnabledSpawnPoints(MapPreview map, Session lobbyInfo) { // If a map doesn't define spawn points we always have enough space var spawnPoints = map.SpawnPoints.Length; if (spawnPoints == 0) return false; return AvailableSpawnPoints(spawnPoints, lobbyInfo).Count < lobbyInfo.Clients.Count(c => !c.IsObserver); } public static Color LatencyColor(Session.Client client) { if (client == null) return Color.Gray; switch (client.ConnectionQuality) { case Session.ConnectionQuality.Good: return Color.LimeGreen; case Session.ConnectionQuality.Moderate: return Color.Orange; case Session.ConnectionQuality.Poor: return Color.Red; default: return Color.Gray; } } public static string LatencyDescription(Session.Client client) { if (client == null) return "Unknown"; switch (client.ConnectionQuality) { case Session.ConnectionQuality.Good: return "Good"; case Session.ConnectionQuality.Moderate: return "Moderate"; case Session.ConnectionQuality.Poor: return "Poor"; default: return "Unknown"; } } public static void SetupLatencyWidget(Widget parent, Session.Client c, OrderManager orderManager) { var visible = c != null && c.Bot == null; var block = parent.GetOrNull("LATENCY"); if (block != null) { block.IsVisible = () => visible; if (visible) block.Get("LATENCY_COLOR").GetColor = () => LatencyColor(c); } var tooltip = parent.Get("LATENCY_REGION"); tooltip.IsVisible = () => visible; if (visible) tooltip.Bind(orderManager, null, c); } public static void SetupProfileWidget(Widget parent, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer) { var visible = c != null && c.Bot == null; var profile = parent.GetOrNull("PROFILE"); if (profile != null) { var imageName = (c != null && c.IsAdmin ? "admin-" : "player-") + (c.Fingerprint != null ? "registered" : "anonymous"); profile.GetImageName = () => imageName; profile.IsVisible = () => visible; } var profileTooltip = parent.GetOrNull("PROFILE_TOOLTIP"); if (profileTooltip != null) { if (c != null && c.Fingerprint != null) profileTooltip.Template = "REGISTERED_PLAYER_TOOLTIP"; if (visible) profileTooltip.Bind(orderManager, worldRenderer, c); profileTooltip.IsVisible = () => visible; } } public static void SetupEditableNameWidget(Widget parent, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer) { var name = parent.Get("NAME"); name.IsVisible = () => true; name.IsDisabled = () => orderManager.LocalClient.IsReady; name.Text = c.Name; var escPressed = false; name.OnLoseFocus = () => { if (escPressed) { escPressed = false; return; } name.Text = name.Text.Trim(); if (name.Text.Length == 0) name.Text = c.Name; else if (name.Text != c.Name) { name.Text = Settings.SanitizedPlayerName(name.Text); orderManager.IssueOrder(Order.Command("name " + name.Text)); Game.Settings.Player.Name = name.Text; Game.Settings.Save(); } }; name.OnEnterKey = _ => { name.YieldKeyboardFocus(); return true; }; name.OnEscKey = _ => { name.Text = c.Name; escPressed = true; name.YieldKeyboardFocus(); return true; }; SetupProfileWidget(name, c, orderManager, worldRenderer); HideChildWidget(parent, "SLOT_OPTIONS"); } public static void SetupNameWidget(Widget parent, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer) { var name = parent.Get("NAME"); name.IsVisible = () => true; var font = Game.Renderer.Fonts[name.Font]; var label = WidgetUtils.TruncateText(c.Name, name.Bounds.Width, font); name.GetText = () => label; SetupProfileWidget(parent, c, orderManager, worldRenderer); } public static void SetupEditableSlotWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, MapPreview map, ModData modData) { var slot = parent.Get("SLOT_OPTIONS"); slot.IsVisible = () => true; slot.IsDisabled = () => orderManager.LocalClient.IsReady; var truncated = new CachedTransform(name => WidgetUtils.TruncateText(name, slot.Bounds.Width - slot.Bounds.Height - slot.LeftMargin - slot.RightMargin, Game.Renderer.Fonts[slot.Font])); var closed = TranslationProvider.GetString(Closed); var open = TranslationProvider.GetString(Open); slot.GetText = () => truncated.Update(c != null ? c.Name : s.Closed ? closed : open); slot.OnMouseDown = _ => ShowSlotDropDown(slot, s, c, orderManager, map, modData); // Ensure Name selector (if present) is hidden HideChildWidget(parent, "NAME"); } public static void SetupSlotWidget(Widget parent, ModData modData, Session.Slot s, Session.Client c) { var name = parent.Get("NAME"); name.IsVisible = () => true; name.GetText = () => c != null ? c.Name : s.Closed ? TranslationProvider.GetString(Closed) : TranslationProvider.GetString(Open); // Ensure Slot selector (if present) is hidden HideChildWidget(parent, "SLOT_OPTIONS"); } public static void SetupPlayerActionWidget(Widget parent, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer, Widget lobby, Action before, Action after) { var slot = parent.Get("PLAYER_ACTION"); slot.IsVisible = () => Game.IsHost && c.Index != orderManager.LocalClient.Index; slot.IsDisabled = () => orderManager.LocalClient.IsReady; var truncated = new CachedTransform(name => WidgetUtils.TruncateText(name, slot.Bounds.Width - slot.Bounds.Height - slot.LeftMargin - slot.RightMargin, Game.Renderer.Fonts[slot.Font])); slot.GetText = () => truncated.Update(c != null ? c.Name : string.Empty); slot.OnMouseDown = _ => ShowPlayerActionDropDown(slot, c, orderManager, lobby, before, after); SetupProfileWidget(slot, c, orderManager, worldRenderer); // Ensure Name selector (if present) is hidden HideChildWidget(parent, "NAME"); } public static void SetupKickSpectatorsWidget(Widget parent, OrderManager orderManager, Widget lobby, Action before, Action after, bool skirmishMode) { var checkBox = parent.Get("TOGGLE_SPECTATORS"); checkBox.IsChecked = () => orderManager.LobbyInfo.GlobalSettings.AllowSpectators; checkBox.IsVisible = () => orderManager.LocalClient.IsAdmin && !skirmishMode; checkBox.IsDisabled = () => false; void OkPressed() { orderManager.IssueOrder(Order.Command($"allow_spectators {!orderManager.LobbyInfo.GlobalSettings.AllowSpectators}")); orderManager.IssueOrders( orderManager.LobbyInfo.Clients.Where( c => c.IsObserver && !c.IsAdmin).Select( client => Order.Command($"kick {client.Index} {client.Name}")).ToArray()); after(); } checkBox.OnClick = () => { before(); var spectatorCount = orderManager.LobbyInfo.Clients.Count(c => c.IsObserver); if (spectatorCount > 0) { Game.LoadWidget(null, "KICK_SPECTATORS_DIALOG", lobby.Get("TOP_PANELS_ROOT"), new WidgetArgs { { "clientCount", spectatorCount }, { "okPressed", OkPressed }, { "cancelPressed", after } }); } else { orderManager.IssueOrder(Order.Command($"allow_spectators {!orderManager.LobbyInfo.GlobalSettings.AllowSpectators}")); after(); } }; } public static void SetupEditableColorWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, WorldRenderer worldRenderer, IColorPickerManagerInfo colorManager) { var colorDropdown = parent.Get("COLOR"); colorDropdown.IsDisabled = () => (s != null && s.LockColor) || orderManager.LocalClient.IsReady; colorDropdown.OnMouseDown = _ => colorManager.ShowColorDropDown(colorDropdown, c.Color, c.Faction, worldRenderer, color => { if (c == orderManager.LocalClient) { Game.Settings.Player.Color = color; Game.Settings.Save(); } orderManager.IssueOrder(Order.Command($"color {c.Index} {color}")); }); SetupColorWidget(colorDropdown, c); } public static void SetupColorWidget(Widget parent, Session.Client c) { var color = parent.Get("COLORBLOCK"); color.GetColor = () => c.Color; } public static void SetupEditableFactionWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, Dictionary factions) { var dropdown = parent.Get("FACTION"); dropdown.IsDisabled = () => s.LockFaction || orderManager.LocalClient.IsReady; dropdown.OnMouseDown = _ => ShowFactionDropDown(dropdown, c, orderManager, factions); var description = factions[c.Faction].Description != null ? TranslationProvider.GetString(factions[c.Faction].Description) : null; var (text, desc) = SplitOnFirstToken(description); dropdown.GetTooltipText = () => text; dropdown.GetTooltipDesc = () => desc; SetupFactionWidget(dropdown, c, factions); } public static void SetupFactionWidget(Widget parent, Session.Client c, Dictionary factions) { var factionName = parent.Get("FACTIONNAME"); var font = Game.Renderer.Fonts[factionName.Font]; var truncated = new CachedTransform(clientFaction => WidgetUtils.TruncateText(TranslationProvider.GetString(factions[clientFaction].Name), factionName.Bounds.Width, font)); factionName.GetText = () => truncated.Update(c.Faction); var factionFlag = parent.Get("FACTIONFLAG"); factionFlag.GetImageName = () => c.Faction; factionFlag.GetImageCollection = () => "flags"; } public static void SetupEditableTeamWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, MapPreview map) { var dropdown = parent.Get("TEAM_DROPDOWN"); dropdown.IsVisible = () => true; dropdown.IsDisabled = () => s.LockTeam || orderManager.LocalClient.IsReady; dropdown.OnMouseDown = _ => ShowTeamDropDown(dropdown, c, orderManager, map.PlayerCount); dropdown.GetText = () => (c.Team == 0) ? "-" : c.Team.ToString(NumberFormatInfo.CurrentInfo); HideChildWidget(parent, "TEAM"); } public static void SetupTeamWidget(Widget parent, Session.Client c) { var team = parent.Get("TEAM"); team.IsVisible = () => true; team.GetText = () => (c.Team == 0) ? "-" : c.Team.ToString(NumberFormatInfo.CurrentInfo); HideChildWidget(parent, "TEAM_DROPDOWN"); } public static void SetupEditableHandicapWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager) { var dropdown = parent.Get("HANDICAP_DROPDOWN"); dropdown.IsVisible = () => true; dropdown.IsDisabled = () => s.LockHandicap || orderManager.LocalClient.IsReady; dropdown.OnMouseDown = _ => ShowHandicapDropDown(dropdown, c, orderManager); var handicapLabel = new CachedTransform(h => $"{h}%"); dropdown.GetText = () => handicapLabel.Update(c.Handicap); HideChildWidget(parent, "HANDICAP"); } public static void SetupHandicapWidget(Widget parent, Session.Client c) { var team = parent.Get("HANDICAP"); team.IsVisible = () => true; var handicapLabel = new CachedTransform(h => $"{h}%"); team.GetText = () => handicapLabel.Update(c.Handicap); HideChildWidget(parent, "HANDICAP_DROPDOWN"); } public static void SetupEditableSpawnWidget(Widget parent, Session.Slot s, Session.Client c, OrderManager orderManager, MapPreview map) { var dropdown = parent.Get("SPAWN_DROPDOWN"); dropdown.IsVisible = () => true; dropdown.IsDisabled = () => s.LockSpawn || orderManager.LocalClient.IsReady; dropdown.OnMouseDown = _ => { var spawnPoints = Enumerable.Range(0, map.SpawnPoints.Length + 1).Except( orderManager.LobbyInfo.Clients.Where( client => client != c && client.SpawnPoint != 0).Select(client => client.SpawnPoint)) .Except(orderManager.LobbyInfo.DisabledSpawnPoints); ShowSpawnDropDown(dropdown, c, orderManager, spawnPoints); }; dropdown.GetText = () => (c.SpawnPoint == 0) ? "-" : Convert.ToChar('A' - 1 + c.SpawnPoint).ToString(); HideChildWidget(parent, "SPAWN"); } public static void SetupSpawnWidget(Widget parent, Session.Client c) { var spawn = parent.Get("SPAWN"); spawn.IsVisible = () => true; spawn.GetText = () => (c.SpawnPoint == 0) ? "-" : Convert.ToChar('A' - 1 + c.SpawnPoint).ToString(); HideChildWidget(parent, "SPAWN_DROPDOWN"); } public static void SetupEditableReadyWidget(Widget parent, Session.Client c, OrderManager orderManager, MapPreview map, bool isEnabled) { var status = parent.Get("STATUS_CHECKBOX"); status.IsVisible = () => true; status.IsDisabled = () => c.Bot != null || map.Status != MapStatus.Available || !isEnabled; if (c.Bot == null) { var isChecked = new PredictedCachedTransform(cc => cc.IsReady); status.IsChecked = () => isChecked.Update(c); status.OnClick = () => { var state = isChecked.Update(c) ? Session.ClientState.NotReady : Session.ClientState.Ready; orderManager.IssueOrder(Order.Command($"state {state}")); isChecked.Predict(!c.IsReady); }; } else status.IsChecked = () => true; } public static void SetupReadyWidget(Widget parent, Session.Client c) { parent.Get("STATUS_IMAGE").IsVisible = () => c.IsReady || c.Bot != null; } public static void HideReadyWidgets(Widget parent) { HideChildWidget(parent, "STATUS_CHECKBOX"); HideChildWidget(parent, "STATUS_IMAGE"); } static void HideChildWidget(Widget parent, string widgetId) { var widget = parent.GetOrNull(widgetId); if (widget != null) widget.IsVisible = () => false; } } sealed class ShowPlayerActionDropDownOption { public Action Click { get; set; } public string Title; public Func Selected = () => false; public ShowPlayerActionDropDownOption(string title, Action click) { Click = click; Title = title; } } }