From 3df3e802d41a1cde22e59a3837d3ced90bf5a231 Mon Sep 17 00:00:00 2001 From: Asval Date: Wed, 28 Aug 2024 17:31:02 +0200 Subject: [PATCH] filterable ue combobox --- CUE4Parse | 2 +- FModel/ViewModels/GameSelectorViewModel.cs | 22 +- FModel/ViewModels/SettingsViewModel.cs | 18 +- FModel/Views/DirectorySelector.xaml | 22 +- .../Resources/Controls/FilterableComboBox.cs | 273 ++++++++++++++++++ FModel/Views/Resources/Resources.xaml | 216 ++++++++++++++ FModel/Views/SettingsView.xaml | 24 +- 7 files changed, 509 insertions(+), 68 deletions(-) create mode 100644 FModel/Views/Resources/Controls/FilterableComboBox.cs diff --git a/CUE4Parse b/CUE4Parse index 2f5fbe8a..2249deef 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit 2f5fbe8a852a4c91d19616d45b784b62efca4753 +Subproject commit 2249deefcbe5e5858229db2f8694623c69009036 diff --git a/FModel/ViewModels/GameSelectorViewModel.cs b/FModel/ViewModels/GameSelectorViewModel.cs index 273248b2..c8bdd253 100644 --- a/FModel/ViewModels/GameSelectorViewModel.cs +++ b/FModel/ViewModels/GameSelectorViewModel.cs @@ -33,28 +33,16 @@ public class GameSelectorViewModel : ViewModel public IList CustomDirectories { get; set; } } - private bool _useCustomEGames; - public bool UseCustomEGames - { - get => _useCustomEGames; - set => SetProperty(ref _useCustomEGames, value); - } - private DirectorySettings _selectedDirectory; public DirectorySettings SelectedDirectory { get => _selectedDirectory; - set - { - SetProperty(ref _selectedDirectory, value); - if (_selectedDirectory != null) UseCustomEGames = EnumerateUeGames().ElementAt(1).Contains(_selectedDirectory.UeVersion); - } + set => SetProperty(ref _selectedDirectory, value); } private readonly ObservableCollection _detectedDirectories; public ReadOnlyObservableCollection DetectedDirectories { get; } public ReadOnlyObservableCollection UeGames { get; } - public ReadOnlyObservableCollection CustomUeGames { get; } public GameSelectorViewModel(string gameDirectory) { @@ -73,9 +61,7 @@ public class GameSelectorViewModel : ViewModel else SelectedDirectory = DetectedDirectories.FirstOrDefault(); - var ueGames = EnumerateUeGames().ToArray(); - UeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[0])); - CustomUeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[1])); + UeGames = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUeGames())); } public void AddUndetectedDir(string gameDirectory) => AddUndetectedDir(gameDirectory.SubstringAfterLast('\\'), gameDirectory); @@ -94,11 +80,11 @@ public class GameSelectorViewModel : ViewModel SelectedDirectory = DetectedDirectories.Last(); } - private IEnumerable> EnumerateUeGames() + private IEnumerable EnumerateUeGames() => Enum.GetValues() .GroupBy(value => (int)value) .Select(group => group.First()) - .GroupBy(value => (int)value == ((int)value & ~0xF)); + .OrderBy(value => (int)value == ((int)value & ~0xF)); private IEnumerable EnumerateDetectedGames() { yield return GetUnrealEngineGame("Fortnite", "\\FortniteGame\\Content\\Paks", EGame.GAME_UE5_5); diff --git a/FModel/ViewModels/SettingsViewModel.cs b/FModel/ViewModels/SettingsViewModel.cs index e76c59b5..b75ebf5b 100644 --- a/FModel/ViewModels/SettingsViewModel.cs +++ b/FModel/ViewModels/SettingsViewModel.cs @@ -27,13 +27,6 @@ public class SettingsViewModel : ViewModel set => SetProperty(ref _useCustomOutputFolders, value); } - private bool _useCustomEGames; - public bool UseCustomEGames - { - get => _useCustomEGames; - set => SetProperty(ref _useCustomEGames, value); - } - private EUpdateMode _selectedUpdateMode; public EUpdateMode SelectedUpdateMode { @@ -177,7 +170,6 @@ public class SettingsViewModel : ViewModel public ReadOnlyObservableCollection UpdateModes { get; private set; } public ReadOnlyObservableCollection UeGames { get; private set; } - public ReadOnlyObservableCollection CustomUeGames { get; private set; } public ReadOnlyObservableCollection AssetLanguages { get; private set; } public ReadOnlyObservableCollection AesReloads { get; private set; } public ReadOnlyObservableCollection DiscordRpcs { get; private set; } @@ -273,12 +265,8 @@ public class SettingsViewModel : ViewModel SelectedAesReload = UserSettings.Default.AesReload; SelectedDiscordRpc = UserSettings.Default.DiscordRpc; - var ueGames = EnumerateUeGames().ToArray(); - UseCustomEGames = ueGames[1].Contains(SelectedUeGame); - UpdateModes = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUpdateModes())); - UeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[0])); - CustomUeGames = new ReadOnlyObservableCollection(new ObservableCollection(ueGames[1])); + UeGames = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateUeGames())); AssetLanguages = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateAssetLanguages())); AesReloads = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateAesReloads())); DiscordRpcs = new ReadOnlyObservableCollection(new ObservableCollection(EnumerateDiscordRpcs())); @@ -343,11 +331,11 @@ public class SettingsViewModel : ViewModel } private IEnumerable EnumerateUpdateModes() => Enum.GetValues(); - private IEnumerable> EnumerateUeGames() + private IEnumerable EnumerateUeGames() => Enum.GetValues() .GroupBy(value => (int)value) .Select(group => group.First()) - .GroupBy(value => (int)value == ((int)value & ~0xF)); + .OrderBy(value => (int)value == ((int)value & ~0xF)); private IEnumerable EnumerateAssetLanguages() => Enum.GetValues(); private IEnumerable EnumerateAesReloads() => Enum.GetValues(); private IEnumerable EnumerateDiscordRpcs() => Enum.GetValues(); diff --git a/FModel/Views/DirectorySelector.xaml b/FModel/Views/DirectorySelector.xaml index badcfc23..64eeed8b 100644 --- a/FModel/Views/DirectorySelector.xaml +++ b/FModel/Views/DirectorySelector.xaml @@ -1,6 +1,7 @@  - - - - + - - + diff --git a/FModel/Views/Resources/Controls/FilterableComboBox.cs b/FModel/Views/Resources/Controls/FilterableComboBox.cs new file mode 100644 index 00000000..4679311a --- /dev/null +++ b/FModel/Views/Resources/Controls/FilterableComboBox.cs @@ -0,0 +1,273 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using System.Windows.Input; + +namespace FModel.Views.Resources.Controls; + +/// +/// https://stackoverflow.com/a/58066259/13389331 +/// +public class FilterableComboBox : ComboBox +{ + /// + /// If true, on lost focus or enter key pressed, checks the text in the combobox. If the text is not present + /// in the list, it leaves it blank. + /// + public bool OnlyValuesInList { + get => (bool)GetValue(OnlyValuesInListProperty); + set => SetValue(OnlyValuesInListProperty, value); + } + public static readonly DependencyProperty OnlyValuesInListProperty = + DependencyProperty.Register(nameof(OnlyValuesInList), typeof(bool), typeof(FilterableComboBox)); + + /// + /// Selected item, changes only on lost focus or enter key pressed + /// + public object EffectivelySelectedItem { + get => (bool)GetValue(EffectivelySelectedItemProperty); + set => SetValue(EffectivelySelectedItemProperty, value); + } + public static readonly DependencyProperty EffectivelySelectedItemProperty = + DependencyProperty.Register(nameof(EffectivelySelectedItem), typeof(object), typeof(FilterableComboBox)); + + private string CurrentFilter = string.Empty; + private bool TextBoxFreezed; + protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox; + private UserChange IsDropDownOpenUC; + + /// + /// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed. + /// + public event Action SelectionEffectivelyChanged; + + public FilterableComboBox() + { + IsDropDownOpenUC = new UserChange(v => IsDropDownOpen = v); + DropDownOpened += FilteredComboBox_DropDownOpened; + + Focusable = true; + IsEditable = true; + IsTextSearchEnabled = true; + StaysOpenOnEdit = true; + IsReadOnly = false; + + Loaded += (s, e) => { + if (EditableTextBox != null) + new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange; + }; + + SelectionChanged += (_, __) => shouldTriggerSelectedItemChanged = true; + + SelectionEffectivelyChanged += (_, o) => EffectivelySelectedItem = o; + } + + protected override void OnPreviewKeyDown(KeyEventArgs e) + { + base.OnPreviewKeyDown(e); + if (e.Key == Key.Down && !IsDropDownOpen) { + IsDropDownOpen = true; + e.Handled = true; + } + else if (e.Key == Key.Escape) { + ClearFilter(); + Text = ""; + IsDropDownOpen = true; + } + else if (e.Key == Key.Enter || e.Key == Key.Tab) { + CheckSelectedItem(); + TriggerSelectedItemChanged(); + } + } + + protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e) + { + base.OnPreviewLostKeyboardFocus(e); + CheckSelectedItem(); + if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox) + TriggerSelectedItemChanged(); + } + + private void CheckSelectedItem() + { + if (OnlyValuesInList) + Text = SelectedItem?.ToString() ?? ""; + } + + private bool shouldTriggerSelectedItemChanged = false; + private void TriggerSelectedItemChanged() + { + if (shouldTriggerSelectedItemChanged) { + SelectionEffectivelyChanged?.Invoke(this, SelectedItem); + shouldTriggerSelectedItemChanged = false; + } + } + + public void ClearFilter() + { + if (string.IsNullOrEmpty(CurrentFilter)) return; + CurrentFilter = ""; + CollectionViewSource.GetDefaultView(ItemsSource).Refresh(); + } + + private void FilteredComboBox_DropDownOpened(object sender, EventArgs e) + { + if (IsDropDownOpenUC.IsUserChange) + ClearFilter(); + } + + private void FilteredComboBox_UserTextChange(object sender, EventArgs e) + { + if (TextBoxFreezed) return; + var tb = EditableTextBox; + if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length) + CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower(); + else + CurrentFilter = tb.Text.ToLower(); + RefreshFilter(); + } + + protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue) + { + if (newValue != null) { + var view = CollectionViewSource.GetDefaultView(newValue); + view.Filter += FilterItem; + } + + if (oldValue != null) { + var view = CollectionViewSource.GetDefaultView(oldValue); + if (view != null) view.Filter -= FilterItem; + } + + base.OnItemsSourceChanged(oldValue, newValue); + } + + private void RefreshFilter() + { + if (ItemsSource == null) return; + + var view = CollectionViewSource.GetDefaultView(ItemsSource); + FreezTextBoxState(() => { + var isDropDownOpen = IsDropDownOpen; + //always hide because showing it enables the user to pick with up and down keys, otherwise it's not working because of the glitch in view.Refresh() + IsDropDownOpenUC.Set(false); + view.Refresh(); + + if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen) + IsDropDownOpenUC.Set(true); + + if (SelectedItem == null) { + foreach (var itm in ItemsSource) + if (itm.ToString() == Text) { + SelectedItem = itm; + break; + } + } + }); + } + + private void FreezTextBoxState(Action action) + { + TextBoxFreezed = true; + var tb = EditableTextBox; + var text = Text; + var selStart = tb.SelectionStart; + var selLen = tb.SelectionLength; + action(); + Text = text; + tb.SelectionStart = selStart; + tb.SelectionLength = selLen; + TextBoxFreezed = false; + } + + private bool FilterItem(object value) + { + if (value == null) return false; + if (CurrentFilter.Length == 0) return true; + + return value.ToString().ToLower().Contains(CurrentFilter); + } + + private class TextBoxBaseUserChangeTracker + { + private bool IsTextInput { get; set; } + + public TextBox TextBoxBase { get; set; } + private List PressedKeys = new List(); + public event EventHandler UserTextChanged; + private string LastText; + + public TextBoxBaseUserChangeTracker(TextBox textBoxBase) + { + TextBoxBase = textBoxBase; + LastText = TextBoxBase.ToString(); + + textBoxBase.PreviewTextInput += (s, e) => { + IsTextInput = true; + }; + + textBoxBase.TextChanged += (s, e) => { + var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString(); + IsTextInput = false; + LastText = TextBoxBase.ToString(); + if (isUserChange) + UserTextChanged?.Invoke(this, e); + }; + + textBoxBase.PreviewKeyDown += (s, e) => { + switch (e.Key) { + case Key.Back: + case Key.Space: + if (!PressedKeys.Contains(e.Key)) + PressedKeys.Add(e.Key); + break; + } + if (e.Key == Key.Back) { + var textBox = textBoxBase as TextBox; + if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length) { + textBox.SelectionStart--; + textBox.SelectionLength++; + e.Handled = true; + UserTextChanged?.Invoke(this, e); + } + } + }; + + textBoxBase.PreviewKeyUp += (s, e) => { + if (PressedKeys.Contains(e.Key)) + PressedKeys.Remove(e.Key); + }; + + textBoxBase.LostFocus += (s, e) => { + PressedKeys.Clear(); + IsTextInput = false; + }; + } + } + + private class UserChange + { + private Action action; + + public bool IsUserChange { get; private set; } = true; + + public UserChange(Action action) + { + this.action = action; + } + + public void Set(T val) + { + try { + IsUserChange = false; + action(val); + } + finally { + IsUserChange = true; + } + } + } +} diff --git a/FModel/Views/Resources/Resources.xaml b/FModel/Views/Resources/Resources.xaml index 4cd0164e..f63e0f62 100644 --- a/FModel/Views/Resources/Resources.xaml +++ b/FModel/Views/Resources/Resources.xaml @@ -1471,6 +1471,222 @@ + + diff --git a/FModel/Views/SettingsView.xaml b/FModel/Views/SettingsView.xaml index 7a15eec9..d8dcc7cf 100644 --- a/FModel/Views/SettingsView.xaml +++ b/FModel/Views/SettingsView.xaml @@ -122,29 +122,17 @@