JT
11 months ago
234 changed files with 11244 additions and 5271 deletions
@ -0,0 +1,16 @@ |
|||||||
|
<Project Sdk="Microsoft.NET.Sdk"> |
||||||
|
<PropertyGroup> |
||||||
|
<TargetFramework>net8.0</TargetFramework> |
||||||
|
<LangVersion>latest</LangVersion> |
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> |
||||||
|
<RuntimeIdentifiers>win-x64;linux-x64;osx-x64;osx-arm64</RuntimeIdentifiers> |
||||||
|
<ImplicitUsings>enable</ImplicitUsings> |
||||||
|
<Nullable>enable</Nullable> |
||||||
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport> |
||||||
|
<EnableWindowsTargeting>true</EnableWindowsTargeting> |
||||||
|
</PropertyGroup> |
||||||
|
<ItemGroup> |
||||||
|
<PackageReference Include="Avalonia" Version="11.0.5" /> |
||||||
|
</ItemGroup> |
||||||
|
|
||||||
|
</Project> |
@ -0,0 +1,10 @@ |
|||||||
|
namespace Avalonia.Gif |
||||||
|
{ |
||||||
|
internal enum BgWorkerCommand |
||||||
|
{ |
||||||
|
Null, |
||||||
|
Play, |
||||||
|
Pause, |
||||||
|
Dispose |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,12 @@ |
|||||||
|
namespace Avalonia.Gif |
||||||
|
{ |
||||||
|
internal enum BgWorkerState |
||||||
|
{ |
||||||
|
Null, |
||||||
|
Start, |
||||||
|
Running, |
||||||
|
Paused, |
||||||
|
Complete, |
||||||
|
Dispose |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
internal enum BlockTypes |
||||||
|
{ |
||||||
|
Empty = 0, |
||||||
|
Extension = 0x21, |
||||||
|
ImageDescriptor = 0x2C, |
||||||
|
Trailer = 0x3B, |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
internal enum ExtensionType |
||||||
|
{ |
||||||
|
GraphicsControl = 0xF9, |
||||||
|
Application = 0xFF |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,10 @@ |
|||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
public enum FrameDisposal |
||||||
|
{ |
||||||
|
Unknown = 0, |
||||||
|
Leave = 1, |
||||||
|
Background = 2, |
||||||
|
Restore = 3 |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
using System.Runtime.InteropServices; |
||||||
|
|
||||||
|
namespace Avalonia.Gif |
||||||
|
{ |
||||||
|
[StructLayout(LayoutKind.Explicit)] |
||||||
|
public readonly struct GifColor |
||||||
|
{ |
||||||
|
[FieldOffset(3)] |
||||||
|
public readonly byte A; |
||||||
|
|
||||||
|
[FieldOffset(2)] |
||||||
|
public readonly byte R; |
||||||
|
|
||||||
|
[FieldOffset(1)] |
||||||
|
public readonly byte G; |
||||||
|
|
||||||
|
[FieldOffset(0)] |
||||||
|
public readonly byte B; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// A struct that represents a ARGB color and is aligned as |
||||||
|
/// a BGRA bytefield in memory. |
||||||
|
/// </summary> |
||||||
|
/// <param name="r">Red</param> |
||||||
|
/// <param name="g">Green</param> |
||||||
|
/// <param name="b">Blue</param> |
||||||
|
/// <param name="a">Alpha</param> |
||||||
|
public GifColor(byte r, byte g, byte b, byte a = byte.MaxValue) |
||||||
|
{ |
||||||
|
A = a; |
||||||
|
R = r; |
||||||
|
G = g; |
||||||
|
B = b; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,653 @@ |
|||||||
|
// This source file's Lempel-Ziv-Welch algorithm is derived from Chromium's Android GifPlayer |
||||||
|
// as seen here (https://github.com/chromium/chromium/blob/master/third_party/gif_player/src/jp/tomorrowkey/android/gifplayer) |
||||||
|
// Licensed under the Apache License, Version 2.0 (https://www.apache.org/licenses/LICENSE-2.0) |
||||||
|
// Copyright (C) 2015 The Gifplayer Authors. All Rights Reserved. |
||||||
|
|
||||||
|
// The rest of the source file is licensed under MIT License. |
||||||
|
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. |
||||||
|
|
||||||
|
using System; |
||||||
|
using System.Buffers; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
using System.Runtime.InteropServices; |
||||||
|
using System.Text; |
||||||
|
using System.Threading; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Media.Imaging; |
||||||
|
using static Avalonia.Gif.Extensions.StreamExtensions; |
||||||
|
|
||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
public sealed class GifDecoder : IDisposable |
||||||
|
{ |
||||||
|
private static readonly ReadOnlyMemory<byte> G87AMagic = "GIF87a"u8.ToArray().AsMemory(); |
||||||
|
|
||||||
|
private static readonly ReadOnlyMemory<byte> G89AMagic = "GIF89a"u8.ToArray().AsMemory(); |
||||||
|
|
||||||
|
private static readonly ReadOnlyMemory<byte> NetscapeMagic = "NETSCAPE2.0"u8.ToArray().AsMemory(); |
||||||
|
|
||||||
|
private static readonly TimeSpan FrameDelayThreshold = TimeSpan.FromMilliseconds(10); |
||||||
|
private static readonly TimeSpan FrameDelayDefault = TimeSpan.FromMilliseconds(100); |
||||||
|
private static readonly GifColor TransparentColor = new(0, 0, 0, 0); |
||||||
|
private static readonly int MaxTempBuf = 768; |
||||||
|
private static readonly int MaxStackSize = 4096; |
||||||
|
private static readonly int MaxBits = 4097; |
||||||
|
|
||||||
|
private readonly Stream _fileStream; |
||||||
|
private readonly CancellationToken _currentCtsToken; |
||||||
|
private readonly bool _hasFrameBackups; |
||||||
|
|
||||||
|
private int _gctSize, |
||||||
|
_bgIndex, |
||||||
|
_prevFrame = -1, |
||||||
|
_backupFrame = -1; |
||||||
|
private bool _gctUsed; |
||||||
|
|
||||||
|
private GifRect _gifDimensions; |
||||||
|
|
||||||
|
// private ulong _globalColorTable; |
||||||
|
private readonly int _backBufferBytes; |
||||||
|
private GifColor[] _bitmapBackBuffer; |
||||||
|
|
||||||
|
private short[] _prefixBuf; |
||||||
|
private byte[] _suffixBuf; |
||||||
|
private byte[] _pixelStack; |
||||||
|
private byte[] _indexBuf; |
||||||
|
private byte[] _backupFrameIndexBuf; |
||||||
|
private volatile bool _hasNewFrame; |
||||||
|
|
||||||
|
public GifHeader Header { get; private set; } |
||||||
|
|
||||||
|
public readonly List<GifFrame> Frames = new(); |
||||||
|
|
||||||
|
public PixelSize Size => new PixelSize(Header.Dimensions.Width, Header.Dimensions.Height); |
||||||
|
|
||||||
|
public GifDecoder(Stream fileStream, CancellationToken currentCtsToken) |
||||||
|
{ |
||||||
|
_fileStream = fileStream; |
||||||
|
_currentCtsToken = currentCtsToken; |
||||||
|
|
||||||
|
ProcessHeaderData(); |
||||||
|
ProcessFrameData(); |
||||||
|
|
||||||
|
Header.IterationCount = Header.Iterations switch |
||||||
|
{ |
||||||
|
-1 => new GifRepeatBehavior { Count = 1 }, |
||||||
|
0 => new GifRepeatBehavior { LoopForever = true }, |
||||||
|
> 0 => new GifRepeatBehavior { Count = Header.Iterations }, |
||||||
|
_ => Header.IterationCount |
||||||
|
}; |
||||||
|
|
||||||
|
var pixelCount = _gifDimensions.TotalPixels; |
||||||
|
|
||||||
|
_hasFrameBackups = Frames.Any(f => f.FrameDisposalMethod == FrameDisposal.Restore); |
||||||
|
|
||||||
|
_bitmapBackBuffer = new GifColor[pixelCount]; |
||||||
|
_indexBuf = new byte[pixelCount]; |
||||||
|
|
||||||
|
if (_hasFrameBackups) |
||||||
|
_backupFrameIndexBuf = new byte[pixelCount]; |
||||||
|
|
||||||
|
_prefixBuf = new short[MaxStackSize]; |
||||||
|
_suffixBuf = new byte[MaxStackSize]; |
||||||
|
_pixelStack = new byte[MaxStackSize + 1]; |
||||||
|
|
||||||
|
_backBufferBytes = pixelCount * Marshal.SizeOf(typeof(GifColor)); |
||||||
|
} |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
Frames.Clear(); |
||||||
|
|
||||||
|
_bitmapBackBuffer = null; |
||||||
|
_prefixBuf = null; |
||||||
|
_suffixBuf = null; |
||||||
|
_pixelStack = null; |
||||||
|
_indexBuf = null; |
||||||
|
_backupFrameIndexBuf = null; |
||||||
|
} |
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
private int PixCoord(int x, int y) => x + y * _gifDimensions.Width; |
||||||
|
|
||||||
|
static readonly (int Start, int Step)[] Pass = { (0, 8), (4, 8), (2, 4), (1, 2) }; |
||||||
|
|
||||||
|
private void ClearImage() |
||||||
|
{ |
||||||
|
Array.Fill(_bitmapBackBuffer, TransparentColor); |
||||||
|
//ClearArea(_gifDimensions); |
||||||
|
|
||||||
|
_prevFrame = -1; |
||||||
|
_backupFrame = -1; |
||||||
|
} |
||||||
|
|
||||||
|
public void RenderFrame(int fIndex, WriteableBitmap writeableBitmap, bool forceClear = false) |
||||||
|
{ |
||||||
|
if (_currentCtsToken.IsCancellationRequested) |
||||||
|
return; |
||||||
|
|
||||||
|
if (fIndex < 0 | fIndex >= Frames.Count) |
||||||
|
return; |
||||||
|
|
||||||
|
if (_prevFrame == fIndex) |
||||||
|
return; |
||||||
|
|
||||||
|
if (fIndex == 0 || forceClear || fIndex < _prevFrame) |
||||||
|
ClearImage(); |
||||||
|
|
||||||
|
DisposePreviousFrame(); |
||||||
|
|
||||||
|
_prevFrame++; |
||||||
|
|
||||||
|
// render intermediate frame |
||||||
|
for (int idx = _prevFrame; idx < fIndex; ++idx) |
||||||
|
{ |
||||||
|
var prevFrame = Frames[idx]; |
||||||
|
|
||||||
|
if (prevFrame.FrameDisposalMethod == FrameDisposal.Restore) |
||||||
|
continue; |
||||||
|
|
||||||
|
if (prevFrame.FrameDisposalMethod == FrameDisposal.Background) |
||||||
|
{ |
||||||
|
ClearArea(prevFrame.Dimensions); |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
RenderFrameAt(idx, writeableBitmap); |
||||||
|
} |
||||||
|
|
||||||
|
RenderFrameAt(fIndex, writeableBitmap); |
||||||
|
} |
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
private void RenderFrameAt(int idx, WriteableBitmap writeableBitmap) |
||||||
|
{ |
||||||
|
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf); |
||||||
|
|
||||||
|
var curFrame = Frames[idx]; |
||||||
|
DecompressFrameToIndexBuffer(curFrame, _indexBuf, tmpB); |
||||||
|
|
||||||
|
if (_hasFrameBackups & curFrame.ShouldBackup) |
||||||
|
{ |
||||||
|
Buffer.BlockCopy(_indexBuf, 0, _backupFrameIndexBuf, 0, curFrame.Dimensions.TotalPixels); |
||||||
|
_backupFrame = idx; |
||||||
|
} |
||||||
|
|
||||||
|
DrawFrame(curFrame, _indexBuf); |
||||||
|
|
||||||
|
_prevFrame = idx; |
||||||
|
_hasNewFrame = true; |
||||||
|
|
||||||
|
using var lockedBitmap = writeableBitmap.Lock(); |
||||||
|
WriteBackBufToFb(lockedBitmap.Address); |
||||||
|
|
||||||
|
ArrayPool<byte>.Shared.Return(tmpB); |
||||||
|
} |
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
private void DrawFrame(GifFrame curFrame, Memory<byte> frameIndexSpan) |
||||||
|
{ |
||||||
|
var activeColorTable = curFrame.IsLocalColorTableUsed ? curFrame.LocalColorTable : Header.GlobarColorTable; |
||||||
|
|
||||||
|
var cX = curFrame.Dimensions.X; |
||||||
|
var cY = curFrame.Dimensions.Y; |
||||||
|
var cH = curFrame.Dimensions.Height; |
||||||
|
var cW = curFrame.Dimensions.Width; |
||||||
|
var tC = curFrame.TransparentColorIndex; |
||||||
|
var hT = curFrame.HasTransparency; |
||||||
|
|
||||||
|
if (curFrame.IsInterlaced) |
||||||
|
{ |
||||||
|
for (var i = 0; i < 4; i++) |
||||||
|
{ |
||||||
|
var curPass = Pass[i]; |
||||||
|
var y = curPass.Start; |
||||||
|
while (y < cH) |
||||||
|
{ |
||||||
|
DrawRow(y); |
||||||
|
y += curPass.Step; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
for (var i = 0; i < cH; i++) |
||||||
|
DrawRow(i); |
||||||
|
} |
||||||
|
|
||||||
|
//for (var row = 0; row < cH; row++) |
||||||
|
void DrawRow(int row) |
||||||
|
{ |
||||||
|
// Get the starting point of the current row on frame's index stream. |
||||||
|
var indexOffset = row * cW; |
||||||
|
|
||||||
|
// Get the target backbuffer offset from the frames coords. |
||||||
|
var targetOffset = PixCoord(cX, row + cY); |
||||||
|
var len = _bitmapBackBuffer.Length; |
||||||
|
|
||||||
|
for (var i = 0; i < cW; i++) |
||||||
|
{ |
||||||
|
var indexColor = frameIndexSpan.Span[indexOffset + i]; |
||||||
|
|
||||||
|
if (activeColorTable == null || targetOffset >= len || indexColor > activeColorTable.Length) |
||||||
|
return; |
||||||
|
|
||||||
|
if (!(hT & indexColor == tC)) |
||||||
|
_bitmapBackBuffer[targetOffset] = activeColorTable[indexColor]; |
||||||
|
|
||||||
|
targetOffset++; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
private void DisposePreviousFrame() |
||||||
|
{ |
||||||
|
if (_prevFrame == -1) |
||||||
|
return; |
||||||
|
|
||||||
|
var prevFrame = Frames[_prevFrame]; |
||||||
|
|
||||||
|
switch (prevFrame.FrameDisposalMethod) |
||||||
|
{ |
||||||
|
case FrameDisposal.Background: |
||||||
|
ClearArea(prevFrame.Dimensions); |
||||||
|
break; |
||||||
|
case FrameDisposal.Restore: |
||||||
|
if (_hasFrameBackups && _backupFrame != -1) |
||||||
|
DrawFrame(Frames[_backupFrame], _backupFrameIndexBuf); |
||||||
|
else |
||||||
|
ClearArea(prevFrame.Dimensions); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
private void ClearArea(GifRect area) |
||||||
|
{ |
||||||
|
for (var y = 0; y < area.Height; y++) |
||||||
|
{ |
||||||
|
var targetOffset = PixCoord(area.X, y + area.Y); |
||||||
|
for (var x = 0; x < area.Width; x++) |
||||||
|
_bitmapBackBuffer[targetOffset + x] = TransparentColor; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
private void DecompressFrameToIndexBuffer(GifFrame curFrame, Span<byte> indexSpan, byte[] tempBuf) |
||||||
|
{ |
||||||
|
_fileStream.Position = curFrame.LzwStreamPosition; |
||||||
|
var totalPixels = curFrame.Dimensions.TotalPixels; |
||||||
|
|
||||||
|
// Initialize GIF data stream decoder. |
||||||
|
var dataSize = curFrame.LzwMinCodeSize; |
||||||
|
var clear = 1 << dataSize; |
||||||
|
var endOfInformation = clear + 1; |
||||||
|
var available = clear + 2; |
||||||
|
var oldCode = -1; |
||||||
|
var codeSize = dataSize + 1; |
||||||
|
var codeMask = (1 << codeSize) - 1; |
||||||
|
|
||||||
|
for (var code = 0; code < clear; code++) |
||||||
|
{ |
||||||
|
_prefixBuf[code] = 0; |
||||||
|
_suffixBuf[code] = (byte)code; |
||||||
|
} |
||||||
|
|
||||||
|
// Decode GIF pixel stream. |
||||||
|
int bits, |
||||||
|
first, |
||||||
|
top, |
||||||
|
pixelIndex; |
||||||
|
var datum = bits = first = top = pixelIndex = 0; |
||||||
|
|
||||||
|
while (pixelIndex < totalPixels) |
||||||
|
{ |
||||||
|
var blockSize = _fileStream.ReadBlock(tempBuf); |
||||||
|
|
||||||
|
if (blockSize == 0) |
||||||
|
break; |
||||||
|
|
||||||
|
var blockPos = 0; |
||||||
|
|
||||||
|
while (blockPos < blockSize) |
||||||
|
{ |
||||||
|
datum += tempBuf[blockPos] << bits; |
||||||
|
blockPos++; |
||||||
|
|
||||||
|
bits += 8; |
||||||
|
|
||||||
|
while (bits >= codeSize) |
||||||
|
{ |
||||||
|
// Get the next code. |
||||||
|
var code = datum & codeMask; |
||||||
|
datum >>= codeSize; |
||||||
|
bits -= codeSize; |
||||||
|
|
||||||
|
// Interpret the code |
||||||
|
if (code == clear) |
||||||
|
{ |
||||||
|
// Reset decoder. |
||||||
|
codeSize = dataSize + 1; |
||||||
|
codeMask = (1 << codeSize) - 1; |
||||||
|
available = clear + 2; |
||||||
|
oldCode = -1; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
// Check for explicit end-of-stream |
||||||
|
if (code == endOfInformation) |
||||||
|
return; |
||||||
|
|
||||||
|
if (oldCode == -1) |
||||||
|
{ |
||||||
|
indexSpan[pixelIndex++] = _suffixBuf[code]; |
||||||
|
oldCode = code; |
||||||
|
first = code; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
var inCode = code; |
||||||
|
if (code >= available) |
||||||
|
{ |
||||||
|
_pixelStack[top++] = (byte)first; |
||||||
|
code = oldCode; |
||||||
|
|
||||||
|
if (top == MaxBits) |
||||||
|
ThrowException(); |
||||||
|
} |
||||||
|
|
||||||
|
while (code >= clear) |
||||||
|
{ |
||||||
|
if (code >= MaxBits || code == _prefixBuf[code]) |
||||||
|
ThrowException(); |
||||||
|
|
||||||
|
_pixelStack[top++] = _suffixBuf[code]; |
||||||
|
code = _prefixBuf[code]; |
||||||
|
|
||||||
|
if (top == MaxBits) |
||||||
|
ThrowException(); |
||||||
|
} |
||||||
|
|
||||||
|
first = _suffixBuf[code]; |
||||||
|
_pixelStack[top++] = (byte)first; |
||||||
|
|
||||||
|
// Add new code to the dictionary |
||||||
|
if (available < MaxStackSize) |
||||||
|
{ |
||||||
|
_prefixBuf[available] = (short)oldCode; |
||||||
|
_suffixBuf[available] = (byte)first; |
||||||
|
available++; |
||||||
|
|
||||||
|
if ((available & codeMask) == 0 && available < MaxStackSize) |
||||||
|
{ |
||||||
|
codeSize++; |
||||||
|
codeMask += available; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
oldCode = inCode; |
||||||
|
|
||||||
|
// Drain the pixel stack. |
||||||
|
do |
||||||
|
{ |
||||||
|
indexSpan[pixelIndex++] = _pixelStack[--top]; |
||||||
|
} while (top > 0); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
while (pixelIndex < totalPixels) |
||||||
|
indexSpan[pixelIndex++] = 0; // clear missing pixels |
||||||
|
|
||||||
|
void ThrowException() => throw new LzwDecompressionException(); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Directly copies the <see cref="GifColor"/> struct array to a bitmap IntPtr. |
||||||
|
/// </summary> |
||||||
|
private void WriteBackBufToFb(IntPtr targetPointer) |
||||||
|
{ |
||||||
|
if (_currentCtsToken.IsCancellationRequested) |
||||||
|
return; |
||||||
|
|
||||||
|
if (!(_hasNewFrame & _bitmapBackBuffer != null)) |
||||||
|
return; |
||||||
|
|
||||||
|
unsafe |
||||||
|
{ |
||||||
|
fixed (void* src = &_bitmapBackBuffer[0]) |
||||||
|
Buffer.MemoryCopy(src, targetPointer.ToPointer(), (uint)_backBufferBytes, (uint)_backBufferBytes); |
||||||
|
_hasNewFrame = false; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Processes GIF Header. |
||||||
|
/// </summary> |
||||||
|
private void ProcessHeaderData() |
||||||
|
{ |
||||||
|
var str = _fileStream; |
||||||
|
var tmpB = ArrayPool<byte>.Shared.Rent(MaxTempBuf); |
||||||
|
var tempBuf = tmpB.AsSpan(); |
||||||
|
|
||||||
|
var _ = str.Read(tmpB, 0, 6); |
||||||
|
|
||||||
|
if (!tempBuf[..3].SequenceEqual(G87AMagic[..3].Span)) |
||||||
|
throw new InvalidGifStreamException("Not a GIF stream."); |
||||||
|
|
||||||
|
if (!(tempBuf[..6].SequenceEqual(G87AMagic.Span) | tempBuf[..6].SequenceEqual(G89AMagic.Span))) |
||||||
|
throw new InvalidGifStreamException( |
||||||
|
"Unsupported GIF Version: " + Encoding.ASCII.GetString(tempBuf[..6].ToArray()) |
||||||
|
); |
||||||
|
|
||||||
|
ProcessScreenDescriptor(tmpB); |
||||||
|
|
||||||
|
Header = new GifHeader |
||||||
|
{ |
||||||
|
Dimensions = _gifDimensions, |
||||||
|
HasGlobalColorTable = _gctUsed, |
||||||
|
// GlobalColorTableCacheID = _globalColorTable, |
||||||
|
GlobarColorTable = ProcessColorTable(ref str, tmpB, _gctSize), |
||||||
|
GlobalColorTableSize = _gctSize, |
||||||
|
BackgroundColorIndex = _bgIndex, |
||||||
|
HeaderSize = _fileStream.Position |
||||||
|
}; |
||||||
|
|
||||||
|
ArrayPool<byte>.Shared.Return(tmpB); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Parses colors from file stream to target color table. |
||||||
|
/// </summary> |
||||||
|
private static GifColor[] ProcessColorTable(ref Stream stream, byte[] rawBufSpan, int nColors) |
||||||
|
{ |
||||||
|
var nBytes = 3 * nColors; |
||||||
|
var target = new GifColor[nColors]; |
||||||
|
|
||||||
|
var n = stream.Read(rawBufSpan, 0, nBytes); |
||||||
|
|
||||||
|
if (n < nBytes) |
||||||
|
throw new InvalidOperationException("Wrong color table bytes."); |
||||||
|
|
||||||
|
int i = 0, |
||||||
|
j = 0; |
||||||
|
|
||||||
|
while (i < nColors) |
||||||
|
{ |
||||||
|
var r = rawBufSpan[j++]; |
||||||
|
var g = rawBufSpan[j++]; |
||||||
|
var b = rawBufSpan[j++]; |
||||||
|
target[i++] = new GifColor(r, g, b); |
||||||
|
} |
||||||
|
|
||||||
|
return target; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Parses screen and other GIF descriptors. |
||||||
|
/// </summary> |
||||||
|
private void ProcessScreenDescriptor(byte[] tempBuf) |
||||||
|
{ |
||||||
|
var width = _fileStream.ReadUShortS(tempBuf); |
||||||
|
var height = _fileStream.ReadUShortS(tempBuf); |
||||||
|
|
||||||
|
var packed = _fileStream.ReadByteS(tempBuf); |
||||||
|
|
||||||
|
_gctUsed = (packed & 0x80) != 0; |
||||||
|
_gctSize = 2 << (packed & 7); |
||||||
|
_bgIndex = _fileStream.ReadByteS(tempBuf); |
||||||
|
|
||||||
|
_gifDimensions = new GifRect(0, 0, width, height); |
||||||
|
_fileStream.Skip(1); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Parses all frame data. |
||||||
|
/// </summary> |
||||||
|
private void ProcessFrameData() |
||||||
|
{ |
||||||
|
_fileStream.Position = Header.HeaderSize; |
||||||
|
|
||||||
|
var tempBuf = ArrayPool<byte>.Shared.Rent(MaxTempBuf); |
||||||
|
|
||||||
|
var terminate = false; |
||||||
|
var curFrame = 0; |
||||||
|
|
||||||
|
Frames.Add(new GifFrame()); |
||||||
|
|
||||||
|
do |
||||||
|
{ |
||||||
|
var blockType = (BlockTypes)_fileStream.ReadByteS(tempBuf); |
||||||
|
|
||||||
|
switch (blockType) |
||||||
|
{ |
||||||
|
case BlockTypes.Empty: |
||||||
|
break; |
||||||
|
|
||||||
|
case BlockTypes.Extension: |
||||||
|
ProcessExtensions(ref curFrame, tempBuf); |
||||||
|
break; |
||||||
|
|
||||||
|
case BlockTypes.ImageDescriptor: |
||||||
|
ProcessImageDescriptor(ref curFrame, tempBuf); |
||||||
|
_fileStream.SkipBlocks(tempBuf); |
||||||
|
break; |
||||||
|
|
||||||
|
case BlockTypes.Trailer: |
||||||
|
Frames.RemoveAt(Frames.Count - 1); |
||||||
|
terminate = true; |
||||||
|
break; |
||||||
|
|
||||||
|
default: |
||||||
|
_fileStream.SkipBlocks(tempBuf); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
// Break the loop when the stream is not valid anymore. |
||||||
|
if (_fileStream.Position >= _fileStream.Length & terminate == false) |
||||||
|
throw new InvalidProgramException("Reach the end of the filestream without trailer block."); |
||||||
|
} while (!terminate); |
||||||
|
|
||||||
|
ArrayPool<byte>.Shared.Return(tempBuf); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Parses GIF Image Descriptor Block. |
||||||
|
/// </summary> |
||||||
|
private void ProcessImageDescriptor(ref int curFrame, byte[] tempBuf) |
||||||
|
{ |
||||||
|
var str = _fileStream; |
||||||
|
var currentFrame = Frames[curFrame]; |
||||||
|
|
||||||
|
// Parse frame dimensions. |
||||||
|
var frameX = str.ReadUShortS(tempBuf); |
||||||
|
var frameY = str.ReadUShortS(tempBuf); |
||||||
|
var frameW = str.ReadUShortS(tempBuf); |
||||||
|
var frameH = str.ReadUShortS(tempBuf); |
||||||
|
|
||||||
|
frameW = (ushort)Math.Min(frameW, _gifDimensions.Width - frameX); |
||||||
|
frameH = (ushort)Math.Min(frameH, _gifDimensions.Height - frameY); |
||||||
|
|
||||||
|
currentFrame.Dimensions = new GifRect(frameX, frameY, frameW, frameH); |
||||||
|
|
||||||
|
// Unpack interlace and lct info. |
||||||
|
var packed = str.ReadByteS(tempBuf); |
||||||
|
currentFrame.IsInterlaced = (packed & 0x40) != 0; |
||||||
|
currentFrame.IsLocalColorTableUsed = (packed & 0x80) != 0; |
||||||
|
currentFrame.LocalColorTableSize = (int)Math.Pow(2, (packed & 0x07) + 1); |
||||||
|
|
||||||
|
if (currentFrame.IsLocalColorTableUsed) |
||||||
|
currentFrame.LocalColorTable = ProcessColorTable(ref str, tempBuf, currentFrame.LocalColorTableSize); |
||||||
|
|
||||||
|
currentFrame.LzwMinCodeSize = str.ReadByteS(tempBuf); |
||||||
|
currentFrame.LzwStreamPosition = str.Position; |
||||||
|
|
||||||
|
curFrame += 1; |
||||||
|
Frames.Add(new GifFrame()); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Parses GIF Extension Blocks. |
||||||
|
/// </summary> |
||||||
|
private void ProcessExtensions(ref int curFrame, byte[] tempBuf) |
||||||
|
{ |
||||||
|
var extType = (ExtensionType)_fileStream.ReadByteS(tempBuf); |
||||||
|
|
||||||
|
switch (extType) |
||||||
|
{ |
||||||
|
case ExtensionType.GraphicsControl: |
||||||
|
|
||||||
|
_fileStream.ReadBlock(tempBuf); |
||||||
|
var currentFrame = Frames[curFrame]; |
||||||
|
var packed = tempBuf[0]; |
||||||
|
|
||||||
|
currentFrame.FrameDisposalMethod = (FrameDisposal)((packed & 0x1c) >> 2); |
||||||
|
|
||||||
|
if ( |
||||||
|
currentFrame.FrameDisposalMethod != FrameDisposal.Restore |
||||||
|
&& currentFrame.FrameDisposalMethod != FrameDisposal.Background |
||||||
|
) |
||||||
|
currentFrame.ShouldBackup = true; |
||||||
|
|
||||||
|
currentFrame.HasTransparency = (packed & 1) != 0; |
||||||
|
|
||||||
|
currentFrame.FrameDelay = TimeSpan.FromMilliseconds(SpanToShort(tempBuf.AsSpan(1)) * 10); |
||||||
|
|
||||||
|
if (currentFrame.FrameDelay <= FrameDelayThreshold) |
||||||
|
currentFrame.FrameDelay = FrameDelayDefault; |
||||||
|
|
||||||
|
currentFrame.TransparentColorIndex = tempBuf[3]; |
||||||
|
break; |
||||||
|
|
||||||
|
case ExtensionType.Application: |
||||||
|
var blockLen = _fileStream.ReadBlock(tempBuf); |
||||||
|
var _ = tempBuf.AsSpan(0, blockLen); |
||||||
|
var blockHeader = tempBuf.AsSpan(0, NetscapeMagic.Length); |
||||||
|
|
||||||
|
if (blockHeader.SequenceEqual(NetscapeMagic.Span)) |
||||||
|
{ |
||||||
|
var count = 1; |
||||||
|
|
||||||
|
while (count > 0) |
||||||
|
count = _fileStream.ReadBlock(tempBuf); |
||||||
|
|
||||||
|
var iterationCount = SpanToShort(tempBuf.AsSpan(1)); |
||||||
|
|
||||||
|
Header.Iterations = iterationCount; |
||||||
|
} |
||||||
|
else |
||||||
|
_fileStream.SkipBlocks(tempBuf); |
||||||
|
|
||||||
|
break; |
||||||
|
|
||||||
|
default: |
||||||
|
_fileStream.SkipBlocks(tempBuf); |
||||||
|
break; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
using System; |
||||||
|
|
||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
public class GifFrame |
||||||
|
{ |
||||||
|
public bool HasTransparency, |
||||||
|
IsInterlaced, |
||||||
|
IsLocalColorTableUsed; |
||||||
|
public byte TransparentColorIndex; |
||||||
|
public int LzwMinCodeSize, |
||||||
|
LocalColorTableSize; |
||||||
|
public long LzwStreamPosition; |
||||||
|
public TimeSpan FrameDelay; |
||||||
|
public FrameDisposal FrameDisposalMethod; |
||||||
|
public bool ShouldBackup; |
||||||
|
public GifRect Dimensions; |
||||||
|
public GifColor[] LocalColorTable; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
// Licensed under the MIT License. |
||||||
|
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. |
||||||
|
|
||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
public class GifHeader |
||||||
|
{ |
||||||
|
public bool HasGlobalColorTable; |
||||||
|
public int GlobalColorTableSize; |
||||||
|
public ulong GlobalColorTableCacheId; |
||||||
|
public int BackgroundColorIndex; |
||||||
|
public long HeaderSize; |
||||||
|
internal int Iterations = -1; |
||||||
|
public GifRepeatBehavior IterationCount; |
||||||
|
public GifRect Dimensions; |
||||||
|
private GifColor[] _globarColorTable; |
||||||
|
public GifColor[] GlobarColorTable; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,43 @@ |
|||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
public readonly struct GifRect |
||||||
|
{ |
||||||
|
public int X { get; } |
||||||
|
public int Y { get; } |
||||||
|
public int Width { get; } |
||||||
|
public int Height { get; } |
||||||
|
public int TotalPixels { get; } |
||||||
|
|
||||||
|
public GifRect(int x, int y, int width, int height) |
||||||
|
{ |
||||||
|
X = x; |
||||||
|
Y = y; |
||||||
|
Width = width; |
||||||
|
Height = height; |
||||||
|
TotalPixels = width * height; |
||||||
|
} |
||||||
|
|
||||||
|
public static bool operator ==(GifRect a, GifRect b) |
||||||
|
{ |
||||||
|
return a.X == b.X && a.Y == b.Y && a.Width == b.Width && a.Height == b.Height; |
||||||
|
} |
||||||
|
|
||||||
|
public static bool operator !=(GifRect a, GifRect b) |
||||||
|
{ |
||||||
|
return !(a == b); |
||||||
|
} |
||||||
|
|
||||||
|
public override bool Equals(object obj) |
||||||
|
{ |
||||||
|
if (obj == null || GetType() != obj.GetType()) |
||||||
|
return false; |
||||||
|
|
||||||
|
return this == (GifRect)obj; |
||||||
|
} |
||||||
|
|
||||||
|
public override int GetHashCode() |
||||||
|
{ |
||||||
|
return X.GetHashCode() ^ Y.GetHashCode() | Width.GetHashCode() ^ Height.GetHashCode(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,8 @@ |
|||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
public class GifRepeatBehavior |
||||||
|
{ |
||||||
|
public bool LoopForever { get; set; } |
||||||
|
public int? Count { get; set; } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
// Licensed under the MIT License. |
||||||
|
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. |
||||||
|
|
||||||
|
using System; |
||||||
|
using System.Runtime.Serialization; |
||||||
|
|
||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
[Serializable] |
||||||
|
public class InvalidGifStreamException : Exception |
||||||
|
{ |
||||||
|
public InvalidGifStreamException() { } |
||||||
|
|
||||||
|
public InvalidGifStreamException(string message) |
||||||
|
: base(message) { } |
||||||
|
|
||||||
|
public InvalidGifStreamException(string message, Exception innerException) |
||||||
|
: base(message, innerException) { } |
||||||
|
|
||||||
|
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context) |
||||||
|
: base(info, context) { } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
// Licensed under the MIT License. |
||||||
|
// Copyright (C) 2018 Jumar A. Macato, All Rights Reserved. |
||||||
|
|
||||||
|
using System; |
||||||
|
using System.Runtime.Serialization; |
||||||
|
|
||||||
|
namespace Avalonia.Gif.Decoding |
||||||
|
{ |
||||||
|
[Serializable] |
||||||
|
public class LzwDecompressionException : Exception |
||||||
|
{ |
||||||
|
public LzwDecompressionException() { } |
||||||
|
|
||||||
|
public LzwDecompressionException(string message) |
||||||
|
: base(message) { } |
||||||
|
|
||||||
|
public LzwDecompressionException(string message, Exception innerException) |
||||||
|
: base(message, innerException) { } |
||||||
|
|
||||||
|
protected LzwDecompressionException(SerializationInfo info, StreamingContext context) |
||||||
|
: base(info, context) { } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,81 @@ |
|||||||
|
using System; |
||||||
|
using System.Diagnostics; |
||||||
|
using System.IO; |
||||||
|
using System.Runtime.CompilerServices; |
||||||
|
|
||||||
|
namespace Avalonia.Gif.Extensions |
||||||
|
{ |
||||||
|
[DebuggerStepThrough] |
||||||
|
internal static class StreamExtensions |
||||||
|
{ |
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
public static ushort SpanToShort(Span<byte> b) => (ushort)(b[0] | (b[1] << 8)); |
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
public static void Skip(this Stream stream, long count) |
||||||
|
{ |
||||||
|
stream.Position += count; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Read a Gif block from stream while advancing the position. |
||||||
|
/// </summary> |
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
public static int ReadBlock(this Stream stream, byte[] tempBuf) |
||||||
|
{ |
||||||
|
stream.Read(tempBuf, 0, 1); |
||||||
|
|
||||||
|
var blockLength = (int)tempBuf[0]; |
||||||
|
|
||||||
|
if (blockLength > 0) |
||||||
|
stream.Read(tempBuf, 0, blockLength); |
||||||
|
|
||||||
|
// Guard against infinite loop. |
||||||
|
if (stream.Position >= stream.Length) |
||||||
|
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); |
||||||
|
|
||||||
|
return blockLength; |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Skips GIF blocks until it encounters an empty block. |
||||||
|
/// </summary> |
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
public static void SkipBlocks(this Stream stream, byte[] tempBuf) |
||||||
|
{ |
||||||
|
int blockLength; |
||||||
|
do |
||||||
|
{ |
||||||
|
stream.Read(tempBuf, 0, 1); |
||||||
|
|
||||||
|
blockLength = tempBuf[0]; |
||||||
|
stream.Position += blockLength; |
||||||
|
|
||||||
|
// Guard against infinite loop. |
||||||
|
if (stream.Position >= stream.Length) |
||||||
|
throw new InvalidGifStreamException("Reach the end of the filestream without trailer block."); |
||||||
|
} while (blockLength > 0); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer. |
||||||
|
/// </summary> |
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
public static ushort ReadUShortS(this Stream stream, byte[] tempBuf) |
||||||
|
{ |
||||||
|
stream.Read(tempBuf, 0, 2); |
||||||
|
return SpanToShort(tempBuf); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Read a <see cref="ushort"/> from stream by providing a temporary buffer. |
||||||
|
/// </summary> |
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
||||||
|
public static byte ReadByteS(this Stream stream, byte[] tempBuf) |
||||||
|
{ |
||||||
|
stream.Read(tempBuf, 0, 1); |
||||||
|
var finalVal = tempBuf[0]; |
||||||
|
return finalVal; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,296 @@ |
|||||||
|
using System; |
||||||
|
using System.IO; |
||||||
|
using System.Numerics; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Animation; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Logging; |
||||||
|
using Avalonia.Media; |
||||||
|
using Avalonia.Rendering.Composition; |
||||||
|
using Avalonia.VisualTree; |
||||||
|
|
||||||
|
namespace Avalonia.Gif |
||||||
|
{ |
||||||
|
public class GifImage : Control |
||||||
|
{ |
||||||
|
public static readonly StyledProperty<string> SourceUriRawProperty = AvaloniaProperty.Register< |
||||||
|
GifImage, |
||||||
|
string |
||||||
|
>("SourceUriRaw"); |
||||||
|
|
||||||
|
public static readonly StyledProperty<Uri> SourceUriProperty = AvaloniaProperty.Register<GifImage, Uri>( |
||||||
|
"SourceUri" |
||||||
|
); |
||||||
|
|
||||||
|
public static readonly StyledProperty<Stream> SourceStreamProperty = AvaloniaProperty.Register< |
||||||
|
GifImage, |
||||||
|
Stream |
||||||
|
>("SourceStream"); |
||||||
|
|
||||||
|
public static readonly StyledProperty<IterationCount> IterationCountProperty = AvaloniaProperty.Register< |
||||||
|
GifImage, |
||||||
|
IterationCount |
||||||
|
>("IterationCount", IterationCount.Infinite); |
||||||
|
|
||||||
|
private GifInstance? _gifInstance; |
||||||
|
|
||||||
|
public static readonly StyledProperty<StretchDirection> StretchDirectionProperty = AvaloniaProperty.Register< |
||||||
|
GifImage, |
||||||
|
StretchDirection |
||||||
|
>("StretchDirection"); |
||||||
|
|
||||||
|
public static readonly StyledProperty<Stretch> StretchProperty = AvaloniaProperty.Register<GifImage, Stretch>( |
||||||
|
"Stretch" |
||||||
|
); |
||||||
|
|
||||||
|
private CompositionCustomVisual? _customVisual; |
||||||
|
|
||||||
|
private object? _initialSource = null; |
||||||
|
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) |
||||||
|
{ |
||||||
|
switch (change.Property.Name) |
||||||
|
{ |
||||||
|
case nameof(SourceUriRaw): |
||||||
|
case nameof(SourceUri): |
||||||
|
case nameof(SourceStream): |
||||||
|
SourceChanged(change); |
||||||
|
break; |
||||||
|
case nameof(Stretch): |
||||||
|
case nameof(StretchDirection): |
||||||
|
InvalidateArrange(); |
||||||
|
InvalidateMeasure(); |
||||||
|
Update(); |
||||||
|
break; |
||||||
|
case nameof(IterationCount): |
||||||
|
IterationCountChanged(change); |
||||||
|
break; |
||||||
|
case nameof(Bounds): |
||||||
|
Update(); |
||||||
|
break; |
||||||
|
} |
||||||
|
|
||||||
|
base.OnPropertyChanged(change); |
||||||
|
} |
||||||
|
|
||||||
|
public string SourceUriRaw |
||||||
|
{ |
||||||
|
get => GetValue(SourceUriRawProperty); |
||||||
|
set => SetValue(SourceUriRawProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public Uri SourceUri |
||||||
|
{ |
||||||
|
get => GetValue(SourceUriProperty); |
||||||
|
set => SetValue(SourceUriProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public Stream SourceStream |
||||||
|
{ |
||||||
|
get => GetValue(SourceStreamProperty); |
||||||
|
set => SetValue(SourceStreamProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public IterationCount IterationCount |
||||||
|
{ |
||||||
|
get => GetValue(IterationCountProperty); |
||||||
|
set => SetValue(IterationCountProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public StretchDirection StretchDirection |
||||||
|
{ |
||||||
|
get => GetValue(StretchDirectionProperty); |
||||||
|
set => SetValue(StretchDirectionProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public Stretch Stretch |
||||||
|
{ |
||||||
|
get => GetValue(StretchProperty); |
||||||
|
set => SetValue(StretchProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
private static void IterationCountChanged(AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
var image = e.Sender as GifImage; |
||||||
|
if (image is null || e.NewValue is not IterationCount iterationCount) |
||||||
|
return; |
||||||
|
|
||||||
|
image.IterationCount = iterationCount; |
||||||
|
} |
||||||
|
|
||||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) |
||||||
|
{ |
||||||
|
var compositor = ElementComposition.GetElementVisual(this)?.Compositor; |
||||||
|
if (compositor == null || _customVisual?.Compositor == compositor) |
||||||
|
return; |
||||||
|
_customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); |
||||||
|
ElementComposition.SetElementChildVisual(this, _customVisual); |
||||||
|
_customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); |
||||||
|
|
||||||
|
if (_initialSource is not null) |
||||||
|
{ |
||||||
|
UpdateGifInstance(_initialSource); |
||||||
|
_initialSource = null; |
||||||
|
} |
||||||
|
|
||||||
|
Update(); |
||||||
|
base.OnAttachedToVisualTree(e); |
||||||
|
} |
||||||
|
|
||||||
|
private void Update() |
||||||
|
{ |
||||||
|
if (_customVisual is null || _gifInstance is null) |
||||||
|
return; |
||||||
|
|
||||||
|
var dpi = this.GetVisualRoot()?.RenderScaling ?? 1.0; |
||||||
|
var sourceSize = _gifInstance.GifPixelSize.ToSize(dpi); |
||||||
|
var viewPort = new Rect(Bounds.Size); |
||||||
|
|
||||||
|
var scale = Stretch.CalculateScaling(Bounds.Size, sourceSize, StretchDirection); |
||||||
|
var scaledSize = sourceSize * scale; |
||||||
|
var destRect = viewPort.CenterRect(new Rect(scaledSize)).Intersect(viewPort); |
||||||
|
|
||||||
|
if (Stretch == Stretch.None) |
||||||
|
{ |
||||||
|
_customVisual.Size = new Vector2((float)sourceSize.Width, (float)sourceSize.Height); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
_customVisual.Size = new Vector2((float)destRect.Size.Width, (float)destRect.Size.Height); |
||||||
|
} |
||||||
|
|
||||||
|
_customVisual.Offset = new Vector3((float)destRect.Position.X, (float)destRect.Position.Y, 0); |
||||||
|
} |
||||||
|
|
||||||
|
private class CustomVisualHandler : CompositionCustomVisualHandler |
||||||
|
{ |
||||||
|
private TimeSpan _animationElapsed; |
||||||
|
private TimeSpan? _lastServerTime; |
||||||
|
private GifInstance? _currentInstance; |
||||||
|
private bool _running; |
||||||
|
|
||||||
|
public static readonly object StopMessage = new(), |
||||||
|
StartMessage = new(); |
||||||
|
|
||||||
|
public override void OnMessage(object message) |
||||||
|
{ |
||||||
|
if (message == StartMessage) |
||||||
|
{ |
||||||
|
_running = true; |
||||||
|
_lastServerTime = null; |
||||||
|
RegisterForNextAnimationFrameUpdate(); |
||||||
|
} |
||||||
|
else if (message == StopMessage) |
||||||
|
{ |
||||||
|
_running = false; |
||||||
|
} |
||||||
|
else if (message is GifInstance instance) |
||||||
|
{ |
||||||
|
_currentInstance?.Dispose(); |
||||||
|
_currentInstance = instance; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnAnimationFrameUpdate() |
||||||
|
{ |
||||||
|
if (!_running) |
||||||
|
return; |
||||||
|
Invalidate(); |
||||||
|
RegisterForNextAnimationFrameUpdate(); |
||||||
|
} |
||||||
|
|
||||||
|
public override void OnRender(ImmediateDrawingContext drawingContext) |
||||||
|
{ |
||||||
|
if (_running) |
||||||
|
{ |
||||||
|
if (_lastServerTime.HasValue) |
||||||
|
_animationElapsed += (CompositionNow - _lastServerTime.Value); |
||||||
|
_lastServerTime = CompositionNow; |
||||||
|
} |
||||||
|
|
||||||
|
try |
||||||
|
{ |
||||||
|
if (_currentInstance is null || _currentInstance.IsDisposed) |
||||||
|
return; |
||||||
|
|
||||||
|
var bitmap = _currentInstance.ProcessFrameTime(_animationElapsed); |
||||||
|
if (bitmap is not null) |
||||||
|
{ |
||||||
|
drawingContext.DrawBitmap( |
||||||
|
bitmap, |
||||||
|
new Rect(_currentInstance.GifPixelSize.ToSize(1)), |
||||||
|
GetRenderBounds() |
||||||
|
); |
||||||
|
} |
||||||
|
} |
||||||
|
catch (Exception e) |
||||||
|
{ |
||||||
|
Logger.Sink?.Log(LogEventLevel.Error, "GifImage Renderer ", this, e.ToString()); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Measures the control. |
||||||
|
/// </summary> |
||||||
|
/// <param name="availableSize">The available size.</param> |
||||||
|
/// <returns>The desired size of the control.</returns> |
||||||
|
protected override Size MeasureOverride(Size availableSize) |
||||||
|
{ |
||||||
|
var result = new Size(); |
||||||
|
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; |
||||||
|
if (_gifInstance != null) |
||||||
|
{ |
||||||
|
result = Stretch.CalculateSize( |
||||||
|
availableSize, |
||||||
|
_gifInstance.GifPixelSize.ToSize(scaling), |
||||||
|
StretchDirection |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc/> |
||||||
|
protected override Size ArrangeOverride(Size finalSize) |
||||||
|
{ |
||||||
|
if (_gifInstance is null) |
||||||
|
return new Size(); |
||||||
|
var scaling = this.GetVisualRoot()?.RenderScaling ?? 1.0; |
||||||
|
var sourceSize = _gifInstance.GifPixelSize.ToSize(scaling); |
||||||
|
var result = Stretch.CalculateSize(finalSize, sourceSize); |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
private void SourceChanged(AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
if ( |
||||||
|
e.NewValue is null |
||||||
|
|| (e.NewValue is string value && !Uri.IsWellFormedUriString(value, UriKind.Absolute)) |
||||||
|
) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (_customVisual is null) |
||||||
|
{ |
||||||
|
_initialSource = e.NewValue; |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
UpdateGifInstance(e.NewValue); |
||||||
|
|
||||||
|
InvalidateArrange(); |
||||||
|
InvalidateMeasure(); |
||||||
|
Update(); |
||||||
|
} |
||||||
|
|
||||||
|
private void UpdateGifInstance(object source) |
||||||
|
{ |
||||||
|
_gifInstance?.Dispose(); |
||||||
|
_gifInstance = new GifInstance(source); |
||||||
|
_gifInstance.IterationCount = IterationCount; |
||||||
|
_customVisual?.SendHandlerMessage(_gifInstance); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,147 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.IO; |
||||||
|
using System.Linq; |
||||||
|
using System.Threading; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Animation; |
||||||
|
using Avalonia.Gif.Decoding; |
||||||
|
using Avalonia.Media.Imaging; |
||||||
|
using Avalonia.Platform; |
||||||
|
|
||||||
|
namespace Avalonia.Gif |
||||||
|
{ |
||||||
|
public class GifInstance : IDisposable |
||||||
|
{ |
||||||
|
public IterationCount IterationCount { get; set; } |
||||||
|
public bool AutoStart { get; private set; } = true; |
||||||
|
private readonly GifDecoder _gifDecoder; |
||||||
|
private readonly WriteableBitmap? _targetBitmap; |
||||||
|
private TimeSpan _totalTime; |
||||||
|
private readonly List<TimeSpan> _frameTimes; |
||||||
|
private uint _iterationCount; |
||||||
|
private int _currentFrameIndex; |
||||||
|
private readonly List<ulong> _colorTableIdList; |
||||||
|
|
||||||
|
public CancellationTokenSource CurrentCts { get; } |
||||||
|
|
||||||
|
internal GifInstance(object newValue) |
||||||
|
: this( |
||||||
|
newValue switch |
||||||
|
{ |
||||||
|
Stream s => s, |
||||||
|
Uri u => GetStreamFromUri(u), |
||||||
|
string str => GetStreamFromString(str), |
||||||
|
_ => throw new InvalidDataException("Unsupported source object") |
||||||
|
} |
||||||
|
) { } |
||||||
|
|
||||||
|
public GifInstance(string uri) |
||||||
|
: this(GetStreamFromString(uri)) { } |
||||||
|
|
||||||
|
public GifInstance(Uri uri) |
||||||
|
: this(GetStreamFromUri(uri)) { } |
||||||
|
|
||||||
|
public GifInstance(Stream currentStream) |
||||||
|
{ |
||||||
|
if (!currentStream.CanSeek) |
||||||
|
throw new InvalidDataException("The provided stream is not seekable."); |
||||||
|
|
||||||
|
if (!currentStream.CanRead) |
||||||
|
throw new InvalidOperationException("Can't read the stream provided."); |
||||||
|
|
||||||
|
currentStream.Seek(0, SeekOrigin.Begin); |
||||||
|
|
||||||
|
CurrentCts = new CancellationTokenSource(); |
||||||
|
|
||||||
|
_gifDecoder = new GifDecoder(currentStream, CurrentCts.Token); |
||||||
|
var pixSize = new PixelSize(_gifDecoder.Header.Dimensions.Width, _gifDecoder.Header.Dimensions.Height); |
||||||
|
|
||||||
|
_targetBitmap = new WriteableBitmap(pixSize, new Vector(96, 96), PixelFormat.Bgra8888, AlphaFormat.Opaque); |
||||||
|
GifPixelSize = pixSize; |
||||||
|
|
||||||
|
_totalTime = TimeSpan.Zero; |
||||||
|
|
||||||
|
_frameTimes = _gifDecoder |
||||||
|
.Frames |
||||||
|
.Select(frame => |
||||||
|
{ |
||||||
|
_totalTime = _totalTime.Add(frame.FrameDelay); |
||||||
|
return _totalTime; |
||||||
|
}) |
||||||
|
.ToList(); |
||||||
|
|
||||||
|
_gifDecoder.RenderFrame(0, _targetBitmap); |
||||||
|
} |
||||||
|
|
||||||
|
private static Stream GetStreamFromString(string str) |
||||||
|
{ |
||||||
|
if (!Uri.TryCreate(str, UriKind.RelativeOrAbsolute, out var res)) |
||||||
|
{ |
||||||
|
throw new InvalidCastException("The string provided can't be converted to URI."); |
||||||
|
} |
||||||
|
|
||||||
|
return GetStreamFromUri(res); |
||||||
|
} |
||||||
|
|
||||||
|
private static Stream GetStreamFromUri(Uri uri) |
||||||
|
{ |
||||||
|
var uriString = uri.OriginalString.Trim(); |
||||||
|
|
||||||
|
if (!uriString.StartsWith("resm") && !uriString.StartsWith("avares")) |
||||||
|
{ |
||||||
|
return new FileStream(uriString, FileMode.Open, FileAccess.Read); |
||||||
|
} |
||||||
|
|
||||||
|
return AssetLoader.Open(uri); |
||||||
|
} |
||||||
|
|
||||||
|
public int GifFrameCount => _frameTimes.Count; |
||||||
|
|
||||||
|
public PixelSize GifPixelSize { get; } |
||||||
|
|
||||||
|
public void Dispose() |
||||||
|
{ |
||||||
|
IsDisposed = true; |
||||||
|
CurrentCts.Cancel(); |
||||||
|
_targetBitmap?.Dispose(); |
||||||
|
} |
||||||
|
|
||||||
|
public bool IsDisposed { get; private set; } |
||||||
|
|
||||||
|
public WriteableBitmap? ProcessFrameTime(TimeSpan stopwatchElapsed) |
||||||
|
{ |
||||||
|
if (!IterationCount.IsInfinite && _iterationCount > IterationCount.Value) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
if (CurrentCts.IsCancellationRequested || _targetBitmap is null) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
var elapsedTicks = stopwatchElapsed.Ticks; |
||||||
|
var timeModulus = TimeSpan.FromTicks(elapsedTicks % _totalTime.Ticks); |
||||||
|
var targetFrame = _frameTimes.FirstOrDefault(x => timeModulus < x); |
||||||
|
var currentFrame = _frameTimes.IndexOf(targetFrame); |
||||||
|
if (currentFrame == -1) |
||||||
|
currentFrame = 0; |
||||||
|
|
||||||
|
if (_currentFrameIndex == currentFrame) |
||||||
|
return _targetBitmap; |
||||||
|
|
||||||
|
_iterationCount = (uint)(elapsedTicks / _totalTime.Ticks); |
||||||
|
|
||||||
|
return ProcessFrameIndex(currentFrame); |
||||||
|
} |
||||||
|
|
||||||
|
internal WriteableBitmap ProcessFrameIndex(int frameIndex) |
||||||
|
{ |
||||||
|
_gifDecoder.RenderFrame(frameIndex, _targetBitmap); |
||||||
|
_currentFrameIndex = frameIndex; |
||||||
|
|
||||||
|
return _targetBitmap; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
using System; |
||||||
|
using System.Runtime.Serialization; |
||||||
|
|
||||||
|
namespace Avalonia.Gif |
||||||
|
{ |
||||||
|
[Serializable] |
||||||
|
internal class InvalidGifStreamException : Exception |
||||||
|
{ |
||||||
|
public InvalidGifStreamException() { } |
||||||
|
|
||||||
|
public InvalidGifStreamException(string message) |
||||||
|
: base(message) { } |
||||||
|
|
||||||
|
public InvalidGifStreamException(string message, Exception innerException) |
||||||
|
: base(message, innerException) { } |
||||||
|
|
||||||
|
protected InvalidGifStreamException(SerializationInfo info, StreamingContext context) |
||||||
|
: base(info, context) { } |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,544 @@ |
|||||||
|
[ |
||||||
|
{ |
||||||
|
"ModelCategory": "BaseModel", |
||||||
|
"ModelName": "Stable Diffusion 1.5", |
||||||
|
"RepositoryPath": "runwayml/stable-diffusion-v1-5", |
||||||
|
"Files": [ |
||||||
|
"v1-5-pruned-emaonly.safetensors" |
||||||
|
], |
||||||
|
"LicenseType": "CreativeML Open RAIL-M" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "BaseModel", |
||||||
|
"ModelName": "Stable Diffusion 2.1", |
||||||
|
"RepositoryPath": "stabilityai/stable-diffusion-2-1", |
||||||
|
"Files": [ |
||||||
|
"v2-1_768-ema-pruned.safetensors" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL++" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "BaseModel", |
||||||
|
"ModelName": "Stable Diffusion XL (Base)", |
||||||
|
"RepositoryPath": "stabilityai/stable-diffusion-xl-base-1.0", |
||||||
|
"Files": [ |
||||||
|
"sd_xl_base_1.0_0.9vae.safetensors", |
||||||
|
"sd_xl_offset_example-lora_1.0.safetensors" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL++", |
||||||
|
"LicensePath": "LICENSE.md" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "BaseModel", |
||||||
|
"ModelName": "Stable Diffusion XL (Refiner)", |
||||||
|
"RepositoryPath": "stabilityai/stable-diffusion-xl-refiner-1.0", |
||||||
|
"Files": [ |
||||||
|
"sd_xl_refiner_1.0_0.9vae.safetensors" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL++", |
||||||
|
"LicensePath": "LICENSE.md" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Canny", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_canny.pth", |
||||||
|
"control_v11p_sd15_canny.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Depth", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11f1p_sd15_depth.pth", |
||||||
|
"control_v11f1p_sd15_depth.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "MLSD", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_mlsd.pth", |
||||||
|
"control_v11p_sd15_mlsd.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Inpaint", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_inpaint.pth", |
||||||
|
"control_v11p_sd15_inpaint.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "IP2P", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11e_sd15_ip2p.pth", |
||||||
|
"control_v11e_sd15_ip2p.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Tile", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11f1e_sd15_tile.pth", |
||||||
|
"control_v11f1e_sd15_tile.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "OpenPose", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_openpose.pth", |
||||||
|
"control_v11p_sd15_openpose.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "LineArt", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_lineart.pth", |
||||||
|
"control_v11p_sd15_lineart.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "LineArt Anime", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15s2_lineart_anime.pth", |
||||||
|
"control_v11p_sd15s2_lineart_anime.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "NormalBae", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_normalbae.pth", |
||||||
|
"control_v11p_sd15_normalbae.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Seg", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_seg.pth", |
||||||
|
"control_v11p_sd15_seg.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Soft Edge", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_softedge.pth", |
||||||
|
"control_v11p_sd15_softedge.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Scribble", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11p_sd15_scribble.pth", |
||||||
|
"control_v11p_sd15_scribble.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "ControlNet", |
||||||
|
"ModelName": "Shuffle", |
||||||
|
"RepositoryPath": "lllyasviel/ControlNet-v1-1", |
||||||
|
"Files": [ |
||||||
|
"control_v11e_sd15_shuffle.pth", |
||||||
|
"control_v11e_sd15_shuffle.yaml" |
||||||
|
], |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Canny", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_canny", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "canny", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Depth", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11f1p_sd15_depth", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "depth", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "MLSD", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_mlsd", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "MLSD", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Inpaint", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_inpaint", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "inpaint", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "IP2P", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11e_sd15_ip2p", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "ip2p", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Tile", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11f1e_sd15_tile", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.bin", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "tile", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "OpenPose", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_openpose", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "openpose", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "LineArt", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_lineart", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "lineart", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "LineArt Anime", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15s2_lineart_anime", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "lineart_anime", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "NormalBae", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_normalbae", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "normalbae", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Seg", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_seg", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "seg", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Soft Edge", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_softedge", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "softedge", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Scribble", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11p_sd15_scribble", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "scribble", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersControlNet", |
||||||
|
"ModelName": "Shuffle", |
||||||
|
"RepositoryPath": "lllyasviel/control_v11e_sd15_shuffle", |
||||||
|
"Files": [ |
||||||
|
"diffusion_pytorch_model.safetensors", |
||||||
|
"config.json" |
||||||
|
], |
||||||
|
"Subfolder": "shuffle", |
||||||
|
"LicenseType": "Open RAIL" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersClipVision", |
||||||
|
"ModelName": "IP Adapter Encoder", |
||||||
|
"RepositoryPath": "InvokeAI/ip_adapter_sd_image_encoder", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_sd_image_encoder", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersClipVision", |
||||||
|
"ModelName": "IP Adapter Encoder (SDXL)", |
||||||
|
"RepositoryPath": "InvokeAI/ip_adapter_sdxl_image_encoder", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_sd_image_encoder", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersIpAdapter", |
||||||
|
"ModelName": "SD 1.5 Adapter", |
||||||
|
"RepositoryPath": "InvokeAI/ip_adapter_sd15", |
||||||
|
"Files": [ |
||||||
|
"image_encoder.txt", |
||||||
|
"ip_adapter.bin" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_sd15", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersIpAdapter", |
||||||
|
"ModelName": "SD 1.5 Light Adapter", |
||||||
|
"RepositoryPath": "InvokeAI/ip_adapter_sd15_light", |
||||||
|
"Files": [ |
||||||
|
"image_encoder.txt", |
||||||
|
"ip_adapter.bin" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_sd15_light", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersIpAdapter", |
||||||
|
"ModelName": "SD 1.5 Plus Adapter", |
||||||
|
"RepositoryPath": "InvokeAI/ip_adapter_plus_sd15", |
||||||
|
"Files": [ |
||||||
|
"image_encoder.txt", |
||||||
|
"ip_adapter.bin" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_plus_sd15", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersIpAdapter", |
||||||
|
"ModelName": "SD 1.5 Face Plus Adapter", |
||||||
|
"RepositoryPath": "InvokeAI/ip_adapter_plus_face_sd15", |
||||||
|
"Files": [ |
||||||
|
"image_encoder.txt", |
||||||
|
"ip_adapter.bin" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_plus_face_sd15", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersIpAdapterXl", |
||||||
|
"ModelName": "SDXL Adapter", |
||||||
|
"RepositoryPath": "InvokeAI/ip_adapter_sdxl", |
||||||
|
"Files": [ |
||||||
|
"image_encoder.txt", |
||||||
|
"ip_adapter.bin" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_sdxl", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersIpAdapterXl", |
||||||
|
"ModelName": "SDXL Plus Adapter", |
||||||
|
"RepositoryPath": "InvokeAI/ip-adapter-plus_sdxl_vit-h", |
||||||
|
"Files": [ |
||||||
|
"image_encoder.txt", |
||||||
|
"ip_adapter.bin" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_plus_sdxl_vit-h", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersIpAdapterXl", |
||||||
|
"ModelName": "SDXL Face Plus Adapter", |
||||||
|
"RepositoryPath": "InvokeAI/ip-adapter-plus-face_sdxl_vit-h", |
||||||
|
"Files": [ |
||||||
|
"image_encoder.txt", |
||||||
|
"ip_adapter.bin" |
||||||
|
], |
||||||
|
"Subfolder": "ip_adapter_plus_face_sdxl_vit-h", |
||||||
|
"LicenseType": "CreativeML Open RAIL-M" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Sketch", |
||||||
|
"RepositoryPath": "TencentARC/t2iadapter_sketch_sd15v2", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.bin" |
||||||
|
], |
||||||
|
"Subfolder": "t2iadapter_sketch_sd15v2", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Depth", |
||||||
|
"RepositoryPath": "TencentARC/t2iadapter_depth_sd15v2", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.bin" |
||||||
|
], |
||||||
|
"Subfolder": "t2iadapter_depth_sd15v2", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Canny", |
||||||
|
"RepositoryPath": "TencentARC/t2iadapter_canny_sd15v2", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.bin" |
||||||
|
], |
||||||
|
"Subfolder": "t2iadapter_canny_sd15v2", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Depth-Zoe", |
||||||
|
"RepositoryPath": "TencentARC/t2iadapter_zoedepth_sd15v1", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.bin" |
||||||
|
], |
||||||
|
"Subfolder": "t2iadapter_zoedepth_sd15v1", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Sketch (SDXL)", |
||||||
|
"RepositoryPath": "TencentARC/t2i-adapter-sketch-sdxl-1.0", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "t2i-adapter-sketch-sdxl-1.0", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Depth-Zoe (SDXL)", |
||||||
|
"RepositoryPath": "TencentARC/t2i-adapter-depth-zoe-sdxl-1.0", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "t2i-adapter-depth-zoe-sdxl-1.0", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "OpenPose (SDXL)", |
||||||
|
"RepositoryPath": "TencentARC/t2i-adapter-openpose-sdxl-1.0", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "t2i-adapter-openpose-sdxl-1.0", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Depth-MiDaS (SDXL)", |
||||||
|
"RepositoryPath": "TencentARC/t2i-adapter-depth-midas-sdxl-1.0", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "t2i-adapter-depth-midas-sdxl-1.0", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "LineArt (SDXL)", |
||||||
|
"RepositoryPath": "TencentARC/t2i-adapter-lineart-sdxl-1.0", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "t2i-adapter-lineart-sdxl-1.0", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
}, |
||||||
|
{ |
||||||
|
"ModelCategory": "DiffusersT2IAdapter", |
||||||
|
"ModelName": "Canny (SDXL)", |
||||||
|
"RepositoryPath": "TencentARC/t2i-adapter-canny-sdxl-1.0", |
||||||
|
"Files": [ |
||||||
|
"config.json", |
||||||
|
"diffusion_pytorch_model.safetensors" |
||||||
|
], |
||||||
|
"Subfolder": "t2i-adapter-canny-sdxl-1.0", |
||||||
|
"LicenseType": "Apache 2.0" |
||||||
|
} |
||||||
|
] |
After Width: | Height: | Size: 68 KiB |
@ -1,30 +1,70 @@ |
|||||||
<UserControl xmlns="https://github.com/avaloniaui" |
<UserControl |
||||||
|
x:Class="StabilityMatrix.Avalonia.Controls.AdvancedImageBoxView" |
||||||
|
xmlns="https://github.com/avaloniaui" |
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
||||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||||
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" |
|
||||||
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" |
xmlns:models="clr-namespace:StabilityMatrix.Avalonia.Models" |
||||||
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|
||||||
x:Class="StabilityMatrix.Avalonia.Controls.AdvancedImageBoxView" |
|
||||||
d:DataContext="{x:Static mocks:DesignData.SampleImageSource}" |
d:DataContext="{x:Static mocks:DesignData.SampleImageSource}" |
||||||
x:DataType="models:ImageSource"> |
d:DesignHeight="450" |
||||||
|
d:DesignWidth="800" |
||||||
|
x:DataType="models:ImageSource" |
||||||
|
mc:Ignorable="d"> |
||||||
|
<Grid> |
||||||
<controls:AdvancedImageBox |
<controls:AdvancedImageBox |
||||||
Name="ImageBox" |
Name="ImageBox" |
||||||
SizeMode="Fit" |
|
||||||
CornerRadius="4" |
CornerRadius="4" |
||||||
Image="{Binding BitmapAsync^}"> |
Image="{Binding BitmapAsync^}" |
||||||
|
SizeMode="Fit"> |
||||||
<controls:AdvancedImageBox.ContextFlyout> |
<controls:AdvancedImageBox.ContextFlyout> |
||||||
<ui:FAMenuFlyout> |
<ui:FAMenuFlyout> |
||||||
<ui:MenuFlyoutItem |
<ui:MenuFlyoutItem |
||||||
x:Name="CopyMenuItem" |
x:Name="CopyMenuItem" |
||||||
IsEnabled="{OnPlatform Windows=True, Default=False}" |
|
||||||
CommandParameter="{Binding #ImageBox.Image}" |
CommandParameter="{Binding #ImageBox.Image}" |
||||||
Text="Copy" |
|
||||||
HotKey="Ctrl+C" |
HotKey="Ctrl+C" |
||||||
IconSource="Copy" /> |
IconSource="Copy" |
||||||
|
IsEnabled="{OnPlatform Windows=True, |
||||||
|
Default=False}" |
||||||
|
Text="Copy" /> |
||||||
</ui:FAMenuFlyout> |
</ui:FAMenuFlyout> |
||||||
</controls:AdvancedImageBox.ContextFlyout> |
</controls:AdvancedImageBox.ContextFlyout> |
||||||
</controls:AdvancedImageBox> |
</controls:AdvancedImageBox> |
||||||
|
|
||||||
|
<!-- Label pill card --> |
||||||
|
<Border |
||||||
|
IsVisible="{Binding Label, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
||||||
|
Grid.Row="0" |
||||||
|
Margin="4" |
||||||
|
HorizontalAlignment="Left" |
||||||
|
VerticalAlignment="Bottom" |
||||||
|
BoxShadow="inset 1.2 0 10 1.8 #66000000" |
||||||
|
CornerRadius="16"> |
||||||
|
<Border.Resources> |
||||||
|
<DropShadowEffect |
||||||
|
x:Key="TextDropShadowEffect" |
||||||
|
BlurRadius="12" |
||||||
|
Opacity="0.9" |
||||||
|
Color="#FF000000" /> |
||||||
|
<DropShadowEffect |
||||||
|
x:Key="ImageDropShadowEffect" |
||||||
|
BlurRadius="12" |
||||||
|
Opacity="0.2" |
||||||
|
Color="#FF000000" /> |
||||||
|
</Border.Resources> |
||||||
|
<Button |
||||||
|
Padding="10,4" |
||||||
|
Classes="transparent" |
||||||
|
CornerRadius="16"> |
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6"> |
||||||
|
<TextBlock |
||||||
|
VerticalAlignment="Center" |
||||||
|
Effect="{StaticResource TextDropShadowEffect}" |
||||||
|
Text="{Binding Label}" /> |
||||||
|
</StackPanel> |
||||||
|
</Button> |
||||||
|
</Border> |
||||||
|
</Grid> |
||||||
</UserControl> |
</UserControl> |
||||||
|
@ -0,0 +1,177 @@ |
|||||||
|
<Styles |
||||||
|
xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
||||||
|
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" |
||||||
|
xmlns:lang="clr-namespace:StabilityMatrix.Avalonia.Languages" |
||||||
|
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData" |
||||||
|
xmlns:ui="using:FluentAvalonia.UI.Controls" |
||||||
|
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" |
||||||
|
x:DataType="vmInference:SelectImageCardViewModel"> |
||||||
|
|
||||||
|
<Design.PreviewWith> |
||||||
|
<Panel Width="600" Height="500"> |
||||||
|
<Grid MaxHeight="400" ColumnDefinitions="*,*"> |
||||||
|
<controls:SelectImageCard Margin="4" DataContext="{x:Static mocks:DesignData.SelectImageCardViewModel}" /> |
||||||
|
<controls:SelectImageCard |
||||||
|
Grid.Column="1" |
||||||
|
Margin="4" |
||||||
|
DataContext="{x:Static mocks:DesignData.SelectImageCardViewModel_WithImage}" /> |
||||||
|
</Grid> |
||||||
|
</Panel> |
||||||
|
</Design.PreviewWith> |
||||||
|
|
||||||
|
<Style Selector="controls|SelectImageCard"> |
||||||
|
<!-- Set Defaults --> |
||||||
|
<Setter Property="Padding" Value="12" /> |
||||||
|
<Setter Property="Template"> |
||||||
|
<ControlTemplate> |
||||||
|
<controls:Card |
||||||
|
Padding="{TemplateBinding Padding}" |
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
||||||
|
VerticalContentAlignment="{TemplateBinding VerticalAlignment}"> |
||||||
|
|
||||||
|
<!-- Background frame --> |
||||||
|
<ExperimentalAcrylicBorder |
||||||
|
VerticalAlignment="Stretch" |
||||||
|
CornerRadius="4" |
||||||
|
Material="{StaticResource OpaqueDarkAcrylicMaterial}"> |
||||||
|
<Panel> |
||||||
|
<!-- Image --> |
||||||
|
<controls:BetterAdvancedImage |
||||||
|
x:Name="PART_BetterAdvancedImage" |
||||||
|
VerticalAlignment="Stretch" |
||||||
|
VerticalContentAlignment="Stretch" |
||||||
|
CornerRadius="4" |
||||||
|
IsVisible="{Binding !IsSelectionAvailable}" |
||||||
|
RenderOptions.BitmapInterpolationMode="HighQuality" |
||||||
|
Source="{Binding ImageSource}" |
||||||
|
Stretch="Uniform" |
||||||
|
StretchDirection="Both" /> |
||||||
|
|
||||||
|
<!-- Missing image --> |
||||||
|
<Border |
||||||
|
BorderBrush="{StaticResource ThemeCoralRedColor}" |
||||||
|
BorderThickness="3" |
||||||
|
BoxShadow="inset 1.2 0 20 1.8 #66000000" |
||||||
|
CornerRadius="4" |
||||||
|
IsVisible="{Binding IsImageFileNotFound}"> |
||||||
|
<Grid |
||||||
|
HorizontalAlignment="Center" |
||||||
|
VerticalAlignment="Center" |
||||||
|
RowDefinitions="Auto,Auto,Auto"> |
||||||
|
|
||||||
|
<fluentIcons:SymbolIcon |
||||||
|
FontSize="28" |
||||||
|
IsFilled="True" |
||||||
|
Symbol="DocumentQuestionMark" /> |
||||||
|
<TextBlock |
||||||
|
Grid.Row="1" |
||||||
|
FontSize="{TemplateBinding FontSize}" |
||||||
|
Text="{x:Static lang:Resources.Label_MissingImageFile}" |
||||||
|
TextAlignment="Center" |
||||||
|
TextWrapping="WrapWithOverflow" /> |
||||||
|
<SelectableTextBlock |
||||||
|
Grid.Row="2" |
||||||
|
Margin="0,4,0,0" |
||||||
|
FontSize="10" |
||||||
|
Foreground="{DynamicResource TextFillColorTertiaryBrush}" |
||||||
|
Text="{Binding NotFoundImagePath}" |
||||||
|
TextAlignment="Center" |
||||||
|
TextWrapping="WrapWithOverflow" /> |
||||||
|
</Grid> |
||||||
|
</Border> |
||||||
|
|
||||||
|
<!-- Active Selection Prompt --> |
||||||
|
<Border |
||||||
|
Margin="4" |
||||||
|
HorizontalAlignment="Right" |
||||||
|
VerticalAlignment="Top" |
||||||
|
BoxShadow="inset 1.2 0 80 1.8 #66000000" |
||||||
|
CornerRadius="10" |
||||||
|
IsVisible="{Binding !IsSelectionAvailable}"> |
||||||
|
<Border.Resources> |
||||||
|
<DropShadowEffect |
||||||
|
x:Key="TextDropShadowEffect" |
||||||
|
BlurRadius="12" |
||||||
|
Opacity="0.9" |
||||||
|
Color="#FF000000" /> |
||||||
|
<DropShadowEffect |
||||||
|
x:Key="ImageDropShadowEffect" |
||||||
|
BlurRadius="12" |
||||||
|
Opacity="0.2" |
||||||
|
Color="#FF000000" /> |
||||||
|
</Border.Resources> |
||||||
|
<Button |
||||||
|
Padding="2" |
||||||
|
Classes="transparent" |
||||||
|
Command="{Binding SelectImageFromFilePickerCommand}" |
||||||
|
CornerRadius="10" |
||||||
|
FontSize="{TemplateBinding FontSize}" |
||||||
|
ToolTip.Tip="{x:Static lang:Resources.Action_ReplaceContents}"> |
||||||
|
<fluentIcons:SymbolIcon |
||||||
|
Effect="{StaticResource ImageDropShadowEffect}" |
||||||
|
FontSize="28" |
||||||
|
IsFilled="True" |
||||||
|
Symbol="ImageArrowCounterclockwise" /> |
||||||
|
</Button> |
||||||
|
</Border> |
||||||
|
|
||||||
|
<!-- No Image Selection Prompt --> |
||||||
|
<controls:LineDashFrame |
||||||
|
Padding="8,4" |
||||||
|
CornerRadius="8" |
||||||
|
IsVisible="{Binding IsSelectionAvailable}" |
||||||
|
Stroke="DimGray" |
||||||
|
StrokeDashLine="6" |
||||||
|
StrokeDashSpace="6" |
||||||
|
StrokeThickness="3"> |
||||||
|
|
||||||
|
<Grid |
||||||
|
HorizontalAlignment="Center" |
||||||
|
VerticalAlignment="Center" |
||||||
|
RowDefinitions="*,Auto,Auto"> |
||||||
|
|
||||||
|
<fluentIcons:SymbolIcon |
||||||
|
FontSize="28" |
||||||
|
IsFilled="True" |
||||||
|
Symbol="ImageCopy" /> |
||||||
|
<TextBlock |
||||||
|
Grid.Row="1" |
||||||
|
FontSize="{TemplateBinding FontSize}" |
||||||
|
Foreground="DarkGray" |
||||||
|
Text="Drag an image here" |
||||||
|
TextAlignment="Center" |
||||||
|
TextWrapping="WrapWithOverflow" /> |
||||||
|
|
||||||
|
<StackPanel |
||||||
|
Grid.Row="2" |
||||||
|
Margin="0,4,0,0" |
||||||
|
HorizontalAlignment="Center" |
||||||
|
Orientation="Horizontal"> |
||||||
|
|
||||||
|
<TextBlock |
||||||
|
VerticalAlignment="Center" |
||||||
|
FontSize="{TemplateBinding FontSize}" |
||||||
|
Foreground="DarkGray" |
||||||
|
Text="or" /> |
||||||
|
|
||||||
|
<Button |
||||||
|
Margin="4,0" |
||||||
|
Padding="4" |
||||||
|
Command="{Binding SelectImageFromFilePickerCommand}" |
||||||
|
Content="Browse" |
||||||
|
FontSize="{TemplateBinding FontSize}" /> |
||||||
|
</StackPanel> |
||||||
|
</Grid> |
||||||
|
</controls:LineDashFrame> |
||||||
|
</Panel> |
||||||
|
</ExperimentalAcrylicBorder> |
||||||
|
|
||||||
|
</controls:Card> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
</Styles> |
@ -0,0 +1,39 @@ |
|||||||
|
using System; |
||||||
|
using System.Drawing; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Controls.Primitives; |
||||||
|
using DynamicData.Binding; |
||||||
|
using StabilityMatrix.Avalonia.ViewModels.Inference; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
[Transient] |
||||||
|
public class SelectImageCard : DropTargetTemplatedControlBase |
||||||
|
{ |
||||||
|
/// <inheritdoc /> |
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
||||||
|
{ |
||||||
|
base.OnApplyTemplate(e); |
||||||
|
|
||||||
|
if (DataContext is not SelectImageCardViewModel vm) |
||||||
|
return; |
||||||
|
|
||||||
|
if (e.NameScope.Find<BetterAdvancedImage>("PART_BetterAdvancedImage") is not { } image) |
||||||
|
return; |
||||||
|
|
||||||
|
image |
||||||
|
.WhenPropertyChanged(x => x.CurrentImage) |
||||||
|
.Subscribe(propertyValue => |
||||||
|
{ |
||||||
|
if (propertyValue.Value?.Size is { } size) |
||||||
|
{ |
||||||
|
vm.CurrentBitmapSize = new Size(Convert.ToInt32(size.Width), Convert.ToInt32(size.Height)); |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
vm.CurrentBitmapSize = Size.Empty; |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,113 @@ |
|||||||
|
<Styles |
||||||
|
xmlns="https://github.com/avaloniaui" |
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||||
|
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
||||||
|
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" |
||||||
|
xmlns:local="clr-namespace:StabilityMatrix.Avalonia" |
||||||
|
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
||||||
|
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" |
||||||
|
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" |
||||||
|
x:DataType="vmInference:StackExpanderViewModel"> |
||||||
|
|
||||||
|
<Design.PreviewWith> |
||||||
|
<Grid Width="500" Height="800"> |
||||||
|
<StackPanel> |
||||||
|
<controls:StackExpander DataContext="{x:Static mocks:DesignData.StackExpanderViewModel}" /> |
||||||
|
|
||||||
|
<controls:StackExpander DataContext="{x:Static mocks:DesignData.StackExpanderViewModel2}" /> |
||||||
|
</StackPanel> |
||||||
|
</Grid> |
||||||
|
</Design.PreviewWith> |
||||||
|
|
||||||
|
<Style Selector="controls|StackExpander"> |
||||||
|
<!-- Set Defaults --> |
||||||
|
<Setter Property="CornerRadius" Value="8"/> |
||||||
|
<Setter Property="HorizontalAlignment" Value="Stretch" /> |
||||||
|
<Setter Property="Template"> |
||||||
|
<ControlTemplate> |
||||||
|
<Expander |
||||||
|
MinHeight="0" |
||||||
|
CornerRadius="{TemplateBinding CornerRadius}" |
||||||
|
ExpandDirection="{TemplateBinding ExpandDirection}" |
||||||
|
IsExpanded="{TemplateBinding IsExpanded}" |
||||||
|
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
||||||
|
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
||||||
|
HorizontalContentAlignment="Stretch" |
||||||
|
VerticalContentAlignment="Top"> |
||||||
|
|
||||||
|
<Expander.Styles> |
||||||
|
<Style Selector="Expander /template/ ToggleButton#PART_toggle"> |
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> |
||||||
|
</Style> |
||||||
|
</Expander.Styles> |
||||||
|
|
||||||
|
<Expander.Header> |
||||||
|
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto" Margin="0,-4"> |
||||||
|
<ToggleSwitch |
||||||
|
Margin="2,0,0,4" |
||||||
|
VerticalContentAlignment="Center" |
||||||
|
IsChecked="{Binding IsEnabled}" |
||||||
|
OffContent="" |
||||||
|
OnContent="" /> |
||||||
|
<TextBlock |
||||||
|
Grid.Column="1" |
||||||
|
VerticalAlignment="Center" |
||||||
|
IsVisible="{Binding TitleExtra, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" |
||||||
|
Text="{Binding TitleExtra}" /> |
||||||
|
<TextBlock |
||||||
|
Grid.Column="2" |
||||||
|
VerticalAlignment="Center" |
||||||
|
Text="{Binding Title}" /> |
||||||
|
<!-- Settings button --> |
||||||
|
<Button |
||||||
|
Grid.Column="3" |
||||||
|
Padding="12,6" |
||||||
|
HorizontalAlignment="Right" |
||||||
|
Classes="transparent-full" |
||||||
|
Command="{Binding SettingsCommand}" |
||||||
|
IsVisible="{Binding IsSettingsEnabled}"> |
||||||
|
<fluentIcons:SymbolIcon FontSize="17" Symbol="Settings" /> |
||||||
|
</Button> |
||||||
|
<!-- Delete button for StackEditableCard --> |
||||||
|
<Button |
||||||
|
Grid.Column="4" |
||||||
|
Padding="12,6" |
||||||
|
HorizontalAlignment="Right" |
||||||
|
Classes="transparent-red" |
||||||
|
Command="{Binding RemoveFromParentListCommand}" |
||||||
|
IsVisible="{Binding IsEditEnabled}"> |
||||||
|
<fluentIcons:SymbolIcon FontSize="16" Symbol="Delete" /> |
||||||
|
</Button> |
||||||
|
</Grid> |
||||||
|
</Expander.Header> |
||||||
|
|
||||||
|
<Panel> |
||||||
|
<ItemsControl VerticalAlignment="Top" ItemsSource="{Binding Cards}"> |
||||||
|
|
||||||
|
<ItemsControl.Styles> |
||||||
|
<Style Selector="controls|Card"> |
||||||
|
<Setter Property="IsCardVisualsEnabled" Value="False" /> |
||||||
|
</Style> |
||||||
|
</ItemsControl.Styles> |
||||||
|
|
||||||
|
<ItemsControl.DataTemplates> |
||||||
|
<local:ViewLocator /> |
||||||
|
</ItemsControl.DataTemplates> |
||||||
|
|
||||||
|
<ItemsControl.ItemsPanel> |
||||||
|
<ItemsPanelTemplate> |
||||||
|
<StackPanel |
||||||
|
x:Name="PART_ItemsPanel" |
||||||
|
VerticalAlignment="Top" |
||||||
|
Spacing="{TemplateBinding Spacing}" /> |
||||||
|
</ItemsPanelTemplate> |
||||||
|
</ItemsControl.ItemsPanel> |
||||||
|
</ItemsControl> |
||||||
|
</Panel> |
||||||
|
|
||||||
|
|
||||||
|
</Expander> |
||||||
|
</ControlTemplate> |
||||||
|
</Setter> |
||||||
|
</Style> |
||||||
|
</Styles> |
@ -0,0 +1,41 @@ |
|||||||
|
using Avalonia; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Controls.Primitives; |
||||||
|
using StabilityMatrix.Core.Attributes; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
[Transient] |
||||||
|
public class StackExpander : TemplatedControl |
||||||
|
{ |
||||||
|
public static readonly StyledProperty<bool> IsExpandedProperty = Expander |
||||||
|
.IsExpandedProperty |
||||||
|
.AddOwner<StackExpander>(); |
||||||
|
|
||||||
|
public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty = Expander |
||||||
|
.ExpandDirectionProperty |
||||||
|
.AddOwner<StackExpander>(); |
||||||
|
|
||||||
|
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register<StackCard, int>( |
||||||
|
"Spacing", |
||||||
|
8 |
||||||
|
); |
||||||
|
|
||||||
|
public ExpandDirection ExpandDirection |
||||||
|
{ |
||||||
|
get => GetValue(ExpandDirectionProperty); |
||||||
|
set => SetValue(ExpandDirectionProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public bool IsExpanded |
||||||
|
{ |
||||||
|
get => GetValue(IsExpandedProperty); |
||||||
|
set => SetValue(IsExpandedProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public int Spacing |
||||||
|
{ |
||||||
|
get => GetValue(SpacingProperty); |
||||||
|
set => SetValue(SpacingProperty, value); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,124 @@ |
|||||||
|
using System; |
||||||
|
using System.Collections.Generic; |
||||||
|
using System.Linq; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.PropertyGrid.Services; |
||||||
|
using JetBrains.Annotations; |
||||||
|
using PropertyModels.ComponentModel; |
||||||
|
using StabilityMatrix.Core.Extensions; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
[PublicAPI] |
||||||
|
public class BetterPropertyGrid : global::Avalonia.PropertyGrid.Controls.PropertyGrid |
||||||
|
{ |
||||||
|
protected override Type StyleKeyOverride => typeof(global::Avalonia.PropertyGrid.Controls.PropertyGrid); |
||||||
|
|
||||||
|
public static readonly StyledProperty<IEnumerable<string>> ExcludedCategoriesProperty = AvaloniaProperty.Register< |
||||||
|
BetterPropertyGrid, |
||||||
|
IEnumerable<string> |
||||||
|
>("ExcludedCategories"); |
||||||
|
|
||||||
|
public IEnumerable<string> ExcludedCategories |
||||||
|
{ |
||||||
|
get => GetValue(ExcludedCategoriesProperty); |
||||||
|
set => SetValue(ExcludedCategoriesProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly StyledProperty<IEnumerable<string>> IncludedCategoriesProperty = AvaloniaProperty.Register< |
||||||
|
BetterPropertyGrid, |
||||||
|
IEnumerable<string> |
||||||
|
>("IncludedCategories"); |
||||||
|
|
||||||
|
public IEnumerable<string> IncludedCategories |
||||||
|
{ |
||||||
|
get => GetValue(IncludedCategoriesProperty); |
||||||
|
set => SetValue(IncludedCategoriesProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
static BetterPropertyGrid() |
||||||
|
{ |
||||||
|
// Register factories |
||||||
|
CellEditFactoryService.Default.AddFactory(new ToggleSwitchCellEditFactory()); |
||||||
|
|
||||||
|
// Initialize localization and name resolver |
||||||
|
LocalizationService.Default.AddExtraService(new PropertyGridLocalizationService()); |
||||||
|
|
||||||
|
ExcludedCategoriesProperty |
||||||
|
.Changed |
||||||
|
.AddClassHandler<BetterPropertyGrid>( |
||||||
|
(grid, args) => |
||||||
|
{ |
||||||
|
if (args.NewValue is IEnumerable<string> excludedCategories) |
||||||
|
{ |
||||||
|
grid.FilterExcludeCategories(excludedCategories); |
||||||
|
} |
||||||
|
} |
||||||
|
); |
||||||
|
|
||||||
|
IncludedCategoriesProperty |
||||||
|
.Changed |
||||||
|
.AddClassHandler<BetterPropertyGrid>( |
||||||
|
(grid, args) => |
||||||
|
{ |
||||||
|
if (args.NewValue is IEnumerable<string> includedCategories) |
||||||
|
{ |
||||||
|
grid.FilterIncludeCategories(includedCategories); |
||||||
|
} |
||||||
|
} |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public void FilterExcludeCategories(IEnumerable<string> excludedCategories) |
||||||
|
{ |
||||||
|
// Get internal property `ViewModel` of internal type `PropertyGridViewModel` |
||||||
|
var gridVm = this.GetProtectedProperty("ViewModel")!; |
||||||
|
// Get public property `CategoryFilter` |
||||||
|
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("CategoryFilter")!; |
||||||
|
|
||||||
|
categoryFilter.BeginUpdate(); |
||||||
|
|
||||||
|
// Uncheck All, then check all except All |
||||||
|
categoryFilter.UnCheck(categoryFilter.All); |
||||||
|
|
||||||
|
foreach (var mask in categoryFilter.Masks.Where(m => m != categoryFilter.All)) |
||||||
|
{ |
||||||
|
categoryFilter.Check(mask); |
||||||
|
} |
||||||
|
|
||||||
|
// Uncheck excluded categories |
||||||
|
foreach (var mask in excludedCategories) |
||||||
|
{ |
||||||
|
categoryFilter.UnCheck(mask); |
||||||
|
} |
||||||
|
|
||||||
|
categoryFilter.EndUpdate(); |
||||||
|
} |
||||||
|
|
||||||
|
public void FilterIncludeCategories(IEnumerable<string> includeCategories) |
||||||
|
{ |
||||||
|
// Get internal property `ViewModel` of internal type `PropertyGridViewModel` |
||||||
|
var gridVm = this.GetProtectedProperty("ViewModel")!; |
||||||
|
// Get public property `CategoryFilter` |
||||||
|
var categoryFilter = gridVm.GetProtectedProperty<CheckedMaskModel>("CategoryFilter")!; |
||||||
|
|
||||||
|
categoryFilter.BeginUpdate(); |
||||||
|
|
||||||
|
// Uncheck non-included categories |
||||||
|
foreach (var mask in categoryFilter.Masks.Where(m => !includeCategories.Contains(m))) |
||||||
|
{ |
||||||
|
categoryFilter.UnCheck(mask); |
||||||
|
} |
||||||
|
|
||||||
|
categoryFilter.UnCheck(categoryFilter.All); |
||||||
|
|
||||||
|
// Check included categories |
||||||
|
foreach (var mask in includeCategories) |
||||||
|
{ |
||||||
|
categoryFilter.Check(mask); |
||||||
|
} |
||||||
|
|
||||||
|
categoryFilter.EndUpdate(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
using System; |
||||||
|
using System.Globalization; |
||||||
|
using PropertyModels.Localilzation; |
||||||
|
using StabilityMatrix.Avalonia.Languages; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
internal class PropertyGridCultureData : ICultureData |
||||||
|
{ |
||||||
|
/// <inheritdoc /> |
||||||
|
public bool Reload() => false; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public CultureInfo Culture => Cultures.Current ?? Cultures.Default; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public Uri Path => new(""); |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public string this[string key] |
||||||
|
{ |
||||||
|
get |
||||||
|
{ |
||||||
|
if (Resources.ResourceManager.GetString(key) is { } result) |
||||||
|
{ |
||||||
|
return result; |
||||||
|
} |
||||||
|
|
||||||
|
return key; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public bool IsLoaded => true; |
||||||
|
} |
@ -0,0 +1,36 @@ |
|||||||
|
using System; |
||||||
|
using PropertyModels.ComponentModel; |
||||||
|
using PropertyModels.Localilzation; |
||||||
|
using StabilityMatrix.Avalonia.Languages; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Implements <see cref="ILocalizationService"/> using static <see cref="Cultures"/>. |
||||||
|
/// </summary> |
||||||
|
internal class PropertyGridLocalizationService : MiniReactiveObject, ILocalizationService |
||||||
|
{ |
||||||
|
/// <inheritdoc /> |
||||||
|
public ICultureData CultureData { get; } = new PropertyGridCultureData(); |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public string this[string key] => CultureData[key]; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public event EventHandler? OnCultureChanged; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public ILocalizationService[] GetExtraServices() => Array.Empty<ILocalizationService>(); |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public void AddExtraService(ILocalizationService service) { } |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public void RemoveExtraService(ILocalizationService service) { } |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public ICultureData[] GetCultures() => new[] { CultureData }; |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public void SelectCulture(string cultureName) { } |
||||||
|
} |
@ -0,0 +1,60 @@ |
|||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.PropertyGrid.Controls; |
||||||
|
using Avalonia.PropertyGrid.Controls.Factories; |
||||||
|
using Avalonia.PropertyGrid.Localization; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Controls; |
||||||
|
|
||||||
|
internal class ToggleSwitchCellEditFactory : AbstractCellEditFactory |
||||||
|
{ |
||||||
|
// make this extend factor only effect on TestExtendPropertyGrid |
||||||
|
public override bool Accept(object accessToken) |
||||||
|
{ |
||||||
|
return accessToken is BetterPropertyGrid; |
||||||
|
} |
||||||
|
|
||||||
|
public override Control? HandleNewProperty(PropertyCellContext context) |
||||||
|
{ |
||||||
|
var propertyDescriptor = context.Property; |
||||||
|
var target = context.Target; |
||||||
|
|
||||||
|
if (propertyDescriptor.PropertyType != typeof(bool)) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
var control = new ToggleSwitch(); |
||||||
|
control.SetLocalizeBinding(ToggleSwitch.OnContentProperty, "On"); |
||||||
|
control.SetLocalizeBinding(ToggleSwitch.OffContentProperty, "Off"); |
||||||
|
|
||||||
|
control.IsCheckedChanged += (s, e) => |
||||||
|
{ |
||||||
|
SetAndRaise(context, control, control.IsChecked); |
||||||
|
}; |
||||||
|
|
||||||
|
return control; |
||||||
|
} |
||||||
|
|
||||||
|
public override bool HandlePropertyChanged(PropertyCellContext context) |
||||||
|
{ |
||||||
|
var propertyDescriptor = context.Property; |
||||||
|
var target = context.Target; |
||||||
|
var control = context.CellEdit; |
||||||
|
|
||||||
|
if (propertyDescriptor.PropertyType != typeof(bool)) |
||||||
|
{ |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
ValidateProperty(control, propertyDescriptor, target); |
||||||
|
|
||||||
|
if (control is ToggleSwitch ts) |
||||||
|
{ |
||||||
|
ts.IsChecked = (bool)(propertyDescriptor.GetValue(target) ?? false); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
return false; |
||||||
|
} |
||||||
|
} |
@ -1,104 +0,0 @@ |
|||||||
<Styles xmlns="https://github.com/avaloniaui" |
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|
||||||
xmlns:ui="using:FluentAvalonia.UI.Controls" |
|
||||||
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
|
||||||
xmlns:mocks="using:StabilityMatrix.Avalonia.DesignData" |
|
||||||
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" |
|
||||||
x:DataType="vmInference:SelectImageCardViewModel"> |
|
||||||
|
|
||||||
<Design.PreviewWith> |
|
||||||
<Panel Width="600" Height="500"> |
|
||||||
<Grid ColumnDefinitions="*,*" MaxHeight="400"> |
|
||||||
<controls:SelectImageCard |
|
||||||
Margin="4" |
|
||||||
DataContext="{x:Static mocks:DesignData.SelectImageCardViewModel}" /> |
|
||||||
<controls:SelectImageCard |
|
||||||
Grid.Column="1" |
|
||||||
Margin="4" |
|
||||||
DataContext="{x:Static mocks:DesignData.SelectImageCardViewModel_WithImage}" /> |
|
||||||
</Grid> |
|
||||||
</Panel> |
|
||||||
</Design.PreviewWith> |
|
||||||
|
|
||||||
<Style Selector="controls|SelectImageCard"> |
|
||||||
<!-- Set Defaults --> |
|
||||||
<Setter Property="Padding" Value="12"/> |
|
||||||
<Setter Property="Template"> |
|
||||||
<ControlTemplate> |
|
||||||
<controls:Card |
|
||||||
IsCardVisualsEnabled="True" |
|
||||||
Padding="{TemplateBinding Padding}" |
|
||||||
VerticalContentAlignment="{TemplateBinding VerticalAlignment}" |
|
||||||
VerticalAlignment="{TemplateBinding VerticalAlignment}" |
|
||||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}"> |
|
||||||
|
|
||||||
<!-- Background frame --> |
|
||||||
<ExperimentalAcrylicBorder |
|
||||||
VerticalAlignment="Stretch" |
|
||||||
Material="{StaticResource OpaqueDarkAcrylicMaterial}" |
|
||||||
CornerRadius="4"> |
|
||||||
<Panel> |
|
||||||
<!-- Image --> |
|
||||||
<controls:BetterAdvancedImage |
|
||||||
CornerRadius="4" |
|
||||||
VerticalAlignment="Stretch" |
|
||||||
VerticalContentAlignment="Stretch" |
|
||||||
CurrentImage="{Binding CurrentBitmap, Mode=OneWayToSource}" |
|
||||||
IsVisible="{Binding !IsSelectionAvailable}" |
|
||||||
Source="{Binding ImageSource}"/> |
|
||||||
|
|
||||||
<!-- Selection Prompt --> |
|
||||||
<controls:LineDashFrame |
|
||||||
IsVisible="{Binding IsSelectionAvailable}" |
|
||||||
Stroke="DimGray" |
|
||||||
StrokeThickness="3" |
|
||||||
StrokeDashLine="6" |
|
||||||
StrokeDashSpace="6"> |
|
||||||
|
|
||||||
<Grid |
|
||||||
Cursor="Hand" |
|
||||||
RowDefinitions="*,Auto,Auto" |
|
||||||
VerticalAlignment="Center" |
|
||||||
HorizontalAlignment="Center"> |
|
||||||
|
|
||||||
<ui:SymbolIcon |
|
||||||
FontSize="28" |
|
||||||
Symbol="ImageCopyFilled"/> |
|
||||||
<TextBlock |
|
||||||
Grid.Row="1" |
|
||||||
FontSize="{TemplateBinding FontSize}" |
|
||||||
Text="Drag an image here" |
|
||||||
TextWrapping="WrapWithOverflow" |
|
||||||
Foreground="DarkGray"> |
|
||||||
</TextBlock> |
|
||||||
|
|
||||||
<StackPanel |
|
||||||
Grid.Row="2" |
|
||||||
Margin="0,4,0,0" |
|
||||||
HorizontalAlignment="Center" |
|
||||||
Orientation="Horizontal"> |
|
||||||
|
|
||||||
<TextBlock |
|
||||||
FontSize="{TemplateBinding FontSize}" |
|
||||||
VerticalAlignment="Center" |
|
||||||
Foreground="DarkGray" |
|
||||||
Text="or"/> |
|
||||||
|
|
||||||
<Button |
|
||||||
FontSize="{TemplateBinding FontSize}" |
|
||||||
Command="{Binding SelectImageFromFilePickerCommand}" |
|
||||||
Content="Browse" |
|
||||||
Margin="4,0" |
|
||||||
Padding="4"/> |
|
||||||
</StackPanel> |
|
||||||
</Grid> |
|
||||||
</controls:LineDashFrame> |
|
||||||
</Panel> |
|
||||||
</ExperimentalAcrylicBorder> |
|
||||||
|
|
||||||
</controls:Card> |
|
||||||
</ControlTemplate> |
|
||||||
</Setter> |
|
||||||
</Style> |
|
||||||
</Styles> |
|
@ -1,6 +0,0 @@ |
|||||||
using StabilityMatrix.Core.Attributes; |
|
||||||
|
|
||||||
namespace StabilityMatrix.Avalonia.Controls; |
|
||||||
|
|
||||||
[Transient] |
|
||||||
public class SelectImageCard : DropTargetTemplatedControlBase { } |
|
@ -1,96 +0,0 @@ |
|||||||
<Styles xmlns="https://github.com/avaloniaui" |
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|
||||||
xmlns:controls="using:StabilityMatrix.Avalonia.Controls" |
|
||||||
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" |
|
||||||
xmlns:vmInference="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Inference" |
|
||||||
xmlns:local="clr-namespace:StabilityMatrix.Avalonia" |
|
||||||
xmlns:fluentIcons="clr-namespace:FluentIcons.FluentAvalonia;assembly=FluentIcons.FluentAvalonia" |
|
||||||
x:DataType="vmInference:StackExpanderViewModel"> |
|
||||||
|
|
||||||
<Design.PreviewWith> |
|
||||||
<Grid Width="500" Height="800"> |
|
||||||
<StackPanel> |
|
||||||
<controls:StackExpander |
|
||||||
DataContext="{x:Static mocks:DesignData.StackExpanderViewModel}"/> |
|
||||||
</StackPanel> |
|
||||||
</Grid> |
|
||||||
</Design.PreviewWith> |
|
||||||
|
|
||||||
<Style Selector="controls|StackExpander"> |
|
||||||
<!-- Set Defaults --> |
|
||||||
<Setter Property="HorizontalAlignment" Value="Stretch"/> |
|
||||||
<Setter Property="Template"> |
|
||||||
<ControlTemplate> |
|
||||||
<Expander |
|
||||||
VerticalContentAlignment="Top" |
|
||||||
HorizontalAlignment="{TemplateBinding HorizontalAlignment}" |
|
||||||
HorizontalContentAlignment="Stretch" |
|
||||||
VerticalAlignment="{TemplateBinding VerticalAlignment}"> |
|
||||||
|
|
||||||
<Expander.Styles> |
|
||||||
<Style Selector="Expander /template/ ToggleButton#PART_toggle"> |
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> |
|
||||||
</Style> |
|
||||||
</Expander.Styles> |
|
||||||
|
|
||||||
<Expander.Header> |
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto"> |
|
||||||
<ToggleSwitch |
|
||||||
Margin="2,0,0,2" |
|
||||||
IsChecked="{Binding IsEnabled}" |
|
||||||
VerticalContentAlignment="Center" |
|
||||||
OnContent="" |
|
||||||
OffContent=""/> |
|
||||||
<TextBlock |
|
||||||
Grid.Column="1" |
|
||||||
VerticalAlignment="Center" |
|
||||||
Text="{Binding TitleExtra}" |
|
||||||
IsVisible="{Binding TitleExtra, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> |
|
||||||
<TextBlock |
|
||||||
Grid.Column="2" |
|
||||||
VerticalAlignment="Center" |
|
||||||
Text="{Binding Title}"/> |
|
||||||
<Button |
|
||||||
IsVisible="{Binding IsEditEnabled}" |
|
||||||
Command="{Binding RemoveFromParentListCommand}" |
|
||||||
Classes="transparent-red" |
|
||||||
Grid.Column="3" |
|
||||||
Padding="10,4" |
|
||||||
HorizontalAlignment="Right"> |
|
||||||
<fluentIcons:SymbolIcon Symbol="Delete" FontSize="16" /> |
|
||||||
</Button> |
|
||||||
</Grid> |
|
||||||
</Expander.Header> |
|
||||||
|
|
||||||
<Panel> |
|
||||||
<ItemsControl |
|
||||||
VerticalAlignment="Top" |
|
||||||
ItemsSource="{Binding Cards}"> |
|
||||||
|
|
||||||
<ItemsControl.Styles> |
|
||||||
<Style Selector="controls|Card"> |
|
||||||
<Setter Property="IsCardVisualsEnabled" Value="False"/> |
|
||||||
</Style> |
|
||||||
</ItemsControl.Styles> |
|
||||||
|
|
||||||
<ItemsControl.DataTemplates> |
|
||||||
<local:ViewLocator/> |
|
||||||
</ItemsControl.DataTemplates> |
|
||||||
|
|
||||||
<ItemsControl.ItemsPanel> |
|
||||||
<ItemsPanelTemplate> |
|
||||||
<StackPanel |
|
||||||
x:Name="PART_ItemsPanel" |
|
||||||
VerticalAlignment="Top" |
|
||||||
Spacing="{TemplateBinding Spacing}" /> |
|
||||||
</ItemsPanelTemplate> |
|
||||||
</ItemsControl.ItemsPanel> |
|
||||||
</ItemsControl> |
|
||||||
</Panel> |
|
||||||
|
|
||||||
|
|
||||||
</Expander> |
|
||||||
</ControlTemplate> |
|
||||||
</Setter> |
|
||||||
</Style> |
|
||||||
</Styles> |
|
@ -1,20 +0,0 @@ |
|||||||
using Avalonia; |
|
||||||
using Avalonia.Controls.Primitives; |
|
||||||
using StabilityMatrix.Core.Attributes; |
|
||||||
|
|
||||||
namespace StabilityMatrix.Avalonia.Controls; |
|
||||||
|
|
||||||
[Transient] |
|
||||||
public class StackExpander : TemplatedControl |
|
||||||
{ |
|
||||||
public static readonly StyledProperty<int> SpacingProperty = AvaloniaProperty.Register< |
|
||||||
StackCard, |
|
||||||
int |
|
||||||
>("Spacing", 8); |
|
||||||
|
|
||||||
public int Spacing |
|
||||||
{ |
|
||||||
get => GetValue(SpacingProperty); |
|
||||||
set => SetValue(SpacingProperty, value); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,22 @@ |
|||||||
|
using System; |
||||||
|
using System.Diagnostics.CodeAnalysis; |
||||||
|
using System.Globalization; |
||||||
|
using Avalonia.Data.Converters; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Converters; |
||||||
|
|
||||||
|
public class CustomStringFormatConverter<T>([StringSyntax("CompositeFormat")] string format) : IValueConverter |
||||||
|
where T : IFormatProvider, new() |
||||||
|
{ |
||||||
|
/// <inheritdoc /> |
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
return value is null ? null : string.Format(new T(), format, value); |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
return value is null ? null : throw new NotImplementedException(); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,31 @@ |
|||||||
|
using System; |
||||||
|
using System.Globalization; |
||||||
|
using Avalonia.Data.Converters; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Converters; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Converts an index to index + 1 |
||||||
|
/// </summary> |
||||||
|
public class IndexPlusOneConverter : IValueConverter |
||||||
|
{ |
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
if (value is int i) |
||||||
|
{ |
||||||
|
return i + 1; |
||||||
|
} |
||||||
|
|
||||||
|
return value; |
||||||
|
} |
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
if (value is int i) |
||||||
|
{ |
||||||
|
return i - 1; |
||||||
|
} |
||||||
|
|
||||||
|
return value; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,41 @@ |
|||||||
|
using System; |
||||||
|
using Size = StabilityMatrix.Core.Helper.Size; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Converters; |
||||||
|
|
||||||
|
public class MemoryBytesFormatter : ICustomFormatter, IFormatProvider |
||||||
|
{ |
||||||
|
/// <inheritdoc /> |
||||||
|
public object? GetFormat(Type? formatType) |
||||||
|
{ |
||||||
|
return formatType == typeof(ICustomFormatter) ? this : null; |
||||||
|
} |
||||||
|
|
||||||
|
/// <inheritdoc /> |
||||||
|
public string Format(string? format, object? arg, IFormatProvider? formatProvider) |
||||||
|
{ |
||||||
|
if (format == null || !format.Trim().StartsWith('M')) |
||||||
|
{ |
||||||
|
if (arg is IFormattable formatArg) |
||||||
|
{ |
||||||
|
return formatArg.ToString(format, formatProvider); |
||||||
|
} |
||||||
|
|
||||||
|
return arg?.ToString() ?? string.Empty; |
||||||
|
} |
||||||
|
|
||||||
|
var value = Convert.ToUInt64(arg); |
||||||
|
|
||||||
|
var result = format.Trim().EndsWith("10", StringComparison.OrdinalIgnoreCase) |
||||||
|
? Size.FormatBase10Bytes(value) |
||||||
|
: Size.FormatBytes(value); |
||||||
|
|
||||||
|
// Strip i if not Mi |
||||||
|
if (!format.Trim().Contains('I', StringComparison.OrdinalIgnoreCase)) |
||||||
|
{ |
||||||
|
result = result.Replace("i", string.Empty, StringComparison.OrdinalIgnoreCase); |
||||||
|
} |
||||||
|
|
||||||
|
return result; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
using System; |
||||||
|
using System.ComponentModel; |
||||||
|
using System.Diagnostics.CodeAnalysis; |
||||||
|
using System.Globalization; |
||||||
|
using System.Numerics; |
||||||
|
using Avalonia.Data.Converters; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Converters; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Converts a possibly boxed nullable value type to its default value |
||||||
|
/// </summary> |
||||||
|
public class NullableDefaultNumericConverter<TSource, TTarget> : IValueConverter |
||||||
|
where TSource : unmanaged, INumber<TSource> |
||||||
|
where TTarget : unmanaged, INumber<TTarget> |
||||||
|
{ |
||||||
|
public ReturnBehavior NanHandling { get; set; } = ReturnBehavior.DefaultValue; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Unboxes a nullable value type |
||||||
|
/// </summary> |
||||||
|
private TSource Unbox(TTarget? value) |
||||||
|
{ |
||||||
|
if (!value.HasValue) |
||||||
|
{ |
||||||
|
return default; |
||||||
|
} |
||||||
|
|
||||||
|
if (TTarget.IsNaN(value.Value)) |
||||||
|
{ |
||||||
|
return NanHandling switch |
||||||
|
{ |
||||||
|
ReturnBehavior.DefaultValue => default, |
||||||
|
ReturnBehavior.Throw => throw new InvalidCastException("Cannot convert NaN to a numeric type"), |
||||||
|
_ |
||||||
|
=> throw new InvalidEnumArgumentException( |
||||||
|
nameof(NanHandling), |
||||||
|
(int)NanHandling, |
||||||
|
typeof(ReturnBehavior) |
||||||
|
) |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
return (TSource)System.Convert.ChangeType(value.Value, typeof(TSource)); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Convert a value type to a nullable value type |
||||||
|
/// </summary> |
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
if (targetType != typeof(TTarget?) && !targetType.IsAssignableTo(typeof(TTarget))) |
||||||
|
{ |
||||||
|
// ReSharper disable once LocalizableElement |
||||||
|
throw new ArgumentException( |
||||||
|
$"Convert Target type {targetType.Name} must be assignable to {typeof(TTarget).Name}" |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return (TTarget?)System.Convert.ChangeType(value, typeof(TTarget)); |
||||||
|
} |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Convert a nullable value type to a value type |
||||||
|
/// </summary> |
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) |
||||||
|
{ |
||||||
|
if (!targetType.IsAssignableTo(typeof(TSource))) |
||||||
|
{ |
||||||
|
// ReSharper disable once LocalizableElement |
||||||
|
throw new ArgumentException( |
||||||
|
$"ConvertBack Target type {targetType.Name} must be assignable to {typeof(TSource).Name}" |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
return Unbox((TTarget?)value); |
||||||
|
} |
||||||
|
|
||||||
|
public enum ReturnBehavior |
||||||
|
{ |
||||||
|
DefaultValue, |
||||||
|
Throw |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
namespace StabilityMatrix.Avalonia.Converters; |
||||||
|
|
||||||
|
public static class NullableDefaultNumericConverters |
||||||
|
{ |
||||||
|
public static readonly NullableDefaultNumericConverter<int, decimal> IntToDecimal = new(); |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
using System; |
||||||
|
using System.Threading.Tasks; |
||||||
|
using StabilityMatrix.Core.Models; |
||||||
|
using StabilityMatrix.Core.Models.FileInterfaces; |
||||||
|
using StabilityMatrix.Core.Models.Progress; |
||||||
|
using StabilityMatrix.Core.Services; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.DesignData; |
||||||
|
|
||||||
|
public class MockMetadataImportService : IMetadataImportService |
||||||
|
{ |
||||||
|
public Task ScanDirectoryForMissingInfo(DirectoryPath directory, IProgress<ProgressReport>? progress = null) |
||||||
|
{ |
||||||
|
return Task.CompletedTask; |
||||||
|
} |
||||||
|
|
||||||
|
public Task<ConnectedModelInfo?> GetMetadataForFile( |
||||||
|
FilePath filePath, |
||||||
|
IProgress<ProgressReport>? progress = null, |
||||||
|
bool forceReimport = false |
||||||
|
) |
||||||
|
{ |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public Task UpdateExistingMetadata(DirectoryPath directory, IProgress<ProgressReport>? progress = null) |
||||||
|
{ |
||||||
|
return Task.CompletedTask; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,54 @@ |
|||||||
|
using System.ComponentModel; |
||||||
|
using CommunityToolkit.Mvvm.ComponentModel; |
||||||
|
using PropertyModels.ComponentModel; |
||||||
|
using StabilityMatrix.Avalonia.Languages; |
||||||
|
|
||||||
|
#pragma warning disable CS0657 // Not a valid attribute location for this declaration |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.DesignData; |
||||||
|
|
||||||
|
public partial class MockPropertyGridObject : ObservableObject |
||||||
|
{ |
||||||
|
[ObservableProperty] |
||||||
|
private string? stringProperty; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private int intProperty; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[property: Trackable(0, 50, Increment = 1, FormatString = "{0:0}")] |
||||||
|
private int intRange = 10; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[property: Trackable(0d, 1d, Increment = 0.01, FormatString = "{0:P0}")] |
||||||
|
private double floatPercentRange = 0.25; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[property: DisplayName("Int Custom Name")] |
||||||
|
private int intCustomNameProperty = 42; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[property: DisplayName(nameof(Resources.Label_Language))] |
||||||
|
private int? intLocalizedNameProperty; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
private bool boolProperty; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[property: Category("Included Category")] |
||||||
|
private string? stringIncludedCategoryProperty; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[property: Category("Excluded Category")] |
||||||
|
private string? stringExcludedCategoryProperty; |
||||||
|
} |
||||||
|
|
||||||
|
public partial class MockPropertyGridObjectAlt : ObservableObject |
||||||
|
{ |
||||||
|
[ObservableProperty] |
||||||
|
private int altIntProperty = 10; |
||||||
|
|
||||||
|
[ObservableProperty] |
||||||
|
[property: Category("Settings")] |
||||||
|
private string? altStringProperty; |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
using Avalonia.Input; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.Extensions; |
||||||
|
|
||||||
|
public static class DataObjectExtensions |
||||||
|
{ |
||||||
|
/// <summary> |
||||||
|
/// Get Context from IDataObject, set by Xaml Behaviors |
||||||
|
/// </summary> |
||||||
|
public static T? GetContext<T>(this IDataObject dataObject) |
||||||
|
{ |
||||||
|
if (dataObject.Get("Context") is T context) |
||||||
|
{ |
||||||
|
return context; |
||||||
|
} |
||||||
|
|
||||||
|
return default; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,783 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<root> |
||||||
|
<!-- |
||||||
|
Microsoft ResX Schema |
||||||
|
|
||||||
|
Version 2.0 |
||||||
|
|
||||||
|
The primary goals of this format is to allow a simple XML format |
||||||
|
that is mostly human readable. The generation and parsing of the |
||||||
|
various data types are done through the TypeConverter classes |
||||||
|
associated with the data types. |
||||||
|
|
||||||
|
Example: |
||||||
|
|
||||||
|
... ado.net/XML headers & schema ... |
||||||
|
<resheader name="resmimetype">text/microsoft-resx</resheader> |
||||||
|
<resheader name="version">2.0</resheader> |
||||||
|
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader> |
||||||
|
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader> |
||||||
|
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data> |
||||||
|
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data> |
||||||
|
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64"> |
||||||
|
<value>[base64 mime encoded serialized .NET Framework object]</value> |
||||||
|
</data> |
||||||
|
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64"> |
||||||
|
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> |
||||||
|
<comment>This is a comment</comment> |
||||||
|
</data> |
||||||
|
|
||||||
|
There are any number of "resheader" rows that contain simple |
||||||
|
name/value pairs. |
||||||
|
|
||||||
|
Each data row contains a name, and value. The row also contains a |
||||||
|
type or mimetype. Type corresponds to a .NET class that support |
||||||
|
text/value conversion through the TypeConverter architecture. |
||||||
|
Classes that don't support this are serialized and stored with the |
||||||
|
mimetype set. |
||||||
|
|
||||||
|
The mimetype is used for serialized objects, and tells the |
||||||
|
ResXResourceReader how to depersist the object. This is currently not |
||||||
|
extensible. For a given mimetype the value must be set accordingly: |
||||||
|
|
||||||
|
Note - application/x-microsoft.net.object.binary.base64 is the format |
||||||
|
that the ResXResourceWriter will generate, however the reader can |
||||||
|
read any of the formats listed below. |
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.binary.base64 |
||||||
|
value : The object must be serialized with |
||||||
|
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter |
||||||
|
: and then encoded with base64 encoding. |
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.soap.base64 |
||||||
|
value : The object must be serialized with |
||||||
|
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter |
||||||
|
: and then encoded with base64 encoding. |
||||||
|
|
||||||
|
mimetype: application/x-microsoft.net.object.bytearray.base64 |
||||||
|
value : The object must be serialized into a byte array |
||||||
|
: using a System.ComponentModel.TypeConverter |
||||||
|
: and then encoded with base64 encoding. |
||||||
|
--> |
||||||
|
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> |
||||||
|
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" /> |
||||||
|
<xsd:element name="root" msdata:IsDataSet="true"> |
||||||
|
<xsd:complexType> |
||||||
|
<xsd:choice maxOccurs="unbounded"> |
||||||
|
<xsd:element name="metadata"> |
||||||
|
<xsd:complexType> |
||||||
|
<xsd:sequence> |
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" /> |
||||||
|
</xsd:sequence> |
||||||
|
<xsd:attribute name="name" use="required" type="xsd:string" /> |
||||||
|
<xsd:attribute name="type" type="xsd:string" /> |
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" /> |
||||||
|
<xsd:attribute ref="xml:space" /> |
||||||
|
</xsd:complexType> |
||||||
|
</xsd:element> |
||||||
|
<xsd:element name="assembly"> |
||||||
|
<xsd:complexType> |
||||||
|
<xsd:attribute name="alias" type="xsd:string" /> |
||||||
|
<xsd:attribute name="name" type="xsd:string" /> |
||||||
|
</xsd:complexType> |
||||||
|
</xsd:element> |
||||||
|
<xsd:element name="data"> |
||||||
|
<xsd:complexType> |
||||||
|
<xsd:sequence> |
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> |
||||||
|
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" /> |
||||||
|
</xsd:sequence> |
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" /> |
||||||
|
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" /> |
||||||
|
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" /> |
||||||
|
<xsd:attribute ref="xml:space" /> |
||||||
|
</xsd:complexType> |
||||||
|
</xsd:element> |
||||||
|
<xsd:element name="resheader"> |
||||||
|
<xsd:complexType> |
||||||
|
<xsd:sequence> |
||||||
|
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" /> |
||||||
|
</xsd:sequence> |
||||||
|
<xsd:attribute name="name" type="xsd:string" use="required" /> |
||||||
|
</xsd:complexType> |
||||||
|
</xsd:element> |
||||||
|
</xsd:choice> |
||||||
|
</xsd:complexType> |
||||||
|
</xsd:element> |
||||||
|
</xsd:schema> |
||||||
|
<resheader name="resmimetype"> |
||||||
|
<value>text/microsoft-resx</value> |
||||||
|
</resheader> |
||||||
|
<resheader name="version"> |
||||||
|
<value>2.0</value> |
||||||
|
</resheader> |
||||||
|
<resheader name="reader"> |
||||||
|
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> |
||||||
|
</resheader> |
||||||
|
<resheader name="writer"> |
||||||
|
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> |
||||||
|
</resheader> |
||||||
|
<data name="Action_Launch" xml:space="preserve"> |
||||||
|
<value>Başlat</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Quit" xml:space="preserve"> |
||||||
|
<value>Çıkış</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Save" xml:space="preserve"> |
||||||
|
<value>Kaydet</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Cancel" xml:space="preserve"> |
||||||
|
<value>İptal</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Language" xml:space="preserve"> |
||||||
|
<value>Dil</value> |
||||||
|
</data> |
||||||
|
<data name="Text_RelaunchRequiredToApplyLanguage" xml:space="preserve"> |
||||||
|
<value>Yeni dil seçeneğinin etkili olması için yeniden başlatma gerekiyor</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Relaunch" xml:space="preserve"> |
||||||
|
<value>Yeniden başlat</value> |
||||||
|
</data> |
||||||
|
<data name="Action_RelaunchLater" xml:space="preserve"> |
||||||
|
<value>Daha Sonra Yeniden Başlat</value> |
||||||
|
</data> |
||||||
|
<data name="Label_RelaunchRequired" xml:space="preserve"> |
||||||
|
<value>Yeniden Başlatma Gerekli</value> |
||||||
|
</data> |
||||||
|
<data name="Label_UnknownPackage" xml:space="preserve"> |
||||||
|
<value>Bilinmeyen Paket</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Import" xml:space="preserve"> |
||||||
|
<value>İçe Aktar</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PackageType" xml:space="preserve"> |
||||||
|
<value>Paket Türü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Version" xml:space="preserve"> |
||||||
|
<value>Sürüm</value> |
||||||
|
</data> |
||||||
|
<data name="Label_VersionType" xml:space="preserve"> |
||||||
|
<value>Sürüm Türü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Releases" xml:space="preserve"> |
||||||
|
<value>Sürümler</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Branches" xml:space="preserve"> |
||||||
|
<value>Dallar</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DragAndDropCheckpointsHereToImport" xml:space="preserve"> |
||||||
|
<value>İçe aktarmak için chekpoints'leri buraya sürükleyip bırakın</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Emphasis" xml:space="preserve"> |
||||||
|
<value>Vurgu</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Deemphasis" xml:space="preserve"> |
||||||
|
<value>Vurguyu Kaldırma</value> |
||||||
|
</data> |
||||||
|
<data name="Label_EmbeddingsOrTextualInversion" xml:space="preserve"> |
||||||
|
<value>Emebeddings / Textual Inversion</value> |
||||||
|
</data> |
||||||
|
<data name="Label_NetworksLoraOrLycoris" xml:space="preserve"> |
||||||
|
<value>Networks (Lora / LyCORIS)</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Comments" xml:space="preserve"> |
||||||
|
<value>Yorumlar</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ShowPixelGridAtHighZoomLevels" xml:space="preserve"> |
||||||
|
<value>Yüksek yakınlaştırma seviyesinde piksel ızgarasını göster</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Steps" xml:space="preserve"> |
||||||
|
<value>Adımlar</value> |
||||||
|
</data> |
||||||
|
<data name="Label_StepsBase" xml:space="preserve"> |
||||||
|
<value>Adımlar - Temel</value> |
||||||
|
</data> |
||||||
|
<data name="Label_StepsRefiner" xml:space="preserve"> |
||||||
|
<value>Adımlar - İyileştirici</value> |
||||||
|
</data> |
||||||
|
<data name="Label_CFGScale" xml:space="preserve"> |
||||||
|
<value>CFG Ölçeği</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DenoisingStrength" xml:space="preserve"> |
||||||
|
<value>Gürültü Azaltma Gücü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Width" xml:space="preserve"> |
||||||
|
<value>Genişlik</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Height" xml:space="preserve"> |
||||||
|
<value>Yükseklik</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Refiner" xml:space="preserve"> |
||||||
|
<value>İyileştirici</value> |
||||||
|
</data> |
||||||
|
<data name="Label_VAE" xml:space="preserve"> |
||||||
|
<value>VAE</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Model" xml:space="preserve"> |
||||||
|
<value>Model</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Connect" xml:space="preserve"> |
||||||
|
<value>Bağlan</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ConnectingEllipsis" xml:space="preserve"> |
||||||
|
<value>Bağlanıyor...</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Close" xml:space="preserve"> |
||||||
|
<value>Kapat</value> |
||||||
|
</data> |
||||||
|
<data name="Label_WaitingToConnectEllipsis" xml:space="preserve"> |
||||||
|
<value>Bağlanmayı bekliyor...</value> |
||||||
|
</data> |
||||||
|
<data name="Label_UpdateAvailable" xml:space="preserve"> |
||||||
|
<value>Güncelleme Mevcut</value> |
||||||
|
</data> |
||||||
|
<data name="Label_BecomeAPatron" xml:space="preserve"> |
||||||
|
<value>Sponsor Ol</value> |
||||||
|
</data> |
||||||
|
<data name="Label_JoinDiscord" xml:space="preserve"> |
||||||
|
<value>Discord Sunucusuna Katıl</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Downloads" xml:space="preserve"> |
||||||
|
<value>İndirmeler</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Install" xml:space="preserve"> |
||||||
|
<value>Yükle</value> |
||||||
|
</data> |
||||||
|
<data name="Label_SkipSetup" xml:space="preserve"> |
||||||
|
<value>İlk kurulum işlemini atla</value> |
||||||
|
</data> |
||||||
|
<data name="Label_UnexpectedErrorOccurred" xml:space="preserve"> |
||||||
|
<value>Beklenmeyen bir hata oluştu</value> |
||||||
|
</data> |
||||||
|
<data name="Action_ExitApplication" xml:space="preserve"> |
||||||
|
<value>Uygulamadan Çık</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DisplayName" xml:space="preserve"> |
||||||
|
<value>Görünen Ad</value> |
||||||
|
</data> |
||||||
|
<data name="Label_InstallationWithThisNameExists" xml:space="preserve"> |
||||||
|
<value>Bu ada sahip bir yükleme zaten mevcut.</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PleaseChooseDifferentName" xml:space="preserve"> |
||||||
|
<value>Lütfen farklı bir ad seçin veya başka bir yükleme konumu seçin.</value> |
||||||
|
</data> |
||||||
|
<data name="Label_AdvancedOptions" xml:space="preserve"> |
||||||
|
<value>Gelişmiş Seçenekler</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Commit" xml:space="preserve"> |
||||||
|
<value>İşlemek</value> |
||||||
|
</data> |
||||||
|
<data name="Label_SharedModelFolderStrategy" xml:space="preserve"> |
||||||
|
<value>Paylaşılan Model Klasör Stratejisi</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PyTorchVersion" xml:space="preserve"> |
||||||
|
<value>PyTorch Sürümü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_CloseDialogWhenFinished" xml:space="preserve"> |
||||||
|
<value>Bittiğinde iletişim kutusunu kapat</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DataDirectory" xml:space="preserve"> |
||||||
|
<value>Veri Klasörü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DataDirectoryExplanation" xml:space="preserve"> |
||||||
|
<value>Model chekpoints, Lora'ların, web UI'lerin, ayarların vb. kurulacağı yer burasıdır.</value> |
||||||
|
</data> |
||||||
|
<data name="Label_FatWarning" xml:space="preserve"> |
||||||
|
<value>FAT32 veya exFAT sürücü kullanırken hatalarla karşılaşabilirsiniz. Daha sorunsuz bir deneyim için farklı bir sürücü seçin.</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PortableMode" xml:space="preserve"> |
||||||
|
<value>Taşınabilir Mod</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PortableModeExplanation" xml:space="preserve"> |
||||||
|
<value>Taşınabilir Modda tüm veriler ve ayarlar uygulamayla aynı dizinde saklanacaktır. Uygulamayı 'Veri' klasörüyle birlikte farklı bir konuma veya bilgisayara taşıyabileceksiniz.</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Continue" xml:space="preserve"> |
||||||
|
<value>Devam</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PreviousImage" xml:space="preserve"> |
||||||
|
<value>Önceki Resim</value> |
||||||
|
</data> |
||||||
|
<data name="Label_NextImage" xml:space="preserve"> |
||||||
|
<value>Sonraki Resim</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ModelDescription" xml:space="preserve"> |
||||||
|
<value>Model Açıklaması</value> |
||||||
|
</data> |
||||||
|
<data name="Label_NewVersionAvailable" xml:space="preserve"> |
||||||
|
<value>Stabilite Matrisi için yeni bir sürüm mevcut!</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ImportLatest" xml:space="preserve"> |
||||||
|
<value>En Yeniyi İçe Aktar -</value> |
||||||
|
</data> |
||||||
|
<data name="Label_AllVersions" xml:space="preserve"> |
||||||
|
<value>Tüm Sürümler</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ModelSearchWatermark" xml:space="preserve"> |
||||||
|
<value>Modelleri, #etiketleri veya @kullanıcıları ara</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Search" xml:space="preserve"> |
||||||
|
<value>Ara</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Sort" xml:space="preserve"> |
||||||
|
<value>Sırala</value> |
||||||
|
</data> |
||||||
|
<data name="Label_TimePeriod" xml:space="preserve"> |
||||||
|
<value>Süre</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ModelType" xml:space="preserve"> |
||||||
|
<value>Model Türü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_BaseModel" xml:space="preserve"> |
||||||
|
<value>Temel Model</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ShowNsfwContent" xml:space="preserve"> |
||||||
|
<value>NSFW İçerik Göster</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DataProvidedByCivitAi" xml:space="preserve"> |
||||||
|
<value>CivitAI tarafından sağlanan veriler</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Page" xml:space="preserve"> |
||||||
|
<value>Sayfa</value> |
||||||
|
</data> |
||||||
|
<data name="Label_FirstPage" xml:space="preserve"> |
||||||
|
<value>İlk Sayfa</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PreviousPage" xml:space="preserve"> |
||||||
|
<value>Önceki Sayfa</value> |
||||||
|
</data> |
||||||
|
<data name="Label_NextPage" xml:space="preserve"> |
||||||
|
<value>Sonraki Sayfa</value> |
||||||
|
</data> |
||||||
|
<data name="Label_LastPage" xml:space="preserve"> |
||||||
|
<value>Son Sayfa</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Rename" xml:space="preserve"> |
||||||
|
<value>Yeniden Adlandır</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Delete" xml:space="preserve"> |
||||||
|
<value>Sil</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenOnCivitAi" xml:space="preserve"> |
||||||
|
<value>CivitAI'de Aç</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ConnectedModel" xml:space="preserve"> |
||||||
|
<value>Bağlı Model</value> |
||||||
|
</data> |
||||||
|
<data name="Label_LocalModel" xml:space="preserve"> |
||||||
|
<value>Yerel Model</value> |
||||||
|
</data> |
||||||
|
<data name="Action_ShowInExplorer" xml:space="preserve"> |
||||||
|
<value>Explorer'da göster</value> |
||||||
|
</data> |
||||||
|
<data name="Action_New" xml:space="preserve"> |
||||||
|
<value>Yeni</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Folder" xml:space="preserve"> |
||||||
|
<value>Klasör</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DropFileToImport" xml:space="preserve"> |
||||||
|
<value>İçe aktarma için dosyayı buraya bırakın</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ImportAsConnected" xml:space="preserve"> |
||||||
|
<value>Metadata ile içeri aktar</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ImportAsConnectedExplanation" xml:space="preserve"> |
||||||
|
<value>Yeni yerel içe aktarmalar için bağlı meta veri arayın</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Indexing" xml:space="preserve"> |
||||||
|
<value>İndeksleniyor...</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ModelsFolder" xml:space="preserve"> |
||||||
|
<value>Model Klasörü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Categories" xml:space="preserve"> |
||||||
|
<value>Kategoriler</value> |
||||||
|
</data> |
||||||
|
<data name="Label_LetsGetStarted" xml:space="preserve"> |
||||||
|
<value>Başlayalım</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ReadAndAgree" xml:space="preserve"> |
||||||
|
<value>Lisans Sözleşmesini Okudum ve Kabul Ediyorum.</value> |
||||||
|
</data> |
||||||
|
<data name="Label_LicenseAgreement" xml:space="preserve"> |
||||||
|
<value>Lisans Sözleşmesi.</value> |
||||||
|
</data> |
||||||
|
<data name="Label_FindConnectedMetadata" xml:space="preserve"> |
||||||
|
<value>Metadata'larını bul</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ShowModelImages" xml:space="preserve"> |
||||||
|
<value>Model Resimlerini Göster</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Appearance" xml:space="preserve"> |
||||||
|
<value>Görünüm</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Theme" xml:space="preserve"> |
||||||
|
<value>Tema</value> |
||||||
|
</data> |
||||||
|
<data name="Label_CheckpointManager" xml:space="preserve"> |
||||||
|
<value>Checkpoint Yöneticisi</value> |
||||||
|
</data> |
||||||
|
<data name="Label_RemoveSymlinksOnShutdown" xml:space="preserve"> |
||||||
|
<value>Kapatma sırasında paylaşılan Checkpoint dizini sembolik bağlantılarını kaldırın</value> |
||||||
|
</data> |
||||||
|
<data name="Label_RemoveSymlinksOnShutdown_Details" xml:space="preserve"> |
||||||
|
<value>Stabilite Matrisini başka bir sürücüye taşımada sorun yaşıyorsanız bu seçeneği seçin</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ResetCheckpointsCache" xml:space="preserve"> |
||||||
|
<value>Checkpoints Önbelleğini Sıfırla</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ResetCheckpointsCache_Details" xml:space="preserve"> |
||||||
|
<value>Kurulu checkpoints önbelleğini yeniden oluştur. Model tarayıcısında checkpointler yanlış etiketlenmişse kullanın</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PackageEnvironment" xml:space="preserve"> |
||||||
|
<value>Paket Ortamı</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Edit" xml:space="preserve"> |
||||||
|
<value>Düzenle</value> |
||||||
|
</data> |
||||||
|
<data name="Label_EnvironmentVariables" xml:space="preserve"> |
||||||
|
<value>Ortam Değişkenleri</value> |
||||||
|
</data> |
||||||
|
<data name="Label_EmbeddedPython" xml:space="preserve"> |
||||||
|
<value>Gömülü Python</value> |
||||||
|
</data> |
||||||
|
<data name="Action_CheckVersion" xml:space="preserve"> |
||||||
|
<value>Sürümü Kontrol Et</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Integrations" xml:space="preserve"> |
||||||
|
<value>Entegrasyonlar</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DiscordRichPresence" xml:space="preserve"> |
||||||
|
<value>Discord Zengin Varlık</value> |
||||||
|
</data> |
||||||
|
<data name="Label_System" xml:space="preserve"> |
||||||
|
<value>Sistem</value> |
||||||
|
</data> |
||||||
|
<data name="Label_AddToStartMenu" xml:space="preserve"> |
||||||
|
<value>Stability Matrix'i başlat menüsüne ekle</value> |
||||||
|
</data> |
||||||
|
<data name="Label_AddToStartMenu_Details" xml:space="preserve"> |
||||||
|
<value>Mevcut uygulama konumu kullanılır, uygulamayı taşırsanız tekrar çalıştırabilirsiniz</value> |
||||||
|
</data> |
||||||
|
<data name="Label_OnlyAvailableOnWindows" xml:space="preserve"> |
||||||
|
<value>Yalnızca Windows'ta kullanılabilir</value> |
||||||
|
</data> |
||||||
|
<data name="Action_AddForCurrentUser" xml:space="preserve"> |
||||||
|
<value>Geçerli Kullanıcı için Ekle</value> |
||||||
|
</data> |
||||||
|
<data name="Action_AddForAllUsers" xml:space="preserve"> |
||||||
|
<value>Tüm Kullanıcılar İçin Ekle</value> |
||||||
|
</data> |
||||||
|
<data name="Label_SelectNewDataDirectory" xml:space="preserve"> |
||||||
|
<value>Yeni Veri Dizini Seç</value> |
||||||
|
</data> |
||||||
|
<data name="Label_SelectNewDataDirectory_Details" xml:space="preserve"> |
||||||
|
<value>Mevcut veriyi taşımaz</value> |
||||||
|
</data> |
||||||
|
<data name="Action_SelectDirectory" xml:space="preserve"> |
||||||
|
<value>Dizin Seçin</value> |
||||||
|
</data> |
||||||
|
<data name="Label_About" xml:space="preserve"> |
||||||
|
<value>Hakkında</value> |
||||||
|
</data> |
||||||
|
<data name="Label_StabilityMatrix" xml:space="preserve"> |
||||||
|
<value>Stability Matrix</value> |
||||||
|
</data> |
||||||
|
<data name="Label_LicenseAndOpenSourceNotices" xml:space="preserve"> |
||||||
|
<value>Lisans ve Açık Kaynak Bildirimleri</value> |
||||||
|
</data> |
||||||
|
<data name="TeachingTip_ClickLaunchToGetStarted" xml:space="preserve"> |
||||||
|
<value>Başlamak için Başlat'a tıklayın!</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Stop" xml:space="preserve"> |
||||||
|
<value>Dur</value> |
||||||
|
</data> |
||||||
|
<data name="Action_SendInput" xml:space="preserve"> |
||||||
|
<value>Giriş Gönder</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Input" xml:space="preserve"> |
||||||
|
<value>Giriş</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Send" xml:space="preserve"> |
||||||
|
<value>Gönder</value> |
||||||
|
</data> |
||||||
|
<data name="Label_InputRequired" xml:space="preserve"> |
||||||
|
<value>Giriş gerekiyor</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ConfirmQuestion" xml:space="preserve"> |
||||||
|
<value>Onaylamak?</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Yes" xml:space="preserve"> |
||||||
|
<value>Evet</value> |
||||||
|
</data> |
||||||
|
<data name="Label_No" xml:space="preserve"> |
||||||
|
<value>Hayır</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenWebUI" xml:space="preserve"> |
||||||
|
<value>Web Arayüzünü Aç</value> |
||||||
|
</data> |
||||||
|
<data name="Text_WelcomeToStabilityMatrix" xml:space="preserve"> |
||||||
|
<value>Stability Matrix'e Hoş Geldiniz!</value> |
||||||
|
</data> |
||||||
|
<data name="Text_OneClickInstaller_SubHeader" xml:space="preserve"> |
||||||
|
<value>Tercih ettiğiniz arayüzü seçin ve başlamak için Yükle'ye tıklayın</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Installing" xml:space="preserve"> |
||||||
|
<value>Yükleniyor</value> |
||||||
|
</data> |
||||||
|
<data name="Text_ProceedingToLaunchPage" xml:space="preserve"> |
||||||
|
<value>Başlatma sayfasına geçiliyor</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_DownloadingPackage" xml:space="preserve"> |
||||||
|
<value>Paket indiriliyor...</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_DownloadComplete" xml:space="preserve"> |
||||||
|
<value>İndirme tamamlandı</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_InstallationComplete" xml:space="preserve"> |
||||||
|
<value>Kurulum tamamlandı</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_InstallingPrerequisites" xml:space="preserve"> |
||||||
|
<value>Önkoşullar kuruluyor...</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_InstallingPackageRequirements" xml:space="preserve"> |
||||||
|
<value>Paket gereksinimleri kuruluyor...</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenInExplorer" xml:space="preserve"> |
||||||
|
<value>Explorer'da Aç</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenInFinder" xml:space="preserve"> |
||||||
|
<value>Finder'da Aç</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Uninstall" xml:space="preserve"> |
||||||
|
<value>Kaldır</value> |
||||||
|
</data> |
||||||
|
<data name="Action_CheckForUpdates" xml:space="preserve"> |
||||||
|
<value>Güncellemeleri Kontrol Et</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Update" xml:space="preserve"> |
||||||
|
<value>Güncelle</value> |
||||||
|
</data> |
||||||
|
<data name="Action_AddPackage" xml:space="preserve"> |
||||||
|
<value>Paket Ekle</value> |
||||||
|
</data> |
||||||
|
<data name="TeachingTip_AddPackageToGetStarted" xml:space="preserve"> |
||||||
|
<value>Başlamak için bir paket ekle!</value> |
||||||
|
</data> |
||||||
|
<data name="Label_EnvVarsTable_Name" xml:space="preserve"> |
||||||
|
<value>Ad</value> |
||||||
|
</data> |
||||||
|
<data name="Label_EnvVarsTable_Value" xml:space="preserve"> |
||||||
|
<value>Değer</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Remove" xml:space="preserve"> |
||||||
|
<value>Kaldır</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Details" xml:space="preserve"> |
||||||
|
<value>Ayrıntılar</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Callstack" xml:space="preserve"> |
||||||
|
<value>Çağrı Yığını</value> |
||||||
|
</data> |
||||||
|
<data name="Label_InnerException" xml:space="preserve"> |
||||||
|
<value>İç istisna</value> |
||||||
|
</data> |
||||||
|
<data name="Label_SearchEllipsis" xml:space="preserve"> |
||||||
|
<value>Ara...</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OK" xml:space="preserve"> |
||||||
|
<value>Tamam</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Retry" xml:space="preserve"> |
||||||
|
<value>Tekrar Dene</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PythonVersionInfo" xml:space="preserve"> |
||||||
|
<value>Python Sürüm Bilgisi</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Restart" xml:space="preserve"> |
||||||
|
<value>Yeniden Başlat</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ConfirmDelete" xml:space="preserve"> |
||||||
|
<value>Silmeyi Onayla</value> |
||||||
|
</data> |
||||||
|
<data name="Text_PackageUninstall_Details" xml:space="preserve"> |
||||||
|
<value>Bu, paket klasörünü ve içindeki tüm içeriği, eklediğiniz tüm oluşturulmuş resimleri ve dosyaları silecek.</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_UninstallingPackage" xml:space="preserve"> |
||||||
|
<value>Paket Kaldırılıyor...</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PackageUninstalled" xml:space="preserve"> |
||||||
|
<value>Paket Kaldırıldı</value> |
||||||
|
</data> |
||||||
|
<data name="Text_SomeFilesCouldNotBeDeleted" xml:space="preserve"> |
||||||
|
<value>Bazı dosyalar silinemedi. Lütfen paket klasöründeki açık dosyaları kapatın ve tekrar deneyin.</value> |
||||||
|
</data> |
||||||
|
<data name="Label_InvalidPackageType" xml:space="preserve"> |
||||||
|
<value>Geçersiz Paket Türü</value> |
||||||
|
</data> |
||||||
|
<data name="TextTemplate_UpdatingPackage" xml:space="preserve"> |
||||||
|
<value>{0} Güncelleniyor</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_UpdateComplete" xml:space="preserve"> |
||||||
|
<value>Güncelleme tamamlandı</value> |
||||||
|
</data> |
||||||
|
<data name="TextTemplate_PackageUpdatedToLatest" xml:space="preserve"> |
||||||
|
<value>{0}, en son sürüme güncellendi</value> |
||||||
|
</data> |
||||||
|
<data name="TextTemplate_ErrorUpdatingPackage" xml:space="preserve"> |
||||||
|
<value>{0} güncelleme hatası</value> |
||||||
|
</data> |
||||||
|
<data name="Progress_UpdateFailed" xml:space="preserve"> |
||||||
|
<value>Güncelleme başarısız oldu</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenInBrowser" xml:space="preserve"> |
||||||
|
<value>Tarayıcıda Aç</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ErrorInstallingPackage" xml:space="preserve"> |
||||||
|
<value>Paket kurulum hatası</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Branch" xml:space="preserve"> |
||||||
|
<value>Dal</value> |
||||||
|
</data> |
||||||
|
<data name="Label_AutoScrollToEnd" xml:space="preserve"> |
||||||
|
<value>Konsol çıktısının sonuna otomatik olarak kaydır</value> |
||||||
|
</data> |
||||||
|
<data name="Label_License" xml:space="preserve"> |
||||||
|
<value>Lisans</value> |
||||||
|
</data> |
||||||
|
<data name="Label_SharedModelStrategyShort" xml:space="preserve"> |
||||||
|
<value>Model Paylaşımı</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PleaseSelectDataDirectory" xml:space="preserve"> |
||||||
|
<value>Lütfen Bir Veri Klasörü Seçin</value> |
||||||
|
</data> |
||||||
|
<data name="Label_DataFolderName" xml:space="preserve"> |
||||||
|
<value>Veri Klasörü Adı</value> |
||||||
|
</data> |
||||||
|
<data name="Label_CurrentDirectory" xml:space="preserve"> |
||||||
|
<value>Geçerli klasör:</value> |
||||||
|
</data> |
||||||
|
<data name="Text_AppWillRelaunchAfterUpdate" xml:space="preserve"> |
||||||
|
<value>Uygulama güncelleme sonrası yeniden başlatılacaktır</value> |
||||||
|
</data> |
||||||
|
<data name="Action_RemindMeLater" xml:space="preserve"> |
||||||
|
<value>Daha Sonra Hatırlat</value> |
||||||
|
</data> |
||||||
|
<data name="Action_InstallNow" xml:space="preserve"> |
||||||
|
<value>Şimdi Yükle</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ReleaseNotes" xml:space="preserve"> |
||||||
|
<value>Sürüm Notları</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenProjectEllipsis" xml:space="preserve"> |
||||||
|
<value>Projeyi Aç...</value> |
||||||
|
</data> |
||||||
|
<data name="Action_SaveAsEllipsis" xml:space="preserve"> |
||||||
|
<value>Farklı Kaydet...</value> |
||||||
|
</data> |
||||||
|
<data name="Action_RestoreDefaultLayout" xml:space="preserve"> |
||||||
|
<value>Varsayılan Düzeni Geri Yükle</value> |
||||||
|
</data> |
||||||
|
<data name="Label_UseSharedOutputFolder" xml:space="preserve"> |
||||||
|
<value>Çıktı Paylaşımı</value> |
||||||
|
</data> |
||||||
|
<data name="Label_BatchIndex" xml:space="preserve"> |
||||||
|
<value>Toplu İndeks</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Copy" xml:space="preserve"> |
||||||
|
<value>Kopyala</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenInViewer" xml:space="preserve"> |
||||||
|
<value>Resim Görüntüleyici'de aç</value> |
||||||
|
</data> |
||||||
|
<data name="Label_NumImagesSelected" xml:space="preserve"> |
||||||
|
<value>{0} resim seçildi</value> |
||||||
|
</data> |
||||||
|
<data name="Label_OutputFolder" xml:space="preserve"> |
||||||
|
<value>Çıkış Klasörü</value> |
||||||
|
</data> |
||||||
|
<data name="Label_OutputType" xml:space="preserve"> |
||||||
|
<value>Çıkış Türü</value> |
||||||
|
</data> |
||||||
|
<data name="Action_ClearSelection" xml:space="preserve"> |
||||||
|
<value>Seçimi Temizle</value> |
||||||
|
</data> |
||||||
|
<data name="Action_SelectAll" xml:space="preserve"> |
||||||
|
<value>Tümünü Seç</value> |
||||||
|
</data> |
||||||
|
<data name="Action_SendToInference" xml:space="preserve"> |
||||||
|
<value>Çıkarıma Gönder</value> |
||||||
|
</data> |
||||||
|
<data name="Label_TextToImage" xml:space="preserve"> |
||||||
|
<value>Metinden Resime</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ImageToImage" xml:space="preserve"> |
||||||
|
<value>Resimden Resime</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Inpainting" xml:space="preserve"> |
||||||
|
<value>Inpainting</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Upscale" xml:space="preserve"> |
||||||
|
<value>Upscale</value> |
||||||
|
</data> |
||||||
|
<data name="Label_OutputsPageTitle" xml:space="preserve"> |
||||||
|
<value>Çıkış Tarayıcısı</value> |
||||||
|
</data> |
||||||
|
<data name="Label_OneImageSelected" xml:space="preserve"> |
||||||
|
<value>1 resim seçildi</value> |
||||||
|
</data> |
||||||
|
<data name="Label_PythonPackages" xml:space="preserve"> |
||||||
|
<value>Python Paketleri</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Consolidate" xml:space="preserve"> |
||||||
|
<value>Birleştir</value> |
||||||
|
</data> |
||||||
|
<data name="Label_AreYouSure" xml:space="preserve"> |
||||||
|
<value>Emin misiniz?</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ConsolidateExplanation" xml:space="preserve"> |
||||||
|
<value>Bu, seçilen paketlerden tüm oluşturulmuş resimleri, paylaşılan çıktılar klasörünün Konsolide dizinine taşıyacak. Bu işlem geri alınamaz.</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Refresh" xml:space="preserve"> |
||||||
|
<value>Yenile</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Upgrade" xml:space="preserve"> |
||||||
|
<value>Yükselt</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Downgrade" xml:space="preserve"> |
||||||
|
<value>Düşür</value> |
||||||
|
</data> |
||||||
|
<data name="Action_OpenGithub" xml:space="preserve"> |
||||||
|
<value>GitHub'da Aç</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Connected" xml:space="preserve"> |
||||||
|
<value>Bağlandı</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Disconnect" xml:space="preserve"> |
||||||
|
<value>Bağlantıyı Kes</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Email" xml:space="preserve"> |
||||||
|
<value>E-posta</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Username" xml:space="preserve"> |
||||||
|
<value>Kullanıcı Adı</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Password" xml:space="preserve"> |
||||||
|
<value>Parola</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Login" xml:space="preserve"> |
||||||
|
<value>Giriş</value> |
||||||
|
</data> |
||||||
|
<data name="Action_Signup" xml:space="preserve"> |
||||||
|
<value>Kaydol</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ConfirmPassword" xml:space="preserve"> |
||||||
|
<value>Parolayı Onayla</value> |
||||||
|
</data> |
||||||
|
<data name="Label_ApiKey" xml:space="preserve"> |
||||||
|
<value>API Key</value> |
||||||
|
</data> |
||||||
|
<data name="Label_Accounts" xml:space="preserve"> |
||||||
|
<value>Hesaplar</value> |
||||||
|
</data> |
||||||
|
</root> |
@ -0,0 +1,113 @@ |
|||||||
|
using System.Linq; |
||||||
|
using Avalonia; |
||||||
|
using Avalonia.Controls; |
||||||
|
using Avalonia.Input; |
||||||
|
using Avalonia.Interactivity; |
||||||
|
using Avalonia.VisualTree; |
||||||
|
using JetBrains.Annotations; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.MarkupExtensions; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// Show tooltip on Controls with IsEffectivelyEnabled = false |
||||||
|
/// https://github.com/AvaloniaUI/Avalonia/issues/3847#issuecomment-1618790059 |
||||||
|
/// </summary> |
||||||
|
[PublicAPI] |
||||||
|
public static class ShowDisabledTooltipExtension |
||||||
|
{ |
||||||
|
static ShowDisabledTooltipExtension() |
||||||
|
{ |
||||||
|
ShowOnDisabledProperty.Changed.AddClassHandler<Control>(HandleShowOnDisabledChanged); |
||||||
|
} |
||||||
|
|
||||||
|
public static bool GetShowOnDisabled(AvaloniaObject obj) |
||||||
|
{ |
||||||
|
return obj.GetValue(ShowOnDisabledProperty); |
||||||
|
} |
||||||
|
|
||||||
|
public static void SetShowOnDisabled(AvaloniaObject obj, bool value) |
||||||
|
{ |
||||||
|
obj.SetValue(ShowOnDisabledProperty, value); |
||||||
|
} |
||||||
|
|
||||||
|
public static readonly AttachedProperty<bool> ShowOnDisabledProperty = AvaloniaProperty.RegisterAttached< |
||||||
|
object, |
||||||
|
Control, |
||||||
|
bool |
||||||
|
>("ShowOnDisabled"); |
||||||
|
|
||||||
|
private static void HandleShowOnDisabledChanged(Control control, AvaloniaPropertyChangedEventArgs e) |
||||||
|
{ |
||||||
|
if (e.GetNewValue<bool>()) |
||||||
|
{ |
||||||
|
control.DetachedFromVisualTree += AttachedControl_DetachedFromVisualOrExtension; |
||||||
|
control.AttachedToVisualTree += AttachedControl_AttachedToVisualTree; |
||||||
|
if (control.IsInitialized) |
||||||
|
{ |
||||||
|
// enabled after visual attached |
||||||
|
AttachedControl_AttachedToVisualTree(control, null!); |
||||||
|
} |
||||||
|
} |
||||||
|
else |
||||||
|
{ |
||||||
|
AttachedControl_DetachedFromVisualOrExtension(control, null!); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private static void AttachedControl_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) |
||||||
|
{ |
||||||
|
if (sender is not Control control || TopLevel.GetTopLevel(control) is not { } tl) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
// NOTE pointermove needed to be tunneled for me but you may not need to... |
||||||
|
tl.AddHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved, RoutingStrategies.Tunnel); |
||||||
|
} |
||||||
|
|
||||||
|
private static void AttachedControl_DetachedFromVisualOrExtension(object? s, VisualTreeAttachmentEventArgs e) |
||||||
|
{ |
||||||
|
if (s is not Control control) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
control.DetachedFromVisualTree -= AttachedControl_DetachedFromVisualOrExtension; |
||||||
|
control.AttachedToVisualTree -= AttachedControl_AttachedToVisualTree; |
||||||
|
if (TopLevel.GetTopLevel(control) is not { } tl) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
tl.RemoveHandler(InputElement.PointerMovedEvent, TopLevel_PointerMoved); |
||||||
|
} |
||||||
|
|
||||||
|
private static void TopLevel_PointerMoved(object? sender, PointerEventArgs e) |
||||||
|
{ |
||||||
|
if (sender is not Control tl) |
||||||
|
{ |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
var attachedControls = tl.GetVisualDescendants().Where(GetShowOnDisabled).Cast<Control>().ToList(); |
||||||
|
|
||||||
|
// find disabled children under pointer w/ this extension enabled |
||||||
|
var disabledChildUnderPointer = attachedControls.FirstOrDefault( |
||||||
|
x => |
||||||
|
x.Bounds.Contains(e.GetPosition(x.Parent as Visual)) |
||||||
|
&& x is { IsEffectivelyVisible: true, IsEffectivelyEnabled: false } |
||||||
|
); |
||||||
|
|
||||||
|
if (disabledChildUnderPointer != null) |
||||||
|
{ |
||||||
|
// manually show tooltip |
||||||
|
ToolTip.SetIsOpen(disabledChildUnderPointer, true); |
||||||
|
} |
||||||
|
|
||||||
|
var disabledTooltipsToHide = attachedControls.Where( |
||||||
|
x => ToolTip.GetIsOpen(x) && x != disabledChildUnderPointer && !x.IsEffectivelyEnabled |
||||||
|
); |
||||||
|
|
||||||
|
foreach (var control in disabledTooltipsToHide) |
||||||
|
{ |
||||||
|
ToolTip.SetIsOpen(control, false); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
using System; |
||||||
|
using System.Globalization; |
||||||
|
using Avalonia.Data; |
||||||
|
using Avalonia.Data.Converters; |
||||||
|
using Avalonia.Markup.Xaml; |
||||||
|
using Avalonia.Markup.Xaml.MarkupExtensions; |
||||||
|
|
||||||
|
namespace StabilityMatrix.Avalonia.MarkupExtensions; |
||||||
|
|
||||||
|
/// <summary> |
||||||
|
/// https://github.com/AvaloniaUI/Avalonia/discussions/7408 |
||||||
|
/// </summary> |
||||||
|
/// <example> |
||||||
|
/// <code>{e:Ternary SomeProperty, True=1, False=0}</code> |
||||||
|
/// </example> |
||||||
|
public class TernaryExtension : MarkupExtension |
||||||
|
{ |
||||||
|
public string Path { get; set; } |
||||||
|
|
||||||
|
public Type Type { get; set; } |
||||||
|
|
||||||
|
public object? True { get; set; } |
||||||
|
|
||||||
|
public object? False { get; set; } |
||||||
|
|
||||||
|
public override object ProvideValue(IServiceProvider serviceProvider) |
||||||
|
{ |
||||||
|
var cultureInfo = CultureInfo.GetCultureInfo("en-US"); |
||||||
|
var binding = new ReflectionBindingExtension(Path) |
||||||
|
{ |
||||||
|
Mode = BindingMode.OneWay, |
||||||
|
Converter = new FuncValueConverter<bool, object?>( |
||||||
|
isTrue => |
||||||
|
isTrue |
||||||
|
? Convert.ChangeType(True, Type, cultureInfo.NumberFormat) |
||||||
|
: Convert.ChangeType(False, Type, cultureInfo.NumberFormat) |
||||||
|
) |
||||||
|
}; |
||||||
|
|
||||||
|
return binding.ProvideValue(serviceProvider); |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue