diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 14fbb2ab..a56c0d4d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,6 +18,9 @@ jobs: with: submodules: 'true' + - name: Fetch Submodules Recursively + run: git submodule update --init --recursive + - name: .NET 5 Setup uses: actions/setup-dotnet@v1 with: @@ -27,7 +30,7 @@ jobs: run: dotnet restore FModel - name: .NET Publish - run: dotnet publish FModel -c Release -f net5.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=true -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} --no-self-contained -r win-x64 + run: dotnet publish FModel -c Release --no-self-contained -r win-x64 -f net5.0-windows -o "./FModel/bin/Publish/" -p:PublishReadyToRun=false -p:PublishSingleFile=true -p:DebugType=None -p:GenerateDocumentationFile=false -p:DebugSymbols=false -p:AssemblyVersion=${{ github.event.inputs.appVersion }} -p:FileVersion=${{ github.event.inputs.appVersion }} - name: ZIP File uses: papeloto/action-zip@v1 diff --git a/CUE4Parse b/CUE4Parse index a520ada3..6d911668 160000 --- a/CUE4Parse +++ b/CUE4Parse @@ -1 +1 @@ -Subproject commit a520ada39b9736d06db1df3321c7695873b23c44 +Subproject commit 6d911668e730b3f4c1eab0b3f7e9a99bc3dde943 diff --git a/FModel/App.xaml.cs b/FModel/App.xaml.cs index 6d8751ec..7c485483 100644 --- a/FModel/App.xaml.cs +++ b/FModel/App.xaml.cs @@ -97,7 +97,7 @@ namespace FModel if (messageBox.Result == MessageBoxResult.Custom && (EErrorKind) messageBox.ButtonPressed.Id != EErrorKind.Ignore) { if ((EErrorKind) messageBox.ButtonPressed.Id == EErrorKind.ResetSettings) - UserSettings.Delete(); + UserSettings.Default = new UserSettings(); ApplicationService.ApplicationView.Restart(); } diff --git a/FModel/Constants.cs b/FModel/Constants.cs index a33827d5..2c00ab02 100644 --- a/FModel/Constants.cs +++ b/FModel/Constants.cs @@ -14,13 +14,14 @@ namespace FModel public const string GREEN = "#98C379"; public const string YELLOW = "#E5C07B"; public const string BLUE = "#528BCC"; - - public const string DONATE_LINK = "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=EP9SSWG8MW4UC&source=url"; - public const string CHANGELOG_LINK = "https://github.com/iAmAsval/FModel/releases/latest"; - public const string ISSUE_LINK = "https://github.com/iAmAsval/FModel/issues/new"; - public const string DISCORD_LINK = "https://discord.gg/fdkNYYQ"; + + public const string ISSUE_LINK = "https://github.com/iAmAsval/FModel/issues/new/choose"; + public const string DONATE_LINK = "https://fmodel.app/donate"; + public const string DISCORD_LINK = "https://fmodel.app/discord"; public const string _FN_LIVE_TRIGGER = "fortnite-live.manifest"; public const string _VAL_LIVE_TRIGGER = "valorant-live.manifest"; + + public const string _NO_PRESET_TRIGGER = "Hand Made"; } } \ No newline at end of file diff --git a/FModel/Creator/Bases/FN/BaseIconStats.cs b/FModel/Creator/Bases/FN/BaseIconStats.cs index c5e7542e..b1c44508 100644 --- a/FModel/Creator/Bases/FN/BaseIconStats.cs +++ b/FModel/Creator/Bases/FN/BaseIconStats.cs @@ -64,13 +64,13 @@ namespace FModel.Creator.Bases.FN if (Object.TryGetValue(out FStructFallback maxStackSize, "MaxStackSize")) { - if (maxStackSize.TryGetValue(out float v, "Value") && v > -1) + if (maxStackSize.TryGetValue(out float v, "Value") && v > 0) { - _statistics.Add(new IconStat("Max Stack", v)); + _statistics.Add(new IconStat("Max Stack", v , 15)); } else if (TryGetCurveTableStat(maxStackSize, out var s)) { - _statistics.Add(new IconStat("Max Stack", s)); + _statistics.Add(new IconStat("Max Stack", s , 15)); } } @@ -84,9 +84,9 @@ namespace FModel.Creator.Bases.FN weaponStatHandle.TryGetValue(out UDataTable dataTable, "DataTable") && dataTable.TryGetDataTableRow(weaponRowName.Text, StringComparison.OrdinalIgnoreCase, out var weaponRowValue)) { - if (weaponRowValue.TryGetValue(out float dmgPb, "DmgPB") && dmgPb != 0f && weaponRowValue.TryGetValue(out int bpc , "BulletsPerCartridge")&& bpc != 0f) + if (weaponRowValue.TryGetValue(out float dmgPb, "DmgPB") && dmgPb != 0f && weaponRowValue.TryGetValue(out int bpc , "BulletsPerCartridge")) { - _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "BF7E3CF34A9ACFF52E95EAAD4F09F133", "Damage to Player"), dmgPb * bpc, 200)); + _statistics.Add(new IconStat(Utils.GetLocalizedResource("", "BF7E3CF34A9ACFF52E95EAAD4F09F133", "Damage to Player"), dmgPb * (bpc != 0f ? bpc : 1), 200)); } if (weaponRowValue.TryGetValue(out int clipSize, "ClipSize") && clipSize != 0) diff --git a/FModel/Creator/Bases/FN/BaseMaterialInstance.cs b/FModel/Creator/Bases/FN/BaseMaterialInstance.cs index 31f211ae..37cfd489 100644 --- a/FModel/Creator/Bases/FN/BaseMaterialInstance.cs +++ b/FModel/Creator/Bases/FN/BaseMaterialInstance.cs @@ -22,7 +22,7 @@ namespace FModel.Creator.Bases.FN texture_finding: foreach (var textureParameter in material.TextureParameterValues) // get texture from base material { - if (textureParameter.ParameterValue is not UTexture2D texture || Preview != null) continue; + if (!textureParameter.ParameterValue.TryLoad(out var texture) || Preview != null) continue; switch (textureParameter.ParameterInfo.Name.Text) { case "SeriesTexture": diff --git a/FModel/Creator/Bases/FN/BasePlaylist.cs b/FModel/Creator/Bases/FN/BasePlaylist.cs index b33a2761..715316d6 100644 --- a/FModel/Creator/Bases/FN/BasePlaylist.cs +++ b/FModel/Creator/Bases/FN/BasePlaylist.cs @@ -38,7 +38,9 @@ namespace FModel.Creator.Bases.FN !_apiEndpointView.FortniteApi.TryGetBytes(playlist.Data.Images.Showcase, out var image)) return; - Preview = Utils.GetBitmap(image).Resize(1024, 512); // Force size to 1024x512 to prevent huge previews. + Preview = Utils.GetBitmap(image).ResizeWithRatio(1024, 512); + Width = Preview.Width; + Height = Preview.Height; } public override SKImage Draw() diff --git a/FModel/Creator/CreatorPackage.cs b/FModel/Creator/CreatorPackage.cs index 6ac1382b..0145853d 100644 --- a/FModel/Creator/CreatorPackage.cs +++ b/FModel/Creator/CreatorPackage.cs @@ -65,6 +65,7 @@ namespace FModel.Creator case "FortBadgeItemDefinition": case "FortAwardItemDefinition": case "FortGadgetItemDefinition": + case "AthenaCharmItemDefinition": case "FortPlaysetItemDefinition": case "FortGiftBoxItemDefinition": case "FortOutpostItemDefinition": @@ -75,6 +76,7 @@ namespace FModel.Creator case "FortResourceItemDefinition": case "FortBackpackItemDefinition": case "FortEventQuestMapDataAsset": + case "FortWeaponModItemDefinition": case "FortCodeTokenItemDefinition": case "FortSchematicItemDefinition": case "FortWorldMultiItemDefinition": @@ -125,9 +127,9 @@ namespace FModel.Creator return true; case "MaterialInstanceConstant" when _object.Owner != null && - (_object.Owner.Name.EndsWith($"/MI_OfferImages/{_object.Name}") || - _object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}") || - _object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}")): + (_object.Owner.Name.EndsWith($"/MI_OfferImages/{_object.Name}", StringComparison.OrdinalIgnoreCase) || + _object.Owner.Name.EndsWith($"/RenderSwitch_Materials/{_object.Name}", StringComparison.OrdinalIgnoreCase) || + _object.Owner.Name.EndsWith($"/MI_BPTile/{_object.Name}", StringComparison.OrdinalIgnoreCase)): creator = new BaseMaterialInstance(_object, _style); return true; case "FortMtxOfferData": @@ -160,6 +162,7 @@ namespace FModel.Creator case "PlaylistUserOptionColorEnum": case "PlaylistUserOptionFloatEnum": case "PlaylistUserOptionFloatRange": + case "PlaylistUserTintedIconIntEnum": case "PlaylistUserOptionPrimaryAsset": case "PlaylistUserOptionCollisionProfileEnum": creator = new BaseUserControl(_object, _style); @@ -272,4 +275,4 @@ namespace FModel.Creator _object = null; } } -} \ No newline at end of file +} diff --git a/FModel/Creator/Utils.cs b/FModel/Creator/Utils.cs index f451a22d..9a6fd51b 100644 --- a/FModel/Creator/Utils.cs +++ b/FModel/Creator/Utils.cs @@ -85,7 +85,7 @@ namespace FModel.Creator if (material == null) return null; foreach (var textureParameter in material.TextureParameterValues) { - if (textureParameter.ParameterValue is not UTexture2D texture) continue; + if (!textureParameter.ParameterValue.TryLoad(out var texture)) continue; switch (textureParameter.ParameterInfo.Name.Text) { case "MainTex": @@ -107,8 +107,14 @@ namespace FModel.Creator public static SKBitmap GetBitmap(UTexture2D texture) => texture.IsVirtual ? null : SKBitmap.Decode(texture.Decode()?.Encode()); public static SKBitmap GetBitmap(byte[] data) => SKBitmap.Decode(data); + public static SKBitmap ResizeWithRatio(this SKBitmap me, double width, double height) + { + var ratioX = width / me.Width; + var ratioY = height / me.Height; + var ratio = ratioX < ratioY ? ratioX : ratioY; + return me.Resize(Convert.ToInt32(me.Width * ratio), Convert.ToInt32(me.Height * ratio)); + } public static SKBitmap Resize(this SKBitmap me, int size) => me.Resize(size, size); - public static SKBitmap Resize(this SKBitmap me, int width, int height) { var bmp = new SKBitmap(new SKImageInfo(width, height), SKBitmapAllocFlags.ZeroPixels); @@ -116,37 +122,19 @@ namespace FModel.Creator me.ScalePixels(pixmap, SKFilterQuality.Medium); return bmp; } + + public static void ClearToTransparent(this SKBitmap me) { + var colors = me.Pixels; + for (var n = 0; n < colors.Length; n++) { + if (colors[n] != SKColors.Black) continue; + colors[n] = SKColors.Transparent; + } + me.Pixels = colors; + } public static bool TryGetPackageIndexExport(FPackageIndex packageIndex, out T export) where T : UObject { - if (packageIndex.ResolvedObject == null) - { - export = default; - return false; - } - - var outerChain = new List(); - var current = packageIndex.ResolvedObject.Outer; - while (current != null) - { - outerChain.Add(current.Name.Text); - current = current.Outer; - } - - if (outerChain.Count < 1) - { - export = default; - return false; - } - - if (!_applicationView.CUE4Parse.Provider.TryLoadPackage(outerChain[^1], out var pkg)) - { - export = default; - return false; - } - - export = pkg.GetExport(packageIndex.ResolvedObject.Index) as T; - return export != null; + return packageIndex.TryLoad(out export); } // fullpath must be either without any extension or with the export objectname diff --git a/FModel/Extensions/AvalonExtensions.cs b/FModel/Extensions/AvalonExtensions.cs index 486c1f35..55c6449c 100644 --- a/FModel/Extensions/AvalonExtensions.cs +++ b/FModel/Extensions/AvalonExtensions.cs @@ -12,6 +12,7 @@ namespace FModel.Extensions private static readonly IHighlightingDefinition _iniHighlighter = LoadHighlighter("Ini.xshd"); private static readonly IHighlightingDefinition _xmlHighlighter = LoadHighlighter("Xml.xshd"); private static readonly IHighlightingDefinition _cppHighlighter = LoadHighlighter("Cpp.xshd"); + private static readonly IHighlightingDefinition _changelogHighlighter = LoadHighlighter("Changelog.xshd"); [MethodImpl(MethodImplOptions.AggressiveInlining)] private static IHighlightingDefinition LoadHighlighter(string resourceName) @@ -35,6 +36,8 @@ namespace FModel.Extensions case "h": case "cpp": return _cppHighlighter; + case "changelog": + return _changelogHighlighter; case "bat": case "txt": case "po": diff --git a/FModel/Extensions/ClipboardExtensions.cs b/FModel/Extensions/ClipboardExtensions.cs new file mode 100644 index 00000000..557784b2 --- /dev/null +++ b/FModel/Extensions/ClipboardExtensions.cs @@ -0,0 +1,198 @@ +using SkiaSharp; + +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Windows; + +namespace FModel.Extensions +{ + public static class ClipboardExtensions + { + public static void SetImage(byte[] pngBytes, string fileName = null) + { + Clipboard.Clear(); + var data = new DataObject(); + using var pngMs = new MemoryStream(pngBytes); + using var image = Image.FromStream(pngMs); + // As standard bitmap, without transparency support + data.SetData(DataFormats.Bitmap, image, true); + // As PNG. Gimp will prefer this over the other two + data.SetData("PNG", pngMs, false); + // As DIB. This is (wrongly) accepted as ARGB by many applications + using var dibMemStream = new MemoryStream(ConvertToDib(image)); + data.SetData(DataFormats.Dib, dibMemStream, false); + // Optional fileName + if (!string.IsNullOrEmpty(fileName)) + { + var htmlFragment = GenerateHTMLFragment($""); + data.SetData(DataFormats.Html, htmlFragment); + } + // The 'copy=true' argument means the MemoryStreams can be safely disposed after the operation + Clipboard.SetDataObject(data, true); + } + + public static byte[] ConvertToDib(Image image) + { + byte[] bm32bData; + var width = image.Width; + var height = image.Height; + + // Ensure image is 32bppARGB by painting it on a new 32bppARGB image. + using (var bm32b = new Bitmap(width, height, PixelFormat.Format32bppPArgb)) + { + using (var gr = Graphics.FromImage(bm32b)) + { + gr.DrawImage(image, new Rectangle(0, 0, width, height)); + } + // Bitmap format has its lines reversed. + bm32b.RotateFlip(RotateFlipType.Rotate180FlipX); + bm32bData = GetRawBytes(bm32b); + } + + // BITMAPINFOHEADER struct for DIB. + const int hdrSize = 0x28; + var fullImage = new byte[hdrSize + 12 + bm32bData.Length]; + //Int32 biSize; + WriteIntToByteArray(fullImage, 0x00, 4, true, hdrSize); + //Int32 biWidth; + WriteIntToByteArray(fullImage, 0x04, 4, true, (uint)width); + //Int32 biHeight; + WriteIntToByteArray(fullImage, 0x08, 4, true, (uint)height); + //Int16 biPlanes; + WriteIntToByteArray(fullImage, 0x0C, 2, true, 1); + //Int16 biBitCount; + WriteIntToByteArray(fullImage, 0x0E, 2, true, 32); + //BITMAPCOMPRESSION biCompression = BITMAPCOMPRESSION.BITFIELDS; + WriteIntToByteArray(fullImage, 0x10, 4, true, 3); + //Int32 biSizeImage; + WriteIntToByteArray(fullImage, 0x14, 4, true, (uint)bm32bData.Length); + // These are all 0. Since .net clears new arrays, don't bother writing them. + //Int32 biXPelsPerMeter = 0; + //Int32 biYPelsPerMeter = 0; + //Int32 biClrUsed = 0; + //Int32 biClrImportant = 0; + + // The aforementioned "BITFIELDS": colour masks applied to the Int32 pixel value to get the R, G and B values. + WriteIntToByteArray(fullImage, hdrSize + 0, 4, true, 0x00FF0000); + WriteIntToByteArray(fullImage, hdrSize + 4, 4, true, 0x0000FF00); + WriteIntToByteArray(fullImage, hdrSize + 8, 4, true, 0x000000FF); + + Unsafe.CopyBlockUnaligned(ref fullImage[hdrSize + 12], ref bm32bData[0], (uint)bm32bData.Length); + return fullImage; + } + + private static byte[] ConvertToDib(byte[] pngBytes = null) + { + byte[] bm32bData; + int width, height; + + using (var skBmp = SKBitmap.Decode(pngBytes)) + { + width = skBmp.Width; + height = skBmp.Height; + using var rotated = new SKBitmap(new SKImageInfo(width, height, skBmp.ColorType)); + using var canvas = new SKCanvas(rotated); + canvas.Scale(1, -1, 0, height / 2.0f); + canvas.DrawBitmap(skBmp, SKPoint.Empty); + canvas.Flush(); + bm32bData = rotated.Bytes; + } + + // BITMAPINFOHEADER struct for DIB. + const int hdrSize = 0x28; + var fullImage = new byte[hdrSize + 12 + bm32bData.Length]; + //Int32 biSize; + WriteIntToByteArray(fullImage, 0x00, 4, true, hdrSize); + //Int32 biWidth; + WriteIntToByteArray(fullImage, 0x04, 4, true, (uint)width); + //Int32 biHeight; + WriteIntToByteArray(fullImage, 0x08, 4, true, (uint)height); + //Int16 biPlanes; + WriteIntToByteArray(fullImage, 0x0C, 2, true, 1); + //Int16 biBitCount; + WriteIntToByteArray(fullImage, 0x0E, 2, true, 32); + //BITMAPCOMPRESSION biCompression = BITMAPCOMPRESSION.BITFIELDS; + WriteIntToByteArray(fullImage, 0x10, 4, true, 3); + //Int32 biSizeImage; + WriteIntToByteArray(fullImage, 0x14, 4, true, (uint)bm32bData.Length); + // These are all 0. Since .net clears new arrays, don't bother writing them. + //Int32 biXPelsPerMeter = 0; + //Int32 biYPelsPerMeter = 0; + //Int32 biClrUsed = 0; + //Int32 biClrImportant = 0; + + // The aforementioned "BITFIELDS": colour masks applied to the Int32 pixel value to get the R, G and B values. + WriteIntToByteArray(fullImage, hdrSize + 0, 4, true, 0x00FF0000); + WriteIntToByteArray(fullImage, hdrSize + 4, 4, true, 0x0000FF00); + WriteIntToByteArray(fullImage, hdrSize + 8, 4, true, 0x000000FF); + + Unsafe.CopyBlockUnaligned(ref fullImage[hdrSize + 12], ref bm32bData[0], (uint)bm32bData.Length); + return fullImage; + } + + public static unsafe byte[] GetRawBytes(Bitmap bmp) + { + var rect = new Rectangle(0, 0, bmp.Width, bmp.Height); + var bmpData = bmp.LockBits(rect, ImageLockMode.ReadOnly, bmp.PixelFormat); + var bytes = (uint)(Math.Abs(bmpData.Stride) * bmp.Height); + var buffer = new byte[bytes]; + fixed (byte* pBuffer = buffer) + { + Unsafe.CopyBlockUnaligned(pBuffer, bmpData.Scan0.ToPointer(), bytes); + } + bmp.UnlockBits(bmpData); + return buffer; + } + + private static void WriteIntToByteArray(byte[] data, int startIndex, int bytes, bool littleEndian, uint value) + { + var lastByte = bytes - 1; + + if (data.Length < startIndex + bytes) + { + throw new ArgumentOutOfRangeException(nameof(startIndex), "Data array is too small to write a " + bytes + "-byte value at offset " + startIndex + "."); + } + + for (var index = 0; index < bytes; index++) + { + var offs = startIndex + (littleEndian ? index : lastByte - index); + data[offs] = (byte)(value >> 8 * index & 0xFF); + } + } + + private static string GenerateHTMLFragment(string html) + { + var sb = new StringBuilder(); + + const string header = "Version:0.9\r\nStartHTML:<<<<<<<<<1\r\nEndHTML:<<<<<<<<<2\r\nStartFragment:<<<<<<<<<3\r\nEndFragment:<<<<<<<<<4\r\n"; + const string startHTML = "\r\n\r\n"; + const string startFragment = ""; + const string endFragment = ""; + const string endHTML = "\r\n\r\n"; + + sb.Append(header); + + var startHTMLLength = header.Length; + var startFragmentLength = startHTMLLength + startHTML.Length + startFragment.Length; + var endFragmentLength = startFragmentLength + Encoding.UTF8.GetByteCount(html); + var endHTMLLength = endFragmentLength + endFragment.Length + endHTML.Length; + + sb.Replace("<<<<<<<<<1", startHTMLLength.ToString("D10")); + sb.Replace("<<<<<<<<<2", endHTMLLength.ToString("D10")); + sb.Replace("<<<<<<<<<3", startFragmentLength.ToString("D10")); + sb.Replace("<<<<<<<<<4", endFragmentLength.ToString("D10")); + + sb.Append(startHTML); + sb.Append(startFragment); + sb.Append(html); + sb.Append(endFragment); + sb.Append(endHTML); + + return sb.ToString(); + } + } +} \ No newline at end of file diff --git a/FModel/FModel.csproj b/FModel/FModel.csproj index 01cc0e98..a081481e 100644 --- a/FModel/FModel.csproj +++ b/FModel/FModel.csproj @@ -6,8 +6,8 @@ true FModel.ico 4.0.0 - 4.0.1.0 - 4.0.1.0 + 4.0.1.1 + 4.0.1.1 false true win-x64 @@ -39,6 +39,7 @@ + @@ -70,6 +71,7 @@ + @@ -96,24 +98,28 @@ + - - + + - - - - + + + + + + - + - - + + + @@ -131,6 +137,7 @@ + diff --git a/FModel/MainWindow.xaml b/FModel/MainWindow.xaml index 9a0ef621..c926fc3c 100644 --- a/FModel/MainWindow.xaml +++ b/FModel/MainWindow.xaml @@ -15,7 +15,7 @@ - + @@ -85,7 +85,13 @@ - + + + + + + + @@ -94,7 +100,13 @@ - + + + + + + + @@ -103,7 +115,13 @@ - + + + + + + + @@ -135,6 +153,9 @@ + @@ -194,7 +215,7 @@ - + @@ -304,99 +325,143 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + @@ -451,7 +522,13 @@ - + + + + + + + @@ -460,7 +537,13 @@ - + + + + + + + @@ -469,7 +552,13 @@ - + + + + + + + @@ -772,6 +861,19 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/FModel/Views/ModelViewer.xaml.cs b/FModel/Views/ModelViewer.xaml.cs new file mode 100644 index 00000000..d7810b67 --- /dev/null +++ b/FModel/Views/ModelViewer.xaml.cs @@ -0,0 +1,31 @@ +using System.Windows.Input; +using CUE4Parse.UE4.Assets.Exports; +using FModel.Services; +using FModel.Settings; +using FModel.ViewModels; + +namespace FModel.Views +{ + public partial class ModelViewer + { + private ApplicationViewModel _applicationView => ApplicationService.ApplicationView; + + public ModelViewer() + { + DataContext = _applicationView; + InitializeComponent(); + } + + public void Load(UObject export) => _applicationView.ModelViewer.LoadExport(export); + + private void OnWindowKeyDown(object sender, KeyEventArgs e) + { + if (UserSettings.Default.DirLeftTab.IsTriggered(e.Key)) + _applicationView.ModelViewer.PreviousLod(); + else if (UserSettings.Default.DirRightTab.IsTriggered(e.Key)) + _applicationView.ModelViewer.NextLod(); + else if (e.Key == Key.W) + _applicationView.ModelViewer.ShowWireframe = !_applicationView.ModelViewer.ShowWireframe; + } + } +} \ No newline at end of file diff --git a/FModel/Views/Resources/Controls/Aed/BraceFoldingStrategy.cs b/FModel/Views/Resources/Controls/Aed/BraceFoldingStrategy.cs index 127090cb..68f5b10e 100644 --- a/FModel/Views/Resources/Controls/Aed/BraceFoldingStrategy.cs +++ b/FModel/Views/Resources/Controls/Aed/BraceFoldingStrategy.cs @@ -47,10 +47,18 @@ namespace FModel.Views.Resources.Controls if (_foldingManager.AllFoldings == null) return; + var dowhat = -1; + var foldunfold = false; foreach (var folding in _foldingManager.AllFoldings) { - if (folding.Tag is not CustomNewFolding realFolding) continue; - if (realFolding.Level == level) folding.IsFolded = !folding.IsFolded; + if (folding.Tag is not CustomNewFolding realFolding || realFolding.Level != level) continue; + + if (dowhat < 0) // determine if we fold or unfold based on the first one + { + dowhat = 1; + foldunfold = !folding.IsFolded; + } + folding.IsFolded = foldunfold; } } } diff --git a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs index 6887e68d..e90ffc22 100644 --- a/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs +++ b/FModel/Views/Resources/Controls/AvalonEditor.xaml.cs @@ -39,9 +39,16 @@ namespace FModel.Views.Resources.Controls case Key.Escape: ((TabItem) DataContext).HasSearchOpen = false; break; - case Key.Enter when ((TabItem) DataContext).HasSearchOpen: + case Key.Enter when !Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) && ((TabItem) DataContext).HasSearchOpen: FindNext(); break; + case Key.Enter when Keyboard.Modifiers.HasFlag(ModifierKeys.Shift) && ((TabItem) DataContext).HasSearchOpen: + var dc = (TabItem)DataContext; + var old = dc.SearchUp; + dc.SearchUp = true; + FindNext(); + dc.SearchUp = old; + break; } } diff --git a/FModel/Views/Resources/Controls/DictionaryEditor.xaml b/FModel/Views/Resources/Controls/DictionaryEditor.xaml new file mode 100644 index 00000000..8e12a075 --- /dev/null +++ b/FModel/Views/Resources/Controls/DictionaryEditor.xaml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +