mirror of
https://github.com/4sval/FModel.git
synced 2026-04-05 00:26:17 -05:00
* 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:
parent
c8a8b79fd9
commit
c5bc12e618
|
|
@ -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);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,6 +250,6 @@ public partial class ImageMerger : Window
|
|||
|
||||
private void OnCopyImage(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ClipboardExtensions.SetImage(_imageBuffer, FILENAME);
|
||||
ClipboardExtensions.SetImage(_imageBuffer);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Windows.Controls;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace FModel.Views.Resources.Controls.TiledExplorer;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Windows.Controls;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace FModel.Views.Resources.Controls.TiledExplorer;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Windows.Controls;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace FModel.Views.Resources.Controls.TiledExplorer;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
16
FModel/Views/Resources/Converters/IsNullToBoolConverter.cs
Normal file
16
FModel/Views/Resources/Converters/IsNullToBoolConverter.cs
Normal 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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user