committed by
GitHub
159 changed files with 5024 additions and 1219 deletions
@ -0,0 +1,113 @@ |
|||||
|
#include "common.h" |
||||
|
|
||||
|
@interface CocoaThemeObserver : NSObject |
||||
|
-(id)initWithCallback:(IAvnActionCallback *)callback; |
||||
|
@end |
||||
|
|
||||
|
class PlatformSettings : public ComSingleObject<IAvnPlatformSettings, &IID_IAvnPlatformSettings> |
||||
|
{ |
||||
|
CocoaThemeObserver* observer; |
||||
|
|
||||
|
public: |
||||
|
FORWARD_IUNKNOWN() |
||||
|
virtual AvnPlatformThemeVariant GetPlatformTheme() override |
||||
|
{ |
||||
|
@autoreleasepool |
||||
|
{ |
||||
|
if (@available(macOS 10.14, *)) |
||||
|
{ |
||||
|
if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAqua |
||||
|
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantLight) { |
||||
|
return AvnPlatformThemeVariant::Light; |
||||
|
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameDarkAqua |
||||
|
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameVibrantDark) { |
||||
|
return AvnPlatformThemeVariant::Dark; |
||||
|
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastAqua |
||||
|
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantLight) { |
||||
|
return AvnPlatformThemeVariant::HighContrastLight; |
||||
|
} else if (NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastDarkAqua |
||||
|
|| NSApplication.sharedApplication.effectiveAppearance.name == NSAppearanceNameAccessibilityHighContrastVibrantDark) { |
||||
|
return AvnPlatformThemeVariant::HighContrastDark; |
||||
|
} |
||||
|
} |
||||
|
return AvnPlatformThemeVariant::Light; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
virtual unsigned int GetAccentColor() override |
||||
|
{ |
||||
|
@autoreleasepool |
||||
|
{ |
||||
|
if (@available(macOS 10.14, *)) |
||||
|
{ |
||||
|
auto color = [NSColor controlAccentColor]; |
||||
|
return to_argb(color); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
return 0; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
virtual void RegisterColorsChange(IAvnActionCallback *callback) override |
||||
|
{ |
||||
|
if (@available(macOS 10.14, *)) |
||||
|
{ |
||||
|
observer = [[CocoaThemeObserver alloc] initWithCallback: callback]; |
||||
|
[[NSApplication sharedApplication] addObserver:observer forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:nil]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private: |
||||
|
unsigned int to_argb(NSColor* color) |
||||
|
{ |
||||
|
const CGFloat* components = CGColorGetComponents(color.CGColor); |
||||
|
unsigned int alpha = static_cast<unsigned int>(CGColorGetAlpha(color.CGColor) * 0xFF); |
||||
|
unsigned int red = static_cast<unsigned int>(components[0] * 0xFF); |
||||
|
unsigned int green = static_cast<unsigned int>(components[1] * 0xFF); |
||||
|
unsigned int blue = static_cast<unsigned int>(components[2] * 0xFF); |
||||
|
return (alpha << 24) + (red << 16) + (green << 8) + blue; |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
@implementation CocoaThemeObserver |
||||
|
{ |
||||
|
ComPtr<IAvnActionCallback> _callback; |
||||
|
} |
||||
|
- (id) initWithCallback:(IAvnActionCallback *)callback{ |
||||
|
self = [super init]; |
||||
|
if (self) { |
||||
|
_callback = callback; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
/*- (void)didChangeValueForKey:(NSString *)key { |
||||
|
if([key isEqualToString:@"effectiveAppearance"]) { |
||||
|
_callback->Run(); |
||||
|
} |
||||
|
else { |
||||
|
[super didChangeValueForKey:key]; |
||||
|
} |
||||
|
}*/ |
||||
|
|
||||
|
- (void)observeValueForKeyPath:(NSString *)keyPath |
||||
|
ofObject:(id)object |
||||
|
change:(NSDictionary *)change |
||||
|
context:(void *)context { |
||||
|
if([keyPath isEqualToString:@"effectiveAppearance"]) { |
||||
|
_callback->Run(); |
||||
|
} else { |
||||
|
[super observeValueForKeyPath:keyPath |
||||
|
ofObject:object |
||||
|
change:change |
||||
|
context:context]; |
||||
|
} |
||||
|
} |
||||
|
@end |
||||
|
|
||||
|
extern IAvnPlatformSettings* CreatePlatformSettings() |
||||
|
{ |
||||
|
return new PlatformSettings(); |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Android |
||||
|
{ |
||||
|
public interface IActivityNavigationService |
||||
|
{ |
||||
|
event EventHandler<AndroidBackRequestedEventArgs> BackRequested; |
||||
|
} |
||||
|
|
||||
|
public class AndroidBackRequestedEventArgs : EventArgs |
||||
|
{ |
||||
|
public bool Handled { get; set; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,94 @@ |
|||||
|
using System; |
||||
|
using Android; |
||||
|
using Android.Content; |
||||
|
using Android.Content.Res; |
||||
|
using Android.Graphics; |
||||
|
using Android.Provider; |
||||
|
using Android.Views.Accessibility; |
||||
|
using AndroidX.Core.Content.Resources; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Color = Avalonia.Media.Color; |
||||
|
|
||||
|
namespace Avalonia.Android.Platform; |
||||
|
|
||||
|
// TODO: ideally should be created per view/activity.
|
||||
|
internal class AndroidPlatformSettings : DefaultPlatformSettings |
||||
|
{ |
||||
|
private PlatformColorValues _latestValues; |
||||
|
|
||||
|
public AndroidPlatformSettings() |
||||
|
{ |
||||
|
_latestValues = base.GetColorValues(); |
||||
|
} |
||||
|
|
||||
|
public override PlatformColorValues GetColorValues() |
||||
|
{ |
||||
|
return _latestValues; |
||||
|
} |
||||
|
|
||||
|
internal void OnViewConfigurationChanged(Context context) |
||||
|
{ |
||||
|
if (context.Resources?.Configuration is null) |
||||
|
{ |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var systemTheme = (context.Resources.Configuration.UiMode & UiMode.NightMask) switch |
||||
|
{ |
||||
|
UiMode.NightYes => PlatformThemeVariant.Dark, |
||||
|
UiMode.NightNo => PlatformThemeVariant.Light, |
||||
|
_ => throw new ArgumentOutOfRangeException() |
||||
|
}; |
||||
|
|
||||
|
if (OperatingSystem.IsAndroidVersionAtLeast(31)) |
||||
|
{ |
||||
|
// See https://developer.android.com/reference/android/R.color
|
||||
|
var accent1 = context.Resources.GetColor(17170494, context.Theme); // Resource.Color.SystemAccent1500
|
||||
|
var accent2 = context.Resources.GetColor(17170507, context.Theme); // Resource.Color.SystemAccent2500
|
||||
|
var accent3 = context.Resources.GetColor(17170520, context.Theme); // Resource.Color.SystemAccent3500
|
||||
|
|
||||
|
_latestValues = new PlatformColorValues |
||||
|
{ |
||||
|
ThemeVariant = systemTheme, |
||||
|
ContrastPreference = IsHighContrast(context), |
||||
|
AccentColor1 = new Color(accent1.A, accent1.R, accent1.G, accent1.B), |
||||
|
AccentColor2 = new Color(accent2.A, accent2.R, accent2.G, accent2.B), |
||||
|
AccentColor3 = new Color(accent3.A, accent3.R, accent3.G, accent3.B), |
||||
|
}; |
||||
|
} |
||||
|
else if (OperatingSystem.IsAndroidVersionAtLeast(23)) |
||||
|
{ |
||||
|
// See https://developer.android.com/reference/android/R.attr
|
||||
|
var array = context.Theme.ObtainStyledAttributes(new[] { 16843829 }); // Resource.Attribute.ColorAccent
|
||||
|
var accent = array.GetColor(0, 0); |
||||
|
|
||||
|
_latestValues = new PlatformColorValues |
||||
|
{ |
||||
|
ThemeVariant = systemTheme, |
||||
|
ContrastPreference = IsHighContrast(context), |
||||
|
AccentColor1 = new Color(accent.A, accent.R, accent.G, accent.B) |
||||
|
}; |
||||
|
array.Recycle(); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_latestValues = _latestValues with { ThemeVariant = systemTheme }; |
||||
|
} |
||||
|
|
||||
|
OnColorValuesChanged(_latestValues); |
||||
|
} |
||||
|
|
||||
|
private static ColorContrastPreference IsHighContrast(Context context) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
return Settings.Secure.GetInt(context.ContentResolver, "high_text_contrast_enabled", 0) == 1 |
||||
|
? ColorContrastPreference.High : ColorContrastPreference.NoPreference; |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
return ColorContrastPreference.NoPreference; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,28 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Android.Platform |
||||
|
{ |
||||
|
internal class AndroidSystemNavigationManager : ISystemNavigationManager |
||||
|
{ |
||||
|
public event EventHandler<RoutedEventArgs> BackRequested; |
||||
|
|
||||
|
public AndroidSystemNavigationManager(IActivityNavigationService? navigationService) |
||||
|
{ |
||||
|
if(navigationService != null) |
||||
|
{ |
||||
|
navigationService.BackRequested += OnBackRequested; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnBackRequested(object sender, AndroidBackRequestedEventArgs e) |
||||
|
{ |
||||
|
var routedEventArgs = new RoutedEventArgs(); |
||||
|
|
||||
|
BackRequested?.Invoke(this, routedEventArgs); |
||||
|
|
||||
|
e.Handled = routedEventArgs.Handled; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,275 +0,0 @@ |
|||||
using System; |
|
||||
using System.Collections; |
|
||||
using System.Collections.Generic; |
|
||||
using System.Runtime.CompilerServices; |
|
||||
using Avalonia.Utilities; |
|
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
public readonly struct CharacterBufferRange : IReadOnlyList<char> |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Getting an empty character string
|
|
||||
/// </summary>
|
|
||||
public static CharacterBufferRange Empty => new CharacterBufferRange(); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct <see cref="CharacterBufferRange"/> from character array
|
|
||||
/// </summary>
|
|
||||
/// <param name="characterArray">character array</param>
|
|
||||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|
||||
/// <param name="characterLength">character length</param>
|
|
||||
public CharacterBufferRange( |
|
||||
char[] characterArray, |
|
||||
int offsetToFirstChar, |
|
||||
int characterLength |
|
||||
) |
|
||||
: this( |
|
||||
new CharacterBufferReference(characterArray, offsetToFirstChar), |
|
||||
characterLength |
|
||||
) |
|
||||
{ } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct <see cref="CharacterBufferRange"/> from string
|
|
||||
/// </summary>
|
|
||||
/// <param name="characterString">character string</param>
|
|
||||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|
||||
/// <param name="characterLength">character length</param>
|
|
||||
public CharacterBufferRange( |
|
||||
string characterString, |
|
||||
int offsetToFirstChar, |
|
||||
int characterLength |
|
||||
) |
|
||||
: this( |
|
||||
new CharacterBufferReference(characterString, offsetToFirstChar), |
|
||||
characterLength |
|
||||
) |
|
||||
{ } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct a <see cref="CharacterBufferRange"/> from <see cref="CharacterBufferReference"/>
|
|
||||
/// </summary>
|
|
||||
/// <param name="characterBufferReference">character buffer reference</param>
|
|
||||
/// <param name="characterLength">number of characters</param>
|
|
||||
public CharacterBufferRange( |
|
||||
CharacterBufferReference characterBufferReference, |
|
||||
int characterLength |
|
||||
) |
|
||||
{ |
|
||||
if (characterLength < 0) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException("characterLength", "ParameterCannotBeNegative"); |
|
||||
} |
|
||||
|
|
||||
int maxLength = characterBufferReference.CharacterBuffer.Length > 0 ? |
|
||||
characterBufferReference.CharacterBuffer.Length - characterBufferReference.OffsetToFirstChar : |
|
||||
0; |
|
||||
|
|
||||
if (characterLength > maxLength) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException("characterLength", $"ParameterCannotBeGreaterThan {maxLength}"); |
|
||||
} |
|
||||
|
|
||||
CharacterBufferReference = characterBufferReference; |
|
||||
Length = characterLength; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct a <see cref="CharacterBufferRange"/> from part of another <see cref="CharacterBufferRange"/>
|
|
||||
/// </summary>
|
|
||||
internal CharacterBufferRange( |
|
||||
CharacterBufferRange characterBufferRange, |
|
||||
int offsetToFirstChar, |
|
||||
int characterLength |
|
||||
) : |
|
||||
this( |
|
||||
characterBufferRange.CharacterBuffer, |
|
||||
characterBufferRange.OffsetToFirstChar + offsetToFirstChar, |
|
||||
characterLength |
|
||||
) |
|
||||
{ } |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct a <see cref="CharacterBufferRange"/> from string
|
|
||||
/// </summary>
|
|
||||
internal CharacterBufferRange( |
|
||||
string charString |
|
||||
) : |
|
||||
this( |
|
||||
charString, |
|
||||
0, |
|
||||
charString.Length |
|
||||
) |
|
||||
{ } |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct <see cref="CharacterBufferRange"/> from memory buffer
|
|
||||
/// </summary>
|
|
||||
internal CharacterBufferRange( |
|
||||
ReadOnlyMemory<char> charBuffer, |
|
||||
int offsetToFirstChar, |
|
||||
int characterLength |
|
||||
) : |
|
||||
this( |
|
||||
new CharacterBufferReference(charBuffer, offsetToFirstChar), |
|
||||
characterLength |
|
||||
) |
|
||||
{ } |
|
||||
|
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct a <see cref="CharacterBufferRange"/> by extracting text info from a text run
|
|
||||
/// </summary>
|
|
||||
internal CharacterBufferRange(TextRun textRun) |
|
||||
{ |
|
||||
CharacterBufferReference = textRun.CharacterBufferReference; |
|
||||
Length = textRun.Length; |
|
||||
} |
|
||||
|
|
||||
public char this[int index] |
|
||||
{ |
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)] |
|
||||
get |
|
||||
{ |
|
||||
#if DEBUG
|
|
||||
if (index.CompareTo(0) < 0 || index.CompareTo(Length) > 0) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException(nameof(index)); |
|
||||
} |
|
||||
#endif
|
|
||||
return CharacterBuffer.Span[CharacterBufferReference.OffsetToFirstChar + index]; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a reference to the character buffer
|
|
||||
/// </summary>
|
|
||||
public CharacterBufferReference CharacterBufferReference { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the number of characters in text source character store
|
|
||||
/// </summary>
|
|
||||
public int Length { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets a span from the character buffer range
|
|
||||
/// </summary>
|
|
||||
public ReadOnlySpan<char> Span => CharacterBuffer.Span.Slice(OffsetToFirstChar, Length); |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the character memory buffer
|
|
||||
/// </summary>
|
|
||||
internal ReadOnlyMemory<char> CharacterBuffer => CharacterBufferReference.CharacterBuffer; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the character offset relative to the beginning of buffer to
|
|
||||
/// the first character of the run
|
|
||||
/// </summary>
|
|
||||
internal int OffsetToFirstChar => CharacterBufferReference.OffsetToFirstChar; |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Indicate whether the character buffer range is empty
|
|
||||
/// </summary>
|
|
||||
internal bool IsEmpty => CharacterBuffer.Length == 0 || Length <= 0; |
|
||||
|
|
||||
internal CharacterBufferRange Take(int length) |
|
||||
{ |
|
||||
if (IsEmpty) |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
if (length > Length) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException(nameof(length)); |
|
||||
} |
|
||||
|
|
||||
return new CharacterBufferRange(CharacterBufferReference, length); |
|
||||
} |
|
||||
|
|
||||
internal CharacterBufferRange Skip(int length) |
|
||||
{ |
|
||||
if (IsEmpty) |
|
||||
{ |
|
||||
return this; |
|
||||
} |
|
||||
|
|
||||
if (length > Length) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException(nameof(length)); |
|
||||
} |
|
||||
|
|
||||
if (length == Length) |
|
||||
{ |
|
||||
return new CharacterBufferRange(new CharacterBufferReference(), 0); |
|
||||
} |
|
||||
|
|
||||
var characterBufferReference = new CharacterBufferReference(CharacterBuffer, OffsetToFirstChar + length); |
|
||||
|
|
||||
return new CharacterBufferRange(characterBufferReference, Length - length); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Compute hash code
|
|
||||
/// </summary>
|
|
||||
public override int GetHashCode() |
|
||||
{ |
|
||||
return CharacterBufferReference.GetHashCode() ^ Length; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Test equality with the input object
|
|
||||
/// </summary>
|
|
||||
/// <param name="obj"> The object to test </param>
|
|
||||
public override bool Equals(object? obj) |
|
||||
{ |
|
||||
if (obj is CharacterBufferRange range) |
|
||||
{ |
|
||||
return Equals(range); |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Test equality with the input CharacterBufferRange
|
|
||||
/// </summary>
|
|
||||
/// <param name="value"> The CharacterBufferRange value to test </param>
|
|
||||
public bool Equals(CharacterBufferRange value) |
|
||||
{ |
|
||||
return CharacterBufferReference.Equals(value.CharacterBufferReference) |
|
||||
&& Length == value.Length; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Compare two CharacterBufferRange for equality
|
|
||||
/// </summary>
|
|
||||
/// <param name="left">left operand</param>
|
|
||||
/// <param name="right">right operand</param>
|
|
||||
/// <returns>whether or not two operands are equal</returns>
|
|
||||
public static bool operator ==(CharacterBufferRange left, CharacterBufferRange right) |
|
||||
{ |
|
||||
return left.Equals(right); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Compare two CharacterBufferRange for inequality
|
|
||||
/// </summary>
|
|
||||
/// <param name="left">left operand</param>
|
|
||||
/// <param name="right">right operand</param>
|
|
||||
/// <returns>whether or not two operands are equal</returns>
|
|
||||
public static bool operator !=(CharacterBufferRange left, CharacterBufferRange right) |
|
||||
{ |
|
||||
return !(left == right); |
|
||||
} |
|
||||
|
|
||||
int IReadOnlyCollection<char>.Count => Length; |
|
||||
|
|
||||
public IEnumerator<char> GetEnumerator() => new ImmutableReadOnlyListStructEnumerator<char>(this); |
|
||||
|
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); |
|
||||
} |
|
||||
} |
|
||||
@ -1,115 +0,0 @@ |
|||||
using System; |
|
||||
|
|
||||
namespace Avalonia.Media.TextFormatting |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Text character buffer reference
|
|
||||
/// </summary>
|
|
||||
public readonly struct CharacterBufferReference : IEquatable<CharacterBufferReference> |
|
||||
{ |
|
||||
/// <summary>
|
|
||||
/// Construct character buffer reference from character array
|
|
||||
/// </summary>
|
|
||||
/// <param name="characterArray">character array</param>
|
|
||||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|
||||
public CharacterBufferReference(char[] characterArray, int offsetToFirstChar = 0) |
|
||||
: this(characterArray.AsMemory(), offsetToFirstChar) |
|
||||
{ } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct character buffer reference from string
|
|
||||
/// </summary>
|
|
||||
/// <param name="characterString">character string</param>
|
|
||||
/// <param name="offsetToFirstChar">character buffer offset to the first character</param>
|
|
||||
public CharacterBufferReference(string characterString, int offsetToFirstChar = 0) |
|
||||
: this(characterString.AsMemory(), offsetToFirstChar) |
|
||||
{ } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Construct character buffer reference from memory buffer
|
|
||||
/// </summary>
|
|
||||
internal CharacterBufferReference(ReadOnlyMemory<char> characterBuffer, int offsetToFirstChar = 0) |
|
||||
{ |
|
||||
if (offsetToFirstChar < 0) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException("offsetToFirstChar", "ParameterCannotBeNegative"); |
|
||||
} |
|
||||
|
|
||||
// maximum offset is one less than CharacterBuffer.Count, except that zero is always a valid offset
|
|
||||
// even in the case of an empty or null character buffer
|
|
||||
var maxOffset = characterBuffer.Length == 0 ? 0 : Math.Max(0, characterBuffer.Length - 1); |
|
||||
if (offsetToFirstChar > maxOffset) |
|
||||
{ |
|
||||
throw new ArgumentOutOfRangeException("offsetToFirstChar", $"ParameterCannotBeGreaterThan, {maxOffset}"); |
|
||||
} |
|
||||
|
|
||||
CharacterBuffer = characterBuffer; |
|
||||
OffsetToFirstChar = offsetToFirstChar; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the character memory buffer
|
|
||||
/// </summary>
|
|
||||
public ReadOnlyMemory<char> CharacterBuffer { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Gets the character offset relative to the beginning of buffer to
|
|
||||
/// the first character of the run
|
|
||||
/// </summary>
|
|
||||
public int OffsetToFirstChar { get; } |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Compute hash code
|
|
||||
/// </summary>
|
|
||||
public override int GetHashCode() |
|
||||
{ |
|
||||
return CharacterBuffer.IsEmpty ? 0 : CharacterBuffer.GetHashCode(); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Test equality with the input object
|
|
||||
/// </summary>
|
|
||||
/// <param name="obj"> The object to test. </param>
|
|
||||
public override bool Equals(object? obj) |
|
||||
{ |
|
||||
if (obj is CharacterBufferReference reference) |
|
||||
{ |
|
||||
return Equals(reference); |
|
||||
} |
|
||||
|
|
||||
return false; |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Test equality with the input CharacterBufferReference
|
|
||||
/// </summary>
|
|
||||
/// <param name="value"> The characterBufferReference value to test </param>
|
|
||||
public bool Equals(CharacterBufferReference value) |
|
||||
{ |
|
||||
return CharacterBuffer.Equals(value.CharacterBuffer); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Compare two CharacterBufferReference for equality
|
|
||||
/// </summary>
|
|
||||
/// <param name="left">left operand</param>
|
|
||||
/// <param name="right">right operand</param>
|
|
||||
/// <returns>whether or not two operands are equal</returns>
|
|
||||
public static bool operator ==(CharacterBufferReference left, CharacterBufferReference right) |
|
||||
{ |
|
||||
return left.Equals(right); |
|
||||
} |
|
||||
|
|
||||
/// <summary>
|
|
||||
/// Compare two CharacterBufferReference for inequality
|
|
||||
/// </summary>
|
|
||||
/// <param name="left">left operand</param>
|
|
||||
/// <param name="right">right operand</param>
|
|
||||
/// <returns>whether or not two operands are equal</returns>
|
|
||||
public static bool operator !=(CharacterBufferReference left, CharacterBufferReference right) |
|
||||
{ |
|
||||
return !(left == right); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@ -0,0 +1,68 @@ |
|||||
|
using Avalonia.Media; |
||||
|
|
||||
|
namespace Avalonia.Platform; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// System theme variant or mode.
|
||||
|
/// </summary>
|
||||
|
public enum PlatformThemeVariant |
||||
|
{ |
||||
|
Light, |
||||
|
Dark |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// System high contrast preference.
|
||||
|
/// </summary>
|
||||
|
public enum ColorContrastPreference |
||||
|
{ |
||||
|
NoPreference, |
||||
|
High |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Information about current system color values, including information about dark mode and accent colors.
|
||||
|
/// </summary>
|
||||
|
public record PlatformColorValues |
||||
|
{ |
||||
|
private static Color DefaultAccent => new(255, 0, 120, 215); |
||||
|
private Color _accentColor2, _accentColor3; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// System theme variant or mode.
|
||||
|
/// </summary>
|
||||
|
public PlatformThemeVariant ThemeVariant { get; init; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// System high contrast preference.
|
||||
|
/// </summary>
|
||||
|
public ColorContrastPreference ContrastPreference { get; init; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Primary system accent color.
|
||||
|
/// </summary>
|
||||
|
public Color AccentColor1 { get; init; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Secondary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
|
||||
|
/// </summary>
|
||||
|
public Color AccentColor2 |
||||
|
{ |
||||
|
get => _accentColor2 != default ? _accentColor2 : AccentColor1; |
||||
|
init => _accentColor2 = value; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Tertiary system accent color. On some platforms can return the same value as <see cref="AccentColor1"/>.
|
||||
|
/// </summary>
|
||||
|
public Color AccentColor3 |
||||
|
{ |
||||
|
get => _accentColor3 != default ? _accentColor3 : AccentColor1; |
||||
|
init => _accentColor3 = value; |
||||
|
} |
||||
|
|
||||
|
public PlatformColorValues() |
||||
|
{ |
||||
|
AccentColor1 = DefaultAccent; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,18 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Metadata; |
||||
|
|
||||
|
namespace Avalonia.Platform |
||||
|
{ |
||||
|
[Unstable] |
||||
|
public interface ITopLevelWithSystemNavigationManager |
||||
|
{ |
||||
|
ISystemNavigationManager SystemNavigationManager { get; } |
||||
|
} |
||||
|
|
||||
|
[Unstable] |
||||
|
public interface ISystemNavigationManager |
||||
|
{ |
||||
|
public event EventHandler<RoutedEventArgs>? BackRequested; |
||||
|
} |
||||
|
} |
||||
@ -1,105 +1,116 @@ |
|||||
using System; |
using System; |
||||
using System.Collections.Generic; |
using System.Collections.Generic; |
||||
using System.IO; |
using System.IO; |
||||
using System.Runtime.Serialization; |
using System.Text; |
||||
using System.Xml.Linq; |
|
||||
using System.Linq; |
|
||||
using System.Diagnostics.CodeAnalysis; |
|
||||
|
|
||||
namespace Avalonia.Utilities |
namespace Avalonia.Utilities |
||||
{ |
{ |
||||
#if !BUILDTASK
|
#if !BUILDTASK
|
||||
public |
public |
||||
#endif
|
#endif
|
||||
static class AvaloniaResourcesIndexReaderWriter |
static class AvaloniaResourcesIndexReaderWriter |
||||
{ |
{ |
||||
private const int LastKnownVersion = 1; |
private const int XmlLegacyVersion = 1; |
||||
public static List<AvaloniaResourcesIndexEntry> Read(Stream stream) |
private const int BinaryCurrentVersion = 2; |
||||
|
|
||||
|
public static List<AvaloniaResourcesIndexEntry> ReadIndex(Stream stream) |
||||
{ |
{ |
||||
var ver = new BinaryReader(stream).ReadInt32(); |
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true); |
||||
if (ver > LastKnownVersion) |
|
||||
throw new Exception("Resources index format version is not known"); |
|
||||
|
|
||||
var assetDoc = XDocument.Load(stream); |
|
||||
XNamespace assetNs = assetDoc.Root!.Attribute("xmlns")!.Value; |
|
||||
List<AvaloniaResourcesIndexEntry> entries = |
|
||||
(from entry in assetDoc.Root.Element(assetNs + "Entries")!.Elements(assetNs + "AvaloniaResourcesIndexEntry") |
|
||||
select new AvaloniaResourcesIndexEntry |
|
||||
{ |
|
||||
Path = entry.Element(assetNs + "Path")!.Value, |
|
||||
Offset = int.Parse(entry.Element(assetNs + "Offset")!.Value), |
|
||||
Size = int.Parse(entry.Element(assetNs + "Size")!.Value) |
|
||||
}).ToList(); |
|
||||
|
|
||||
return entries; |
var version = reader.ReadInt32(); |
||||
|
return version switch |
||||
|
{ |
||||
|
XmlLegacyVersion => ReadXmlIndex(), |
||||
|
BinaryCurrentVersion => ReadBinaryIndex(reader), |
||||
|
_ => throw new Exception($"Unknown resources index format version {version}") |
||||
|
}; |
||||
} |
} |
||||
|
|
||||
[RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] |
private static List<AvaloniaResourcesIndexEntry> ReadXmlIndex() |
||||
public static void Write(Stream stream, List<AvaloniaResourcesIndexEntry> entries) |
=> throw new NotSupportedException("Found legacy resources index format: please recompile your XAML files"); |
||||
|
|
||||
|
private static List<AvaloniaResourcesIndexEntry> ReadBinaryIndex(BinaryReader reader) |
||||
{ |
{ |
||||
new BinaryWriter(stream).Write(LastKnownVersion); |
var entryCount = reader.ReadInt32(); |
||||
new DataContractSerializer(typeof(AvaloniaResourcesIndex)).WriteObject(stream, |
var entries = new List<AvaloniaResourcesIndexEntry>(entryCount); |
||||
new AvaloniaResourcesIndex() |
|
||||
{ |
for (var i = 0; i < entryCount; ++i) |
||||
Entries = entries |
{ |
||||
|
entries.Add(new AvaloniaResourcesIndexEntry { |
||||
|
Path = reader.ReadString(), |
||||
|
Offset = reader.ReadInt32(), |
||||
|
Size = reader.ReadInt32() |
||||
}); |
}); |
||||
|
} |
||||
|
|
||||
|
return entries; |
||||
} |
} |
||||
|
|
||||
[RequiresUnreferencedCode("AvaloniaResources uses Data Contract Serialization, which might require unreferenced code")] |
public static void WriteIndex(Stream output, List<AvaloniaResourcesIndexEntry> entries) |
||||
public static byte[] Create(Dictionary<string, byte[]> data) |
|
||||
{ |
{ |
||||
var sources = data.ToList(); |
using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true); |
||||
var offsets = new Dictionary<string, int>(); |
|
||||
var coffset = 0; |
WriteIndex(writer, entries); |
||||
foreach (var s in sources) |
} |
||||
|
|
||||
|
private static void WriteIndex(BinaryWriter writer, List<AvaloniaResourcesIndexEntry> entries) |
||||
|
{ |
||||
|
writer.Write(BinaryCurrentVersion); |
||||
|
writer.Write(entries.Count); |
||||
|
|
||||
|
foreach (var entry in entries) |
||||
{ |
{ |
||||
offsets[s.Key] = coffset; |
writer.Write(entry.Path ?? string.Empty); |
||||
coffset += s.Value.Length; |
writer.Write(entry.Offset); |
||||
|
writer.Write(entry.Size); |
||||
} |
} |
||||
var index = sources.Select(s => new AvaloniaResourcesIndexEntry |
} |
||||
{ |
|
||||
Path = s.Key, |
public static void WriteResources(Stream output, List<(string Path, int Size, Func<Stream> Open)> resources) |
||||
Size = s.Value.Length, |
{ |
||||
Offset = offsets[s.Key] |
var entries = new List<AvaloniaResourcesIndexEntry>(resources.Count); |
||||
}).ToList(); |
var offset = 0; |
||||
var output = new MemoryStream(); |
|
||||
var ms = new MemoryStream(); |
foreach (var resource in resources) |
||||
AvaloniaResourcesIndexReaderWriter.Write(ms, index); |
|
||||
new BinaryWriter(output).Write((int)ms.Length); |
|
||||
ms.Position = 0; |
|
||||
ms.CopyTo(output); |
|
||||
foreach (var s in sources) |
|
||||
{ |
{ |
||||
output.Write(s.Value,0,s.Value.Length); |
entries.Add(new AvaloniaResourcesIndexEntry |
||||
|
{ |
||||
|
Path = resource.Path, |
||||
|
Offset = offset, |
||||
|
Size = resource.Size |
||||
|
}); |
||||
|
offset += resource.Size; |
||||
} |
} |
||||
|
|
||||
return output.ToArray(); |
using var writer = new BinaryWriter(output, Encoding.UTF8, leaveOpen: true); |
||||
} |
writer.Write(0); // index size placeholder, overwritten below
|
||||
} |
|
||||
|
|
||||
[DataContract] |
var posBeforeEntries = output.Position; |
||||
#if !BUILDTASK
|
WriteIndex(writer, entries); |
||||
public |
|
||||
#endif
|
var posAfterEntries = output.Position; |
||||
class AvaloniaResourcesIndex |
var indexSize = (int) (posAfterEntries - posBeforeEntries); |
||||
{ |
output.Position = 0L; |
||||
[DataMember] |
writer.Write(indexSize); |
||||
public List<AvaloniaResourcesIndexEntry> Entries { get; set; } = new List<AvaloniaResourcesIndexEntry>(); |
output.Position = posAfterEntries; |
||||
|
|
||||
|
foreach (var resource in resources) |
||||
|
{ |
||||
|
using var resourceStream = resource.Open(); |
||||
|
resourceStream.CopyTo(output); |
||||
|
} |
||||
|
} |
||||
} |
} |
||||
|
|
||||
[DataContract] |
|
||||
#if !BUILDTASK
|
#if !BUILDTASK
|
||||
public |
public |
||||
#endif
|
#endif
|
||||
class AvaloniaResourcesIndexEntry |
class AvaloniaResourcesIndexEntry |
||||
{ |
{ |
||||
[DataMember] |
|
||||
public string? Path { get; set; } |
public string? Path { get; set; } |
||||
|
|
||||
[DataMember] |
|
||||
public int Offset { get; set; } |
public int Offset { get; set; } |
||||
|
|
||||
[DataMember] |
|
||||
public int Size { get; set; } |
public int Size { get; set; } |
||||
} |
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,77 @@ |
|||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Logging; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop; |
||||
|
|
||||
|
internal class DBusPlatformSettings : DefaultPlatformSettings |
||||
|
{ |
||||
|
private readonly IDBusSettings? _settings; |
||||
|
private PlatformColorValues? _lastColorValues; |
||||
|
|
||||
|
public DBusPlatformSettings() |
||||
|
{ |
||||
|
_settings = DBusHelper.TryInitialize()? |
||||
|
.CreateProxy<IDBusSettings>("org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop"); |
||||
|
|
||||
|
if (_settings is not null) |
||||
|
{ |
||||
|
_ = _settings.WatchSettingChangedAsync(SettingsChangedHandler); |
||||
|
|
||||
|
_ = TryGetInitialValue(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public override PlatformColorValues GetColorValues() |
||||
|
{ |
||||
|
return _lastColorValues ?? base.GetColorValues(); |
||||
|
} |
||||
|
|
||||
|
private async Task TryGetInitialValue() |
||||
|
{ |
||||
|
var colorSchemeTask = _settings!.ReadAsync("org.freedesktop.appearance", "color-scheme"); |
||||
|
if (colorSchemeTask.Status == TaskStatus.RanToCompletion) |
||||
|
{ |
||||
|
_lastColorValues = GetColorValuesFromSetting(colorSchemeTask.Result); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var value = await colorSchemeTask; |
||||
|
_lastColorValues = GetColorValuesFromSetting(value); |
||||
|
OnColorValuesChanged(_lastColorValues); |
||||
|
} |
||||
|
catch (Exception ex) |
||||
|
{ |
||||
|
_lastColorValues = base.GetColorValues(); |
||||
|
Logger.TryGet(LogEventLevel.Error, LogArea.FreeDesktopPlatform)?.Log(this, "Unable to get setting value", ex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void SettingsChangedHandler((string @namespace, string key, object value) tuple) |
||||
|
{ |
||||
|
if (tuple.@namespace == "org.freedesktop.appearance" |
||||
|
&& tuple.key == "color-scheme") |
||||
|
{ |
||||
|
/* |
||||
|
<member>0: No preference</member> |
||||
|
<member>1: Prefer dark appearance</member> |
||||
|
<member>2: Prefer light appearance</member> |
||||
|
*/ |
||||
|
_lastColorValues = GetColorValuesFromSetting(tuple.value); |
||||
|
OnColorValuesChanged(_lastColorValues); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static PlatformColorValues GetColorValuesFromSetting(object value) |
||||
|
{ |
||||
|
var isDark = value?.ToString() == "1"; |
||||
|
return new PlatformColorValues |
||||
|
{ |
||||
|
ThemeVariant = isDark ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,16 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Threading.Tasks; |
||||
|
using Tmds.DBus; |
||||
|
|
||||
|
namespace Avalonia.FreeDesktop; |
||||
|
|
||||
|
[DBusInterface("org.freedesktop.portal.Settings")] |
||||
|
internal interface IDBusSettings : IDBusObject |
||||
|
{ |
||||
|
Task<(string @namespace, IDictionary<string, object>)> ReadAllAsync(string[] namespaces); |
||||
|
|
||||
|
Task<object> ReadAsync(string @namespace, string key); |
||||
|
|
||||
|
Task<IDisposable> WatchSettingChangedAsync(Action<(string @namespace, string key, object value)> handler, Action<Exception>? onError = null); |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Native.Interop; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Native; |
||||
|
|
||||
|
internal class NativePlatformSettings : DefaultPlatformSettings |
||||
|
{ |
||||
|
private readonly IAvnPlatformSettings _platformSettings; |
||||
|
private PlatformColorValues _lastColorValues; |
||||
|
|
||||
|
public NativePlatformSettings(IAvnPlatformSettings platformSettings) |
||||
|
{ |
||||
|
_platformSettings = platformSettings; |
||||
|
platformSettings.RegisterColorsChange(new ColorsChangeCallback(this)); |
||||
|
} |
||||
|
|
||||
|
public override PlatformColorValues GetColorValues() |
||||
|
{ |
||||
|
var (theme, contrast) = _platformSettings.PlatformTheme switch |
||||
|
{ |
||||
|
AvnPlatformThemeVariant.Dark => (PlatformThemeVariant.Dark, ColorContrastPreference.NoPreference), |
||||
|
AvnPlatformThemeVariant.Light => (PlatformThemeVariant.Light, ColorContrastPreference.NoPreference), |
||||
|
AvnPlatformThemeVariant.HighContrastDark => (PlatformThemeVariant.Dark, ColorContrastPreference.High), |
||||
|
AvnPlatformThemeVariant.HighContrastLight => (PlatformThemeVariant.Dark, ColorContrastPreference.High), |
||||
|
_ => throw new ArgumentOutOfRangeException() |
||||
|
}; |
||||
|
var color = _platformSettings.AccentColor; |
||||
|
|
||||
|
if (color > 0) |
||||
|
{ |
||||
|
_lastColorValues = new PlatformColorValues |
||||
|
{ |
||||
|
ThemeVariant = theme, |
||||
|
ContrastPreference = contrast, |
||||
|
AccentColor1 = Color.FromUInt32(color) |
||||
|
}; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_lastColorValues = new PlatformColorValues |
||||
|
{ |
||||
|
ThemeVariant = theme, |
||||
|
ContrastPreference = contrast |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
return _lastColorValues; |
||||
|
} |
||||
|
|
||||
|
public void OnColorValuesChanged() |
||||
|
{ |
||||
|
var oldColorValues = _lastColorValues; |
||||
|
var colorValues = GetColorValues(); |
||||
|
|
||||
|
if (oldColorValues != colorValues) |
||||
|
{ |
||||
|
OnColorValuesChanged(colorValues); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private class ColorsChangeCallback : NativeCallbackBase, IAvnActionCallback |
||||
|
{ |
||||
|
private readonly NativePlatformSettings _settings; |
||||
|
|
||||
|
public ColorsChangeCallback(NativePlatformSettings settings) |
||||
|
{ |
||||
|
_settings = settings; |
||||
|
} |
||||
|
|
||||
|
public void Run() |
||||
|
{ |
||||
|
_settings.OnColorValuesChanged(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
using Avalonia.Browser.Interop; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Browser; |
||||
|
|
||||
|
internal class BrowserPlatformSettings : DefaultPlatformSettings |
||||
|
{ |
||||
|
private bool _isDarkMode; |
||||
|
private bool _isHighContrast; |
||||
|
|
||||
|
public BrowserPlatformSettings() |
||||
|
{ |
||||
|
var obj = DomHelper.ObserveDarkMode((isDarkMode, isHighContrast) => |
||||
|
{ |
||||
|
_isDarkMode = isDarkMode; |
||||
|
_isHighContrast = isHighContrast; |
||||
|
OnColorValuesChanged(GetColorValues()); |
||||
|
}); |
||||
|
_isDarkMode = obj.GetPropertyAsBoolean("isDarkMode"); |
||||
|
_isHighContrast = obj.GetPropertyAsBoolean("isHighContrast"); |
||||
|
} |
||||
|
|
||||
|
public override PlatformColorValues GetColorValues() |
||||
|
{ |
||||
|
return base.GetColorValues() with |
||||
|
{ |
||||
|
ThemeVariant = _isDarkMode ? PlatformThemeVariant.Dark : PlatformThemeVariant.Light, |
||||
|
ContrastPreference = _isHighContrast ? ColorContrastPreference.High : ColorContrastPreference.NoPreference |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,24 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Browser.Interop; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Browser |
||||
|
{ |
||||
|
internal class BrowserSystemNavigationManager : ISystemNavigationManager |
||||
|
{ |
||||
|
public event EventHandler<RoutedEventArgs>? BackRequested; |
||||
|
|
||||
|
public BrowserSystemNavigationManager() |
||||
|
{ |
||||
|
NavigationHelper.AddBackHandler(() => |
||||
|
{ |
||||
|
var routedEventArgs = new RoutedEventArgs(); |
||||
|
|
||||
|
BackRequested?.Invoke(this, routedEventArgs); |
||||
|
|
||||
|
return routedEventArgs.Handled; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue