fix: address remaining WPF→Avalonia migration gaps (issue #82) (#83)

* fix: address remaining WPF→Avalonia migration gaps (issue #82)

Critical fixes:
- C1: RatioToGridLengthConverter — add missing using Avalonia.Controls
- C2/C3: ApplicationViewModel — remove AdonisUI MessageBox, async ShowDialog,
  IClassicDesktopStyleApplicationLifetime.Shutdown()
- C4: ImageCommand — Avalonia Window, BitmapInterpolationMode, PixelSize
- C5: TabCommand/CopyCommand — Avalonia async clipboard with error logging
- C6: ImGuiController — DPI via screen.PixelDensity
- C7: ClipboardExtensions — Avalonia DataObject with PNG bytes, InvokeAsync

Major fixes:
- M1: Timeclock — full StyledProperty rewrite, UI-thread-safe event handlers
- M2: DictionaryEditor/EndpointEditor — Avalonia Window + AvaloniaEdit
- M3: CommitDownloaderControl — Avalonia UserControl + StyledProperty
- M4: FileButton2/FolderButton2/FolderButton3 — Avalonia XAML, restore
  NumTextures badge and colored separator in FileButton2
- M5: Breadcrumb — Avalonia XAML + pointer events
- M6: CUE4ParseViewModel — Helper.CloseWindow<Window>
- M7: App.xaml.cs — e.RequestCatch replacing e.Handled
- M8: FileContextMenu — Avalonia namespace + IsVisible

Minor/other fixes:
- Enable <Nullable> in csproj
- OnTagDataTemplateSelector stub + SettingsView code-behind template selection
- TiledExplorer/Resources.xaml — full Avalonia rewrite (style selectors,
  WrapPanel, IDataTemplate, attached behaviors, converter-based empty state)
- ListBoxItemBehavior — Avalonia AttachedProperty with 3 behaviors
- TypeDataTemplateSelector — IDataTemplate with FolderContextMenu attachment
- SmoothScroll — Avalonia AttachedProperty + PointerWheelChanged
- Remove dead code-behind from Resources.xaml.cs
- Remove unsupported IsAsync=True from bindings
- Remove duplicate CornerRadius property from Timeclock
- New converters: AssetExtensionToIconConverter, IntGreaterThanZeroConverter,
  IsNullToBoolConverter, ItemsSourceEmptyToBoolConverter
- FolderToGeometryConverter — add bool return for IsVisible bindings

Closes #82

* fix: address PR #83 review comments

- RestartWithWarning: restore user-visible Avalonia dialog before restart
  (matches original WPF MessageBox.Show behavior)
- UpdateProvider: add null guards for nullable AesManager/CUE4Parse
- FolderContextMenu: add .ContinueWith error logging on clipboard write
- SettingsView: replace throwing FindResource with TryFindResource
- SmoothScroll: cache ScrollViewer in ConditionalWeakTable to avoid
  per-event visual tree walks
- ListBoxItemBehavior: replace throwing FindResource with TryFindResource;
  move e.Handled inside success block so it's only set when menu opens

* fix: address correctness review and second round PR comments

- C1: Fix FolderButton3 LinearGradientBrush StartPoint/EndPoint to use
  relative percentage format (0%,0%/0%,100%) instead of absolute pixels
- M1: TypeDataTemplateSelector resolves FolderContextMenu per-control via
  AttachedToVisualTree + TryFindResource instead of shared Application lookup
- M2: Replace 19 unresolvable SystemColors.ControlTextBrushKey references
  with #DAE5F2 in FileContextMenu.xaml and FolderContextMenu.xaml
- M4: RestartWithWarningAsync non-modal fallback awaits dialog.Closed via
  TaskCompletionSource before calling Restart()
- mn1: EndpointEditor adds _isInitialized flag to suppress TextChanged
  during construction (restores WPF IsLoaded guard behavior)
- S1: Timeclock unsubscribes from Source events in OnDetachedFromVisualTree

Second round PR comments:
- Fix SmoothScroll ConditionalWeakTable.AddOrUpdate → Add (API doesn't exist)
- AvoidEmptyGameDirectoryAsync falls back to MainWindow when owner is null
- Remove unused fileName parameter from ClipboardExtensions.SetImage

* Address PR review findings and reviewer comments

Review findings addressed:
- [M1] Wire 5 Settings dialog handlers (OpenCustomVersions, OpenOptions,
  OpenMapStructTypes, OpenAesEndpoint, OpenMappingEndpoint) to actual
  DictionaryEditor/EndpointEditor modal calls instead of no-op stubs
- [M2] Add TODO(P3-perf) comment documenting WrapPanel virtualization tradeoff
- [m1] Add template fallback in SettingsView to clear ContentTemplate on
  failed resource lookup
- [m2] Improve clipboard image format parity by adding PNG and Bitmap formats
- [S1] Replace hardcoded #DAE5F2 icon fills with DynamicResource
  SystemColors.ControlTextBrushKey in both context menus
- [S2] Simplify TypeDataTemplateSelector by removing redundant global guard

PR reviewer comments addressed:
- EndpointEditor: Fix OnTextChanged signature (EventArgs -> TextChangedEventArgs)
- FolderContextMenu: Fix FindAncestor to use GetVisualAncestors() instead of
  unreliable Parent cast chain
- CommitDownloaderControl: Fix button Width binding (.Bounds.Height ->
  .Height) to avoid zero during initial layout
- ApplicationViewModel: Make RestartWithWarningAsync okButton a direct reference
  instead of brittle Children[1] index cast

* Address review findings and PR comments (round 2)

Major fixes:
- M1: ImGuiController now probes Linux font directories (DejaVu Sans,
  Liberation Sans, Noto Sans) instead of hardcoding Windows paths
- M2: ClipboardExtensions.SetImage wraps MemoryStream/Bitmap in using
  statements to prevent native resource leaks
- M3: ImageCommand divides PixelSize by DPI scale for correct HiDPI
  window sizing

Minor fixes:
- m1: Document Timeclock threading contract for CalculateTime
- m2: CommitDownloaderControl uses fixed 32x32 button size instead of
  Bounds-based binding that yields 0 during initial layout
- m3: Resources.xaml reformatted to consistent indentation
- m4: FolderToGeometryConverter adds parentheses for operator precedence

Suggestions applied:
- S1: Extract ClipboardExtensions.SetText helper; update CopyCommand,
  TabCommand, and FolderContextMenu to use it consistently

PR comments addressed:
- FileContextMenu.xaml: Replace fragile $parent[Window].DataContext with
  Opened handler that resolves DataContext from PlacementTarget visual tree
  (new FileContextMenu.xaml.cs code-behind)
- RestartWithWarningAsync: Accept optional Window owner parameter so
  callers (SettingsView) can pass their window for proper modal parenting
- ClipboardExtensions bitmap leak: Fixed (same as M2)

No new build errors introduced (78 pre-existing in unmigrated files).

* fix: address second review findings (M1, M2, m1, m2, S1) and TitleExtra null guard

- Remove [AggressiveInlining] from ResolveFontPaths and cache result
  in static Lazy<> (M1 + S1)
- Remove using disposal on Bitmap/MemoryStream in SetImage to prevent
  ObjectDisposedException on X11 deferred clipboard reads (M2)
- CommitDownloader button now binds Height to sibling grid, restoring
  parent-relative sizing with MinWidth/MinHeight fallback (m1)
- Add Log.Warning when FileContextMenu cannot find Window ancestor (m2)
- Guard TitleExtra against null CurrentDir (unresolved PR comment)

* fix: address third review findings and 3 unresolved PR comments

- ClipboardExtensions.SetImage: dispose MemoryStream after Bitmap
  decoding (Bitmap copies pixel data during construction); keep Bitmap
  alive for X11/Wayland deferred clipboard rendering
- MenuCommand: add null guards for CUE4Parse before dereferencing in
  Directory_Backup, Directory_ArchivesInfo, Views_3dViewer, and
  ToolBox_Collapse_All cases
- CommitDownloaderControl: revert to deterministic Width=32 Height=32
  to avoid Bounds.Height instability during initial layout

* fix: breadcrumb button filter, one-shot context menu, nullable guards

- Breadcrumb: filter PointerReleased by left button only (M2)
- TypeDataTemplateSelector: one-shot AttachedToVisualTree handler (m1)
- DictionaryEditor: initialize properties to empty defaults, null-coalesce
  DeserializeObject results (PR comment)
- Resources.xaml: document SelectionMode mapping (S1)

* fix: CurrentDir NRE guard, discard ExtractAsync, clean up null checks

- ApplicationViewModel: null-safe CurrentDir?.Equals() in AvoidEmptyGameDirectoryAsync (M1)
- ListBoxItemBehavior: explicit discard on fire-and-forget ExtractAsync (S1/PR)
- SettingsView: remove redundant null checks on DictionaryEditor properties (m1)

* fix: add Cancel click handler to DictionaryEditor

Avalonia's IsCancel="True" maps Escape to the button's Click event but
does not auto-close the window like WPF does. Without a handler, the
Cancel button and Escape key left the dialog stuck.
This commit is contained in:
Rob Trame 2026-03-16 14:17:00 -06:00 committed by GitHub
parent c8a8b79fd9
commit c5bc12e618
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
45 changed files with 1772 additions and 1364 deletions

View File

@ -146,7 +146,7 @@ public partial class App : Application
Dispatcher.UIThread.UnhandledExceptionFilter += (_, e) =>
{
Log.Error("{Exception}", e.Exception);
e.Handled = true;
e.RequestCatch = true;
ShowErrorDialog(e.Exception);
};

View File

@ -1,198 +1,68 @@
using SkiaSharp;
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
using System.Windows;
using Avalonia.Input;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using FModel.Views;
using Serilog;
namespace FModel.Extensions;
public static class ClipboardExtensions
{
public static void SetImage(byte[] pngBytes, string fileName = null)
/// <summary>
/// Copies text to the system clipboard. Fire-and-forget; runs on the UI thread.
/// </summary>
public static void SetText(string text)
{
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))
_ = Dispatcher.UIThread.InvokeAsync(async () =>
{
var htmlFragment = GenerateHTMLFragment($"<img src=\"{fileName}\"/>");
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))
try
{
gr.DrawImage(image, new Rectangle(0, 0, width, height));
var clipboard = MainWindow.YesWeCats?.Clipboard;
if (clipboard == null)
return;
await clipboard.SetTextAsync(text);
}
// 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;
catch (Exception ex)
{
Log.Error(ex, "Failed to copy text to clipboard");
}
});
}
private static byte[] ConvertToDib(byte[] pngBytes = null)
/// <summary>
/// Copies PNG image bytes to the system clipboard. Fire-and-forget; runs on the UI thread.
/// </summary>
public static void SetImage(byte[] pngBytes)
{
byte[] bm32bData;
int width, height;
using (var skBmp = SKBitmap.Decode(pngBytes))
_ = Dispatcher.UIThread.InvokeAsync(async () =>
{
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;
}
try
{
var clipboard = MainWindow.YesWeCats?.Clipboard;
if (clipboard == null)
return;
// 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;
var dataObject = new DataObject();
// Keep both MIME and generic bitmap formats for better cross-app paste compatibility.
dataObject.Set("image/png", pngBytes);
dataObject.Set("PNG", pngBytes);
// The MemoryStream can be disposed after decoding — Bitmap copies
// the pixel data during construction. The Bitmap itself must stay
// alive because on X11/Wayland the clipboard uses deferred rendering
// and the DataObject may be read when another app pastes.
Bitmap bitmap;
using (var ms = new MemoryStream(pngBytes))
bitmap = new Bitmap(ms);
dataObject.Set(DataFormats.Bitmap, bitmap);
await clipboard.SetDataObjectAsync(dataObject);
}
catch (Exception ex)
{
Log.Error(ex, "Failed to copy image to clipboard");
}
});
}
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 = "<html>\r\n<body>\r\n";
const string startFragment = "<!--StartFragment-->";
const string endFragment = "<!--EndFragment-->";
const string endHTML = "\r\n</body>\r\n</html>";
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();
}
}
}

View File

@ -3,6 +3,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationIcon>FModel.ico</ApplicationIcon>
<Version>4.4.4.0</Version>
<AssemblyVersion>4.4.4.0</AssemblyVersion>

View File

@ -4,8 +4,6 @@ using System.Diagnostics;
using System.IO;
using System.Numerics;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Forms;
using FModel.Settings;
using ImGuiNET;
using ImGuizmoNET;
@ -64,19 +62,18 @@ public class ImGuiController : IDisposable
unsafe
{
var iniFileNamePtr = Marshal.StringToCoTaskMemUTF8(Path.Combine(UserSettings.Default.OutputDirectory, ".data", "imgui.ini"));
io.NativePtr->IniFilename = (byte*)iniFileNamePtr;
io.NativePtr->IniFilename = (byte*) iniFileNamePtr;
}
// If not found, Fallback to default ImGui Font
var normalPath = @"C:\Windows\Fonts\segoeui.ttf";
var boldPath = @"C:\Windows\Fonts\segoeuib.ttf";
var semiBoldPath = @"C:\Windows\Fonts\seguisb.ttf";
if (File.Exists(normalPath))
// Platform-specific font probing: prefer Segoe UI on Windows,
// fall back to common Linux fonts (DejaVu Sans, Liberation Sans).
var (normalPath, boldPath, semiBoldPath) = FontPaths.Value;
if (normalPath != null && File.Exists(normalPath))
FontNormal = io.Fonts.AddFontFromFileTTF(normalPath, 16 * DpiScale);
if (File.Exists(boldPath))
if (boldPath != null && File.Exists(boldPath))
FontBold = io.Fonts.AddFontFromFileTTF(boldPath, 16 * DpiScale);
if (File.Exists(semiBoldPath))
if (semiBoldPath != null && File.Exists(semiBoldPath))
FontSemiBold = io.Fonts.AddFontFromFileTTF(semiBoldPath, 16 * DpiScale);
io.Fonts.AddFontDefault();
@ -192,7 +189,7 @@ void main()
ImGuiIOPtr io = ImGui.GetIO();
io.Fonts.GetTexDataAsRGBA32(out IntPtr pixels, out int width, out int height, out int bytesPerPixel);
int mips = (int)Math.Floor(Math.Log(Math.Max(width, height), 2));
int mips = (int) Math.Floor(Math.Log(Math.Max(width, height), 2));
int prevActiveTexture = GL.GetInteger(GetPName.ActiveTexture);
GL.ActiveTexture(TextureUnit.Texture0);
@ -207,19 +204,19 @@ void main()
GL.GenerateMipmap(GenerateMipmapTarget.Texture2D);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int) TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int) TextureWrapMode.Repeat);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMaxLevel, mips - 1);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int) TextureMagFilter.Linear);
GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int) TextureMinFilter.Linear);
// Restore state
GL.BindTexture(TextureTarget.Texture2D, prevTexture2D);
GL.ActiveTexture((TextureUnit)prevActiveTexture);
GL.ActiveTexture((TextureUnit) prevActiveTexture);
io.Fonts.SetTexID((IntPtr)_fontTexture);
io.Fonts.SetTexID((IntPtr) _fontTexture);
io.Fonts.ClearTexData();
}
@ -288,7 +285,8 @@ void main()
foreach (Keys key in Enum.GetValues<Keys>())
{
if (key == Keys.Unknown) continue;
if (key == Keys.Unknown)
continue;
io.AddKeyEvent(TranslateKey(key), kState.IsKeyDown(key));
}
@ -353,7 +351,7 @@ void main()
int vertexSize = cmd_list.VtxBuffer.Size * Marshal.SizeOf<ImDrawVert>();
if (vertexSize > _vertexBufferSize)
{
int newSize = (int)Math.Max(_vertexBufferSize * 1.5f, vertexSize);
int newSize = (int) Math.Max(_vertexBufferSize * 1.5f, vertexSize);
GL.BufferData(BufferTarget.ArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
_vertexBufferSize = newSize;
@ -362,7 +360,7 @@ void main()
int indexSize = cmd_list.IdxBuffer.Size * sizeof(ushort);
if (indexSize > _indexBufferSize)
{
int newSize = (int)Math.Max(_indexBufferSize * 1.5f, indexSize);
int newSize = (int) Math.Max(_indexBufferSize * 1.5f, indexSize);
GL.BufferData(BufferTarget.ElementArrayBuffer, newSize, IntPtr.Zero, BufferUsageHint.DynamicDraw);
_indexBufferSize = newSize;
}
@ -416,21 +414,21 @@ void main()
else
{
GL.ActiveTexture(TextureUnit.Texture0);
GL.BindTexture(TextureTarget.Texture2D, (int)pcmd.TextureId);
GL.BindTexture(TextureTarget.Texture2D, (int) pcmd.TextureId);
CheckGLError("Texture");
// We do _windowHeight - (int)clip.W instead of (int)clip.Y because gl has flipped Y when it comes to these coordinates
var clip = pcmd.ClipRect;
GL.Scissor((int)clip.X, _windowHeight - (int)clip.W, (int)(clip.Z - clip.X), (int)(clip.W - clip.Y));
GL.Scissor((int) clip.X, _windowHeight - (int) clip.W, (int) (clip.Z - clip.X), (int) (clip.W - clip.Y));
CheckGLError("Scissor");
if ((io.BackendFlags & ImGuiBackendFlags.RendererHasVtxOffset) != 0)
{
GL.DrawElementsBaseVertex(PrimitiveType.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (IntPtr)(pcmd.IdxOffset * sizeof(ushort)), unchecked((int)pcmd.VtxOffset));
GL.DrawElementsBaseVertex(PrimitiveType.Triangles, (int) pcmd.ElemCount, DrawElementsType.UnsignedShort, (IntPtr) (pcmd.IdxOffset * sizeof(ushort)), unchecked((int) pcmd.VtxOffset));
}
else
{
GL.DrawElements(BeginMode.Triangles, (int)pcmd.ElemCount, DrawElementsType.UnsignedShort, (int)pcmd.IdxOffset * sizeof(ushort));
GL.DrawElements(BeginMode.Triangles, (int) pcmd.ElemCount, DrawElementsType.UnsignedShort, (int) pcmd.IdxOffset * sizeof(ushort));
}
CheckGLError("Draw");
}
@ -442,21 +440,33 @@ void main()
// Reset state
GL.BindTexture(TextureTarget.Texture2D, prevTexture2D);
GL.ActiveTexture((TextureUnit)prevActiveTexture);
GL.ActiveTexture((TextureUnit) prevActiveTexture);
GL.UseProgram(prevProgram);
GL.BindVertexArray(prevVAO);
GL.Scissor(prevScissorBox[0], prevScissorBox[1], prevScissorBox[2], prevScissorBox[3]);
GL.BindBuffer(BufferTarget.ArrayBuffer, prevArrayBuffer);
GL.BlendEquationSeparate((BlendEquationMode)prevBlendEquationRgb, (BlendEquationMode)prevBlendEquationAlpha);
GL.BlendEquationSeparate((BlendEquationMode) prevBlendEquationRgb, (BlendEquationMode) prevBlendEquationAlpha);
GL.BlendFuncSeparate(
(BlendingFactorSrc)prevBlendFuncSrcRgb,
(BlendingFactorDest)prevBlendFuncDstRgb,
(BlendingFactorSrc)prevBlendFuncSrcAlpha,
(BlendingFactorDest)prevBlendFuncDstAlpha);
if (prevBlendEnabled) GL.Enable(EnableCap.Blend); else GL.Disable(EnableCap.Blend);
if (prevDepthTestEnabled) GL.Enable(EnableCap.DepthTest); else GL.Disable(EnableCap.DepthTest);
if (prevCullFaceEnabled) GL.Enable(EnableCap.CullFace); else GL.Disable(EnableCap.CullFace);
if (prevScissorTestEnabled) GL.Enable(EnableCap.ScissorTest); else GL.Disable(EnableCap.ScissorTest);
(BlendingFactorSrc) prevBlendFuncSrcRgb,
(BlendingFactorDest) prevBlendFuncDstRgb,
(BlendingFactorSrc) prevBlendFuncSrcAlpha,
(BlendingFactorDest) prevBlendFuncDstAlpha);
if (prevBlendEnabled)
GL.Enable(EnableCap.Blend);
else
GL.Disable(EnableCap.Blend);
if (prevDepthTestEnabled)
GL.Enable(EnableCap.DepthTest);
else
GL.Disable(EnableCap.DepthTest);
if (prevCullFaceEnabled)
GL.Enable(EnableCap.CullFace);
else
GL.Disable(EnableCap.CullFace);
if (prevScissorTestEnabled)
GL.Enable(EnableCap.ScissorTest);
else
GL.Disable(EnableCap.ScissorTest);
}
/// <summary>
@ -484,7 +494,8 @@ void main()
for (int i = 0; i < n; i++)
{
string extension = GL.GetString(StringNameIndexed.Extensions, i);
if (extension == name) return true;
if (extension == name)
return true;
}
return false;
@ -549,7 +560,87 @@ void main()
public static float GetDpiScale()
{
return Math.Max((float)(Screen.PrimaryScreen.Bounds.Width / SystemParameters.PrimaryScreenWidth), (float)(Screen.PrimaryScreen.Bounds.Height / SystemParameters.PrimaryScreenHeight));
// Avalonia: use the primary screen's pixel density as the DPI scale factor.
// Screens.Primary is available on the UI thread once the platform is initialised.
var screen = Avalonia.Application.Current?.ApplicationLifetime is
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktop
? desktop.MainWindow?.Screens?.Primary : null;
return screen is not null ? (float) screen.PixelDensity : 1.0f;
}
/// <summary>
/// Lazily-resolved platform font paths. Computed once and cached for the process lifetime.
/// </summary>
private static readonly Lazy<(string? normal, string? bold, string? semiBold)> FontPaths = new(ResolveFontPaths);
/// <summary>
/// Resolves platform-appropriate font file paths.
/// Returns (normal, bold, semiBold) — any element may be null if not found.
/// </summary>
private static (string? normal, string? bold, string? semiBold) ResolveFontPaths()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return (
@"C:\Windows\Fonts\segoeui.ttf",
@"C:\Windows\Fonts\segoeuib.ttf",
@"C:\Windows\Fonts\seguisb.ttf"
);
}
// Linux / macOS: probe common directories for DejaVu Sans, Liberation Sans, or Noto Sans.
string[] searchDirs =
[
"/usr/share/fonts",
"/usr/local/share/fonts",
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".local/share/fonts")
];
// Preferred font families in order. Each entry: (normal, bold, semibold-or-bold-fallback).
(string normal, string bold, string semiBold)[] fontFamilies =
[
("DejaVuSans.ttf", "DejaVuSans-Bold.ttf", "DejaVuSans-Bold.ttf"),
("LiberationSans-Regular.ttf", "LiberationSans-Bold.ttf", "LiberationSans-Bold.ttf"),
("NotoSans-Regular.ttf", "NotoSans-Bold.ttf", "NotoSans-SemiBold.ttf"),
];
foreach (var family in fontFamilies)
{
foreach (var dir in searchDirs)
{
if (!Directory.Exists(dir))
continue;
var normalPath = FindFont(dir, family.normal);
if (normalPath == null)
continue;
var boldPath = FindFont(dir, family.bold);
var semiBoldPath = FindFont(dir, family.semiBold) ?? boldPath;
return (normalPath, boldPath, semiBoldPath);
}
}
return (null, null, null);
}
private static string? FindFont(string directory, string fileName)
{
// Direct match in the directory.
var path = Path.Combine(directory, fileName);
if (File.Exists(path))
return path;
// Search subdirectories (fonts are often in type-specific subfolders).
try
{
var files = Directory.GetFiles(directory, fileName, SearchOption.AllDirectories);
return files.Length > 0 ? files[0] : null;
}
catch
{
return null;
}
}
public static ImGuiKey TranslateKey(Keys key)

View File

@ -143,6 +143,9 @@ public partial class MainWindow : Window
Height = screen.WorkingArea.Height * 0.95 / dpi;
}
// First-run: may show the DirectorySelector dialog before proceeding.
await _applicationView.EnsureInitializedAsync(this);
UpdateStatusBarColor();
var newOrUpdated = UserSettings.Default.ShowChangelog;

View File

@ -5,7 +5,8 @@ using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using CUE4Parse_Conversion.Textures.BC;
using CUE4Parse.Compression;
using CUE4Parse.Encryption.Aes;
@ -18,9 +19,7 @@ using FModel.Settings;
using FModel.ViewModels.Commands;
using FModel.Views;
using FModel.Views.Resources.Controls;
using MessageBox = AdonisUI.Controls.MessageBox;
using MessageBoxButton = AdonisUI.Controls.MessageBoxButton;
using MessageBoxImage = AdonisUI.Controls.MessageBoxImage;
using Serilog;
namespace FModel.ViewModels;
@ -65,7 +64,8 @@ public class ApplicationViewModel : ViewModel
get => _selectedLeftTabIndex;
set
{
if (value is < 0 or > 2) return;
if (value is < 0 or > 2)
return;
SetProperty(ref _selectedLeftTabIndex, value);
}
}
@ -78,15 +78,17 @@ public class ApplicationViewModel : ViewModel
private CopyCommand _copyCommand;
public string InitialWindowTitle => $"FModel ({Constants.APP_SHORT_COMMIT_ID} - {Constants.APP_BUILD_DATE:MMM d, yyyy})";
public string GameDisplayName => CUE4Parse.Provider.GameDisplayName ?? "Unknown";
public string TitleExtra => $"({UserSettings.Default.CurrentDir.UeVersion}){(Build != EBuildKind.Release ? $" ({Build})" : "")}";
public string GameDisplayName => CUE4Parse?.Provider.GameDisplayName ?? "Unknown";
public string TitleExtra => UserSettings.Default.CurrentDir is { } dir
? $"({dir.UeVersion}){(Build != EBuildKind.Release ? $" ({Build})" : "")}"
: Build != EBuildKind.Release ? $"({Build})" : string.Empty;
public LoadingModesViewModel LoadingModes { get; }
public CustomDirectoriesViewModel CustomDirectories { get; }
public CUE4ParseViewModel CUE4Parse { get; }
public SettingsViewModel SettingsView { get; }
public AesManagerViewModel AesManager { get; }
public AudioPlayerViewModel AudioPlayer { get; }
public CustomDirectoriesViewModel? CustomDirectories { get; private set; }
public CUE4ParseViewModel? CUE4Parse { get; private set; }
public SettingsViewModel? SettingsView { get; private set; }
public AesManagerViewModel? AesManager { get; private set; }
public AudioPlayerViewModel? AudioPlayer { get; private set; }
public ApplicationViewModel()
{
@ -100,30 +102,60 @@ public class ApplicationViewModel : ViewModel
#endif
LoadingModes = new LoadingModesViewModel();
UserSettings.Default.CurrentDir = AvoidEmptyGameDirectory(false);
if (UserSettings.Default.CurrentDir is null)
// For existing installations, use the cached directory settings immediately.
// For first-run (no settings), initialization is deferred to EnsureInitializedAsync()
// which is called from MainWindow.OnLoaded after the window is visible.
var gameDirectory = UserSettings.Default.GameDirectory;
if (!string.IsNullOrEmpty(gameDirectory) &&
UserSettings.Default.PerDirectory.TryGetValue(gameDirectory, out var currentDir))
{
UserSettings.Default.CurrentDir = currentDir;
InitializeInternals();
}
}
/// <summary>
/// Handles the first-run case: shows the directory-selector dialog and then
/// completes internal initialization. Should be called from MainWindow.OnLoaded
/// when <see cref="CUE4Parse"/> is still null (i.e., no prior configuration exists).
/// </summary>
public async Task EnsureInitializedAsync(Window owner)
{
if (CUE4Parse != null)
return; // Already initialized synchronously in constructor.
var dir = await AvoidEmptyGameDirectoryAsync(false, owner);
if (dir is null)
{
//If no game is selected, many things will break before a shutdown request is processed in the normal way.
//A hard exit is preferable to an unhandled expection in this case
Environment.Exit(0);
return;
}
UserSettings.Default.CurrentDir = dir;
InitializeInternals();
}
private void InitializeInternals()
{
CUE4Parse = new CUE4ParseViewModel();
CUE4Parse.Provider.VfsRegistered += (sender, count) =>
{
if (sender is not IAesVfsReader reader) return;
if (sender is not IAesVfsReader reader)
return;
Status.UpdateStatusLabel($"{count} Archives ({reader.Name})", "Registered");
CUE4Parse.GameDirectory.Add(reader);
};
CUE4Parse.Provider.VfsMounted += (sender, count) =>
{
if (sender is not IAesVfsReader reader) return;
if (sender is not IAesVfsReader reader)
return;
Status.UpdateStatusLabel($"{count:N0} Packages ({reader.Name})", "Mounted");
CUE4Parse.GameDirectory.Verify(reader);
};
CUE4Parse.Provider.VfsUnmounted += (sender, _) =>
{
if (sender is not IAesVfsReader reader) return;
if (sender is not IAesVfsReader reader)
return;
CUE4Parse.GameDirectory.Disable(reader);
};
@ -135,29 +167,85 @@ public class ApplicationViewModel : ViewModel
Status.SetStatus(EStatusKind.Ready);
}
public DirectorySettings AvoidEmptyGameDirectory(bool bAlreadyLaunched)
public async Task<DirectorySettings?> AvoidEmptyGameDirectoryAsync(bool bAlreadyLaunched, Window? owner)
{
var gameDirectory = UserSettings.Default.GameDirectory;
if (!bAlreadyLaunched && UserSettings.Default.PerDirectory.TryGetValue(gameDirectory, out var currentDir))
return currentDir;
var gameLauncherViewModel = new GameSelectorViewModel(gameDirectory);
var result = new DirectorySelector(gameLauncherViewModel).ShowDialog();
if (!result.HasValue || !result.Value) return null;
var selector = new DirectorySelector(gameLauncherViewModel);
// Fall back to MainWindow when no explicit owner is provided.
owner ??= (Avalonia.Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
if (owner == null)
return null;
var ok = await selector.ShowDialog<bool?>(owner);
if (ok != true)
return null;
UserSettings.Default.GameDirectory = gameLauncherViewModel.SelectedDirectory.GameDirectory;
if (!bAlreadyLaunched || UserSettings.Default.CurrentDir.Equals(gameLauncherViewModel.SelectedDirectory))
if (!bAlreadyLaunched || UserSettings.Default.CurrentDir?.Equals(gameLauncherViewModel.SelectedDirectory) == true)
return gameLauncherViewModel.SelectedDirectory;
// UserSettings.Save(); // ??? change key then change game, key saved correctly what?
UserSettings.Default.CurrentDir = gameLauncherViewModel.SelectedDirectory;
RestartWithWarning();
await RestartWithWarningAsync();
return null;
}
public void RestartWithWarning()
public Task RestartWithWarningAsync() => RestartWithWarningAsync(null);
public async Task RestartWithWarningAsync(Window? owner)
{
MessageBox.Show("It looks like you just changed something.\nFModel will restart to apply your changes.", "Uh oh, a restart is needed", MessageBoxButton.OK, MessageBoxImage.Warning);
Log.Information("FModel will restart to apply your changes.");
var okButton = new Avalonia.Controls.Button
{
Content = "OK",
HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right
};
var dialog = new Window
{
Title = "Uh oh, a restart is needed",
Width = 420,
Height = 160,
CanResize = false,
WindowStartupLocation = WindowStartupLocation.CenterOwner,
Content = new Avalonia.Controls.StackPanel
{
Margin = new Avalonia.Thickness(16),
Spacing = 12,
Children =
{
new Avalonia.Controls.TextBlock
{
Text = "It looks like you just changed something.\nFModel will restart to apply your changes.",
TextWrapping = Avalonia.Media.TextWrapping.Wrap
},
okButton
}
}
};
okButton.Click += (_, _) => dialog.Close();
owner ??= (Avalonia.Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
if (owner != null)
{
await dialog.ShowDialog(owner);
}
else
{
// No owner available — show non-modal and wait for it to close.
var tcs = new TaskCompletionSource();
dialog.Closed += (_, _) => tcs.TrySetResult();
dialog.Show();
await tcs.Task;
}
Restart();
}
@ -194,12 +282,19 @@ public class ApplicationViewModel : ViewModel
}.Start();
}
Application.Current.Shutdown();
if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
desktop.Shutdown();
else
Environment.Exit(0);
}
public async Task UpdateProvider(bool isLaunch)
{
if (!isLaunch && !AesManager.HasChange) return;
if (AesManager is null || CUE4Parse is null)
return;
if (!isLaunch && !AesManager.HasChange)
return;
CUE4Parse.ClearProvider();
await ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
@ -210,7 +305,8 @@ public class ApplicationViewModel : ViewModel
cancellationToken.ThrowIfCancellationRequested(); // cancel if needed
var k = x.Key.Trim();
if (k.Length != 66) k = Constants.ZERO_64_CHAR;
if (k.Length != 66)
k = Constants.ZERO_64_CHAR;
return new KeyValuePair<FGuid, FAesKey>(x.Guid, new FAesKey(k));
});
@ -256,8 +352,10 @@ public class ApplicationViewModel : ViewModel
const string imgui = "imgui.ini";
var imguiPath = Path.Combine(UserSettings.Default.OutputDirectory, ".data", imgui);
if (File.Exists(imgui)) File.Move(imgui, imguiPath, true);
if (File.Exists(imguiPath) && !forceDownload) return;
if (File.Exists(imgui))
File.Move(imgui, imguiPath, true);
if (File.Exists(imguiPath) && !forceDownload)
return;
await ApplicationService.ApiEndpointView.DownloadFileAsync($"https://cdn.fmodel.app/d/configurations/{imgui}", imguiPath);
if (new FileInfo(imguiPath).Length == 0)
@ -289,7 +387,8 @@ public class ApplicationViewModel : ViewModel
if (!await ZlibHelper.DownloadDllAsync(zlibPath))
{
FLogger.Append(ELog.Error, () => FLogger.Text("Failed to download Zlib-ng", Constants.WHITE, true));
if (!zlibFileInfo.Exists) return;
if (!zlibFileInfo.Exists)
return;
}
}

View File

@ -9,9 +9,7 @@ using System.Net.Http.Headers;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AdonisUI.Controls;
using Avalonia.Controls;
using CUE4Parse;
using CUE4Parse.Compression;
@ -367,7 +365,7 @@ public class CUE4ParseViewModel : ViewModel
AssetsFolder.Folders.Clear();
SearchVm.SearchResults.Clear();
Helper.CloseWindow<AdonisWindow>("Search For Packages");
Helper.CloseWindow<Window>("Search For Packages");
Provider.UnloadNonStreamedVfs();
GC.Collect();
}

View File

@ -2,8 +2,8 @@ using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using CUE4Parse.FileProvider.Objects;
using FModel.Extensions;
using FModel.Framework;
namespace FModel.ViewModels.Commands;
@ -34,22 +34,28 @@ public class CopyCommand : ViewModelCommand<ApplicationViewModel>
switch (trigger)
{
case "File_Path":
foreach (var entry in entries) sb.AppendLine(entry.Path);
foreach (var entry in entries)
sb.AppendLine(entry.Path);
break;
case "File_Name":
foreach (var entry in entries) sb.AppendLine(entry.Name);
foreach (var entry in entries)
sb.AppendLine(entry.Name);
break;
case "Directory_Path":
foreach (var entry in entries) sb.AppendLine(entry.Directory);
foreach (var entry in entries)
sb.AppendLine(entry.Directory);
break;
case "File_Path_No_Extension":
foreach (var entry in entries) sb.AppendLine(entry.PathWithoutExtension);
foreach (var entry in entries)
sb.AppendLine(entry.PathWithoutExtension);
break;
case "File_Name_No_Extension":
foreach (var entry in entries) sb.AppendLine(entry.NameWithoutExtension);
foreach (var entry in entries)
sb.AppendLine(entry.NameWithoutExtension);
break;
}
Clipboard.SetText(sb.ToString().TrimEnd());
var text = sb.ToString().TrimEnd();
ClipboardExtensions.SetText(text);
}
}

View File

@ -1,10 +1,11 @@
using AdonisUI.Controls;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using FModel.Extensions;
using FModel.Framework;
using FModel.Views.Resources.Controls;
using System.Windows;
using System.Windows.Media;
using FModel.Views.Resources.Converters;
using ImGuiController = FModel.Framework.ImGuiController;
namespace FModel.ViewModels.Commands;
@ -16,29 +17,33 @@ public class ImageCommand : ViewModelCommand<TabItem>
public override void Execute(TabItem tabViewModel, object parameter)
{
if (parameter == null || !tabViewModel.HasImage) return;
if (parameter == null || !tabViewModel.HasImage)
return;
switch (parameter)
{
case "Open":
{
Helper.OpenWindow<AdonisWindow>(tabViewModel.SelectedImage.ExportName + " (Image)", () =>
{
var popout = new ImagePopout
Helper.OpenWindow<Window>(tabViewModel.SelectedImage.ExportName + " (Image)", () =>
{
Title = tabViewModel.SelectedImage.ExportName + " (Image)",
Width = tabViewModel.SelectedImage.Image.Width,
Height = tabViewModel.SelectedImage.Image.Height,
WindowState = tabViewModel.SelectedImage.Image.Height > 1000 ? WindowState.Maximized : WindowState.Normal,
ImageCtrl = { Source = tabViewModel.SelectedImage.Image }
};
RenderOptions.SetBitmapScalingMode(popout.ImageCtrl, BoolToRenderModeConverter.Instance.Convert(tabViewModel.SelectedImage.RenderNearestNeighbor));
popout.Show();
});
break;
}
var pixelWidth = tabViewModel.SelectedImage.Image.PixelSize.Width;
var pixelHeight = tabViewModel.SelectedImage.Image.PixelSize.Height;
var dpiScale = ImGuiController.GetDpiScale();
var popout = new ImagePopout
{
Title = tabViewModel.SelectedImage.ExportName + " (Image)",
Width = pixelWidth / dpiScale,
Height = pixelHeight / dpiScale,
WindowState = pixelHeight > 1000 ? WindowState.Maximized : WindowState.Normal,
};
popout.ImageCtrl.Source = tabViewModel.SelectedImage.Image;
RenderOptions.SetBitmapInterpolationMode(popout.ImageCtrl, BoolToRenderModeConverter.Instance.Convert(tabViewModel.SelectedImage.RenderNearestNeighbor));
popout.Show();
});
break;
}
case "Copy":
ClipboardExtensions.SetImage(tabViewModel.SelectedImage.ImageBuffer, $"{tabViewModel.SelectedImage.ExportName}.png");
ClipboardExtensions.SetImage(tabViewModel.SelectedImage.ImageBuffer);
break;
case "Save":
tabViewModel.SaveImage();

View File

@ -1,7 +1,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using AdonisUI.Controls;
using Avalonia.Controls;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
@ -23,40 +23,43 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
switch (parameter)
{
case "Directory_Selector":
contextViewModel.AvoidEmptyGameDirectory(true);
await contextViewModel.AvoidEmptyGameDirectoryAsync(true, MainWindow.YesWeCats);
break;
case "Directory_AES":
Helper.OpenWindow<AdonisWindow>("AES Manager", () => new AesManager().Show());
Helper.OpenWindow<Window>("AES Manager", () => new AesManager().Show());
break;
case "Directory_Backup":
Helper.OpenWindow<AdonisWindow>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.ProjectName).Show());
if (contextViewModel.CUE4Parse is null) return;
Helper.OpenWindow<Window>("Backup Manager", () => new BackupManager(contextViewModel.CUE4Parse.Provider.ProjectName).Show());
break;
case "Directory_ArchivesInfo":
if (contextViewModel.CUE4Parse is null) return;
ApplicationService.ApplicationView.IsAssetsExplorerVisible = false;
contextViewModel.CUE4Parse.TabControl.AddTab("Archives Info");
contextViewModel.CUE4Parse.TabControl.SelectedTab.Highlighter = AvalonExtensions.HighlighterSelector("json");
contextViewModel.CUE4Parse.TabControl.SelectedTab.SetDocumentText(JsonConvert.SerializeObject(contextViewModel.CUE4Parse.GameDirectory.DirectoryFiles, Formatting.Indented), false, false);
break;
case "Views_3dViewer":
if (contextViewModel.CUE4Parse is null) return;
contextViewModel.CUE4Parse.SnooperViewer.Run();
break;
case "Views_AudioPlayer":
Helper.OpenWindow<AdonisWindow>("Audio Player", () => new AudioPlayer().Show());
Helper.OpenWindow<Window>("Audio Player", () => new AudioPlayer().Show());
break;
case "Views_ImageMerger":
Helper.OpenWindow<AdonisWindow>("Image Merger", () => new ImageMerger().Show());
Helper.OpenWindow<Window>("Image Merger", () => new ImageMerger().Show());
break;
case "Settings":
Helper.OpenWindow<AdonisWindow>("Settings", () => new SettingsView().Show());
Helper.OpenWindow<Window>("Settings", () => new SettingsView().Show());
break;
case "Help_About":
Helper.OpenWindow<AdonisWindow>("About", () => new About().Show());
Helper.OpenWindow<Window>("About", () => new About().Show());
break;
case "Help_Donate":
Process.Start(new ProcessStartInfo { FileName = Constants.DONATE_LINK, UseShellExecute = true });
break;
case "Help_Releases":
Helper.OpenWindow<AdonisWindow>("Releases", () => new UpdateView().Show());
Helper.OpenWindow<Window>("Releases", () => new UpdateView().Show());
break;
case "Help_BugsReport":
Process.Start(new ProcessStartInfo { FileName = Constants.ISSUE_LINK, UseShellExecute = true });
@ -78,6 +81,7 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
// });
// break;
case "ToolBox_Collapse_All":
if (contextViewModel.CUE4Parse is null) return;
await ApplicationService.ThreadWorkerView.Begin(cancellationToken =>
{
SetFoldersIsExpanded(contextViewModel.CUE4Parse.AssetsFolder, false, cancellationToken);
@ -117,7 +121,8 @@ public class MenuCommand : ViewModelCommand<ApplicationViewModel>
current = current.Next;
}
if (!expand) return;
if (!expand)
return;
// Expand bottom-up (reduce layout updates)
for (var node = nodes.Last; node != null; node = node.Previous)

View File

@ -1,5 +1,5 @@
using System.Windows;
using AdonisUI.Controls;
using Avalonia.Controls;
using FModel.Extensions;
using FModel.Framework;
using FModel.Services;
using FModel.Views.Resources.Controls;
@ -68,8 +68,9 @@ public class TabCommand : ViewModelCommand<TabItem>
});
break;
case "Open_Properties":
if (tabViewModel.Header == "New Tab" || tabViewModel.Document == null) return;
Helper.OpenWindow<AdonisWindow>(tabViewModel.Header + " (Properties)", () =>
if (tabViewModel.Header == "New Tab" || tabViewModel.Document == null)
return;
Helper.OpenWindow<Window>(tabViewModel.Header + " (Properties)", () =>
{
new PropertiesPopout(tabViewModel)
{
@ -78,7 +79,7 @@ public class TabCommand : ViewModelCommand<TabItem>
});
break;
case "Copy_Asset_Path":
Clipboard.SetText(tabViewModel.Entry.Path);
ClipboardExtensions.SetText(tabViewModel.Entry.Path);
break;
}
}

View File

@ -250,6 +250,6 @@ public partial class ImageMerger : Window
private void OnCopyImage(object sender, RoutedEventArgs e)
{
ClipboardExtensions.SetImage(_imageBuffer, FILENAME);
ClipboardExtensions.SetImage(_imageBuffer);
}
}

View File

