Skip to content

Commit a643118

Browse files
Merge pull request #125 from FishingCactus/feature/multi_select_dropdowns
Feature/multi select dropdowns
2 parents ffc6b30 + 1d7b79c commit a643118

10 files changed

Lines changed: 269 additions & 1 deletion

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## [2.5.0] - 2025-05-13
4+
### Added
5+
- Added `MultiSelectDropdown` class and inheritors to help creating editor dropdowns to select multiple items.
6+
37
## [2.4.0] - 2025-05-09
48
### Added
59
- Add utility functions for the inspector:

Editor/DropDowns/MultiSelectDropdowns.meta

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using UnityEditor;
4+
using UnityEngine;
5+
6+
namespace FishingCactus.CommonCode
7+
{
8+
public class CyclingMultiSelectDropdown : MultiSelectDropdown<uint>
9+
{
10+
public const float DEFAULT_CYCLING_BUTTON_WIDTH = 40f;
11+
12+
public Func<uint, GUIContent> ValueToLabelFunction { get; set; }
13+
public uint ValuesCount { get; set; }
14+
public float CyclingButtonWidth { get; set; } = DEFAULT_CYCLING_BUTTON_WIDTH;
15+
16+
public static CyclingMultiSelectDropdown Show( Rect activator_rect,
17+
IReadOnlyList<string> options,
18+
IReadOnlyDictionary<string, uint> options_selected_states,
19+
Func<uint, GUIContent> value_to_label_function,
20+
uint default_value = default,
21+
uint values_count = 1,
22+
float dropdown_max_height = DEFAULT_DROPDOWN_MAX_HEIGHT )
23+
{
24+
CyclingMultiSelectDropdown dropdown = Show<CyclingMultiSelectDropdown>( activator_rect, options, options_selected_states, default_value, dropdown_max_height );
25+
26+
dropdown.ValueToLabelFunction = value_to_label_function;
27+
dropdown.ValuesCount = values_count;
28+
29+
return dropdown;
30+
}
31+
32+
protected override uint DrawOptionLine( string option, uint current_value )
33+
{
34+
uint new_value = current_value;
35+
36+
EditorGUILayout.BeginHorizontal();
37+
38+
GUIContent button_label = ValueToLabelFunction.Invoke( current_value );
39+
40+
if( GUILayout.Button( button_label, GUILayout.Width( CyclingButtonWidth ) ) )
41+
{
42+
new_value = ( current_value + 1 ) % ValuesCount;
43+
}
44+
45+
EditorGUILayout.LabelField( option );
46+
47+
EditorGUILayout.EndHorizontal();
48+
49+
return new_value;
50+
}
51+
}
52+
}

Editor/DropDowns/MultiSelectDropdowns/CyclingMultiSelectDropdown.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
using FishingCactus.CommonCode;
2+
using UnityEditor;
3+
using UnityEngine;
4+
using System.Collections.Generic;
5+
using System.Linq;
6+
using UnityEngine.Events;
7+
8+
namespace FishingCactus.CommonCode
9+
{
10+
public abstract class MultiSelectDropdown<TSelectType> : EditorWindow
11+
{
12+
public const float DEFAULT_DROPDOWN_MAX_HEIGHT = 400;
13+
public const string DEFAULT_DONE_BUTTON_LABEL = "Done";
14+
15+
private Vector2 ScrollValue { get; set; }
16+
private List<string> Options { get; } = new();
17+
private Dictionary<string, TSelectType> OptionSelectedStates { get; } = new();
18+
public Optional<string> DoneButton { get; set; } = DEFAULT_DONE_BUTTON_LABEL;
19+
protected virtual float OptionHeight => EditorGUIUtility.singleLineHeight;
20+
21+
public UnityEvent<IReadOnlyDictionary<string, TSelectType>> OnAnyOptionSelectedStateChanged { get; } = new();
22+
public UnityEvent<IReadOnlyDictionary<string, TSelectType>> OnClosedWithFocusLost { get; } = new();
23+
public UnityEvent<IReadOnlyDictionary<string, TSelectType>> OnClosedWithDoneButton { get; } = new();
24+
25+
public static TDropdown Show<TDropdown>( Rect activator_rect,
26+
IReadOnlyList<string> options,
27+
IReadOnlyDictionary<string, TSelectType> initial_selected_states,
28+
TSelectType default_select_type = default,
29+
float dropdown_max_height = DEFAULT_DROPDOWN_MAX_HEIGHT ) where TDropdown : MultiSelectDropdown<TSelectType>
30+
{
31+
TDropdown dropdown = CreateInstance<TDropdown>();
32+
33+
IEnumerable<string> all_options = options.Union( initial_selected_states.Keys );
34+
35+
foreach( string option in all_options )
36+
{
37+
dropdown.Options.Add( option );
38+
dropdown.OptionSelectedStates.Add( option, initial_selected_states.GetValueOrDefault( option, default_select_type ) );
39+
}
40+
41+
float dropdown_height = Mathf.Min( ( 2 + dropdown.Options.Count ) * dropdown.OptionHeight, dropdown_max_height );
42+
43+
dropdown.ShowAsDropDown( activator_rect, new Vector2( activator_rect.width, dropdown_height ) );
44+
45+
return dropdown;
46+
}
47+
48+
/// <summary>
49+
/// Draws one line in the context of OnGUI. Returns the new value if it was modified by the user, <paramref name="current_value"/> otherwise.
50+
/// </summary>
51+
protected abstract TSelectType DrawOptionLine( string option, TSelectType current_value );
52+
53+
private void OnGUI()
54+
{
55+
ScrollValue = EditorGUILayout.BeginScrollView( ScrollValue );
56+
57+
bool any_changed = false;
58+
59+
foreach( string option in Options )
60+
{
61+
TSelectType current_value = OptionSelectedStates[ option ];
62+
TSelectType new_value = DrawOptionLine( option, current_value );
63+
64+
if( !current_value.Equals( new_value ) )
65+
{
66+
OptionSelectedStates[ option ] = new_value;
67+
any_changed = true;
68+
}
69+
}
70+
71+
if( any_changed )
72+
{
73+
OnAnyOptionSelectedStateChanged.Invoke( OptionSelectedStates );
74+
}
75+
76+
EditorGUILayout.EndScrollView();
77+
78+
GUILayout.FlexibleSpace();
79+
80+
if( DoneButton.HasValue( out string done_button_label ) && GUILayout.Button( done_button_label ) )
81+
{
82+
OnClosedWithDoneButton.Invoke( OptionSelectedStates );
83+
Close();
84+
}
85+
}
86+
87+
protected virtual void OnLostFocus()
88+
{
89+
OnClosedWithFocusLost.Invoke( OptionSelectedStates );
90+
}
91+
92+
protected virtual void OnDisable()
93+
{
94+
OnAnyOptionSelectedStateChanged.RemoveAllListeners();
95+
OnClosedWithFocusLost.RemoveAllListeners();
96+
OnClosedWithDoneButton.RemoveAllListeners();
97+
}
98+
}
99+
}

