/* * Copyright 2022 Peter Han * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without * restriction, including without limitation the rights to use, copy, modify, merge, publish, * distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the * Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ using System.Collections.Generic; using UnityEngine; using UnityEngine.UI; namespace PeterHan.PLib.UI { /// /// A custom UI combo box factory class. /// public sealed class PComboBox : IUIComponent, IDynamicSizable where T : class, IListableOption { /// /// The default margin around items in the pulldown. /// private static readonly RectOffset DEFAULT_ITEM_MARGIN = new RectOffset(3, 3, 3, 3); /// /// Sets the selected option in a realized combo box. /// /// The realized combo box. /// The option to set. /// true to fire the on select listener, or false otherwise. public static void SetSelectedItem(GameObject realized, IListableOption option, bool fireListener = false) { if (option != null && realized != null && realized.TryGetComponent( out PComboBoxComponent component)) component.SetSelectedItem(option, fireListener); } /// /// The size of the sprite used to expand/contract the options. /// public Vector2 ArrowSize { get; set; } /// /// The combo box's background color. /// public ColorStyleSetting BackColor { get; set; } /// /// The size of the check mark sprite used on the selected option. /// public Vector2 CheckSize { get; set; } /// /// The content of this combo box. /// public IEnumerable Content { get; set; } public bool DynamicSize { get; set; } /// /// The background color for each entry in the combo box pulldown. /// public ColorStyleSetting EntryColor { get; set; } /// /// The flexible size bounds of this combo box. /// public Vector2 FlexSize { get; set; } /// /// The initially selected item of the combo box. /// public T InitialItem { get; set; } /// /// The margin around each item in the pulldown. /// public RectOffset ItemMargin { get; set; } /// /// The margin around the component. /// public RectOffset Margin { get; set; } /// /// The maximum number of items to be shown at once before a scroll bar is added. /// public int MaxRowsShown { get; set; } /// /// The minimum width in units (not characters!) of this text field. /// public int MinWidth { get; set; } public string Name { get; } /// /// The action to trigger when an item is selected. It is passed the realized source /// object. /// public PUIDelegates.OnDropdownChanged OnOptionSelected { get; set; } public event PUIDelegates.OnRealize OnRealize; /// /// The text alignment in the combo box. /// public TextAnchor TextAlignment { get; set; } /// /// The combo box's text color, font, word wrap settings, and font size. /// public TextStyleSetting TextStyle { get; set; } /// /// The tool tip text. /// public string ToolTip { get; set; } public PComboBox() : this("Dropdown") { } public PComboBox(string name) { ArrowSize = new Vector2(8.0f, 8.0f); BackColor = null; CheckSize = new Vector2(12.0f, 12.0f); Content = null; DynamicSize = false; FlexSize = Vector2.zero; InitialItem = null; ItemMargin = DEFAULT_ITEM_MARGIN; Margin = PButton.BUTTON_MARGIN; MaxRowsShown = 6; MinWidth = 0; Name = name; TextAlignment = TextAnchor.MiddleLeft; TextStyle = null; ToolTip = null; } /// /// Adds a handler when this combo box is realized. /// /// The handler to invoke on realization. /// This combo box for call chaining. public PComboBox AddOnRealize(PUIDelegates.OnRealize onRealize) { OnRealize += onRealize; return this; } public GameObject Build() { var combo = PUIElements.CreateUI(null, Name); var style = TextStyle ?? PUITuning.Fonts.UILightStyle; var entryColor = EntryColor ?? PUITuning.Colors.ButtonBlueStyle; RectOffset margin = Margin, im = ItemMargin; // Background color var bgImage = combo.AddComponent(); var backColorStyle = BackColor ?? PUITuning.Colors.ButtonBlueStyle; UIDetours.COLOR_STYLE_SETTING.Set(bgImage, backColorStyle); PButton.SetupButtonBackground(bgImage); // Need a LocText (selected item) var selection = PUIElements.CreateUI(combo, "SelectedItem"); if (MinWidth > 0) selection.SetMinUISize(new Vector2(MinWidth, 0.0f)); var selectedLabel = PUIElements.AddLocText(selection, style); // Vertical flow panel with the choices var contentContainer = PUIElements.CreateUI(null, "Content"); contentContainer.AddComponent().childForceExpandWidth = true; // Scroll pane with items is laid out below everything else var pullDown = new PScrollPane("PullDown") { ScrollHorizontal = false, ScrollVertical = true, AlwaysShowVertical = true, FlexSize = Vector2.right, TrackSize = 8.0f, BackColor = entryColor. inactiveColor }.BuildScrollPane(combo, contentContainer); // Add a black border (Does not work, covered by the combo box buttons...) #if false var pdImage = pullDown.GetComponent(); pdImage.sprite = PUITuning.Images.BoxBorder; pdImage.type = Image.Type.Sliced; #endif pullDown.rectTransform().pivot = new Vector2(0.5f, 1.0f); // Initialize the drop down var comboBox = combo.AddComponent(); comboBox.CheckColor = style.textColor; comboBox.ContentContainer = contentContainer.rectTransform(); comboBox.EntryPrefab = BuildRowPrefab(style, entryColor); comboBox.MaxRowsShown = MaxRowsShown; comboBox.Pulldown = pullDown; comboBox.SelectedLabel = selectedLabel; comboBox.SetItems(Content); comboBox.SetSelectedItem(InitialItem); comboBox.OnSelectionChanged = (obj, item) => OnOptionSelected?.Invoke(obj. gameObject, item as T); // Inner component with the pulldown image var image = PUIElements.CreateUI(combo, "OpenImage"); var icon = image.AddComponent(); icon.sprite = PUITuning.Images.Contract; icon.color = style.textColor; // Button component var dropButton = combo.AddComponent(); PButton.SetupButton(dropButton, bgImage); UIDetours.FG_IMAGE.Set(dropButton, icon); dropButton.onClick += comboBox.OnClick; // Add tooltip PUIElements.SetToolTip(selection, ToolTip); combo.SetActive(true); // Button gets laid out on the right, rest of space goes to the label // Scroll pane is laid out on the bottom var layout = combo.AddComponent(); layout.AnchorYAxis(selection).SetLeftEdge(selection, fraction: 0.0f).SetRightEdge(selection, toLeft: image).AnchorYAxis(image).SetRightEdge( image, fraction: 1.0f).SetMargin(selection, new RectOffset(margin.left, im.right, margin.top, margin.bottom)).SetMargin(image, new RectOffset(0, margin.right, margin.top, margin.bottom)).OverrideSize(image, ArrowSize). AnchorYAxis(pullDown, 0.0f).OverrideSize(pullDown, Vector2.up); layout.LockLayout(); if (DynamicSize) layout.UnlockLayout(); // Disable sizing on the pulldown pullDown.AddOrGet().ignoreLayout = true; // Scroll pane is hidden right away pullDown.SetActive(false); layout.flexibleWidth = FlexSize.x; layout.flexibleHeight = FlexSize.y; OnRealize?.Invoke(combo); return combo; } /// /// Builds a row selection prefab object for this combo box. /// /// The text style for the entries. /// The color for the entry backgrounds. /// A template for each row in the dropdown. private GameObject BuildRowPrefab(TextStyleSetting style, ColorStyleSetting entryColor) { var im = ItemMargin; var rowPrefab = PUIElements.CreateUI(null, "RowEntry"); // Background of the entry var bgImage = rowPrefab.AddComponent(); UIDetours.COLOR_STYLE_SETTING.Set(bgImage, entryColor); UIDetours.APPLY_COLOR_STYLE.Invoke(bgImage); // Checkmark for the front of the entry var isSelected = PUIElements.CreateUI(rowPrefab, "Selected"); var fgImage = isSelected.AddComponent(); fgImage.color = style.textColor; fgImage.preserveAspect = true; fgImage.sprite = PUITuning.Images.Checked; // Button for the entry to select it var entryButton = rowPrefab.AddComponent(); PButton.SetupButton(entryButton, bgImage); UIDetours.FG_IMAGE.Set(entryButton, fgImage); // Tooltip for the entry rowPrefab.AddComponent(); // Text for the entry var textContainer = PUIElements.CreateUI(rowPrefab, "Text"); PUIElements.AddLocText(textContainer, style).SetText(" "); // Configure the entire layout in 1 statement! (jk this is awful) var group = rowPrefab.AddComponent(); group.AnchorYAxis(isSelected).OverrideSize(isSelected, CheckSize).SetLeftEdge( isSelected, fraction: 0.0f).SetMargin(isSelected, im).AnchorYAxis( textContainer).SetLeftEdge(textContainer, toRight: isSelected).SetRightEdge( textContainer, 1.0f).SetMargin(textContainer, new RectOffset(0, im.right, im.top, im.bottom)).LockLayout(); rowPrefab.SetActive(false); return rowPrefab; } /// /// Sets the default Klei pink button style as this combo box's foreground color and text style. /// /// This combo box for call chaining. public PComboBox SetKleiPinkStyle() { TextStyle = PUITuning.Fonts.UILightStyle; BackColor = PUITuning.Colors.ButtonPinkStyle; return this; } /// /// Sets the default Klei blue button style as this combo box's foreground color and text style. /// /// This combo box for call chaining. public PComboBox SetKleiBlueStyle() { TextStyle = PUITuning.Fonts.UILightStyle; BackColor = PUITuning.Colors.ButtonBlueStyle; return this; } /// /// Sets the minimum (and preferred) width of this combo box in characters. /// /// The width is computed using the currently selected text style. /// /// The number of characters to be displayed. /// This combo box for call chaining. public PComboBox SetMinWidthInCharacters(int chars) { int width = Mathf.RoundToInt(chars * PUIUtils.GetEmWidth(TextStyle)); if (width > 0) MinWidth = width; return this; } public override string ToString() { return string.Format("PComboBox[Name={0}]", Name); } } }