@ -1,12 +1,17 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
namespace FModel.Views.Resources.Controls.Aup;
[TemplatePart(Name = "PART_Timeclock", Type = typeof(Grid))]
[TemplatePart(Name = "PART_Time", Type = typeof(TextBlock))]
/// <summary>
/// Displays a running clock (elapsed or remaining) bound to an <see cref="ISource"/>.
/// Avalonia port of the WPF Timeclock UserControl — ControlTemplate replaced by a
/// visual tree built in the constructor.
/// </summary>
public sealed class Timeclock : UserControl
{
public enum EClockType
@ -15,302 +20,191 @@ public sealed class Timeclock : UserControl
TimeRemaining
}
private Grid _timeclockGrid;
private TextBlock _timeText;
private readonly TextBlock _labelText;
private readonly TextBlock _timeText;
private const string DefaultTimeFormat = "hh\\:mm\\:ss\\.ff";
static Timeclock()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Timeclock), new FrameworkPropertyMetadata(typeof(Timeclock)));
}
// -----------------------------------------------------------------------
// Styled properties
// -----------------------------------------------------------------------
private ISource _source;
public ISource Source
public static readonly StyledProperty<ISource?> SourceProperty =
AvaloniaProperty.Register<Timeclock, ISource?>(nameof(Source));
public ISource? Source
{
get => (ISource) GetValue(SourceProperty);
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
public static readonly DependencyProperty SourceProperty =
DependencyProperty.Register("Source", typeof(ISource), typeof(Timeclock),
new UIPropertyMetadata(null, OnSourceChanged, OnCoerceSource));
public static readonly StyledProperty<EClockType> ClockTypeProperty =
AvaloniaProperty.Register<Timeclock, EClockType>(nameof(ClockType), defaultValue: EClockType.TimeElapsed);
public EClockType ClockType
{
get => (EClockType) GetValue(ClockTypeProperty);
get => GetValue(ClockTypeProperty);
set => SetValue(ClockTypeProperty, value);
}
public static readonly DependencyProperty ClockTypeProperty =
DependencyProperty.Register("ClockType", typeof(EClockType), typeof(Timeclock),
new UIPropertyMetadata(EClockType.TimeElapsed, OnClockTypeChanged, OnCoerceClockType));
public static readonly StyledProperty<FontFamily> LabelFontProperty =
AvaloniaProperty.Register<Timeclock, FontFamily>(nameof(LabelFont), defaultValue: new FontFamily("Segoe UI"));
public FontFamily LabelFont
{
get => (FontFamily) GetValue(LabelFontProperty);
get => GetValue(LabelFontProperty);
set => SetValue(LabelFontProperty, value);
}
public static readonly DependencyProperty LabelFontProperty =
DependencyProperty.Register("LabelFont", typeof(FontFamily), typeof(Timeclock),
new UIPropertyMetadata(new FontFamily("Segoe UI"), OnLabelFontChanged, OnCoerceLabelFont));
public Brush LabelForeground
public static readonly StyledProperty<IBrush?> LabelForegroundProperty =
AvaloniaProperty.Register<Timeclock, IBrush?>(nameof(LabelForeground), defaultValue: Brushes.Coral);
public IBrush? LabelForeground
{
get => (Brush) GetValue(LabelForegroundProperty);
get => GetValue(LabelForegroundProperty);
set => SetValue(LabelForegroundProperty, value);
}
public static readonly DependencyProperty LabelForegroundProperty =
DependencyProperty.Register("LabelForeground", typeof(Brush), typeof(Timeclock),
new UIPropertyMetadata(Brushes.Coral, OnLabelForegroundChanged, OnCoerceLabelForeground));
public static readonly StyledProperty<FontFamily> TimeFontProperty =
AvaloniaProperty.Register<Timeclock, FontFamily>(nameof(TimeFont), defaultValue: new FontFamily("Ebrima"));
public FontFamily TimeFont
{
get => (FontFamily) GetValue(TimeFontProperty);
get => GetValue(TimeFontProperty);
set => SetValue(TimeFontProperty, value);
}
public static readonly DependencyProperty TimeFontProperty =
DependencyProperty.Register("TimeFont", typeof(FontFamily), typeof(Timeclock),
new UIPropertyMetadata(new FontFamily("Ebrima"), OnTimeFontChanged, OnCoerceTimeFont));
public Brush TimeForeground
public static readonly StyledProperty<IBrush?> TimeForegroundProperty =
AvaloniaProperty.Register<Timeclock, IBrush?>(nameof(TimeForeground), defaultValue: Brushes.Silver);
public IBrush? TimeForeground
{
get => (Brush) GetValue(TimeForegroundProperty);
get => GetValue(TimeForegroundProperty);
set => SetValue(TimeForegroundProperty, value);
}
public static readonly DependencyProperty TimeForegroundProperty =
DependencyProperty.Register("TimeForeground", typeof(Brush), typeof(Timeclock),
new UIPropertyMetadata(Brushes.Silver, OnTimeForegroundChanged, OnCoerceTimeForeground));
public CornerRadius CornerRadius
{
get => (CornerRadius) GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public static readonly DependencyProperty CornerRadiusProperty =
DependencyProperty.Register("CornerRadius", typeof(CornerRadius), typeof(Timeclock),
new UIPropertyMetadata(new CornerRadius(3), OnCornerRadiusChanged, OnCoerceCornerRadius));
// CornerRadius is inherited from TemplatedControl via UserControl.
// Default is set in the constructor via SetCurrentValue.
public static readonly StyledProperty<string> LabelProperty =
AvaloniaProperty.Register<Timeclock, string>(nameof(Label), defaultValue: string.Empty);
public string Label
{
get => (string) GetValue(LabelProperty);
get => GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public static readonly DependencyProperty LabelProperty =
DependencyProperty.Register("Label", typeof(string), typeof(Timeclock),
new UIPropertyMetadata(string.Empty, OnLabelChanged, OnCoerceLabel));
public static readonly StyledProperty<string> TimeFormatProperty =
AvaloniaProperty.Register<Timeclock, string>(nameof(TimeFormat), defaultValue: DefaultTimeFormat);
public string TimeFormat
{
get => (string) GetValue(TimeFormatProperty);
get => GetValue(TimeFormatProperty);
set => SetValue(TimeFormatProperty, value);
}
public static readonly DependencyProperty TimeFormatProperty =
DependencyProperty.Register("TimeFormat", typeof(string), typeof(Timeclock),
new UIPropertyMetadata(DefaultTimeFormat, OnTimeFormatChanged, OnCoerceTimeFormat));
private void OnSourceChanged(ISource oldValue, ISource newValue)
// -----------------------------------------------------------------------
// Constructor — build visual tree directly (no ControlTemplate needed)
// -----------------------------------------------------------------------
public Timeclock()
{
_source = Source;
_source.SourceEvent += OnSourceEvent;
_source.SourcePropertyChangedEvent += OnSourcePropertyChangedEvent;
OnSourceEvent(this, null);
}
private void OnSourceEvent(object sender, SourceEventArgs e)
{
if (Source == null) return;
Label = Source.PlayedFile.FileName;
Dispatcher.BeginInvoke((Action) CalculateTime);
}
private void OnSourcePropertyChangedEvent(object sender, SourcePropertyChangedEventArgs e)
{
if (_timeText == null || e.Property != ESourceProperty.Position) return;
CalculateTime();
}
private ISource OnCoerceSource(ISource value) => value;
private static object OnCoerceSource(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceSource((ISource) value);
return value;
}
private static void OnSourceChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnSourceChanged((ISource) e.OldValue, (ISource) e.NewValue);
}
private EClockType OnCoerceClockType(EClockType value) => value;
private static object OnCoerceClockType(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceClockType((EClockType) value);
return value;
}
private void OnClockTypeChanged(EClockType oldValue, EClockType newValue) => CalculateTime();
private static void OnClockTypeChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnClockTypeChanged((EClockType) e.OldValue, (EClockType) e.NewValue);
}
private FontFamily OnCoerceLabelFont(FontFamily value) => value;
private static object OnCoerceLabelFont(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceLabelFont((FontFamily) value);
return value;
}
private void OnLabelFontChanged(FontFamily oldValue, FontFamily newValue)
{
}
private static void OnLabelFontChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnLabelFontChanged((FontFamily) e.OldValue, (FontFamily) e.NewValue);
}
private Brush OnCoerceLabelForeground(Brush value) => value;
private static object OnCoerceLabelForeground(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceLabelForeground((Brush) value);
return value;
}
private void OnLabelForegroundChanged(Brush oldValue, Brush newValue)
{
}
private static void OnLabelForegroundChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnLabelForegroundChanged((Brush) e.OldValue, (Brush) e.NewValue);
}
private FontFamily OnCoerceTimeFont(FontFamily value) => value;
private static object OnCoerceTimeFont(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceTimeFont((FontFamily) value);
return value;
}
private void OnTimeFontChanged(FontFamily oldValue, FontFamily newValue)
{
}
private static void OnTimeFontChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnTimeFontChanged((FontFamily) e.OldValue, (FontFamily) e.NewValue);
}
private Brush OnCoerceTimeForeground(Brush value) => value;
private static object OnCoerceTimeForeground(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceTimeForeground((Brush) value);
return value;
}
private void OnTimeForegroundChanged(Brush oldValue, Brush newValue)
{
}
private static void OnTimeForegroundChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnTimeForegroundChanged((Brush) e.OldValue, (Brush) e.NewValue);
}
private CornerRadius OnCoerceCornerRadius(CornerRadius value) => value;
private static object OnCoerceCornerRadius(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceCornerRadius((CornerRadius) value);
return value;
}
private void OnCornerRadiusChanged(CornerRadius oldValue, CornerRadius newValue)
{
}
private static void OnCornerRadiusChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnCornerRadiusChanged((CornerRadius) e.OldValue, (CornerRadius) e.NewValue);
}
private string OnCoerceLabel(string value) => value;
private static object OnCoerceLabel(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceLabel((string) value);
return value;
}
private void OnLabelChanged(string oldValue, string newValue)
{
}
private static void OnLabelChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnLabelChanged((string) e.OldValue, (string) e.NewValue);
}
private string OnCoerceTimeFormat(string value) => value;
private static object OnCoerceTimeFormat(DependencyObject o, object value)
{
if (o is Timeclock timeclock)
return timeclock.OnCoerceTimeFormat((string) value);
return value;
}
private void OnTimeFormatChanged(string oldValue, string newValue)
{
try
_labelText = new TextBlock
{
TimeSpan.Zero.ToString(newValue);
}
catch
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
};
_timeText = new TextBlock
{
TimeFormat = DefaultTimeFormat;
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
};
var border = new Border
{
Child = new StackPanel
{
Orientation = Orientation.Vertical,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
Children = { _labelText, _timeText }
}
};
// Bind border CornerRadius to the inherited CornerRadius styled property.
border.Bind(Border.CornerRadiusProperty, this.GetObservable(UserControl.CornerRadiusProperty));
SetCurrentValue(UserControl.CornerRadiusProperty, new CornerRadius(3));
// Bind textblock fonts / foregrounds to styled properties.
_labelText.Bind(TextBlock.FontFamilyProperty, this.GetObservable(LabelFontProperty));
_labelText.Bind(TextBlock.ForegroundProperty, this.GetObservable(LabelForegroundProperty));
_labelText.Bind(TextBlock.TextProperty, this.GetObservable(LabelProperty));
_timeText.Bind(TextBlock.FontFamilyProperty, this.GetObservable(TimeFontProperty));
_timeText.Bind(TextBlock.ForegroundProperty, this.GetObservable(TimeForegroundProperty));
Content = border;
ZeroTime();
}
// -----------------------------------------------------------------------
// Property-change reactions
// -----------------------------------------------------------------------
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SourceProperty)
{
if (change.OldValue is ISource old)
{
old.SourceEvent -= OnSourceEvent;
old.SourcePropertyChangedEvent -= OnSourcePropertyChangedEvent;
}
if (change.NewValue is ISource src)
{
src.SourceEvent += OnSourceEvent;
src.SourcePropertyChangedEvent += OnSourcePropertyChangedEvent;
}
// Refresh display immediately.
CalculateTime();
}
else if (change.Property == ClockTypeProperty
|| change.Property == TimeFormatProperty)
{
if (change.Property == TimeFormatProperty)
{
// Validate the new format string; revert to default on error.
try
{ TimeSpan.Zero.ToString(TimeFormat); }
catch { SetCurrentValue(TimeFormatProperty, DefaultTimeFormat); }
}
CalculateTime();
}
}
private void OnSourceEvent(object? sender, SourceEventArgs? e)
{
if (Source == null)
return;
Dispatcher.UIThread.Post(() =>
{
SetCurrentValue(LabelProperty, Source.PlayedFile.FileName);
CalculateTime();
});
}
// NOTE: This handler may be called from a background (audio) thread.
// CalculateTime() reads Source.PlayedFile.Position/Duration before marshalling
// to the UI thread. This is safe because ISource properties are immutable value
// types (TimeSpan) and the audio engine guarantees coherent reads.
private void OnSourcePropertyChangedEvent(object? sender, SourcePropertyChangedEventArgs e)
{
if (e.Property != ESourceProperty.Position)
return;
CalculateTime();
}
private static void OnTimeFormatChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
if (o is Timeclock timeclock)
timeclock.OnTimeFormatChanged((string) e.OldValue, (string) e.NewValue);
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
_timeclockGrid = GetTemplateChild("PART_Timeclock") as Grid;
_timeText = GetTemplateChild("PART_Time") as TextBlock;
_timeclockGrid.CacheMode = new BitmapCache();
OnSourceEvent(this, null);
}
private void CalculateTime()
{
if (_source != null)
if (Source != null)
{
var position = _source.PlayedFile.Position;
var length = _source.PlayedFile.Duration;
Dispatcher.BeginInvoke((Action) delegate
var position = Source.PlayedFile.Position;
var length = Source.PlayedFile.Duration;
Dispatcher.UIThread.Post(() =>
{
_timeText.Text = ClockType switch
{
@ -326,9 +220,20 @@ public sealed class Timeclock : UserControl
}
}
protected override void OnDetachedFromVisualTree(Avalonia.VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
if (Source is { } src)
{
src.SourceEvent -= OnSourceEvent;
src.SourcePropertyChangedEvent -= OnSourcePropertyChangedEvent;
}
}
private void ZeroTime()
{
Dispatcher.BeginInvoke((Action) delegate
Dispatcher.UIThread.Post(() =>
{
_timeText.Text = TimeSpan.Zero.ToString(TimeFormat);
});

View File

@ -1,6 +1,9 @@
<UserControl x:Class="FModel.Views.Resources.Controls.Breadcrumb"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
DataContextChanged="OnDataContextChanged">
<StackPanel x:Name="InMeDaddy" Orientation="Horizontal" HorizontalAlignment="Right" Height="24" />
<StackPanel x:Name="InMeDaddy"
Orientation="Horizontal"
HorizontalAlignment="Right"
Height="24" />
</UserControl>

View File

@ -1,9 +1,11 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using FModel.Services;
namespace FModel.Views.Resources.Controls;
@ -17,22 +19,26 @@ public partial class Breadcrumb
InitializeComponent();
}
private void OnDataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (e.NewValue is not string pathAtThisPoint) return;
if (DataContext is not string pathAtThisPoint)
return;
InMeDaddy.Children.Clear();
var folders = pathAtThisPoint.Split('/');
for (var i = 0; i < folders.Length; i++)
{
var capturedIndex = i + 1;
var capturedPath = pathAtThisPoint;
var border = new Border
{
BorderThickness = new Thickness(1),
BorderBrush = Brushes.Transparent,
Background = Brushes.Transparent,
Padding = new Thickness(6, 3, 6, 3),
Cursor = Cursors.Hand,
Tag = i + 1,
Cursor = new Cursor(StandardCursorType.Hand),
Tag = capturedIndex,
IsEnabled = i < folders.Length - 1,
Child = new TextBlock
{
@ -41,12 +47,29 @@ public partial class Breadcrumb
}
};
border.MouseEnter += OnMouseEnter;
border.MouseLeave += OnMouseLeave;
border.MouseUp += OnMouseClick;
border.PointerEntered += (_, _) =>
{
border.BorderBrush = new ImmutableSolidColorBrush(Color.FromRgb(127, 127, 144));
border.Background = new ImmutableSolidColorBrush(Color.FromRgb(72, 73, 92));
};
border.PointerExited += (_, _) =>
{
border.BorderBrush = Brushes.Transparent;
border.Background = Brushes.Transparent;
};
border.PointerReleased += (_, args) =>
{
if (args.InitialPressMouseButton != Avalonia.Input.MouseButton.Left)
return;
var directory = string.Join('/', capturedPath.Split('/').Take(capturedIndex));
if (capturedPath.Equals(directory))
return;
ApplicationService.ApplicationView.CustomDirectories?.GoToCommand.JumpTo(directory);
};
InMeDaddy.Children.Add(border);
if (i >= folders.Length - 1) continue;
if (i >= folders.Length - 1)
continue;
InMeDaddy.Children.Add(new Viewbox
{
@ -59,7 +82,7 @@ public partial class Breadcrumb
Height = 24,
Children =
{
new Path
new Avalonia.Controls.Shapes.Path
{
Fill = Brushes.White,
Data = Geometry.Parse(NavigateNext),
@ -70,32 +93,4 @@ public partial class Breadcrumb
});
}
}
private void OnMouseEnter(object sender, MouseEventArgs e)
{
if (sender is Border border)
{
border.BorderBrush = new SolidColorBrush(Color.FromRgb(127, 127, 144));
border.Background = new SolidColorBrush(Color.FromRgb(72, 73, 92));
}
}
private void OnMouseLeave(object sender, MouseEventArgs e)
{
if (sender is Border border)
{
border.BorderBrush = Brushes.Transparent;
border.Background = Brushes.Transparent;
}
}
private void OnMouseClick(object sender, MouseButtonEventArgs e)
{
if (sender is not Border { DataContext: string pathAtThisPoint, Tag: int index }) return;
var directory = string.Join('/', pathAtThisPoint.Split('/').Take(index));
if (pathAtThisPoint.Equals(directory)) return;
ApplicationService.ApplicationView.CustomDirectories.GoToCommand.JumpTo(directory);
}
}

View File

@ -1,8 +1,7 @@
<UserControl x:Class="FModel.Views.Resources.Controls.CommitDownloaderControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI">
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -10,39 +9,52 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" HorizontalAlignment="Center" VerticalAlignment="Center">
<Grid Grid.Column="0"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Viewbox Grid.Column="0" Width="16" Height="16" VerticalAlignment="Center" HorizontalAlignment="Center">
<Canvas Width="16" Height="16">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
<Viewbox Grid.Column="0"
Width="16"
Height="16"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<Canvas Width="16"
Height="16">
<Path Fill="#888888"
Data="{StaticResource ArchiveIcon}" />
</Canvas>
</Viewbox>
<StackPanel Grid.Column="2">
<TextBlock Text="Size" FontSize="10" />
<TextBlock FontSize="10" Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}}" />
<TextBlock Text="Size"
FontSize="10" />
<TextBlock FontSize="10"
Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}}" />
</StackPanel>
</Grid>
<Button Grid.Column="2" Style="{DynamicResource {x:Static adonisUi:Styles.ToolbarButton}}" ToolTip="Download"
Height="{Binding ActualHeight, RelativeSource={RelativeSource AncestorType=Grid}}"
Width="{Binding ActualHeight, RelativeSource={RelativeSource Self}}"
<Button Grid.Column="2"
ToolTip.Tip="Download"
Width="32"
Height="32"
IsEnabled="{Binding IsCurrent, Converter={x:Static converters:InvertBooleanConverter.Instance}}"
Click="OnDownload">
<Viewbox Width="16" Height="16" VerticalAlignment="Center" HorizontalAlignment="Center">
<Canvas Width="16" Height="16">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
<Viewbox Width="16"
Height="16"
VerticalAlignment="Center"
HorizontalAlignment="Center">
<Canvas Width="16"
Height="16">
<Path Fill="#888888"
Data="M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14Z" />
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}"
<Path Fill="#888888"
Data="M11.78 4.72a.749.749 0 1 1-1.06 1.06L8.75 3.811V9.5a.75.75 0 0 1-1.5 0V3.811L5.28 5.78a.749.749 0 1 1-1.06-1.06l3.25-3.25a.749.749 0 0 1 1.06 0l3.25 3.25Z" />
</Canvas>
</Viewbox>
</Button>
</Grid>
</UserControl>

View File

@ -1,5 +1,6 @@
using System.Windows;
using System.Windows.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using FModel.ViewModels.ApiEndpoints.Models;
namespace FModel.Views.Resources.Controls;
@ -11,18 +12,18 @@ public partial class CommitDownloaderControl : UserControl
InitializeComponent();
}
public static readonly DependencyProperty CommitProperty =
DependencyProperty.Register(nameof(Commit), typeof(GitHubCommit), typeof(CommitDownloaderControl), new PropertyMetadata(null));
public static readonly StyledProperty<GitHubCommit?> CommitProperty =
AvaloniaProperty.Register<CommitDownloaderControl, GitHubCommit?>(nameof(Commit));
public GitHubCommit Commit
public GitHubCommit? Commit
{
get { return (GitHubCommit)GetValue(CommitProperty); }
set { SetValue(CommitProperty, value); }
get => GetValue(CommitProperty);
set => SetValue(CommitProperty, value);
}
private void OnDownload(object sender, RoutedEventArgs e)
private void OnDownload(object? sender, RoutedEventArgs e)
{
Commit.Download();
Commit?.Download();
}
}

View File

@ -1,34 +1,43 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters">
<ContextMenu x:Key="FileContextMenu" x:Shared="False"
DataContext="{Binding DataContext, RelativeSource={RelativeSource AncestorType=Window}}">
<MenuItem Header="Extract in New Tab" Command="{Binding RightClickMenuCommand}">
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
x:Class="FModel.Views.Resources.Controls.ContextMenus.FileContextMenuDictionary">
<ContextMenu x:Key="FileContextMenu"
x:Shared="False"
Opened="FileContextMenu_OnOpened">
<MenuItem Header="Extract in New Tab"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Extract_New_Tab" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExtractIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource ExtractIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Show Metadata" Command="{Binding RightClickMenuCommand}">
<MenuItem Header="Show Metadata"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Show_Metadata" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
@ -39,22 +48,28 @@
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource InfoIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource InfoIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Find References" Command="{Binding RightClickMenuCommand}">
<MenuItem Header="Find References"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Show_References" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
@ -66,23 +81,29 @@
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SearchIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource SearchIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Decompile Blueprint"
Command="{Binding RightClickMenuCommand}">
Command="{Binding RightClickMenuCommand}"
IsVisible="{Binding ShowDecompileOption, Source={x:Static settings:UserSettings.Default}}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Decompile" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
@ -93,28 +114,21 @@
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CppIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource CppIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem.Style>
<Style TargetType="{x:Type MenuItem}" BasedOn="{StaticResource {x:Type MenuItem}}">
<Style.Triggers>
<DataTrigger Binding="{Binding ShowDecompileOption, Source={x:Static settings:UserSettings.Default}}" Value="False">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</MenuItem.Style>
</MenuItem>
<Separator />
<MenuItem Command="{Binding RightClickMenuCommand}"
Visibility="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}">
IsVisible="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}}">
<MenuItem.Header>
<TextBlock
Text="{Binding PlacementTarget.SelectedItem.Asset.Extension,
<TextBlock Text="{Binding PlacementTarget.SelectedItem.Asset.Extension,
FallbackValue='Export Raw Data',
StringFormat='Export Raw Data (.{0})',
RelativeSource={RelativeSource AncestorType=ContextMenu}}" />
@ -122,41 +136,53 @@
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Export_Data" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ExportIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource ExportIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Properties (.json)" Command="{Binding RightClickMenuCommand}">
<MenuItem Header="Save Properties (.json)"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Properties" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource SaveIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource SaveIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Texture" Command="{Binding RightClickMenuCommand}">
<MenuItem Header="Save Texture"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Textures" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
@ -167,22 +193,28 @@
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource TextureIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Model" Command="{Binding RightClickMenuCommand}">
<MenuItem Header="Save Model"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Models" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
@ -193,22 +225,28 @@
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource ModelIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Animation" Command="{Binding RightClickMenuCommand}">
<MenuItem Header="Save Animation"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Animations" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
@ -219,22 +257,28 @@
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AnimationIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Save Audio" Command="{Binding RightClickMenuCommand}">
<MenuItem Header="Save Audio"
Command="{Binding RightClickMenuCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Assets_Save_Audio" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
<MenuItem.IsEnabled>
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}">
<Binding.Converter>
<converters:AnyItemMeetsConditionConverter>
<converters:AnyItemMeetsConditionConverter.Conditions>
@ -245,9 +289,12 @@
</Binding>
</MenuItem.IsEnabled>
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource AudioIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
@ -255,49 +302,62 @@
<Separator />
<MenuItem Header="Copy">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
<MenuItem Header="Package Path" Command="{Binding CopyCommand}">
<MenuItem Header="Package Path"
Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name" Command="{Binding CopyCommand}">
<MenuItem Header="Package Name"
Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Directory Path" Command="{Binding CopyCommand}">
<MenuItem Header="Directory Path"
Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Directory_Path" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Path w/o Extension" Command="{Binding CopyCommand}">
<MenuItem Header="Package Path w/o Extension"
Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Path_No_Extension" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>
<MenuItem Header="Package Name w/o Extension" Command="{Binding CopyCommand}">
<MenuItem Header="Package Name w/o Extension"
Command="{Binding CopyCommand}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="File_Name_No_Extension" />
<Binding Path="PlacementTarget.SelectedItems" RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
<Binding Path="PlacementTarget.SelectedItems"
RelativeSource="{RelativeSource AncestorType=ContextMenu}" />
</MultiBinding>
</MenuItem.CommandParameter>
</MenuItem>

View File

@ -0,0 +1,37 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Serilog;
using System.Linq;
namespace FModel.Views.Resources.Controls.ContextMenus;
public partial class FileContextMenuDictionary : ResourceDictionary
{
public FileContextMenuDictionary()
{
InitializeComponent();
}
/// <summary>
/// Resolves the DataContext from the placement target's visual tree.
/// ContextMenus are hosted in popups (not under a Window), so
/// $parent[Window].DataContext doesn't reliably resolve.
/// </summary>
private void FileContextMenu_OnOpened(object? sender, RoutedEventArgs e)
{
if (sender is not ContextMenu { PlacementTarget: { } target } menu)
return;
// Walk up the visual tree to the Window and grab its DataContext.
var window = target.GetVisualAncestors().OfType<Window>().FirstOrDefault();
if (window != null)
{
menu.DataContext = window.DataContext;
}
else
{
Log.Warning("FileContextMenu: could not find a Window ancestor for PlacementTarget {Target}", target.GetType().Name);
}
}
}

View File

@ -1,14 +1,14 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:settings="clr-namespace:FModel.Settings"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
x:Class="FModel.Views.Resources.Controls.ContextMenus.FolderContextMenuDictionary">
<ContextMenu x:Key="FolderContextMenu" x:Shared="False"
<ContextMenu x:Key="FolderContextMenu"
x:Shared="False"
Opened="FolderContextMenu_OnOpened">
<MenuItem Header="Export Folder's Packages Raw Data (.uasset)"
Command="{Binding RightClickMenuCommand}"
Visibility="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}, Converter={StaticResource BoolToVisibilityConverter}}">
IsVisible="{Binding CanExportRawData, Source={x:Static settings:UserSettings.Default}}">
<MenuItem.CommandParameter>
<MultiBinding Converter="{x:Static converters:MultiParameterConverter.Instance}">
<Binding Source="Folders_Export_Data" />
@ -21,7 +21,7 @@
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource ExportIcon}" />
</Canvas>
</Viewbox>
@ -41,7 +41,7 @@
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource SaveIcon}" />
</Canvas>
</Viewbox>
@ -61,7 +61,7 @@
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource TextureIcon}" />
</Canvas>
</Viewbox>
@ -81,7 +81,7 @@
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource ModelIcon}" />
</Canvas>
</Viewbox>
@ -101,7 +101,7 @@
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource AnimationIcon}" />
</Canvas>
</Viewbox>
@ -121,29 +121,37 @@
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource AudioIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Favorite Directory" Click="OnFavoriteDirectoryClick"
<MenuItem Header="Favorite Directory"
Click="OnFavoriteDirectoryClick"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource DirectoriesAddIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource DirectoriesAddIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Copy Directory Path" Click="OnCopyDirectoryPathClick"
<MenuItem Header="Copy Directory Path"
Click="OnCopyDirectoryPathClick"
CommandParameter="{Binding Tag, RelativeSource={RelativeSource AncestorType=ContextMenu}}">
<MenuItem.Icon>
<Viewbox Width="16" Height="16">
<Canvas Width="24" Height="24">
<Path Fill="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" Data="{StaticResource CopyIcon}" />
<Viewbox Width="16"
Height="16">
<Canvas Width="24"
Height="24">
<Path Fill="{DynamicResource SystemColors.ControlTextBrushKey}"
Data="{StaticResource CopyIcon}" />
</Canvas>
</Viewbox>
</MenuItem.Icon>

View File

@ -1,11 +1,13 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using FModel.Extensions;
using FModel.Services;
using FModel.Settings;
using FModel.ViewModels;
using FModel.Views;
namespace FModel.Views.Resources.Controls.ContextMenus;
@ -18,39 +20,39 @@ public partial class FolderContextMenuDictionary
InitializeComponent();
}
private void FolderContextMenu_OnOpened(object sender, RoutedEventArgs e)
private void FolderContextMenu_OnOpened(object? sender, RoutedEventArgs e)
{
if (sender is not ContextMenu { PlacementTarget: FrameworkElement fe } menu)
if (sender is not ContextMenu { PlacementTarget: Control control } menu)
return;
var listBox = FindAncestor<ListBox>(fe);
var listBox = FindAncestor<ListBox>(control);
if (listBox != null)
{
menu.DataContext = listBox.DataContext;
menu.Tag = listBox.SelectedItems;
menu.Tag = listBox.SelectedItems?.Cast<object>().ToList() ?? [];
return;
}
var treeView = FindAncestor<TreeView>(fe);
var treeView = FindAncestor<TreeView>(control);
if (treeView != null)
{
menu.DataContext = treeView.DataContext;
menu.Tag = new[] { treeView.SelectedItem }.ToList();
menu.Tag = treeView.SelectedItem is not null ? new[] { treeView.SelectedItem }.ToList() : [];
}
}
private static T FindAncestor<T>(DependencyObject current) where T : DependencyObject
private static T? FindAncestor<T>(Control? current) where T : class
{
while (current != null)
{
if (current is T t)
return t;
current = VisualTreeHelper.GetParent(current);
}
return null;
if (current is null)
return null;
if (current is T self)
return self;
return current.GetVisualAncestors().OfType<T>().FirstOrDefault();
}
private void OnFavoriteDirectoryClick(object sender, RoutedEventArgs e)
private void OnFavoriteDirectoryClick(object? sender, RoutedEventArgs e)
{
if (sender is not MenuItem { CommandParameter: IEnumerable<object> list } || list.FirstOrDefault() is not TreeItem folder)
return;
@ -60,11 +62,11 @@ public partial class FolderContextMenuDictionary
FLogger.Text($"Successfully saved '{folder.PathAtThisPoint}' as a new favorite directory", Constants.WHITE, true));
}
private void OnCopyDirectoryPathClick(object sender, RoutedEventArgs e)
private void OnCopyDirectoryPathClick(object? sender, RoutedEventArgs e)
{
if (sender is not MenuItem { CommandParameter: IEnumerable<object> list } || list.FirstOrDefault() is not TreeItem folder)
return;
Clipboard.SetText(folder.PathAtThisPoint);
ClipboardExtensions.SetText(folder.PathAtThisPoint);
}
}

View File

@ -1,45 +1,74 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.Resources.Controls.DictionaryEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" ResizeMode="NoResize" SizeToContent="Width"
MinWidth="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.30'}"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.20'}">
<Window x:Class="FModel.Views.Resources.Controls.DictionaryEditor"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
WindowStartupLocation="CenterScreen"
CanResize="False"
SizeToContent="Width"
MinWidth="480"
Height="320">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<avalonEdit:TextEditor x:Name="MyAvalonEditor" Grid.Row="0" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" ShowLineNumbers="True" Foreground="#DAE5F2" />
<avalonEdit:TextEditor x:Name="MyAvalonEditor"
Grid.Row="0"
Background="#2A2A3E"
FontFamily="Consolas"
FontSize="8pt"
ShowLineNumbers="True"
Foreground="#DAE5F2" />
<Border Grid.Row="1"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer1BackgroundBrush}}"
adonisExtensions:LayerExtension.IncreaseLayer="True">
Background="#1A1A2E">
<Grid Margin="30, 12, 6, 12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="HeBrokeIt" Grid.Column="0" Text="IF YOU DON'T KNOW WHAT THIS DOES, DON'T TOUCH IT!"
HorizontalAlignment="Right" VerticalAlignment="Center" FontSize="11" Margin="0 0 10 0" FontWeight="DemiBold"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.Layer1InteractionForegroundBrush}}" />
<TextBlock x:Name="HeBrokeIt"
Grid.Column="0"
Text="IF YOU DON'T KNOW WHAT THIS DOES, DON'T TOUCH IT!"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="11"
Margin="0 0 10 0"
FontWeight="DemiBold"
Foreground="#E5C07B" />
<Button Grid.Column="1" 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="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Reset" Click="OnReset" />
<Button Grid.Column="3" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="True"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Cancel" />
<Button Grid.Column="1"
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="False"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Content="Reset"
Click="OnReset" />
<Button Grid.Column="3"
MinWidth="78"
Margin="0 0 12 0"
IsDefault="False"
IsCancel="True"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Content="Cancel"
Click="OnCancel" />
</Grid>
</Border>
</Grid>
</adonisControls:AdonisWindow>
</Window>

View File

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using CUE4Parse.UE4.Objects.Core.Misc;
using CUE4Parse.UE4.Objects.Core.Serialization;
using FModel.Extensions;
@ -10,15 +11,15 @@ using Newtonsoft.Json;
namespace FModel.Views.Resources.Controls;
public partial class DictionaryEditor
public partial class DictionaryEditor : Window
{
private readonly List<FCustomVersion> _defaultCustomVersions;
private readonly Dictionary<string, bool> _defaultOptions;
private readonly Dictionary<string, KeyValuePair<string, string>> _defaultMapStructTypes;
public List<FCustomVersion> CustomVersions { get; private set; }
public Dictionary<string, bool> Options { get; private set; }
public Dictionary<string, KeyValuePair<string, string>> MapStructTypes { get; private set; }
public List<FCustomVersion> CustomVersions { get; private set; } = new();
public Dictionary<string, bool> Options { get; private set; } = new();
public Dictionary<string, KeyValuePair<string, string>> MapStructTypes { get; private set; } = new();
public DictionaryEditor(string title)
{
@ -56,29 +57,26 @@ public partial class DictionaryEditor
};
}
private void OnClick(object sender, RoutedEventArgs e)
private void OnClick(object? sender, RoutedEventArgs e)
{
try
{
switch (Title)
{
case "Versioning Configuration (Custom Versions)":
CustomVersions = JsonConvert.DeserializeObject<List<FCustomVersion>>(MyAvalonEditor.Document.Text);
CustomVersions = JsonConvert.DeserializeObject<List<FCustomVersion>>(MyAvalonEditor.Document.Text) ?? new();
// DialogResult = !CustomVersions.SequenceEqual(_defaultCustomVersions);
DialogResult = true;
Close();
Close(true);
break;
case "Versioning Configuration (Options)":
Options = JsonConvert.DeserializeObject<Dictionary<string, bool>>(MyAvalonEditor.Document.Text);
Options = JsonConvert.DeserializeObject<Dictionary<string, bool>>(MyAvalonEditor.Document.Text) ?? new();
// DialogResult = !Options.SequenceEqual(_defaultOptions);
DialogResult = true;
Close();
Close(true);
break;
case "Versioning Configuration (MapStructTypes)":
MapStructTypes = JsonConvert.DeserializeObject<Dictionary<string, KeyValuePair<string, string>>>(MyAvalonEditor.Document.Text);
MapStructTypes = JsonConvert.DeserializeObject<Dictionary<string, KeyValuePair<string, string>>>(MyAvalonEditor.Document.Text) ?? new();
// DialogResult = !Options.SequenceEqual(_defaultOptions);
DialogResult = true;
Close();
Close(true);
break;
default:
throw new NotImplementedException();
@ -87,11 +85,11 @@ public partial class DictionaryEditor
catch
{
HeBrokeIt.Text = "GG YOU BROKE THE FORMAT, FIX THE JSON OR RESET THE CHANGES!";
HeBrokeIt.Foreground = new SolidColorBrush((Color) ColorConverter.ConvertFromString(Constants.RED));
HeBrokeIt.Foreground = new SolidColorBrush(Color.Parse(Constants.RED));
}
}
private void OnReset(object sender, RoutedEventArgs e)
private void OnReset(object? sender, RoutedEventArgs e)
{
MyAvalonEditor.Document = Title switch
{
@ -110,4 +108,9 @@ public partial class DictionaryEditor
_ => throw new NotImplementedException()
};
}
private void OnCancel(object? sender, RoutedEventArgs e)
{
Close(false);
}
}

View File

@ -1,18 +1,15 @@
<adonisControls:AdonisWindow x:Class="FModel.Views.Resources.Controls.EndpointEditor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:avalonEdit="http://icsharpcode.net/sharpdevelop/avalonedit"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisControls="clr-namespace:AdonisUI.Controls;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
WindowStartupLocation="CenterScreen" IconVisibility="Collapsed" ResizeMode="NoResize"
Width="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.50'}"
Height="{Binding Source={x:Static SystemParameters.MaximizedPrimaryScreenWidth}, Converter={converters:RatioConverter}, ConverterParameter='0.35'}">
<Window x:Class="FModel.Views.Resources.Controls.EndpointEditor"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
WindowStartupLocation="CenterScreen"
CanResize="False"
Width="960"
Height="640">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid>
@ -24,12 +21,13 @@
<Grid Grid.Column="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Margin="{adonisUi:Space 1, 0.5}">
<Grid Grid.Row="0"
Margin="8,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
@ -38,30 +36,54 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Endpoint" VerticalAlignment="Center" Margin="0 0 0 5" />
<TextBox Grid.Column="2" Margin="0 0 0 5" Text="{Binding Url, Mode=TwoWay}" TextChanged="OnTextChanged" />
<Button Grid.Column="4" Content="Send" HorizontalAlignment="Right" Margin="0 0 0 5"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Click="OnSend"/>
<TextBlock Grid.Column="0"
Text="Endpoint"
VerticalAlignment="Center"
Margin="0 0 0 5" />
<TextBox Grid.Column="2"
Margin="0 0 0 5"
Text="{Binding Url, Mode=TwoWay}"
TextChanged="OnTextChanged" />
<Button Grid.Column="4"
Content="Send"
HorizontalAlignment="Right"
Margin="0 0 0 5"
Click="OnSend" />
</Grid>
<avalonEdit:TextEditor x:Name="EndpointResponse" Grid.Row="2" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2" />
<avalonEdit:TextEditor x:Name="EndpointResponse"
Grid.Row="2"
Background="#2A2A3E"
FontFamily="Consolas"
FontSize="8pt"
IsReadOnly="True"
ShowLineNumbers="True"
Foreground="#DAE5F2" />
</Grid>
<Grid Grid.Column="2">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="5"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="5" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Vertical" Margin="10 5 10 10">
<TextBlock Text="Instruction" HorizontalAlignment="Center" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="InstructionBox" TextAlignment="Justify" TextWrapping="Wrap" HorizontalAlignment="Center" />
<StackPanel Grid.Row="0"
Orientation="Vertical"
Margin="10 5 10 10">
<TextBlock Text="Instruction"
HorizontalAlignment="Center"
FontSize="20"
FontWeight="SemiBold" />
<TextBlock x:Name="InstructionBox"
TextAlignment="Justify"
TextWrapping="Wrap"
HorizontalAlignment="Center" />
</StackPanel>
<Grid Grid.Row="1" Margin="{adonisUi:Space 1, 0.5}">
<Grid Grid.Row="1"
Margin="8,4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="10" />
@ -70,40 +92,79 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="Expression" VerticalAlignment="Center" Margin="0 0 0 5" />
<TextBox Grid.Column="2" Margin="0 0 0 5" Text="{Binding Path, Mode=TwoWay}" TextChanged="OnTextChanged" />
<Button Grid.Column="4" Content="Test" HorizontalAlignment="Right" Margin="0 0 0 5"
Style="{DynamicResource {x:Static adonisUi:Styles.AccentButton}}" Click="OnTest"/>
<TextBlock Grid.Column="0"
Text="Expression"
VerticalAlignment="Center"
Margin="0 0 0 5" />
<TextBox Grid.Column="2"
Margin="0 0 0 5"
Text="{Binding Path, Mode=TwoWay}"
TextChanged="OnTextChanged" />
<Button Grid.Column="4"
Content="Test"
HorizontalAlignment="Right"
Margin="0 0 0 5"
Click="OnTest" />
</Grid>
<avalonEdit:TextEditor x:Name="TargetResponse" Grid.Row="3" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
FontFamily="Consolas" FontSize="8pt" IsReadOnly="True" ShowLineNumbers="True" Foreground="#DAE5F2" />
<avalonEdit:TextEditor x:Name="TargetResponse"
Grid.Row="3"
Background="#2A2A3E"
FontFamily="Consolas"
FontSize="8pt"
IsReadOnly="True"
ShowLineNumbers="True"
Foreground="#DAE5F2" />
</Grid>
</Grid>
<Border Grid.Row="1"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer1BackgroundBrush}}"
adonisExtensions:LayerExtension.IncreaseLayer="True">
Background="#1A1A2E">
<Grid Margin="30, 12, 6, 12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Label}"
HorizontalAlignment="Right" VerticalAlignment="Center" FontSize="11" Margin="0 0 10 0" FontWeight="DemiBold"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.Layer1InteractionForegroundBrush}}" />
<TextBlock Grid.Column="0"
Text="{Binding Label}"
HorizontalAlignment="Right"
VerticalAlignment="Center"
FontSize="11"
Margin="0 0 10 0"
FontWeight="DemiBold"
Foreground="#E5C07B" />
<Button Grid.Column="1" 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="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Expression Syntax" Click="OnSyntax" />
<Button Grid.Column="3" MinWidth="78" Margin="0 0 12 0" IsDefault="False" IsCancel="False"
HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Online Evaluator" Click="OnEvaluator" />
<Button Grid.Column="1"
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="False"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Content="Expression Syntax"
Click="OnSyntax" />
<Button Grid.Column="3"
MinWidth="78"
Margin="0 0 12 0"
IsDefault="False"
IsCancel="False"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Content="Online Evaluator"
Click="OnEvaluator" />
</Grid>
</Border>
</Grid>
</adonisControls:AdonisWindow>
</Window>

View File