Editor/DropDowns/MultiSelectDropdowns/MultiSelectDropdown.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
using UnityEditor;
2+
using UnityEngine;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using UnityEngine.Events;
6+
7+
namespace FishingCactus.CommonCode
8+
{
9+
public class ToggleMultiSelectDropdown : MultiSelectDropdown<bool>
10+
{
11+
private List<string> SelectedOptions { get; } = new();
12+
13+
public UnityEvent<IReadOnlyList<string>> OnSelectionChanged { get; } = new();
14+
public new UnityEvent<IReadOnlyList<string>> OnClosedWithFocusLost { get; } = new();
15+
public new UnityEvent<IReadOnlyList<string>> OnClosedWithDoneButton { get; } = new();
16+
17+
public static ToggleMultiSelectDropdown Show( Rect activator_rect,
18+
IReadOnlyList<string> options,
19+
IReadOnlyList<string> selected_options,
20+
float dropdown_max_height = DEFAULT_DROPDOWN_MAX_HEIGHT )
21+
{
22+
ToggleMultiSelectDropdown dropdown = Show<ToggleMultiSelectDropdown>( activator_rect,
23+
options,
24+
selected_options.ToDictionary( option => option, _ => true ),
25+
false,
26+
dropdown_max_height );
27+
28+
dropdown.OnAnyOptionSelectedStateChanged.AddListener( dropdown.HandleSelectionChanged );
29+
( ( MultiSelectDropdown<bool> )dropdown ).OnClosedWithFocusLost.AddListener( dropdown.HandleClosedWithFocusLost );
30+
( ( MultiSelectDropdown<bool> )dropdown ).OnClosedWithDoneButton.AddListener( dropdown.HandleClosedWithDoneButton );
31+
32+
return dropdown;
33+
}
34+
35+
protected override bool DrawOptionLine( string option, bool current_value ) => EditorGUILayout.ToggleLeft( option, current_value );
36+
37+
private void HandleClosedWithDoneButton( IReadOnlyDictionary<string, bool> option_selected_states ) => OnClosedWithDoneButton.Invoke( SelectedOptions );
38+
private void HandleClosedWithFocusLost( IReadOnlyDictionary<string, bool> option_selected_states ) => OnClosedWithFocusLost.Invoke( SelectedOptions );
39+
40+
private void HandleSelectionChanged( IReadOnlyDictionary<string, bool> option_selected_states )
41+
{
42+
SelectedOptions.Clear();
43+
44+
foreach( KeyValuePair<string, bool> option_selected_state in option_selected_states )
45+
{
46+
if( option_selected_state.Value )
47+
{
48+
SelectedOptions.Add( option_selected_state.Key );
49+
}
50+
}
51+
52+
OnSelectionChanged.Invoke( SelectedOptions );
53+
}
54+
55+
protected override void OnDisable()
56+
{
57+
base.OnDisable();
58+
OnSelectionChanged.RemoveAllListeners();
59+
OnClosedWithFocusLost.RemoveAllListeners();
60+
OnClosedWithDoneButton.RemoveAllListeners();
61+
}
62+
63+
}
64+
}

Editor/DropDowns/MultiSelectDropdowns/ToggleMultiSelectDropdown.cs.meta

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,14 @@ Provide readonly access to common collections types with garbage free enumeratin
293293
|--------------------|-------|
294294
| `TypeDropdownItem` | |
295295

296+
### MultiSelect
297+
298+
| Classes | Notes |
299+
|------------------------------|--------------------------------------------------------------------------------------------------------------------------|
300+
| `MultiSelectDropdown` | This is the base class for dropdowns that allow selecting multiple items |
301+
| `ToggleMultiSelectDropdown` | Implementation of `MultiSelectDropdown` that shows toggles for each item |
302+
| `CyclingMultiSelectDropdown` | Implementation of `MultiSelectDropdown` to shows a button to cycle over unsigned integer values, up to a given max value |
303+
296304
## DropHandlers
297305

298306
| Classes | Notes |

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "com.fishingcactus.common-code",
3-
"version": "2.4.0",
3+
"version": "2.5.0",
44
"displayName": "FC - Common Code",
55
"description": "Code, Extensions and extra features for Fishing Cactus Games.",
66
"unity": "2020.3",

0 commit comments

Comments
 (0)