mirror of
https://github.com/4sval/FModel.git
synced 2026-09-25 10:39:14 -05:00
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
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:
Submodule CUE4Parse updated: 89ef88f6a3...33529e8f7e
@@ -171,6 +171,14 @@ public enum EAssetCategory : uint
|
||||
GothamKnights = GameSpecific + 7,
|
||||
}
|
||||
|
||||
public enum EAssetFamily
|
||||
{
|
||||
None,
|
||||
Criware,
|
||||
Wwise,
|
||||
FMod,
|
||||
}
|
||||
|
||||
public enum EUnluacMode
|
||||
{
|
||||
Decompile,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user