@ -1,5 +1,7 @@
using System;
using System.Diagnostics;
using System.Windows.Controls;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Threading;
using FModel.Extensions;
using FModel.Services;
@ -9,10 +11,11 @@ using Newtonsoft.Json;
namespace FModel.Views.Resources.Controls;
public partial class EndpointEditor
public partial class EndpointEditor : Window
{
private readonly EEndpointType _type;
private bool _isTested;
private bool _isInitialized;
public EndpointEditor(EndpointSettings endpoint, string title, EEndpointType type)
{
@ -26,6 +29,7 @@ public partial class EndpointEditor
TargetResponse.SyntaxHighlighting =
EndpointResponse.SyntaxHighlighting = AvalonExtensions.HighlighterSelector("json");
_isInitialized = true;
InstructionBox.Text = type switch
{
EEndpointType.Aes =>
@ -48,13 +52,12 @@ public partial class EndpointEditor
};
}
private void OnClick(object sender, RoutedEventArgs e)
private void OnClick(object? sender, RoutedEventArgs e)
{
DialogResult = _isTested && DataContext is EndpointSettings { IsValid: true };
Close();
Close(_isTested && DataContext is EndpointSettings { IsValid: true });
}
private async void OnSend(object sender, RoutedEventArgs e)
private async void OnSend(object? sender, RoutedEventArgs e)
{
if (DataContext is not EndpointSettings endpoint)
return;
@ -67,7 +70,7 @@ public partial class EndpointEditor
});
}
private void OnTest(object sender, RoutedEventArgs e)
private void OnTest(object? sender, RoutedEventArgs e)
{
if (DataContext is not EndpointSettings endpoint)
return;
@ -79,20 +82,21 @@ public partial class EndpointEditor
TargetResponse.Document.Text = JsonConvert.SerializeObject(response, Formatting.Indented);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (sender is not TextBox { IsLoaded: true } ||
if (!_isInitialized ||
sender is not TextBox ||
DataContext is not EndpointSettings endpoint)
return;
endpoint.IsValid = false;
}
private void OnSyntax(object sender, RoutedEventArgs e)
private void OnSyntax(object? sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo { FileName = "https://support.smartbear.com/alertsite/docs/monitors/api/endpoint/jsonpath.html", UseShellExecute = true });
}
private void OnEvaluator(object sender, RoutedEventArgs e)
private void OnEvaluator(object? sender, RoutedEventArgs e)
{
Process.Start(new ProcessStartInfo { FileName = "https://jsonpath.herokuapp.com/", UseShellExecute = true });
}

View File

@ -1,42 +1,151 @@
using System.Windows;
using System.Windows.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Threading;
using FModel.Services;
using FModel.ViewModels;
namespace FModel.Views.Resources.Controls;
public sealed class ListBoxItemBehavior
public static class ListBoxItemBehavior
{
public static readonly AttachedProperty<bool> IsBroughtIntoViewWhenSelectedProperty =
AvaloniaProperty.RegisterAttached<ListBoxItemBehavior, ListBoxItem, bool>("IsBroughtIntoViewWhenSelected");
public static readonly AttachedProperty<bool> OpenOnDoubleTapProperty =
AvaloniaProperty.RegisterAttached<ListBoxItemBehavior, ListBoxItem, bool>("OpenOnDoubleTap");
public static readonly AttachedProperty<bool> SelectFileOnRightClickProperty =
AvaloniaProperty.RegisterAttached<ListBoxItemBehavior, ListBoxItem, bool>("SelectFileOnRightClick");
public static bool GetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem)
{
return (bool) listBoxItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
}
=> listBoxItem.GetValue(IsBroughtIntoViewWhenSelectedProperty);
public static void SetIsBroughtIntoViewWhenSelected(ListBoxItem listBoxItem, bool value)
=> listBoxItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
public static bool GetOpenOnDoubleTap(ListBoxItem listBoxItem)
=> listBoxItem.GetValue(OpenOnDoubleTapProperty);
public static void SetOpenOnDoubleTap(ListBoxItem listBoxItem, bool value)
=> listBoxItem.SetValue(OpenOnDoubleTapProperty, value);
public static bool GetSelectFileOnRightClick(ListBoxItem listBoxItem)
=> listBoxItem.GetValue(SelectFileOnRightClickProperty);
public static void SetSelectFileOnRightClick(ListBoxItem listBoxItem, bool value)
=> listBoxItem.SetValue(SelectFileOnRightClickProperty, value);
static ListBoxItemBehavior()
{
listBoxItem.SetValue(IsBroughtIntoViewWhenSelectedProperty, value);
IsBroughtIntoViewWhenSelectedProperty.Changed.AddClassHandler<ListBoxItem>(OnIsBroughtIntoViewWhenSelectedChanged);
OpenOnDoubleTapProperty.Changed.AddClassHandler<ListBoxItem>(OnOpenOnDoubleTapChanged);
SelectFileOnRightClickProperty.Changed.AddClassHandler<ListBoxItem>(OnSelectFileOnRightClickChanged);
}
public static readonly DependencyProperty IsBroughtIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached("IsBroughtIntoViewWhenSelected", typeof(bool), typeof(ListBoxItemBehavior),
new UIPropertyMetadata(false, OnIsBroughtIntoViewWhenSelectedChanged));
private static void OnIsBroughtIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
private static void OnIsBroughtIntoViewWhenSelectedChanged(ListBoxItem item, AvaloniaPropertyChangedEventArgs e)
{
if (depObj is not ListBoxItem item)
return;
item.PropertyChanged -= OnListBoxItemPropertyChanged;
if (e.NewValue is not bool value)
return;
if (value)
item.Selected += OnListBoxItemSelected;
else
item.Selected -= OnListBoxItemSelected;
if (e.GetNewValue<bool>())
item.PropertyChanged += OnListBoxItemPropertyChanged;
}
private static void OnListBoxItemSelected(object sender, RoutedEventArgs e)
private static void OnOpenOnDoubleTapChanged(ListBoxItem item, AvaloniaPropertyChangedEventArgs e)
{
if (e.OriginalSource is ListBoxItem item)
item.DoubleTapped -= OnListBoxItemDoubleTapped;
if (e.GetNewValue<bool>())
item.DoubleTapped += OnListBoxItemDoubleTapped;
}
private static void OnSelectFileOnRightClickChanged(ListBoxItem item, AvaloniaPropertyChangedEventArgs e)
{
item.PointerPressed -= OnListBoxItemPointerPressed;
if (e.GetNewValue<bool>())
item.PointerPressed += OnListBoxItemPointerPressed;
}
private static void OnListBoxItemPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is ListBoxItem item &&
e.Property.Name == nameof(ListBoxItem.IsSelected) &&
item.IsSelected)
{
item.BringIntoView();
}
}
private static void OnListBoxItemDoubleTapped(object? sender, TappedEventArgs e)
{
if (sender is not ListBoxItem item)
return;
switch (item.DataContext)
{
case GameFileViewModel file:
ApplicationService.ApplicationView.SelectedLeftTabIndex = 2;
file.IsSelected = true;
_ = file.ExtractAsync();
break;
case TreeItem folder:
ApplicationService.ApplicationView.SelectedLeftTabIndex = 1;
var parent = folder.Parent;
while (parent != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
}
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
break;
}
}
private static void OnListBoxItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (sender is not ListBoxItem item)
return;
if (!e.GetCurrentPoint(item).Properties.IsRightButtonPressed)
return;
if (item.DataContext is not GameFileViewModel)
return;
var listBox = ItemsControl.ItemsControlFromItemContainer(item) as ListBox;
if (listBox == null)
return;
if (!item.IsSelected)
{
listBox.UnselectAll();
item.IsSelected = true;
}
item.Focus();
if (listBox.TryFindResource("FileContextMenu", out var resource) && resource is ContextMenu contextMenu)
{
listBox.ContextMenu = null;
Dispatcher.UIThread.Post(() =>
{
contextMenu.DataContext = listBox.DataContext;
listBox.ContextMenu = contextMenu;
contextMenu.PlacementTarget = listBox;
contextMenu.Open(listBox);
});
e.Handled = true;
}
}
}

View File

@ -1,13 +1,6 @@
using System.Windows;
using System.Windows.Controls;
// WPF's DataTemplateSelector has no direct Avalonia equivalent.
// Template selection is handled in code-behind (SettingsView.xaml.cs → OnSelectedItemChanged).
namespace FModel.Views.Resources.Controls;
public class OnTagDataTemplateSelector : DataTemplateSelector
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
{
if (item is not string s || container is not FrameworkElement f) return null;
return f.FindResource(s) as DataTemplate;
}
}
// kept as a stub so any stale XAML/project references don't cause build errors.
public sealed class OnTagDataTemplateSelector { }

View File

@ -1,16 +1,11 @@
<UserControl x:Class="FModel.Views.Resources.Controls.TiledExplorer.FileButton2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="128" d:DesignHeight="192"
d:DataContext="{d:DesignInstance Type=vm:GameFileViewModel, IsDesignTimeCreatable=False}"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
Width="128" Height="192"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer4BackgroundBrush}}">
Width="128"
Height="192"
Background="#1A1A2E">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -18,19 +13,16 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" Height="128" Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BorderBrush}}">
<Image Stretch="Uniform" Source="{Binding PreviewImage}" />
<Path x:Name="FallbackIcon" Width="64" Stretch="Uniform">
<Path.Style>
<Style TargetType="{x:Type Path}">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding PreviewImage}" Value="{x:Null}">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
<Grid Grid.Row="0"
Height="128"
Background="#2A2A3E">
<Image Stretch="Uniform"
Source="{Binding PreviewImage}"
IsVisible="{Binding PreviewImage, Converter={x:Static converters:IsNullToBoolReversedConverter.Instance}}" />
<Path x:Name="FallbackIcon"
Width="64"
Stretch="Uniform"
IsVisible="{Binding PreviewImage, Converter={x:Static converters:IsNullToBoolConverter.Instance}}">
<Path.Data>
<MultiBinding Converter="{x:Static converters:FileToGeometryConverter.Instance}">
<Binding Path="AssetCategory" />
@ -44,40 +36,31 @@
</MultiBinding>
</Path.Fill>
</Path>
<ContentPresenter Content="{Binding NumTextures}">
<ContentPresenter.Style>
<Style TargetType="ContentPresenter">
<Setter Property="ContentTemplate"
Value="{StaticResource TextureNumTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Content, RelativeSource={RelativeSource Self}}"
Value="0">
<Setter Property="Visibility"
Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Content, RelativeSource={RelativeSource Self}}"
Value="{x:Null}">
<Setter Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentPresenter.Style>
</ContentPresenter>
<ContentControl Content="{Binding NumTextures}"
ContentTemplate="{StaticResource TextureNumTemplate}"
IsVisible="{Binding NumTextures, Converter={x:Static converters:IntGreaterThanZeroConverter.Instance}}" />
</Grid>
<Rectangle Grid.Row="1" Fill="{Binding Fill, ElementName=FallbackIcon, FallbackValue=Red}" />
<Border Grid.Row="1"
Background="{Binding #FallbackIcon.Fill, FallbackValue=Red}" />
<Grid Grid.Row="2" Margin="5">
<Grid Grid.Row="2"
Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Asset.NameWithoutExtension, FallbackValue=Asset Name}"
FontSize="13" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" FontWeight="DemiBold"
TextAlignment="Left" HorizontalAlignment="Left" VerticalAlignment="Top"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"/>
<TextBlock Grid.Row="0"
Text="{Binding Asset.NameWithoutExtension, FallbackValue='Asset Name'}"
FontSize="13"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
FontWeight="DemiBold"
TextAlignment="Left"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Foreground="#DAE5F2" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
@ -86,15 +69,27 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="{Binding ResolvedAssetType, FallbackValue=Asset Type}"
FontSize="9" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" FontWeight="Normal"
TextAlignment="Left" HorizontalAlignment="Left" VerticalAlignment="Bottom"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
<TextBlock Grid.Column="0"
Text="{Binding ResolvedAssetType, FallbackValue='Asset Type'}"
FontSize="9"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
FontWeight="Normal"
TextAlignment="Left"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
Foreground="#888888" />
<TextBlock Grid.Column="2" Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}, FallbackValue=0 B}"
FontSize="9" TextWrapping="NoWrap" TextTrimming="CharacterEllipsis" FontWeight="Normal"
TextAlignment="Right" HorizontalAlignment="Right" VerticalAlignment="Bottom"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.DisabledForegroundBrush}}" />
<TextBlock Grid.Column="2"
Text="{Binding Asset.Size, Converter={x:Static converters:SizeToStringConverter.Instance}, FallbackValue='0 B'}"
FontSize="9"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
FontWeight="Normal"
TextAlignment="Right"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Foreground="#888888" />
</Grid>
</Grid>
</Grid>

View File

@ -1,4 +1,4 @@
using System.Windows.Controls;
using Avalonia.Controls;
namespace FModel.Views.Resources.Controls.TiledExplorer;

View File

@ -1,16 +1,11 @@
<UserControl x:Class="FModel.Views.Resources.Controls.TiledExplorer.FolderButton2"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="128" d:DesignHeight="192"
d:DataContext="{d:DesignInstance Type=vm:TreeItem, IsDesignTimeCreatable=False}"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
Width="128" Height="192"
Width="128"
Height="192"
Padding="5"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer2BackgroundBrush}}">
Background="#252535">
<Grid VerticalAlignment="Center">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -18,12 +13,15 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Viewbox Grid.Row="0" Height="96" VerticalAlignment="Center">
<Canvas Width="128" Height="128">
<Viewbox Grid.Row="0"
Height="96"
VerticalAlignment="Center">
<Canvas Width="128"
Height="128">
<Canvas.Resources>
<LinearGradientBrush x:Key="FolderTabGradient"
StartPoint="0,0"
EndPoint="0,1">
StartPoint="0%,0%"
EndPoint="0%,100%">
<GradientStop Color="#FFE8C480"
Offset="0" />
<GradientStop Color="#FFD9AA63"
@ -31,8 +29,8 @@
</LinearGradientBrush>
<LinearGradientBrush x:Key="FolderBodyGradient"
StartPoint="0,0"
EndPoint="0,1">
StartPoint="0%,0%"
EndPoint="0%,100%">
<GradientStop Color="#FFF3D9A4"
Offset="0" />
<GradientStop Color="#FFE5BD77"
@ -53,25 +51,26 @@
StrokeThickness="2"
Data="M13 60 H115 V104 C115 110 110 115 104 115 H24 C18 115 13 110 13 104 Z" />
<Ellipse Canvas.Left="90" Canvas.Top="90" Width="36" Height="36"
Opacity="0.95" Stroke="#b28c53" StrokeThickness="1.5"
Fill="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
Visibility="{Binding Visibility, ElementName=HintIcon}" />
<!-- Hint icon badge -->
<Ellipse Canvas.Left="90"
Canvas.Top="90"
Width="36"
Height="36"
Opacity="0.95"
Stroke="#b28c53"
StrokeThickness="1.5"
Fill="#2A2A3E"
IsVisible="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}, ConverterParameter=visible}" />
<Path x:Name="HintIcon" Canvas.Left="98" Canvas.Top="98" Width="20" Height="20" Stretch="Uniform"
<Path x:Name="HintIcon"
Canvas.Left="98"
Canvas.Top="98"
Width="20"
Height="20"
Stretch="Uniform"
Data="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}}"
Fill="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}}">
<Path.Style>
<Style TargetType="Path">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Data, RelativeSource={RelativeSource Self}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
Fill="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}}"
IsVisible="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}, ConverterParameter=visible}" />
</Canvas>
</Viewbox>
@ -84,26 +83,26 @@
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Text="{Binding Header, FallbackValue=Folder Name}"
Text="{Binding Header, FallbackValue='Folder Name'}"
FontSize="13"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis"
FontWeight="DemiBold"
TextAlignment="Center"
HorizontalAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}" />
Foreground="#DAE5F2" />
<TextBlock Grid.Row="2"
Text="{Binding Folders.Count, StringFormat=Subfolders: {0}}"
Text="{Binding Folders.Count, StringFormat='Subfolders: {0}'}"
FontSize="11"
HorizontalAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Foreground="#DAE5F2"
Opacity="0.8" />
<TextBlock Grid.Row="3"
Text="{Binding AssetsList.Assets.Count, StringFormat=Assets: {0}}"
Text="{Binding AssetsList.Assets.Count, StringFormat='Assets: {0}'}"
FontSize="11"
HorizontalAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"
Foreground="#DAE5F2"
Opacity="0.8" />
</Grid>
</Grid>

View File

@ -1,4 +1,4 @@
using System.Windows.Controls;
using Avalonia.Controls;
namespace FModel.Views.Resources.Controls.TiledExplorer;

View File

