Arc Raiders/PUBG Mobile 4.6/Splitgate 2/InZOI updates, Delta Force VT fix, Fixed PSK exports, Added support for new Roco Kingdom: World encryption, Criware fixes, Option to clear empty game directories, APKs can now be loaded directly, Fixed laggy game version filtering, Added update notification, Audio middleware assets are now grouped
Some checks failed
FModel QA Builder / build (push) Has been cancelled

Co-Authored-By: LongerWarrior <37636768+LongerWarrior@users.noreply.github.com>
Co-Authored-By: Luke <17146677+lukefz@users.noreply.github.com>
Co-Authored-By: Abishek <73873078+abishek82828@users.noreply.github.com>
This commit is contained in:
Masusder
2026-09-13 21:03:01 +02:00
parent fafcda5305
commit 98bfdc690d
19 changed files with 339 additions and 61 deletions

View File

@@ -171,6 +171,14 @@ public enum EAssetCategory : uint
GothamKnights = GameSpecific + 7,
}
public enum EAssetFamily
{
None,
Criware,
Wwise,
FMod,
}
public enum EUnluacMode
{
Decompile,

View File

@@ -807,6 +807,33 @@
HorizontalAlignment="Right" VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<StackPanel Orientation="Horizontal">
<StatusBarItem Margin="0 0 10 0"
Padding="0"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
Visibility="{Binding IsUpdateAvailable, Converter={StaticResource BoolToVisibilityConverter}}">
<Button HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Center"
Style="{StaticResource StatusBarButton}"
Background="{DynamicResource {x:Static adonisUi:Brushes.AlertBrush}}"
ToolTip="A new version of FModel is available. Click to view it."
Command="{Binding MenuCommand}"
CommandParameter="Help_Releases">
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center">
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.AccentForegroundBrush}}"
Data="{StaticResource UpdateIcon}" />
</Canvas>
</Viewbox>
<TextBlock Margin="10 0 0 0"
Text="Update Available" />
</StackPanel>
</Button>
</StatusBarItem>
<StatusBarItem Margin="0 0 10 0" Padding="0" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
<Button x:Name="ExportSessionButton"
HorizontalContentAlignment="Stretch"

View File

@@ -131,6 +131,7 @@ public class FModelApiEndpoint : AbstractApiProvider
UserSettings.Default.LastUpdateCheck = DateTime.Now;
var targetHash = ((CustomMandatory) args.Mandatory).CommitHash;
_applicationView.IsUpdateAvailable = targetHash != Constants.APP_COMMIT_ID;
if (targetHash == Constants.APP_COMMIT_ID)
{
if (UserSettings.Default.ShowChangelog)

View File

@@ -61,6 +61,13 @@ public class ApplicationViewModel : ViewModel
}
}
private bool _isUpdateAvailable;
public bool IsUpdateAvailable
{
get => _isUpdateAvailable;
internal set => SetProperty(ref _isUpdateAvailable, value);
}
private int _selectedLeftTabIndex;
public int SelectedLeftTabIndex
{
@@ -139,7 +146,7 @@ public class ApplicationViewModel : ViewModel
public DirectorySettings AvoidEmptyGameDirectory(bool bAlreadyLaunched)
{
var gameDirectory = UserSettings.Default.GameDirectory;
if (!bAlreadyLaunched && UserSettings.Default.PerDirectory.TryGetValue(gameDirectory, out var currentDir))
if (!bAlreadyLaunched && GameSelectorViewModel.IsGameDirectoryAvailable(gameDirectory) && UserSettings.Default.PerDirectory.TryGetValue(gameDirectory, out var currentDir))
return currentDir;
Status.SetStatus(EStatusKind.Configuring);

View File

@@ -14,9 +14,9 @@ using CSCore.CoreAudioAPI;
using CSCore.DSP;
using CSCore.SoundOut;
using CSCore.Streams;
using CUE4Parse.UE4.CriWare.Decoders;
using CUE4Parse.UE4.CriWare.Decoders.ADX;
using CUE4Parse.UE4.CriWare.Decoders.HCA;
using CUE4Parse.UE4.Criware.Decoders;
using CUE4Parse.UE4.Criware.Decoders.ADX;
using CUE4Parse.UE4.Criware.Decoders.HCA;
using CUE4Parse.Utils;
using FModel.Extensions;
using FModel.Framework;

View File

@@ -31,7 +31,7 @@ using CUE4Parse.GameTypes.HonorOfKings.FileProvider;
using CUE4Parse.GameTypes.KRD.Assets.Exports;
using CUE4Parse.GameTypes.LegoBatman.Assets;
using CUE4Parse.GameTypes.LordOfMysteries.FileProvider;
using CUE4Parse.GameTypes.RocoKingdomWorld.Assets.Objects;
using CUE4Parse.GameTypes.Tencent.RocoKingdomWorld.Assets.Objects;
using CUE4Parse.GameTypes.SMG.UE4.Assets.Exports.Wwise;
using CUE4Parse.GameTypes.SquareEnix.UE4.Assets.Exports;
using CUE4Parse.GameTypes.Theia.FileProvider;
@@ -43,7 +43,7 @@ using CUE4Parse.UE4.AssetRegistry;
using CUE4Parse.UE4.Assets;
using CUE4Parse.UE4.Assets.Exports;
using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.CriWare;
using CUE4Parse.UE4.Assets.Exports.Criware;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Fmod;
using CUE4Parse.UE4.Assets.Exports.GeometryCollection;
@@ -55,8 +55,8 @@ using CUE4Parse.UE4.Assets.Exports.Verse;
using CUE4Parse.UE4.Assets.Exports.Wwise;
using CUE4Parse.UE4.Assets.Objects;
using CUE4Parse.UE4.BinaryConfig;
using CUE4Parse.UE4.CriWare;
using CUE4Parse.UE4.CriWare.Readers;
using CUE4Parse.UE4.Criware;
using CUE4Parse.UE4.Criware.Readers;
using CUE4Parse.UE4.FMod;
using CUE4Parse.UE4.GameFeatures;
using CUE4Parse.UE4.IO;

View File

@@ -148,7 +148,7 @@ public class LoadCommand : ViewModelCommand<LoadingModesViewModel>
{
entries.Add(asset);
}
else if (includeLooseFiles && asset is OsGameFile)
else if (includeLooseFiles && asset is not VfsEntry)
{
entries.Add(asset);
}

View File

@@ -22,7 +22,8 @@ using CUE4Parse.UE4.Assets.Exports.Animation;
using CUE4Parse.UE4.Assets.Exports.BuildData;
using CUE4Parse.UE4.Assets.Exports.ChaosClothAsset;
using CUE4Parse.UE4.Assets.Exports.Component;
using CUE4Parse.UE4.Assets.Exports.CriWare;
using CUE4Parse.UE4.Assets.Exports.Criware;
using CUE4Parse.UE4.Assets.Exports.Criware.Media;
using CUE4Parse.UE4.Assets.Exports.CustomizableObject;
using CUE4Parse.UE4.Assets.Exports.Engine;
using CUE4Parse.UE4.Assets.Exports.Engine.Font;
@@ -97,6 +98,13 @@ public class GameFileViewModel(GameFile asset) : ViewModel
}
}
private EAssetFamily _assetFamily = EAssetFamily.None;
public EAssetFamily AssetFamily
{
get => _assetFamily;
private set => SetProperty(ref _assetFamily, value);
}
private EBulkType _assetActions = EBulkType.None;
public EBulkType AssetActions
{
@@ -200,6 +208,20 @@ public class GameFileViewModel(GameFile asset) : ViewModel
var dummy = ((AbstractUePackage) pkg).ConstructObject(pointer.Class, pkg);
ResolvedAssetType = dummy.ExportType;
AssetFamily = dummy switch
{
UAtomSoundBase or UAtomSoundBank or USoundAtomCue or USoundAtomCueSheet
or USoundAtomConfig or UAtomDspBusSetting or UAtomRackBase => EAssetFamily.Criware,
UAkAudioType or UAkAssetData or UAkAssetPlatformData or UAkMediaAssetData
or UAkMediaAsset or UWwiseAssetLibrary or UExternalSource or UExternalSourceBank => EAssetFamily.Wwise,
UFMODEvent or UFMODBank or UFMODBankLookup or UFMODBus
or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA => EAssetFamily.FMod,
_ => EAssetFamily.None,
};
(AssetCategory, AssetActions) = dummy switch
{
URigVMBlueprintGeneratedClass => (EAssetCategory.RigVMBlueprintGeneratedClass, EBulkType.Code),
@@ -242,18 +264,23 @@ public class GameFileViewModel(GameFile asset) : ViewModel
UObjectRedirector => (EAssetCategory.ObjectRedirector, EBulkType.None),
UPhysicalMaterial => (EAssetCategory.PhysicalMaterial, EBulkType.None),
USoundAtomCue or UAkAudioEvent or USoundCue or UFMODEvent
USoundAtomCue or UAtomSoundCue or UAkAudioEvent or USoundCue or UFMODEvent
or UAkAssetData or UAkAssetPlatformData => (EAssetCategory.AudioEvent, EBulkType.Audio),
UFMODBankLookup => (EAssetCategory.Data, EBulkType.None),
UFileManaMovie => (EAssetCategory.Video, EBulkType.None),
UFMODBus or UFMODSnapshot or UFMODSnapshotReverb or UFMODVCA or USQEXSEADSoundAttenuation => (EAssetCategory.Audio, EBulkType.None),
UFMODBankLookup or USoundAtomConfig or UFMODSnapshotReverb or UFMODBus or UFMODSnapshot
or UFMODVCA or UAtomDspBusSetting => (EAssetCategory.Data, EBulkType.None),
UFMODBank or UAkAudioBank or UAtomWaveBank or UAkInitBank or USQEXSEADSoundBank => (EAssetCategory.SoundBank, EBulkType.Audio),
USQEXSEADSoundAttenuation => (EAssetCategory.Audio, EBulkType.None),
UWwiseAssetLibrary or USoundBase or UAkMediaAssetData or UAtomCueSheet
or USoundAtomCueSheet or UAkAudioType or UExternalSource or UExternalSourceBank
or UAkMediaAsset => (EAssetCategory.Audio, EBulkType.Audio),
UFMODBank or UAkAudioBank or UAtomSoundBank or USoundAtomCueSheet
or UAkInitBank or USQEXSEADSoundBank => (EAssetCategory.SoundBank, EBulkType.Audio),
UWwiseAssetLibrary or UAtomSoundBase or USoundBase or UAkMediaAssetData
or UExternalSource or UExternalSourceBank or UAkMediaAsset => (EAssetCategory.Audio, EBulkType.Audio),
UAkAudioType => (EAssetCategory.Data, EBulkType.None),
UFileMediaSource => (EAssetCategory.Video, EBulkType.None),
UFont or UFontFace or USMGLocaleFontUMG => (EAssetCategory.Font, EBulkType.None),
@@ -334,6 +361,15 @@ public class GameFileViewModel(GameFile asset) : ViewModel
{
Resolved |= EResolveCompute.Preview;
var lowercaseExtension = Asset.Extension.ToLowerInvariant();
AssetFamily = lowercaseExtension switch
{
"acb" or "acf" or "adx" or "awb" or "hca" or "usm" => EAssetFamily.Criware,
"bnk" or "pck" or "wem" => EAssetFamily.Wwise,
"bank" or "fsb" => EAssetFamily.FMod,
_ => EAssetFamily.None,
};
switch (lowercaseExtension)
{
case "uproject":
@@ -363,6 +399,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
case "sql":
case "py":
case "cs":
case "acf":
AssetCategory = EAssetCategory.Data;
break;
case "stinfo":
@@ -373,6 +410,8 @@ public class GameFileViewModel(GameFile asset) : ViewModel
break;
case "wav":
case "awb": // This is technically soundbank and should be below but I want it to be distinguishable from "acb"
case "adx":
case "hca":
case "xvag":
case "flac":
case "at9":
@@ -383,6 +422,7 @@ public class GameFileViewModel(GameFile asset) : ViewModel
break;
case "acb":
case "bank":
case "fsb":
case "bnk":
case "pck":
AssetCategory = EAssetCategory.SoundBank;
@@ -393,7 +433,9 @@ public class GameFileViewModel(GameFile asset) : ViewModel
case "ttf":
AssetCategory = EAssetCategory.Font;
break;
case "webm":
case "mp4":
case "usm":
AssetCategory = EAssetCategory.Video;
break;
case "jpg":

View File

@@ -58,7 +58,7 @@ public class GameSelectorViewModel : ViewModel
if (DetectedDirectories.FirstOrDefault(x => x.GameDirectory == gameDirectory) is { } detectedGame)
SelectedDirectory = detectedGame;
else if (!string.IsNullOrEmpty(gameDirectory))
else if (IsGameDirectoryAvailable(gameDirectory))
AddUndetectedDir(gameDirectory);
else
SelectedDirectory = DetectedDirectories.FirstOrDefault();
@@ -189,6 +189,39 @@ public class GameSelectorViewModel : ViewModel
SelectedDirectory = DetectedDirectories.Last();
}
public int ClearMissingDirectories()
{
var removedDirectories = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var missingDirectories = UserSettings.Default.PerDirectory
.Where(x => !IsGameDirectoryAvailable(x.Value.GameDirectory))
.Select(x => new KeyValuePair<string, string>(x.Key, x.Value.GameDirectory))
.ToArray();
foreach (var (key, gameDirectory) in missingDirectories)
{
UserSettings.Default.PerDirectory.Remove(key);
removedDirectories.Add(gameDirectory);
}
var missingDetectedDirectories = _detectedDirectories
.Where(x => !IsGameDirectoryAvailable(x.GameDirectory))
.ToArray();
foreach (var directory in missingDetectedDirectories)
{
_detectedDirectories.Remove(directory);
removedDirectories.Add(directory.GameDirectory);
}
if (SelectedDirectory is null || !_detectedDirectories.Contains(SelectedDirectory))
SelectedDirectory = DetectedDirectories.FirstOrDefault();
return removedDirectories.Count;
}
public static bool IsGameDirectoryAvailable(string gameDirectory)
=> gameDirectory is Constants._FN_LIVE_TRIGGER or Constants._VAL_LIVE_TRIGGER || Directory.Exists(gameDirectory);
private IEnumerable<EGame> EnumerateUeGames()
=> Enum.GetValues<EGame>()
.GroupBy(value => (int)value)

View File

@@ -130,14 +130,18 @@
adonisExtensions:LayerExtension.IncreaseLayer="True">
<Grid Margin="30, 12, 6, 12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="1" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False"
<Button Grid.Column="0" MinWidth="78" Margin="0 0 12 0"
HorizontalAlignment="Left" VerticalAlignment="Bottom" Content="Clear"
ToolTip="Remove game directories that no longer exist" Click="OnClearDirectories" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="True" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="OK" Click="OnClick" />
<Button Grid.Column="2" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True"
<Button Grid.Column="3" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Cancel" />
</Grid>
</Border>

View File

@@ -1,6 +1,13 @@
using FModel.ViewModels;
using FModel.Views.Resources.Controls;
using Ookii.Dialogs.Wpf;
using Serilog;
using System.Windows;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxButtons = AdonisUI.Controls.MessageBoxButtons;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
using MessageBoxModel = AdonisUI.Controls.MessageBoxModel;
using MessageBoxResult = AdonisUI.Controls.MessageBoxResult;
namespace FModel.Views;
@@ -63,6 +70,37 @@ public partial class DirectorySelector
gameLauncherViewModel.DeleteSelectedGame();
}
private void OnClearDirectories(object sender, RoutedEventArgs e)
{
if (DataContext is not GameSelectorViewModel gameSelectorViewModel)
return;
var messageBox = new MessageBoxModel
{
Text = "Remove all saved game directories that no longer exist?",
Caption = "Clear Empty Directories",
Icon = MessageBoxImage.Question,
Buttons = MessageBoxButtons.YesNo(),
IsSoundEnabled = false
};
MessageBox.Show(messageBox);
if (messageBox.Result != MessageBoxResult.Yes)
return;
var removedCount = gameSelectorViewModel.ClearMissingDirectories();
var message = removedCount switch
{
0 => "No empty directories were found",
1 => "Removed 1 empty game directory",
_ => $"Removed {removedCount} empty game directories"
};
Log.Information("{Message}", message);
if (FLogger.Logger is not null) // When only directory selector is open, FLogger.Logger is null
FLogger.Append(ELog.Information, () => FLogger.Text(message, Constants.WHITE, true));
}
public void AddManualGame(string directory)
{
ManualGameExpander.IsExpanded = true;

View File

@@ -54,6 +54,11 @@
<SolidColorBrush x:Key="PythonBrush" Color="#3d77a8" />
<SolidColorBrush x:Key="CSharpBrush" Color="#9e559a" />
<!-- Asset family brushes -->
<SolidColorBrush x:Key="CriwareBrush" Color="#005bab" />
<SolidColorBrush x:Key="WwiseBrush" Color="#0152a1" />
<SolidColorBrush x:Key="FModBrush" Color="#00758f" />
<!-- For specific games -->
<SolidColorBrush x:Key="BorderlandsBrush" Color="Yellow"></SolidColorBrush>
<SolidColorBrush x:Key="AionBrush" Color="DeepSkyBlue"></SolidColorBrush>

View File

@@ -44,12 +44,14 @@
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding>
</Path.Data>
<Path.Fill>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding>
</Path.Fill>
</Path>

View File

@@ -35,12 +35,14 @@
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding>
</Path.Data>
<Path.Fill>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
<Binding Path="ResolvedAssetType" />
<Binding Path="AssetFamily" />
</MultiBinding>
</Path.Fill>
</Path>

View File

@@ -5,6 +5,7 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Threading;
namespace FModel.Views.Resources.Controls;
@@ -17,8 +18,9 @@ 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.
/// </summary>
public bool OnlyValuesInList {
get => (bool)GetValue(OnlyValuesInListProperty);
public bool OnlyValuesInList
{
get => (bool) GetValue(OnlyValuesInListProperty);
set => SetValue(OnlyValuesInListProperty, value);
}
public static readonly DependencyProperty OnlyValuesInListProperty =
@@ -27,8 +29,9 @@ public class FilterableComboBox : ComboBox
/// <summary>
/// Selected item, changes only on lost focus or enter key pressed
/// </summary>
public object EffectivelySelectedItem {
get => (bool)GetValue(EffectivelySelectedItemProperty);
public object EffectivelySelectedItem
{
get => (bool) GetValue(EffectivelySelectedItemProperty);
set => SetValue(EffectivelySelectedItemProperty, value);
}
public static readonly DependencyProperty EffectivelySelectedItemProperty =
@@ -38,6 +41,7 @@ public class FilterableComboBox : ComboBox
private bool TextBoxFreezed;
protected TextBox EditableTextBox => GetTemplateChild("PART_EditableTextBox") as TextBox;
private UserChange<bool> IsDropDownOpenUC;
private readonly DispatcherTimer FilterTimer;
/// <summary>
/// Triggers on lost focus or enter key pressed, if the selected item changed since the last time focus was lost or enter was pressed.
@@ -47,6 +51,15 @@ public class FilterableComboBox : ComboBox
public FilterableComboBox()
{
IsDropDownOpenUC = new UserChange<bool>(v => IsDropDownOpen = v);
FilterTimer = new DispatcherTimer(DispatcherPriority.Background, Dispatcher)
{
Interval = TimeSpan.FromMilliseconds(125)
};
FilterTimer.Tick += (_, _) =>
{
FilterTimer.Stop();
RefreshFilter();
};
DropDownOpened += FilteredComboBox_DropDownOpened;
Focusable = true;
@@ -55,7 +68,8 @@ public class FilterableComboBox : ComboBox
StaysOpenOnEdit = true;
IsReadOnly = false;
Loaded += (s, e) => {
Loaded += (s, e) =>
{
if (EditableTextBox != null)
new TextBoxBaseUserChangeTracker(EditableTextBox).UserTextChanged += FilteredComboBox_UserTextChange;
};
@@ -67,25 +81,53 @@ public class FilterableComboBox : ComboBox
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
if (IsDropDownOpen && Keyboard.Modifiers == ModifierKeys.None && e.Key is Key.Up or Key.Down)
{
MoveSelection(e.Key);
e.Handled = true;
return;
}
base.OnPreviewKeyDown(e);
if (e.Key == Key.Down && !IsDropDownOpen) {
if (e.Key == Key.Down && !IsDropDownOpen)
{
ApplyPendingFilter();
IsDropDownOpen = true;
e.Handled = true;
}
else if (e.Key == Key.Escape) {
else if (e.Key == Key.Escape)
{
ClearFilter();
Text = "";
IsDropDownOpen = true;
}
else if (e.Key == Key.Enter || e.Key == Key.Tab) {
else if (e.Key == Key.Enter || e.Key == Key.Tab)
{
ApplyPendingFilter();
CheckSelectedItem();
TriggerSelectedItemChanged();
}
}
private void MoveSelection(Key key)
{
ApplyPendingFilter();
if (Items.Count == 0)
return;
var index = SelectedItem == null ? -1 : Items.IndexOf(SelectedItem);
index = index < 0
? key == Key.Up ? Items.Count - 1 : 0
: Math.Clamp(index + (key == Key.Up ? -1 : 1), 0, Items.Count - 1);
SelectedItem = Items[index];
(ItemContainerGenerator.ContainerFromIndex(index) as ComboBoxItem)?.BringIntoView();
}
protected override void OnPreviewLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnPreviewLostKeyboardFocus(e);
ApplyPendingFilter();
CheckSelectedItem();
if ((e.OldFocus == this || e.OldFocus == EditableTextBox) && e.NewFocus != this && e.NewFocus != EditableTextBox)
TriggerSelectedItemChanged();
@@ -100,7 +142,8 @@ public class FilterableComboBox : ComboBox
private bool shouldTriggerSelectedItemChanged = false;
private void TriggerSelectedItemChanged()
{
if (shouldTriggerSelectedItemChanged) {
if (shouldTriggerSelectedItemChanged)
{
SelectionEffectivelyChanged?.Invoke(this, SelectedItem);
shouldTriggerSelectedItemChanged = false;
}
@@ -108,9 +151,12 @@ public class FilterableComboBox : ComboBox
public void ClearFilter()
{
if (string.IsNullOrEmpty(CurrentFilter)) return;
FilterTimer.Stop();
if (string.IsNullOrEmpty(CurrentFilter))
return;
CurrentFilter = "";
CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
if (ItemsSource != null)
CollectionViewSource.GetDefaultView(ItemsSource).Refresh();
}
private void FilteredComboBox_DropDownOpened(object sender, EventArgs e)
@@ -121,47 +167,64 @@ public class FilterableComboBox : ComboBox
private void FilteredComboBox_UserTextChange(object sender, EventArgs e)
{
if (TextBoxFreezed) return;
if (TextBoxFreezed)
return;
var tb = EditableTextBox;
if (tb.SelectionStart + tb.SelectionLength == tb.Text.Length)
CurrentFilter = tb.Text.Substring(0, tb.SelectionStart).ToLower();
CurrentFilter = tb.Text.Substring(0, tb.SelectionStart);
else
CurrentFilter = tb.Text.ToLower();
RefreshFilter();
CurrentFilter = tb.Text;
FilterTimer.Stop();
FilterTimer.Start();
}
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
{
if (newValue != null) {
FilterTimer.Stop();
if (newValue != null)
{
var view = CollectionViewSource.GetDefaultView(newValue);
view.Filter += FilterItem;
}
if (oldValue != null) {
if (oldValue != null)
{
var view = CollectionViewSource.GetDefaultView(oldValue);
if (view != null) view.Filter -= FilterItem;
if (view != null)
view.Filter -= FilterItem;
}
base.OnItemsSourceChanged(oldValue, newValue);
}
private void ApplyPendingFilter()
{
if (!FilterTimer.IsEnabled)
return;
FilterTimer.Stop();
RefreshFilter();
}
private void RefreshFilter()
{
if (ItemsSource == null) return;
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);
FreezTextBoxState(() =>
{
view.Refresh();
if (!string.IsNullOrEmpty(CurrentFilter) || isDropDownOpen)
if (!IsDropDownOpen && !string.IsNullOrEmpty(CurrentFilter))
IsDropDownOpenUC.Set(true);
if (SelectedItem == null) {
if (SelectedItem == null)
{
foreach (var itm in ItemsSource)
if (itm.ToString() == Text) {
if (itm.ToString() == Text)
{
SelectedItem = itm;
break;
}
@@ -185,10 +248,16 @@ public class FilterableComboBox : ComboBox
private bool FilterItem(object value)
{
if (value == null) return false;
if (CurrentFilter.Length == 0) return true;
if (value == null)
return false;
if (CurrentFilter.Length == 0)
return true;
return value.ToString().ToLower().Contains(CurrentFilter);
var text = value.ToString();
if (text?.StartsWith("GAME_", StringComparison.OrdinalIgnoreCase) == true)
text = text[5..];
return text?.Contains(CurrentFilter, StringComparison.OrdinalIgnoreCase) == true;
}
private class TextBoxBaseUserChangeTracker
@@ -205,11 +274,13 @@ public class FilterableComboBox : ComboBox
TextBoxBase = textBoxBase;
LastText = TextBoxBase.ToString();
textBoxBase.PreviewTextInput += (s, e) => {
textBoxBase.PreviewTextInput += (s, e) =>
{
IsTextInput = true;
};
textBoxBase.TextChanged += (s, e) => {
textBoxBase.TextChanged += (s, e) =>
{
var isUserChange = PressedKeys.Count > 0 || IsTextInput || LastText == TextBoxBase.ToString();
IsTextInput = false;
LastText = TextBoxBase.ToString();
@@ -217,17 +288,21 @@ public class FilterableComboBox : ComboBox
UserTextChanged?.Invoke(this, e);
};
textBoxBase.PreviewKeyDown += (s, e) => {
switch (e.Key) {
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) {
if (e.Key == Key.Back)
{
var textBox = textBoxBase as TextBox;
if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length) {
if (textBox.SelectionStart > 0 && textBox.SelectionLength > 0 && (textBox.SelectionStart + textBox.SelectionLength) == textBox.Text.Length)
{
textBox.SelectionStart--;
textBox.SelectionLength++;
e.Handled = true;
@@ -236,12 +311,14 @@ public class FilterableComboBox : ComboBox
}
};
textBoxBase.PreviewKeyUp += (s, e) => {
textBoxBase.PreviewKeyUp += (s, e) =>
{
if (PressedKeys.Contains(e.Key))
PressedKeys.Remove(e.Key);
};
textBoxBase.LostFocus += (s, e) => {
textBoxBase.LostFocus += (s, e) =>
{
PressedKeys.Clear();
IsTextInput = false;
};
@@ -261,11 +338,13 @@ public class FilterableComboBox : ComboBox
public void Set(T val)
{
try {
try
{
IsUserChange = false;
action(val);
}
finally {
finally
{
IsUserChange = true;
}
}

View File

@@ -12,7 +12,7 @@ public class FileToGeometryConverter : IMultiValueConverter
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length < 2 || values[0] is not EAssetCategory category || values[1] is not string resolvedAssetType)
if (values.Length < 3 || values[0] is not EAssetCategory category || values[1] is not string resolvedAssetType || values[2] is not EAssetFamily family)
return null;
resolvedAssetType = resolvedAssetType.ToLowerInvariant();
@@ -106,6 +106,20 @@ public class FileToGeometryConverter : IMultiValueConverter
_ => ("AssetIcon", "NeutralBrush")
};
(geometry, brush) = (family, category) switch
{
(EAssetFamily.Criware, EAssetCategory.Data) => ("CriwareIcon", "CriwareBrush"),
(EAssetFamily.Criware, _) => ("CriwareIcon", brush),
(EAssetFamily.Wwise, EAssetCategory.Data) => ("WwiseIcon", "WwiseBrush"),
(EAssetFamily.Wwise, _) => ("WwiseIcon", brush),
(EAssetFamily.FMod, EAssetCategory.Data) => ("FModIcon", "FModBrush"),
(EAssetFamily.FMod, _) => ("FModIcon", brush),
_ => (geometry, brush),
};
if (targetType == typeof(Geometry))
return Application.Current.FindResource(geometry) as Geometry;
if (targetType == typeof(Brush))

File diff suppressed because one or more lines are too long

View File

@@ -1682,6 +1682,16 @@
<Style x:Key="UComboBox" TargetType="ComboBox" BasedOn="{StaticResource {x:Type ComboBox}}">
<Setter Property="adonisExtensions:WatermarkExtension.Watermark" Value="UE5 / UE4 / GameName..." />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ComboBox">