@ -1,15 +1,10 @@
<UserControl x:Class="FModel.Views.Resources.Controls.TiledExplorer.FolderButton3"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" d:DesignWidth="128" d:DesignHeight="192"
d:DataContext="{d:DesignInstance Type=vm:TreeItem, IsDesignTimeCreatable=False}"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
Width="128" Height="192"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer4BackgroundBrush}}">
Width="128"
Height="192"
Background="#1A1A2E">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
@ -17,58 +12,80 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Viewbox Grid.Row="0" Height="128">
<Canvas Width="128" Height="128">
<Viewbox Grid.Row="0"
Height="128">
<Canvas Width="128"
Height="128">
<Canvas.Resources>
<LinearGradientBrush x:Key="TabGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#c9975f" Offset="0" />
<GradientStop Color="#b28c53" Offset="0.5" />
<GradientStop Color="#a67f47" Offset="1" />
<LinearGradientBrush x:Key="TabGradient"
StartPoint="0%,0%"
EndPoint="0%,100%">
<GradientStop Color="#c9975f"
Offset="0" />
<GradientStop Color="#b28c53"
Offset="0.5" />
<GradientStop Color="#a67f47"
Offset="1" />
</LinearGradientBrush>
<LinearGradientBrush x:Key="BodyGradient" StartPoint="0,0" EndPoint="0,1">
<GradientStop Color="#d4ae75" Offset="0" />
<GradientStop Color="#c9a167" Offset="0.3" />
<GradientStop Color="#b89456" Offset="1" />
<LinearGradientBrush x:Key="BodyGradient"
StartPoint="0%,0%"
EndPoint="0%,100%">
<GradientStop Color="#d4ae75"
Offset="0" />
<GradientStop Color="#c9a167"
Offset="0.3" />
<GradientStop Color="#b89456"
Offset="1" />
</LinearGradientBrush>
</Canvas.Resources>
<Path Fill="{StaticResource TabGradient}" Data="M16.0018 36l0 -0.0701281 -0.00162402 -6.66681c-0.00509351,-0.616462 0.113829,-1.21757 0.342594,-1.77092 0.231423,-0.559917 0.572171,-1.07266 1.00696,-1.50532 0.433169,-0.431029 0.945252,-0.767794 1.50347,-0.997075 0.565823,-0.232308 1.16664,-0.355291 1.76937,-0.355291l21.1332 0c0.951601,0 1.85847,0.293947 2.61673,0.810088 0.736639,0.501452 1.3282,1.21875 1.67805,2.08502l0.00118111 -0.000442914 3.63056 8.46288 0.00346949 0.00797243 -33.6839 0z" />
<Path Fill="{StaticResource BodyGradient}" Data="M108.153 43.8407l-0.0177904 9.81969c0.785875,0.192373 1.50296,0.514888 2.10509,0.981201 1.08706,0.841681 1.75062,2.06176 1.75298,3.71021l-0.000664368 39.2644 0.00752957 0c0,1.58298 -0.652563,3.01883 -1.70758,4.06093 -1.045,1.03266 -2.48541,1.67126 -4.072,1.67126l-5.88529 0c-0.0760337,0.00967031 -0.159523,0.017126 -0.266782,0.017126l-76.1944 0c-2.15884,0 -4.12043,-0.871213 -5.54403,-2.27754 -1.43312,-1.4157 -2.32116,-3.36711 -2.32116,-5.51553l0.00752957 0 -0.0145423 -59.5726 33.6839 0 0.053814 0.125493 50.7405 0.0420768 0 -0.00745568c2.16629,0 4.07872,0.839394 5.45766,2.21856 1.38698,1.38706 2.22269,3.30945 2.22269,5.46209l-0.00752957 0z" />
<Path Fill="#b28c53" Data="M108 53.6604l-74.8763 0c-0.499091,0 -0.926356,0.196998 -1.22768,0.512895 -0.339715,0.356103 -0.542126,0.865894 -0.542126,1.43438l0 47.7579 -3.84043 0 0 -47.7579c0,-1.56880 0.600001,-3.01883 1.60733,-4.07466 1.00667,-1.05509 2.4017,-1.71304 4.00291,-1.71304l74.8763 0 0 3.84043z" />
<Path Fill="{StaticResource TabGradient}"
Data="M16.0018 36l0 -0.0701281 -0.00162402 -6.66681c-0.00509351,-0.616462 0.113829,-1.21757 0.342594,-1.77092 0.231423,-0.559917 0.572171,-1.07266 1.00696,-1.50532 0.433169,-0.431029 0.945252,-0.767794 1.50347,-0.997075 0.565823,-0.232308 1.16664,-0.355291 1.76937,-0.355291l21.1332 0c0.951601,0 1.85847,0.293947 2.61673,0.810088 0.736639,0.501452 1.3282,1.21875 1.67805,2.08502l0.00118111 -0.000442914 3.63056 8.46288 0.00346949 0.00797243 -33.6839 0z" />
<Path Fill="{StaticResource BodyGradient}"
Data="M108.153 43.8407l-0.0177904 9.81969c0.785875,0.192373 1.50296,0.514888 2.10509,0.981201 1.08706,0.841681 1.75062,2.06176 1.75298,3.71021l-0.000664368 39.2644 0.00752957 0c0,1.58298 -0.652563,3.01883 -1.70758,4.06093 -1.045,1.03266 -2.48541,1.67126 -4.072,1.67126l-5.88529 0c-0.0760337,0.00967031 -0.159523,0.017126 -0.266782,0.017126l-76.1944 0c-2.15884,0 -4.12043,-0.871213 -5.54403,-2.27754 -1.43312,-1.4157 -2.32116,-3.36711 -2.32116,-5.51553l0.00752957 0 -0.0145423 -59.5726 33.6839 0 0.053814 0.125493 50.7405 0.0420768 0 -0.00745568c2.16629,0 4.07872,0.839394 5.45766,2.21856 1.38698,1.38706 2.22269,3.30945 2.22269,5.46209l-0.00752957 0z" />
<Path Fill="#b28c53"
Data="M108 53.6604l-74.8763 0c-0.499091,0 -0.926356,0.196998 -1.22768,0.512895 -0.339715,0.356103 -0.542126,0.865894 -0.542126,1.43438l0 47.7579 -3.84043 0 0 -47.7579c0,-1.56880 0.600001,-3.01883 1.60733,-4.07466 1.00667,-1.05509 2.4017,-1.71304 4.00291,-1.71304l74.8763 0 0 3.84043z" />
<Ellipse Canvas.Left="80" Canvas.Top="80" Width="36" Height="36"
Opacity="0.95" Stroke="#b28c53" StrokeThickness="1.5"
Fill="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}"
Visibility="{Binding Visibility, ElementName=HintIcon}" />
<Ellipse Canvas.Left="80"
Canvas.Top="80"
Width="36"
Height="36"
Opacity="0.95"
Stroke="#b28c53"
StrokeThickness="1.5"
Fill="#2A2A3E"
IsVisible="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}, ConverterParameter=visible}" />
<Path x:Name="HintIcon" Canvas.Left="88" Canvas.Top="88" Width="20" Height="20" Stretch="Uniform"
<Path x:Name="HintIcon"
Canvas.Left="88"
Canvas.Top="88"
Width="20"
Height="20"
Stretch="Uniform"
Data="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}}"
Fill="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}}">
<Path.Style>
<Style TargetType="Path">
<Setter Property="Visibility" Value="Visible" />
<Style.Triggers>
<DataTrigger Binding="{Binding Data, RelativeSource={RelativeSource Self}}" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
Fill="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}}"
IsVisible="{Binding Header, Converter={x:Static converters:FolderToGeometryConverter.Instance}, ConverterParameter=visible}" />
</Canvas>
</Viewbox>
<Grid Grid.Row="2" Margin="5">
<Grid Grid.Row="2"
Margin="5">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="2" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Text="{Binding Header, FallbackValue=Folder Name}"
FontSize="13" TextWrapping="Wrap" TextTrimming="CharacterEllipsis" FontWeight="DemiBold"
TextAlignment="Center" HorizontalAlignment="Stretch" VerticalAlignment="Top"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ForegroundBrush}}"/>
<TextBlock Grid.Row="0"
Text="{Binding Header, FallbackValue='Folder Name'}"
FontSize="13"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
FontWeight="DemiBold"
TextAlignment="Center"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Foreground="#DAE5F2" />
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
@ -84,8 +101,10 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="#E8C480" />
<Border Grid.Column="2" Background="#A8D8EA" />
<Border Grid.Column="0"
Background="#E8C480" />
<Border Grid.Column="2"
Background="#A8D8EA" />
</Grid>
</Grid>
</Grid>

View File

@ -1,4 +1,4 @@
using System.Windows.Controls;
using Avalonia.Controls;
namespace FModel.Views.Resources.Controls.TiledExplorer;

View File

@ -1,155 +1,153 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vwp="clr-namespace:WpfToolkit.Controls;assembly=VirtualizingWrapPanel"
xmlns:vm="clr-namespace:FModel.ViewModels"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:local="clr-namespace:FModel.Views.Resources.Controls.TiledExplorer"
xmlns:adonisUi="clr-namespace:AdonisUI;assembly=AdonisUI"
xmlns:adonisExtensions="clr-namespace:AdonisUI.Extensions;assembly=AdonisUI"
xmlns:adonisConverters="clr-namespace:AdonisUI.Converters;assembly=AdonisUI"
x:Class="FModel.Views.Resources.Controls.TiledExplorer.ResourcesDictionary">
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:FModel.Views.Resources.Controls"
xmlns:local="clr-namespace:FModel.Views.Resources.Controls.TiledExplorer"
xmlns:converters="clr-namespace:FModel.Views.Resources.Converters"
x:Class="FModel.Views.Resources.Controls.TiledExplorer.ResourcesDictionary">
<controls:TypeDataTemplateSelector x:Key="TemplateSelector" />
<Style x:Key="TiledExplorer" TargetType="ListBox" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="local:SmoothScroll.IsEnabled" Value="True" />
<Setter Property="local:SmoothScroll.Factor" Value="1.25" />
<Setter Property="SelectionMode" Value="Extended" />
<Setter Property="ContextMenu" Value="{StaticResource FileContextMenu}" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="ScrollViewer.CanContentScroll" Value="True" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=OneWay}" />
<Setter Property="Margin" Value="5" />
<Setter Property="Padding" Value="5" />
<Setter Property="BorderThickness" Value="1.5" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="controls:ListBoxItemBehavior.IsBroughtIntoViewWhenSelected" Value="True" />
<Setter Property="Background" Value="{DynamicResource {x:Static adonisUi:Brushes.Layer0BackgroundBrush}}" />
<EventSetter Event="MouseDoubleClick" Handler="OnMouseDoubleClick" />
<EventSetter Event="PreviewMouseRightButtonDown" Handler="OnPreviewMouseRightButtonDown" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<Grid>
<Border x:Name="Border"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding adonisExtensions:CornerRadiusExtension.CornerRadius}">
<ContentPresenter />
</Border>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Setter.Value>
</Setter>
<Style x:Key="TiledExplorer"
Selector="ListBox">
<Setter Property="local:SmoothScroll.IsEnabled"
Value="True" />
<Setter Property="local:SmoothScroll.Factor"
Value="1.25" />
<!-- Avalonia Multiple = WPF Extended (Ctrl/Shift multi-select) -->
<Setter Property="SelectionMode"
Value="Multiple" />
<Setter Property="ContextMenu"
Value="{StaticResource FileContextMenu}" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility"
Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled" />
<Setter Property="BorderThickness"
Value="0" />
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<vwp:VirtualizingWrapPanel Orientation="Horizontal" SpacingMode="Uniform" ScrollUnit="Pixel" />
<!-- TODO(P3-perf): Replace with a virtualizing wrap layout
when an Avalonia-compatible option is adopted. -->
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<ContentControl Content="{Binding}" ContentTemplateSelector="{StaticResource TemplateSelector}" />
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate"
Value="{StaticResource TemplateSelector}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBox">
<Grid>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer Focusable="False">
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<ScrollViewer x:Name="PART_ScrollViewer"
Focusable="False"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsPresenter />
</ScrollViewer>
</Border>
</Grid>
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
TextAlignment="Center"
Foreground="#FF8A80"
Text="No folders or packages found"
IsHitTestVisible="False">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static converters:ItemsSourceEmptyToBoolConverter.Instance}">
<Binding Path="ItemsSource"
RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="Items.Count"
RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding ItemsSource, RelativeSource={RelativeSource Self}, Converter={x:Static adonisConverters:IsNullToBoolConverter.Instance}}" Value="False" />
<Condition Binding="{Binding Items.Count, RelativeSource={RelativeSource Self}, FallbackValue=0}" Value="0" />
</MultiDataTrigger.Conditions>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<TextBlock Text="No folders or packages found" FontWeight="SemiBold" TextAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ErrorBrush}}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</MultiDataTrigger>
</Style.Triggers>
<Style Selector="^ > ListBoxItem">
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=OneWay}" />
<Setter Property="Margin"
Value="5" />
<Setter Property="Padding"
Value="5" />
<Setter Property="BorderThickness"
Value="1.5" />
<Setter Property="Cursor"
Value="Hand" />
<Setter Property="Background"
Value="#252535" />
<Setter Property="BorderBrush"
Value="#3A3F57" />
<Setter Property="controls:ListBoxItemBehavior.IsBroughtIntoViewWhenSelected"
Value="True" />
<Setter Property="controls:ListBoxItemBehavior.OpenOnDoubleTap"
Value="True" />
<Setter Property="controls:ListBoxItemBehavior.SelectFileOnRightClick"
Value="True" />
</Style>
<Style Selector="^ > ListBoxItem:pointerover">
<Setter Property="BorderBrush"
Value="#575F7A" />
</Style>
<Style Selector="^ > ListBoxItem:selected">
<Setter Property="Background"
Value="#2E3850" />
<Setter Property="BorderBrush"
Value="#80B8FF" />
</Style>
</Style>
<DataTemplate x:Key="TiledFileDataTemplate" DataType="{x:Type vm:GameFileViewModel}">
<local:FileButton2 />
</DataTemplate>
<DataTemplate x:Key="TiledFolderDataTemplate" DataType="{x:Type vm:TreeItem}">
<local:FolderButton2 ContextMenu="{StaticResource FolderContextMenu}" />
</DataTemplate>
<DataTemplate x:Key="TextureNumTemplate">
<Border VerticalAlignment="Top"
HorizontalAlignment="Right"
Margin="4"
Padding="6,2"
CornerRadius="3"
Opacity="0.85"
Background="{DynamicResource {x:Static adonisUi:Brushes.Layer3BackgroundBrush}}">
HorizontalAlignment="Right"
Margin="4"
Padding="6,2"
CornerRadius="3"
Opacity="0.85"
Background="#2C3245">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding}"
Foreground="White"
FontSize="11"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="0,0,5,0" />
Foreground="White"
FontSize="11"
FontWeight="SemiBold"
VerticalAlignment="Center"
Margin="0,0,5,0" />
<Path Data="{StaticResource TextureIcon}"
Fill="White"
Width="12"
Height="12"
Stretch="Uniform"
VerticalAlignment="Center" />
Fill="White"
Width="12"
Height="12"
Stretch="Uniform"
VerticalAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
<Style x:Key="AssetsListBox" TargetType="ListBox" BasedOn="{StaticResource {x:Type ListBox}}">
<Setter Property="ItemsSource" Value="{Binding SelectedItem.AssetsList.AssetsView, ElementName=AssetsFolderName, IsAsync=True}" />
<Setter Property="SelectionMode" Value="Extended" />
<Setter Property="ContextMenu" Value="{StaticResource FileContextMenu}" />
<Setter Property="VirtualizingPanel.IsVirtualizing" Value="True" />
<Setter Property="VirtualizingPanel.VirtualizationMode" Value="Recycling" />
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarExpansionMode" Value="NeverExpand"/>
<Setter Property="adonisExtensions:ScrollViewerExtension.VerticalScrollBarPlacement" Value="Docked"/>
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarExpansionMode" Value="NeverExpand"/>
<Setter Property="adonisExtensions:ScrollViewerExtension.HorizontalScrollBarPlacement" Value="Docked"/>
<Style x:Key="AssetsListBox"
Selector="ListBox">
<Setter Property="ItemsSource"
Value="{Binding SelectedItem.AssetsList.AssetsView, ElementName=AssetsFolderName}" />
<!-- Avalonia Multiple = WPF Extended (Ctrl/Shift multi-select) -->
<Setter Property="SelectionMode"
Value="Multiple" />
<Setter Property="ContextMenu"
Value="{StaticResource FileContextMenu}" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility"
Value="Auto" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility"
Value="Disabled" />
<Setter Property="BorderThickness"
Value="0" />
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
@ -159,53 +157,66 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image x:Name="ListImage" Source="/FModel;component/Resources/unknown_asset.png"
Width="16" Height="16" HorizontalAlignment="Center" Margin="0 0 3 0" />
<TextBlock Grid.Column="1" HorizontalAlignment="Left" Text="{Binding Asset.Name}" />
<Image Source="{Binding Asset.Extension, Converter={x:Static converters:AssetExtensionToIconConverter.Instance}}"
Width="16"
Height="16"
HorizontalAlignment="Center"
Margin="0 0 3 0" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Left"
Text="{Binding Asset.Name}"
TextTrimming="CharacterEllipsis" />
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding Asset.Extension}" Value="uasset">
<Setter TargetName="ListImage" Property="Source" Value="/FModel;component/Resources/asset.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Asset.Extension}" Value="ini">
<Setter TargetName="ListImage" Property="Source" Value="/FModel;component/Resources/asset_ini.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Asset.Extension}" Value="png">
<Setter TargetName="ListImage" Property="Source" Value="/FModel;component/Resources/asset_png.png" />
</DataTrigger>
<DataTrigger Binding="{Binding Asset.Extension}" Value="psd">
<Setter TargetName="ListImage" Property="Source" Value="/FModel;component/Resources/asset_psd.png" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter Property="Template">
<Setter.Value>
<Style TargetType="{x:Type ListBoxItem}" BasedOn="{StaticResource {x:Type ListBoxItem}}">
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Padding" Value="5 3" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="controls:ListBoxItemBehavior.IsBroughtIntoViewWhenSelected" Value="True" />
<EventSetter Event="PreviewMouseRightButtonDown" Handler="OnPreviewMouseRightButtonDown" />
</Style>
<ControlTemplate>
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid>
<ScrollViewer x:Name="PART_ScrollViewer"
Focusable="False"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<ItemsPresenter />
</ScrollViewer>
<TextBlock HorizontalAlignment="Center"
VerticalAlignment="Center"
FontWeight="SemiBold"
TextAlignment="Center"
Foreground="#FF8A80"
Text="No packages found in folder"
IsHitTestVisible="False">
<TextBlock.IsVisible>
<MultiBinding Converter="{x:Static converters:ItemsSourceEmptyToBoolConverter.Instance}">
<Binding Path="ItemsSource"
RelativeSource="{RelativeSource TemplatedParent}" />
<Binding Path="Items.Count"
RelativeSource="{RelativeSource TemplatedParent}" />
</MultiBinding>
</TextBlock.IsVisible>
</TextBlock>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding Items.Count, RelativeSource={RelativeSource Self}, FallbackValue=0}" Value="0">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid>
<TextBlock Text="No packages found in folder" FontWeight="SemiBold" TextAlignment="Center"
Foreground="{DynamicResource {x:Static adonisUi:Brushes.ErrorBrush}}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
<Style Selector="^ > ListBoxItem">
<Setter Property="HorizontalContentAlignment"
Value="Stretch" />
<Setter Property="Padding"
Value="5 3" />
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="controls:ListBoxItemBehavior.IsBroughtIntoViewWhenSelected"
Value="True" />
<Setter Property="controls:ListBoxItemBehavior.SelectFileOnRightClick"
Value="True" />
</Style>
</Style>
</ResourceDictionary>

View File

@ -1,9 +1,4 @@
using System;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
using FModel.Services;
using FModel.ViewModels;
using Avalonia.Controls;
namespace FModel.Views.Resources.Controls.TiledExplorer;
@ -13,75 +8,4 @@ public partial class ResourcesDictionary
{
InitializeComponent();
}
private void OnMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
if (sender is not ListBoxItem item)
return;
switch (item.DataContext)
{
case GameFileViewModel file:
ApplicationService.ApplicationView.SelectedLeftTabIndex = 2;
file.IsSelected = true;
file.ExtractAsync();
break;
case TreeItem folder:
ApplicationService.ApplicationView.SelectedLeftTabIndex = 1;
// Expand all parent folders if not expanded
var parent = folder.Parent;
while (parent != null)
{
parent.IsExpanded = true;
parent = parent.Parent;
}
// Auto expand single child folders
var childFolder = folder;
while (childFolder.Folders.Count == 1 && childFolder.AssetsList.Assets.Count == 0)
{
childFolder.IsExpanded = true;
childFolder = childFolder.Folders[0];
}
childFolder.IsExpanded = true;
childFolder.IsSelected = true;
break;
}
}
// Hack to force re-evaluation of context menu options, also prevents menu flicker from happening
private void OnPreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
if (sender is not ListBoxItem item)
return;
if (item.DataContext is not GameFileViewModel)
return;
var listBox = ItemsControl.ItemsControlFromItemContainer(item) as ListBox;
if (listBox == null)
return;
if (!item.IsSelected)
{
listBox.UnselectAll();
item.IsSelected = true;
}
item.Focus();
var contextMenu = listBox.FindResource("FileContextMenu") as ContextMenu;
if (contextMenu is not null)
{
listBox.ContextMenu = null;
item.Dispatcher.BeginInvoke(new Action(() =>
{
contextMenu.DataContext = listBox.DataContext;
listBox.ContextMenu = contextMenu;
contextMenu.PlacementTarget = listBox;
contextMenu.IsOpen = true;
}), DispatcherPriority.Input);
}
e.Handled = true;
}
}

View File

@ -1,7 +1,10 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.VisualTree;
namespace FModel.Views.Resources.Controls.TiledExplorer;
@ -12,79 +15,94 @@ namespace FModel.Views.Resources.Controls.TiledExplorer;
/// </summary>
public static class SmoothScroll
{
public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.RegisterAttached(
"IsEnabled", typeof(bool), typeof(SmoothScroll), new PropertyMetadata(false, OnIsEnabledChanged));
public static readonly AttachedProperty<bool> IsEnabledProperty =
AvaloniaProperty.RegisterAttached<SmoothScroll, Control, bool>("IsEnabled");
public static readonly DependencyProperty FactorProperty = DependencyProperty.RegisterAttached(
"Factor", typeof(double), typeof(SmoothScroll), new PropertyMetadata(0.25));
public static readonly AttachedProperty<double> FactorProperty =
AvaloniaProperty.RegisterAttached<SmoothScroll, Control, double>("Factor", 0.25);
public static void SetIsEnabled(DependencyObject obj, bool value) => obj.SetValue(IsEnabledProperty, value);
public static bool GetIsEnabled(DependencyObject obj) => (bool)obj.GetValue(IsEnabledProperty);
public static void SetFactor(DependencyObject obj, double value) => obj.SetValue(FactorProperty, value);
public static double GetFactor(DependencyObject obj) => (double)obj.GetValue(FactorProperty);
public static void SetIsEnabled(Control obj, bool value) => obj.SetValue(IsEnabledProperty, value);
public static bool GetIsEnabled(Control obj) => obj.GetValue(IsEnabledProperty);
public static void SetFactor(Control obj, double value) => obj.SetValue(FactorProperty, value);
public static double GetFactor(Control obj) => obj.GetValue(FactorProperty);
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
private static readonly ConditionalWeakTable<Control, ScrollViewer> _scrollViewerCache = new();
static SmoothScroll()
{
if (d is UIElement element)
IsEnabledProperty.Changed.Subscribe(OnIsEnabledChanged);
}
private static void OnIsEnabledChanged(AvaloniaPropertyChangedEventArgs<bool> e)
{
if (e.Sender is not Control element)
return;
if (e.NewValue.GetValueOrDefault())
element.PointerWheelChanged += Element_PointerWheelChanged;
else
{
if ((bool)e.NewValue)
element.PreviewMouseWheel += Element_PreviewMouseWheel;
else
element.PreviewMouseWheel -= Element_PreviewMouseWheel;
element.PointerWheelChanged -= Element_PointerWheelChanged;
_scrollViewerCache.Remove(element);
}
}
private static void Element_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
private static void Element_PointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (sender is not DependencyObject dep) return;
if (sender is not Control control)
return;
var sv = FindScrollViewer(dep);
if (sv == null) return;
var sv = FindScrollViewer(control);
if (sv == null)
return;
double factor = GetFactor(dep);
if (double.IsNaN(factor) || factor <= 0) factor = 0.25;
double factor = GetFactor(control);
if (double.IsNaN(factor) || factor <= 0)
factor = 0.25;
// e.Delta is typically +/-120 per notch
double notches = e.Delta / 120.0;
// Base pixels per notch (tweakable); smaller value gives smoother/less jumpy scroll
double notches = e.Delta.Y;
const double basePixelsPerNotch = 50.0;
double adjustedPixels = notches * basePixelsPerNotch * factor;
// Prefer vertical scrolling when possible
if (sv.ScrollableHeight > 0)
var scrollableHeight = Math.Max(0, sv.Extent.Height - sv.Viewport.Height);
var scrollableWidth = Math.Max(0, sv.Extent.Width - sv.Viewport.Width);
if (scrollableHeight > 0)
{
double newOffset = sv.VerticalOffset - adjustedPixels;
if (newOffset < 0) newOffset = 0;
if (newOffset > sv.ScrollableHeight) newOffset = sv.ScrollableHeight;
sv.ScrollToVerticalOffset(newOffset);
double newOffset = sv.Offset.Y - adjustedPixels;
if (newOffset < 0)
newOffset = 0;
if (newOffset > scrollableHeight)
newOffset = scrollableHeight;
sv.Offset = sv.Offset.WithY(newOffset);
e.Handled = true;
return;
}
if (sv.ScrollableWidth > 0)
if (scrollableWidth > 0)
{
double newOffset = sv.HorizontalOffset - adjustedPixels;
if (newOffset < 0) newOffset = 0;
if (newOffset > sv.ScrollableWidth) newOffset = sv.ScrollableWidth;
sv.ScrollToHorizontalOffset(newOffset);
double newOffset = sv.Offset.X - adjustedPixels;
if (newOffset < 0)
newOffset = 0;
if (newOffset > scrollableWidth)
newOffset = scrollableWidth;
sv.Offset = sv.Offset.WithX(newOffset);
e.Handled = true;
}
}
private static ScrollViewer FindScrollViewer(DependencyObject d)
private static ScrollViewer? FindScrollViewer(Control control)
{
if (d == null) return null;
if (d is ScrollViewer sv) return sv;
if (control is ScrollViewer sv)
return sv;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(d); i++)
{
var child = VisualTreeHelper.GetChild(d, i);
var result = FindScrollViewer(child);
if (result != null) return result;
}
if (_scrollViewerCache.TryGetValue(control, out var cached))
return cached;
return null;
var found = control.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (found != null)
_scrollViewerCache.Add(control, found);
return found;
}
}

View File

@ -1,18 +1,43 @@
using System.Windows;
using System.Windows.Controls;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using FModel.ViewModels;
using FModel.Views.Resources.Controls.TiledExplorer;
namespace FModel.Views.Resources.Controls;
public class TypeDataTemplateSelector : DataTemplateSelector
public class TypeDataTemplateSelector : IDataTemplate
{
public override DataTemplate SelectTemplate(object item, DependencyObject container)
public Control? Build(object? item)
{
return item switch
{
TreeItem when container is FrameworkElement f => f.FindResource("TiledFolderDataTemplate") as DataTemplate,
GameFileViewModel when container is FrameworkElement f => f.FindResource("TiledFileDataTemplate") as DataTemplate,
_ => base.SelectTemplate(item, container)
TreeItem folder => BuildFolderControl(folder),
GameFileViewModel asset => new FileButton2 { DataContext = asset },
_ => null
};
}
public bool Match(object? data)
=> data is TreeItem or GameFileViewModel;
private static Control BuildFolderControl(TreeItem folder)
{
var control = new FolderButton2 { DataContext = folder };
// Resolve context menu from visual-tree resources so x:Shared="False"
// returns a fresh instance per folder control.
// One-shot: unsubscribe after first successful resolution.
control.AttachedToVisualTree += OnAttached;
return control;
void OnAttached(object? sender, Avalonia.VisualTree.VisualTreeAttachmentEventArgs e)
{
if (control.TryFindResource("FolderContextMenu", out var res) && res is ContextMenu menu)
{
control.ContextMenu = menu;
control.AttachedToVisualTree -= OnAttached;
}
}
}
}

View File

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia.Data.Converters;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace FModel.Views.Resources.Converters;
public class AssetExtensionToIconConverter : IValueConverter
{
public static readonly AssetExtensionToIconConverter Instance = new();
private static readonly Lazy<IReadOnlyDictionary<string, Bitmap>> CachedIcons = new(CreateIcons);
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
var extension = value as string ?? string.Empty;
return CachedIcons.Value.TryGetValue(extension, out var icon)
? icon
: CachedIcons.Value[string.Empty];
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
private static IReadOnlyDictionary<string, Bitmap> CreateIcons()
{
return new Dictionary<string, Bitmap>(StringComparer.OrdinalIgnoreCase)
{
[string.Empty] = LoadBitmap("unknown_asset.png"),
["uasset"] = LoadBitmap("asset.png"),
["ini"] = LoadBitmap("asset_ini.png"),
["png"] = LoadBitmap("asset_png.png"),
["psd"] = LoadBitmap("asset_psd.png")
};
}
private static Bitmap LoadBitmap(string fileName)
{
using var stream = AssetLoader.Open(new Uri($"avares://FModel/Resources/{fileName}"));
return new Bitmap(stream);
}
}

View File

@ -46,6 +46,9 @@ public class FolderToGeometryConverter : IValueConverter
_ => (null, "NeutralBrush"),
};
if (targetType == typeof(bool) || (parameter is string { Length: > 0 } p && p.Equals("visible", StringComparison.OrdinalIgnoreCase)))
return geometry != null;
if (targetType == typeof(Geometry) && geometry != null)
{
Application.Current!.TryGetResource(geometry, null, out var geomRes);

View File

@ -0,0 +1,16 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace FModel.Views.Resources.Converters;
public class IntGreaterThanZeroConverter : IValueConverter
{
public static readonly IntGreaterThanZeroConverter Instance = new();
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value is int n and > 0;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}

View File

@ -0,0 +1,16 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace FModel.Views.Resources.Converters;
public class IsNullToBoolConverter : IValueConverter
{
public static readonly IsNullToBoolConverter Instance = new();
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
=> value == null;
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
=> throw new NotImplementedException();
}

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
namespace FModel.Views.Resources.Converters;
public class ItemsSourceEmptyToBoolConverter : IMultiValueConverter
{
public static readonly ItemsSourceEmptyToBoolConverter Instance = new();
public object Convert(IList<object?> values, Type targetType, object? parameter, CultureInfo culture)
{
if (values.Count < 2)
return false;
var itemsSource = values[0];
if (itemsSource == null || ReferenceEquals(itemsSource, AvaloniaProperty.UnsetValue))
return false;
var rawCount = values[1];
if (rawCount == null || ReferenceEquals(rawCount, AvaloniaProperty.UnsetValue))
return false;
var count = rawCount switch
{
int intCount => intCount,
long longCount => (int) longCount,
_ => -1
};
return count == 0;
}
}

View File

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Data.Converters;
namespace FModel.Views.Resources.Converters;
@ -19,7 +20,8 @@ public class RatioToGridLengthConverter : IMultiValueConverter
var count2 = values[1] is int c2 ? c2 : 0;
var total = count1 + count2;
if (total == 0) return new GridLength(1, GridUnitType.Star);
if (total == 0)
return new GridLength(1, GridUnitType.Star);
var ratio = (double) count1 / total;
return new GridLength(ratio, GridUnitType.Star);

View File

@ -13,7 +13,6 @@
Title="Settings">
<Window.Resources>
<ResourceDictionary>
<controls:OnTagDataTemplateSelector x:Key="TagTemplateSelector" />
<DataTemplate x:Key="GeneralTemplate">
<Grid>
<Grid.RowDefinitions>
@ -1181,7 +1180,7 @@
Grid.Column="1"
Margin="10,5"
HorizontalAlignment="Stretch">
<ContentControl ContentTemplateSelector="{StaticResource TagTemplateSelector}"
<ContentControl x:Name="SettingsContentControl"
Content="{Binding SelectedItem.Tag, ElementName=SettingsTree}" />
</Grid>

View File

@ -40,7 +40,7 @@ public partial class SettingsView : Window
{
var restart = _applicationView.SettingsView.Save(out var whatShouldIDo);
if (restart)
_applicationView.RestartWithWarning();
await _applicationView.RestartWithWarningAsync(this);
Close();
@ -165,60 +165,61 @@ public partial class SettingsView : Window
}
UserSettings.Default.LastOpenedSettingTab = i;
// Select the DataTemplate that matches the TreeViewItem's Tag.
if (treeItem.Tag is string tagKey &&
this.TryFindResource(tagKey, out var resource) &&
resource is Avalonia.Controls.Templates.IDataTemplate dt)
{
SettingsContentControl.ContentTemplate = dt;
}
else
{
// Clear stale template if a tag key is missing or invalid.
SettingsContentControl.ContentTemplate = null;
}
break;
}
}
private async void OpenCustomVersions(object sender, RoutedEventArgs e)
{
// TODO(P2-016): DictionaryEditor not yet migrated to Avalonia.
// Once migrated, replace body with:
// var editor = new DictionaryEditor(_applicationView.SettingsView.SelectedCustomVersions, "Versioning Configuration (Custom Versions)");
// if (await editor.ShowDialog<bool?>(this) != true) return;
// _applicationView.SettingsView.SelectedCustomVersions = editor.CustomVersions;
await Task.CompletedTask;
var editor = new DictionaryEditor(_applicationView.SettingsView.SelectedCustomVersions, "Versioning Configuration (Custom Versions)");
if (await editor.ShowDialog<bool?>(this) != true)
return;
_applicationView.SettingsView.SelectedCustomVersions = editor.CustomVersions;
}
private async void OpenOptions(object sender, RoutedEventArgs e)
{
// TODO(P2-016): DictionaryEditor not yet migrated to Avalonia.
// Once migrated, replace body with:
// var editor = new DictionaryEditor(_applicationView.SettingsView.SelectedOptions, "Versioning Configuration (Options)");
// if (await editor.ShowDialog<bool?>(this) != true) return;
// _applicationView.SettingsView.SelectedOptions = editor.Options;
await Task.CompletedTask;
var editor = new DictionaryEditor(_applicationView.SettingsView.SelectedOptions, "Versioning Configuration (Options)");
if (await editor.ShowDialog<bool?>(this) != true)
return;
_applicationView.SettingsView.SelectedOptions = editor.Options;
}
private async void OpenMapStructTypes(object sender, RoutedEventArgs e)
{
// TODO(P2-016): DictionaryEditor not yet migrated to Avalonia.
// Once migrated, replace body with:
// var editor = new DictionaryEditor(_applicationView.SettingsView.SelectedMapStructTypes, "Versioning Configuration (MapStructTypes)");
// if (await editor.ShowDialog<bool?>(this) != true) return;
// _applicationView.SettingsView.SelectedMapStructTypes = editor.MapStructTypes;
await Task.CompletedTask;
var editor = new DictionaryEditor(_applicationView.SettingsView.SelectedMapStructTypes, "Versioning Configuration (MapStructTypes)");
if (await editor.ShowDialog<bool?>(this) != true)
return;
_applicationView.SettingsView.SelectedMapStructTypes = editor.MapStructTypes;
}
private async void OpenAesEndpoint(object sender, RoutedEventArgs e)
{
// TODO(P2-016): EndpointEditor not yet migrated to Avalonia.
// Note: the original WPF code used ShowDialog() (modal). Preserve modal behaviour
// in the migration — use ShowDialog(this), not Show().
// Once migrated, replace body with:
// var editor = new EndpointEditor(_applicationView.SettingsView.AesEndpoint, "Endpoint Configuration (AES)", EEndpointType.Aes);
// await editor.ShowDialog(this);
await Task.CompletedTask;
var editor = new EndpointEditor(_applicationView.SettingsView.AesEndpoint, "Endpoint Configuration (AES)", EEndpointType.Aes);
await editor.ShowDialog<bool?>(this);
}
private async void OpenMappingEndpoint(object sender, RoutedEventArgs e)
{
// TODO(P2-016): EndpointEditor not yet migrated to Avalonia.
// Note: the original WPF code used ShowDialog() (modal). Preserve modal behaviour
// in the migration — use ShowDialog(this), not Show().
// Once migrated, replace body with:
// var editor = new EndpointEditor(_applicationView.SettingsView.MappingEndpoint, "Endpoint Configuration (Mapping)", EEndpointType.Mapping);
// await editor.ShowDialog(this);
await Task.CompletedTask;
var editor = new EndpointEditor(_applicationView.SettingsView.MappingEndpoint, "Endpoint Configuration (Mapping)", EEndpointType.Mapping);
await editor.ShowDialog<bool?>(this);
}
private void CriwareKeyBox_Loaded(object sender, RoutedEventArgs e)