diff --git a/.editorconfig b/.editorconfig index bfeab8c84f..4167733d05 100644 --- a/.editorconfig +++ b/.editorconfig @@ -199,6 +199,18 @@ dotnet_diagnostic.AVP1032.severity = warning dotnet_diagnostic.AVP1040.severity = warning dotnet_diagnostic.AVA2001.severity = warning +# Avalonia property source generator (GeneratedPropertyAnalyzer) +dotnet_diagnostic.AVP2001.severity = error +dotnet_diagnostic.AVP2002.severity = error +dotnet_diagnostic.AVP2003.severity = error +dotnet_diagnostic.AVP2004.severity = error +dotnet_diagnostic.AVP2005.severity = error +dotnet_diagnostic.AVP2006.severity = error +dotnet_diagnostic.AVP2007.severity = error +dotnet_diagnostic.AVP2008.severity = error +dotnet_diagnostic.AVP2100.severity = warning +dotnet_diagnostic.AVP2101.severity = warning + # Xaml files [*.{xaml,axaml}] indent_size = 2 diff --git a/.ncrunch/Avalonia.Controls.ColorPicker.v3.ncrunchproject b/.ncrunch/Avalonia.Controls.ColorPicker.v3.ncrunchproject new file mode 100644 index 0000000000..0bcc569d05 --- /dev/null +++ b/.ncrunch/Avalonia.Controls.ColorPicker.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/Avalonia.DBus.Generators.v3.ncrunchproject b/.ncrunch/Avalonia.DBus.Generators.v3.ncrunchproject new file mode 100644 index 0000000000..08b7dfa6ee --- /dev/null +++ b/.ncrunch/Avalonia.DBus.Generators.v3.ncrunchproject @@ -0,0 +1,8 @@ + + + + TargetFramework = netstandard2.0 + + False + + \ No newline at end of file diff --git a/.ncrunch/Avalonia.FreeDesktop.AtSpi.v3.ncrunchproject b/.ncrunch/Avalonia.FreeDesktop.AtSpi.v3.ncrunchproject new file mode 100644 index 0000000000..0bcc569d05 --- /dev/null +++ b/.ncrunch/Avalonia.FreeDesktop.AtSpi.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 2d66481319..bf7554a66d 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -55,6 +55,7 @@ "src\\Windows\\Avalonia.Win32.Automation\\Avalonia.Win32.Automation.csproj", "src\\Windows\\Avalonia.Win32.Interoperability\\Avalonia.Win32.Interoperability.csproj", "src\\Windows\\Avalonia.Win32\\Avalonia.Win32.csproj", + "tests\\Avalonia.Analyzers.Tests\\Avalonia.Analyzers.Tests.csproj", "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Build.Tasks.UnitTest\\Avalonia.Build.Tasks.UnitTest.csproj", diff --git a/Avalonia.slnx b/Avalonia.slnx index d902fabe98..9da4ac18a9 100644 --- a/Avalonia.slnx +++ b/Avalonia.slnx @@ -137,6 +137,7 @@ + diff --git a/Directory.Build.props b/Directory.Build.props index 931a386d31..a602c6015e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,6 +4,7 @@ $(MSBuildThisFileDirectory)build-intermediate/nuget $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\$(AvsCurrentTargetFramework)\Avalonia.Designer.HostApp.dll + $(MSBuildThisFileDirectory)\build\Assets\ false False diff --git a/Directory.Packages.props b/Directory.Packages.props index d38cb33b28..3f4de3371d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,7 +6,7 @@ - + @@ -23,6 +23,9 @@ + + + @@ -69,6 +72,7 @@ + @@ -78,4 +82,4 @@ - \ No newline at end of file + diff --git a/api/Avalonia.nupkg.xml b/api/Avalonia.nupkg.xml index 5c720c9413..1dda132a59 100644 --- a/api/Avalonia.nupkg.xml +++ b/api/Avalonia.nupkg.xml @@ -1,4 +1,4 @@ - + @@ -145,6 +145,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.OpenGL.dll current/Avalonia/lib/net8.0/Avalonia.OpenGL.dll + + CP0006 + E:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguageChanged + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions) @@ -175,6 +181,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Base.dll current/Avalonia/lib/net10.0/Avalonia.Base.dll + + CP0006 + P:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguage + baseline/Avalonia/lib/net10.0/Avalonia.Base.dll + current/Avalonia/lib/net10.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference) @@ -193,6 +205,12 @@ baseline/Avalonia/lib/net10.0/Avalonia.Controls.dll current/Avalonia/lib/net10.0/Avalonia.Controls.dll + + CP0006 + E:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguageChanged + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Platform.IBitmapImpl.Save(System.IO.Stream,Avalonia.Media.Imaging.BitmapEncoderOptions) @@ -223,6 +241,12 @@ baseline/Avalonia/lib/net8.0/Avalonia.Base.dll current/Avalonia/lib/net8.0/Avalonia.Base.dll + + CP0006 + P:Avalonia.Platform.IPlatformSettings.PreferredApplicationLanguage + baseline/Avalonia/lib/net8.0/Avalonia.Base.dll + current/Avalonia/lib/net8.0/Avalonia.Base.dll + CP0006 M:Avalonia.Controls.Platform.IWin32OptionsTopLevelImpl.SetWindowCornerPreference(Avalonia.Controls.Win32Properties.WindowCornerPreference) @@ -241,4 +265,4 @@ baseline/Avalonia/lib/net8.0/Avalonia.Controls.dll current/Avalonia/lib/net8.0/Avalonia.Controls.dll - \ No newline at end of file + diff --git a/build/Assets/Icon.png b/build/Assets/Icon.png index 41a2a618fb..54fe581f21 100644 Binary files a/build/Assets/Icon.png and b/build/Assets/Icon.png differ diff --git a/build/Assets/icon-32.png b/build/Assets/icon-32.png new file mode 100644 index 0000000000..f8afccc962 Binary files /dev/null and b/build/Assets/icon-32.png differ diff --git a/build/Assets/icon.ico b/build/Assets/icon.ico new file mode 100644 index 0000000000..b5ce607b0e Binary files /dev/null and b/build/Assets/icon.ico differ diff --git a/build/Assets/icon.svg b/build/Assets/icon.svg new file mode 100644 index 0000000000..d5e1dcbf61 --- /dev/null +++ b/build/Assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 2a4e07c3d2..c4c914fb49 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -10,7 +10,7 @@ true $(NoWarn);CS1591;NU5104 MIT - Icon.png + icon.png Avalonia is a cross-platform UI framework for .NET providing a flexible styling system and supporting a wide range of Operating Systems such as Windows, Linux, macOS and with experimental support for Android, iOS and WebAssembly. avalonia;avaloniaui;mvvm;rx;reactive extensions;android;ios;mac;forms;wpf;net;netstandard;net461;uwp;xamarin https://github.com/AvaloniaUI/Avalonia/releases @@ -20,6 +20,6 @@ - + diff --git a/native/Avalonia.Native/src/OSX/AvnView.mm b/native/Avalonia.Native/src/OSX/AvnView.mm index d4ab79bc47..805cd7ee02 100644 --- a/native/Avalonia.Native/src/OSX/AvnView.mm +++ b/native/Avalonia.Native/src/OSX/AvnView.mm @@ -4,6 +4,7 @@ // #import +#import /* For the TIS functions used to classify the keyboard input source. */ #include "AvnView.h" #include "automation.h" #import "WindowInterfaces.h" @@ -22,8 +23,9 @@ NSMutableAttributedString* _text; NSRange _selectedRange; NSRange _markedRange; - NSEvent* _lastKeyDownEvent; NSMutableArray* _accessibilityChildren; + NSString* _keyboardInputSourceId; + bool _keyboardInputSourceComposes; } - (void)onClosed @@ -649,9 +651,100 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) [super flagsChanged:event]; } -- (bool) handleKeyDown: (NSTimeInterval) timestamp withKey:(AvnKey)key withPhysicalKey:(AvnPhysicalKey)physicalKey withModifiers:(AvnInputModifiers)modifiers withKeySymbol:(NSString*)keySymbol { - auto parent = _parent.tryGet(); - return parent->TopLevelEvents->RawKeyEvent(KeyDown, timestamp, modifiers, key, physicalKey, [keySymbol UTF8String]); +// The input mode an input method uses for direct alphanumeric typing. In this mode the input +// method passes keys straight through instead of composing, so keys must not be masked. +static CFStringRef const AlphanumericInputModeId = CFSTR("com.apple.inputmethod.Roman"); + +static bool InputSourceComposes(TISInputSourceRef source) +{ + // A plain keyboard layout never composes. Dead keys are not driven by the input source, they + // show up as a composition in progress and are covered by hasMarkedText. + auto type = static_cast(TISGetInputSourceProperty(source, kTISPropertyInputSourceType)); + + if(type == nullptr || CFEqual(type, kTISTypeKeyboardLayout)) + { + return false; + } + + auto mode = static_cast(TISGetInputSourceProperty(source, kTISPropertyInputModeID)); + + return mode == nullptr || !CFEqual(mode, AlphanumericInputModeId); +} + +// Whether this keystroke could be the one that starts a composition. Only a key that produces a +// printable character can, and masking any of the others would stop controls like TextBox from +// reacting to them at all. +static bool CanStartComposition(NSString* keySymbol, NSEventModifierFlags modifierFlags) +{ + // No symbol at all: the arrows, the function keys, Home, End and friends. + if(keySymbol == nullptr || [keySymbol length] == 0) + { + return false; + } + + // KeySymbolFromScanCode deliberately reports the control character for Backspace, Enter, Tab + // and Escape, and Forward Delete reports DEL (0x7F), so a symbol alone does not mean the key + // produces text. Those keys edit or cancel, they never start a composition. + auto firstChar = [keySymbol characterAtIndex:0]; + + if(firstChar < 0x20 || firstChar == 0x7F) + { + return false; + } + + // Command and Control combinations are shortcuts, which an input method does not consume. + return (modifierFlags & (NSEventModifierFlagCommand | NSEventModifierFlagControl)) == 0; +} + +// Returns true when the input source with the given id is an input method that can start a +// composition, as opposed to a plain keyboard layout or an input method in alphanumeric mode. +static bool KeyboardInputSourceComposes(NSString* sourceId) +{ + NSDictionary* filter = @{ (__bridge NSString*)kTISPropertyInputSourceID: sourceId }; + + // Include every installed source, not just the enabled ones: an input method mode can be + // selected without being listed as enabled, and failing to find it would classify a + // composing input method as a plain layout. + auto sources = TISCreateInputSourceList((__bridge CFDictionaryRef)filter, true); + + if(sources == nullptr) + { + return false; + } + + auto composes = CFArrayGetCount(sources) > 0 && + InputSourceComposes((TISInputSourceRef)CFArrayGetValueAtIndex(sources, 0)); + + CFRelease(sources); + + return composes; +} + +// Whether the currently selected keyboard input source is a composing input method. The result +// is cached per input source id, since this is queried for every key down. +- (bool)isComposingInputSourceSelected +{ + auto inputContext = [self inputContext]; + + if(inputContext == nullptr) + { + return false; + } + + NSString* sourceId = [inputContext selectedKeyboardInputSource]; + + if(sourceId == nullptr) + { + return false; + } + + if(![sourceId isEqualToString:_keyboardInputSourceId]) + { + _keyboardInputSourceId = sourceId; + _keyboardInputSourceComposes = KeyboardInputSourceComposes(sourceId); + } + + return _keyboardInputSourceComposes; } - (void)keyDown:(NSEvent *)event @@ -661,57 +754,57 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) { return; } - - _lastKeyDownEvent = event; - + auto timestamp = static_cast([event timestamp] * 1000); - + auto scanCode = [event keyCode]; - auto key = VirtualKeyFromScanCode(scanCode, [event modifierFlags]); auto physicalKey = PhysicalKeyFromScanCode(scanCode); auto keySymbol = KeySymbolFromScanCode(scanCode, [event modifierFlags]); - + auto keySymbolUtf8 = keySymbol == nullptr ? nullptr : [keySymbol UTF8String]; + auto modifiers = [self getModifiers:[event modifierFlags]]; - - //InputMethod is active - if(parent->InputMethod->IsActive()){ - auto hasInputModifier = modifiers != AvnInputModifiersNone; - - //Handle keyDown first if an input modifier is present - if(hasInputModifier){ - if([self handleKeyDown:timestamp withKey:key withPhysicalKey:physicalKey withModifiers:modifiers withKeySymbol:keySymbol]){ - //User code has handled the event - _lastKeyDownEvent = nullptr; - - return; - } - } - - if([[self inputContext] handleEvent:event] == NO){ - //KeyDown has not been consumed by the input context - - //Only raise a keyDown if we don't have a modifier - if(!hasInputModifier){ - [self handleKeyDown:timestamp withKey:key withPhysicalKey:physicalKey withModifiers:modifiers withKeySymbol:keySymbol]; - } - } - + + // The keystroke belongs to the input method when a composition is already in progress, since + // it then selects a candidate, commits or cancels it. It also belongs to the input method + // when a character key is typed into a text input client while a composing input method is + // selected, because that keystroke may be the one that starts the composition and we cannot + // know that before the input context has seen it. + // + // In both cases mask the key the way Win32 reports VK_PROCESSKEY, so that user code still + // observes a KeyDown but no KeyGesture can match it. The physical key and the key symbol + // keep their real values, so the underlying key remains recoverable. + // + // Both cases require an active client: without one the input context is never consulted, so + // marked text left over from a composition that lost its client mid-way must not mask keys. + auto imeProcessed = parent->InputMethod->IsActive() && + ([self hasMarkedText] || + (CanStartComposition(keySymbol, [event modifierFlags]) && + [self isComposingInputSourceSelected])); + + auto key = imeProcessed + ? AvnKeyImeProcessed + : VirtualKeyFromScanCode(scanCode, [event modifierFlags]); + + // A KeyDown is always raised before the input context sees the event, otherwise the + // input context silently consumes printable keys (space, letters, digits) and user code + // never gets a chance to react to them. + auto handled = parent->TopLevelEvents->RawKeyEvent(KeyDown, timestamp, modifiers, key, physicalKey, keySymbolUtf8); + + if(handled) + { + // User code has handled the event, so no text may be produced from it. + return; } - //InputMethod not active - else{ - auto keyDownHandled = [self handleKeyDown:timestamp withKey:key withPhysicalKey:physicalKey withModifiers:modifiers withKeySymbol:keySymbol]; - - //Raise text input event for unhandled key down - if(!keyDownHandled){ - if(keySymbol != nullptr && key != AvnKeyEnter){ - auto timestamp = static_cast([event timestamp] * 1000); - - parent->TopLevelEvents->RawTextInputEvent(timestamp, [keySymbol UTF8String]); - } - } + + if(parent->InputMethod->IsActive()) + { + // Let the input context produce the text or drive the composition. + [[self inputContext] handleEvent:event]; + } + else if(keySymbol != nullptr && key != AvnKeyEnter) + { + parent->TopLevelEvents->RawTextInputEvent(timestamp, keySymbolUtf8); } - - _lastKeyDownEvent = nullptr; } - (void)keyUp:(NSEvent *)event @@ -721,9 +814,9 @@ static void ConvertTilt(NSPoint tilt, float* xTilt, float* yTilt) } - (void) doCommandBySelector:(SEL)selector{ - if(_lastKeyDownEvent != nullptr){ - [self keyboardEvent:_lastKeyDownEvent withType:KeyDown]; - } + // -keyDown: already raised a KeyDown for this event before handing it to the input + // context, so nothing is left to do here. The method still has to be implemented: + // falling back to NSResponder would perform the default action and emit a system beep. } - (AvnInputModifiers)getModifiers:(NSEventModifierFlags)mod diff --git a/native/Avalonia.Native/src/OSX/PlatformSettings.mm b/native/Avalonia.Native/src/OSX/PlatformSettings.mm index 34ceb93186..d30d55e2ec 100644 --- a/native/Avalonia.Native/src/OSX/PlatformSettings.mm +++ b/native/Avalonia.Native/src/OSX/PlatformSettings.mm @@ -1,12 +1,19 @@ #include "common.h" +#include "AvnString.h" @interface CocoaThemeObserver : NSObject -(id)initWithCallback:(IAvnActionCallback *)callback; @end +@interface CocoaLocaleObserver : NSObject +-(id)initWithCallback:(IAvnActionCallback *)callback; +-(void)localeDidChange:(NSNotification *)notification; +@end + class PlatformSettings : public ComSingleObject { CocoaThemeObserver* observer; + CocoaLocaleObserver* localeObserver; public: FORWARD_IUNKNOWN() @@ -58,6 +65,28 @@ public: [[NSApplication sharedApplication] addObserver:observer forKeyPath:@"effectiveAppearance" options:NSKeyValueObservingOptionNew context:nil]; } } + + virtual HRESULT GetPreferredLanguage(IAvnString** ret) override + { + @autoreleasepool + { + if (ret == nullptr) + return E_POINTER; + + auto language = [[NSLocale preferredLanguages] firstObject]; + *ret = language == nil ? nullptr : CreateAvnString(language); + return S_OK; + } + } + + virtual void RegisterLanguageChange(IAvnActionCallback *callback) override + { + localeObserver = [[CocoaLocaleObserver alloc] initWithCallback: callback]; + [[NSNotificationCenter defaultCenter] addObserver:localeObserver + selector:@selector(localeDidChange:) + name:NSCurrentLocaleDidChangeNotification + object:nil]; + } private: unsigned int to_argb(NSColor* color) @@ -107,6 +136,23 @@ private: } @end +@implementation CocoaLocaleObserver +{ + ComPtr _callback; +} +- (id) initWithCallback:(IAvnActionCallback *)callback { + self = [super init]; + if (self) { + _callback = callback; + } + return self; +} + +- (void)localeDidChange:(NSNotification *)notification { + _callback->Run(); +} +@end + extern IAvnPlatformSettings* CreatePlatformSettings() { return new PlatformSettings(); diff --git a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm index 33ab1529fc..205f1efbdf 100644 --- a/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm +++ b/native/Avalonia.Native/src/OSX/WindowBaseImpl.mm @@ -465,11 +465,21 @@ void WindowBaseImpl::CleanNSWindow() { } void WindowBaseImpl::CreateNSWindow(bool usePanel) { + // Anchor the content rect to the primary screen's origin. It's usually at (0,0), but can be offset while the + // display server is mid-reconfiguration. This ensures the window is always on a proper screen. + // See https://github.com/AvaloniaUI/Avalonia/issues/18895 + NSPoint origin = NSZeroPoint; + NSArray* screens = [NSScreen screens]; + if (screens.count > 0) + origin = screens.firstObject.frame.origin; + + NSRect contentRect = NSRect { origin.x, origin.y, lastSize }; + if (usePanel) { - Window = [[AvnPanel alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; + Window = [[AvnPanel alloc] initWithParent:this contentRect:contentRect styleMask:NSWindowStyleMaskBorderless]; [Window setHidesOnDeactivate:false]; } else { - Window = [[AvnWindow alloc] initWithParent:this contentRect:NSRect{0, 0, lastSize} styleMask:NSWindowStyleMaskBorderless]; + Window = [[AvnWindow alloc] initWithParent:this contentRect:contentRect styleMask:NSWindowStyleMaskBorderless]; } } diff --git a/packages/Avalonia/AvaloniaRules.Project.xml b/packages/Avalonia/AvaloniaRules.Project.xml index 0a5c1b8243..120cc1c255 100644 --- a/packages/Avalonia/AvaloniaRules.Project.xml +++ b/packages/Avalonia/AvaloniaRules.Project.xml @@ -5,6 +5,7 @@ + @@ -139,4 +140,11 @@ Example: MyApp.Presentation.*, MyApp.Presentation.Views;MyApp.Presentation.Contr + + + + diff --git a/readme.md b/readme.md index 9ed6c59f90..e291498703 100644 --- a/readme.md +++ b/readme.md @@ -10,7 +10,7 @@ Considered by many to be the spiritual successor to WPF, Avalonia UI provides a familiar, modern development experience for XAML developers creating cross-platform applications. While Avalonia UI is [similar to WPF](https://docs.avaloniaui.net/docs/next/get-started/wpf/), it isn't a 1:1 copy, and you'll find plenty of improvements. -For those seeking a cross-platform WPF, we have created [Avalonia XPF]([https://avaloniaui.net/xpf](https://avaloniaui.net/xpf?utm_source=github&utm_medium=referral&utm_content=readme_link)), enabling WPF applications to run on macOS and Linux with little to no code changes. Avalonia XPF is a commercial product and is licensed per-app, per-platform. +For those seeking a cross-platform WPF, we have created [Avalonia XPF](https://avaloniaui.net/xpf?utm_source=github&utm_medium=referral&utm_content=readme_link), enabling WPF applications to run on macOS and Linux with little to no code changes. Avalonia XPF is a commercial product and is licensed per-app, per-platform. ### Blog To see the latest announcements and read about the state of Avalonia, check out the [Avalonia UI Blog](https://www.avaloniaui.net/blog?utm_source=github&utm_medium=referral&utm_content=readme_link). diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index 978eb361c4..a1e9b45c2c 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -9,8 +9,8 @@ apk - - Resources\drawable\Icon.png + + Resources\drawable\icon.png diff --git a/samples/ControlCatalog.Browser/wwwroot/favicon.ico b/samples/ControlCatalog.Browser/wwwroot/favicon.ico index da8d49ff9b..b5ce607b0e 100644 Binary files a/samples/ControlCatalog.Browser/wwwroot/favicon.ico and b/samples/ControlCatalog.Browser/wwwroot/favicon.ico differ diff --git a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj index 779e9b3462..4fe7c499fb 100644 --- a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj +++ b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj @@ -6,6 +6,7 @@ true true true + $(SharedAssetsPath)\icon.ico @@ -35,6 +36,7 @@ app.manifest + diff --git a/samples/ControlCatalog/App.xaml b/samples/ControlCatalog/App.xaml index 42f35c7205..336eae7553 100644 --- a/samples/ControlCatalog/App.xaml +++ b/samples/ControlCatalog/App.xaml @@ -74,7 +74,7 @@ - + @@ -83,7 +83,7 @@ - + diff --git a/samples/ControlCatalog/Assets/Icon.png b/samples/ControlCatalog/Assets/Icon.png deleted file mode 100644 index 41a2a618fb..0000000000 Binary files a/samples/ControlCatalog/Assets/Icon.png and /dev/null differ diff --git a/samples/ControlCatalog/Assets/avalonia-32.png b/samples/ControlCatalog/Assets/avalonia-32.png deleted file mode 100644 index 7b443e7a25..0000000000 Binary files a/samples/ControlCatalog/Assets/avalonia-32.png and /dev/null differ diff --git a/samples/ControlCatalog/Assets/banner-bg.png b/samples/ControlCatalog/Assets/banner-bg.png new file mode 100644 index 0000000000..0ad18f2cc4 Binary files /dev/null and b/samples/ControlCatalog/Assets/banner-bg.png differ diff --git a/samples/ControlCatalog/Assets/banner-logo.png b/samples/ControlCatalog/Assets/banner-logo.png new file mode 100644 index 0000000000..48e825d993 Binary files /dev/null and b/samples/ControlCatalog/Assets/banner-logo.png differ diff --git a/samples/ControlCatalog/Assets/logo1.png b/samples/ControlCatalog/Assets/logo1.png new file mode 100644 index 0000000000..93164bfe1e Binary files /dev/null and b/samples/ControlCatalog/Assets/logo1.png differ diff --git a/samples/ControlCatalog/Assets/logo2.png b/samples/ControlCatalog/Assets/logo2.png new file mode 100644 index 0000000000..6e5e63c0ee Binary files /dev/null and b/samples/ControlCatalog/Assets/logo2.png differ diff --git a/samples/ControlCatalog/Assets/logo3.png b/samples/ControlCatalog/Assets/logo3.png new file mode 100644 index 0000000000..53331bf02e Binary files /dev/null and b/samples/ControlCatalog/Assets/logo3.png differ diff --git a/samples/ControlCatalog/Assets/logo4.png b/samples/ControlCatalog/Assets/logo4.png new file mode 100644 index 0000000000..930046865a Binary files /dev/null and b/samples/ControlCatalog/Assets/logo4.png differ diff --git a/samples/ControlCatalog/Assets/test_icon.ico b/samples/ControlCatalog/Assets/test_icon.ico deleted file mode 100644 index da8d49ff9b..0000000000 Binary files a/samples/ControlCatalog/Assets/test_icon.ico and /dev/null differ diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 0cc232bfe2..b2900f8dbe 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -4,6 +4,7 @@ true true + %(Filename) @@ -11,23 +12,13 @@ Designer - - - - - - - - - - - + - - - - + + + + diff --git a/samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs b/samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs new file mode 100644 index 0000000000..c5c3e0e2ed --- /dev/null +++ b/samples/ControlCatalog/Converter/FlexDemoNumberToThicknessConverter.cs @@ -0,0 +1,24 @@ +using System; +using System.Globalization; +using Avalonia; +using Avalonia.Data.Converters; + +namespace ControlCatalog.Converter +{ + internal sealed class FlexDemoNumberToThicknessConverter : IValueConverter + { + public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) + { + if (value is int x && targetType.IsAssignableFrom(typeof(Thickness))) + { + var y = 16 + 2 * ((x * 5) % 9); + return new Thickness(2 * y, y); + } + + throw new NotSupportedException(); + } + + public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => + throw new NotSupportedException(); + } +} diff --git a/samples/ControlCatalog/Icons.cs b/samples/ControlCatalog/Icons.cs index 58d0e0b7d4..5f99bdc877 100644 --- a/samples/ControlCatalog/Icons.cs +++ b/samples/ControlCatalog/Icons.cs @@ -190,5 +190,8 @@ namespace ControlCatalog // Slider tune/adjust public const string Tune = "M3,17V19H9V17H3M3,5V7H13V5H3M13,21V19H21V17H13V15H11V21H13M7,9V11H3V13H7V15H9V9H7M21,13V11H11V13H21M15,9H17V7H21V5H17V3H15V9Z"; + + // Home + public const string Home = "M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z"; } } diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index f7b2658b0f..08f47ca630 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -2,10 +2,8 @@ xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:ControlCatalog" - xmlns:controls="using:ControlSamples" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:models="using:ControlCatalog.Models" - xmlns:pages="using:ControlCatalog.Pages" xmlns:viewModels="using:ControlCatalog.ViewModels" d:DesignWidth="1200" d:DesignHeight="720" @@ -45,7 +43,7 @@ ItemContainerTheme="{StaticResource NavHeaderItem}" SelectedIndex="{Binding SelectedPageIndex}"> - + - - + + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d4a45a127d..8d4da7fec2 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; +using Avalonia.Input; using Avalonia.Media; using Avalonia.Media.Immutable; using Avalonia.Styling; @@ -199,5 +200,10 @@ namespace ControlCatalog ViewModel.SelectedPageIndex = 0; } + + private async void AvaloniaIcon_OnTapped(object? sender, TappedEventArgs e) + { + await TopLevel.GetTopLevel(this)!.Launcher.LaunchUriAsync(new Uri("https://avaloniaui.net/")); + } } } diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index d839689db7..82478ebdd0 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -1,8 +1,6 @@  @@ -19,7 +18,7 @@ - + diff --git a/samples/ControlCatalog/Models/HomeSection.cs b/samples/ControlCatalog/Models/HomeSection.cs new file mode 100644 index 0000000000..a7c3bdeee3 --- /dev/null +++ b/samples/ControlCatalog/Models/HomeSection.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Models; + +public class HomeSection(string title, IReadOnlyList items) +{ + public string Title { get; } = title; + public IReadOnlyList Items { get; } = items; +} diff --git a/samples/ControlCatalog/Models/PageItem.cs b/samples/ControlCatalog/Models/PageItem.cs new file mode 100644 index 0000000000..af8a361101 --- /dev/null +++ b/samples/ControlCatalog/Models/PageItem.cs @@ -0,0 +1,53 @@ +using System; +using System.Globalization; +using System.Text; +using Avalonia.Controls; +using Avalonia.Media; + +namespace ControlCatalog.Models; + +public class PageItem(string header, Func factory, StreamGeometry iconData, string description, string section) +{ + public string Header { get; } = header; + public StreamGeometry? IconData { get; } = iconData; + public string? Description { get; } = description; + private string SearchKey { get; } = CreateSearchKey(header, section); + + public bool IsVisible { get; set; } = true; + + public Page CreatePage() => factory(); + + public bool MatchesSearch(string searchKey) + { + return SearchKey.Contains(searchKey, StringComparison.Ordinal); + } + + public static string CreateSearchKey(params ReadOnlySpan values) + { + var builder = new StringBuilder(256); + + foreach (var value in values) + { + var normalizedValue = value.Normalize(NormalizationForm.FormKD); + + foreach (var c in normalizedValue) + { + var category = CharUnicodeInfo.GetUnicodeCategory(c); + + if (category is UnicodeCategory.NonSpacingMark or + UnicodeCategory.SpacingCombiningMark or + UnicodeCategory.EnclosingMark) + { + continue; + } + + if (char.IsLetterOrDigit(c)) + { + builder.Append(char.ToUpperInvariant(c)); + } + } + } + + return builder.ToString(); + } +} diff --git a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml index 769dbc0dac..c00af6c801 100644 --- a/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarDatePickerPage.xaml @@ -49,6 +49,9 @@ + + + diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml index 99727e9124..44ab767dea 100644 --- a/samples/ControlCatalog/Pages/CalendarPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarPage.xaml @@ -48,6 +48,17 @@ + + + + + # + + + diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 149fd73d0a..db29ecbf3e 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -103,7 +103,7 @@ WrapSelection="{Binding WrapSelection}" ItemsSource="{Binding Values}" DisplayMemberBinding="{Binding Name}" - TextSearch.TextBinding="{Binding SearchText, DataType=viewModels:IdAndName}" /> + TextSearch.TextBinding="{Binding SearchText, x:DataType=viewModels:IdAndName}" /> diff --git a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs index 782435ae06..278899cd2f 100644 --- a/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs +++ b/samples/ControlCatalog/Pages/CustomDrawingExampleControl.cs @@ -11,31 +11,25 @@ using Avalonia.Controls.Shapes; namespace ControlCatalog.Pages { - public class CustomDrawingExampleControl : Control + public partial class CustomDrawingExampleControl : Control { private Point _cursorPoint; + private Geometry _smileGeometry; - - public static readonly StyledProperty ScaleProperty = AvaloniaProperty.Register(nameof(Scale), 1.0d); - public double Scale { get => GetValue(ScaleProperty); set => SetValue(ScaleProperty, value); } - - public static readonly StyledProperty RotationProperty = AvaloniaProperty.Register(nameof(Rotation), - coerce: (_, val) => val % (Math.PI * 2)); + [GeneratedStyledProperty(DefaultValue = 1.0d)] + public partial double Scale { get; set; } /// /// Rotation, measured in Radians! /// - public double Rotation - { - get => GetValue(RotationProperty); - set => SetValue(RotationProperty, value); - } + [GeneratedStyledProperty(CoerceMethodName = nameof(CoerceRotation))] + public partial double Rotation { get; set; } - public static readonly StyledProperty ViewportCenterYProperty = AvaloniaProperty.Register(nameof(ViewportCenterY), 0.0d); - public double ViewportCenterY { get => GetValue(ViewportCenterYProperty); set => SetValue(ViewportCenterYProperty, value); } + [GeneratedStyledProperty] + public partial double ViewportCenterY { get; set; } - public static readonly StyledProperty ViewportCenterXProperty = AvaloniaProperty.Register(nameof(ViewportCenterX), 0.0d); - public double ViewportCenterX { get => GetValue(ViewportCenterXProperty); set => SetValue(ViewportCenterXProperty, value); } + [GeneratedStyledProperty] + public partial double ViewportCenterX { get; set; } private IPen _pen; @@ -66,7 +60,7 @@ namespace ControlCatalog.Pages _smileGeometry = sg.Clone(); } - private Geometry _smileGeometry; + private static double CoerceRotation(AvaloniaObject sender, double value) => value % (Math.PI * 2); protected override void OnPointerMoved(PointerEventArgs e) { diff --git a/samples/ControlCatalog/Pages/FlexPage.axaml b/samples/ControlCatalog/Pages/FlexPage.axaml new file mode 100644 index 0000000000..bd923486e1 --- /dev/null +++ b/samples/ControlCatalog/Pages/FlexPage.axaml @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/HomePage.xaml.cs b/samples/ControlCatalog/Pages/HomePage.xaml.cs new file mode 100644 index 0000000000..d2172d3012 --- /dev/null +++ b/samples/ControlCatalog/Pages/HomePage.xaml.cs @@ -0,0 +1,82 @@ +using System; +using Avalonia; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; +using Avalonia.Styling; + +namespace ControlCatalog.Pages; + +public partial class HomePage : ContentPage +{ + public HomePage() + { + InitializeComponent(); + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + + StartFloatingAnimation(BannerLogo1, 12, 8, TimeSpan.FromSeconds(12)); + StartFloatingAnimation(BannerLogo2, -14, 8, TimeSpan.FromSeconds(10)); + StartFloatingAnimation(BannerLogo3, -12, -12, TimeSpan.FromSeconds(14)); + StartFloatingAnimation(BannerLogo4, 12, -8, TimeSpan.FromSeconds(11)); + } + + private static void StartFloatingAnimation(Control target, double dx, double dy, TimeSpan duration) + { + var transform = new TranslateTransform(); + target.RenderTransform = transform; + + var animation = new Animation + { + Duration = duration, + IterationCount = IterationCount.Infinite, + Children = + { + new KeyFrame + { + Cue = new Cue(0d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(TranslateTransform.YProperty, 0d), + } + }, + new KeyFrame + { + Cue = new Cue(0.5d), + Setters = + { + new Setter(TranslateTransform.XProperty, dx), + new Setter(TranslateTransform.YProperty, dy), + } + }, + new KeyFrame + { + Cue = new Cue(1d), + Setters = + { + new Setter(TranslateTransform.XProperty, 0d), + new Setter(TranslateTransform.YProperty, 0d), + } + }, + } + }; + + _ = animation.RunAsync(target); + } + + private void UniformGrid_OnSizeChanged(object? sender, SizeChangedEventArgs e) + { + if (sender is not UniformGrid grid) + return; + + const int minItemWidth = 248; + grid.Columns = Math.Max( + 1, + (int)((e.NewSize.Width + grid.ColumnSpacing) / (minItemWidth + grid.ColumnSpacing))); + } +} diff --git a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml index 17d7ff2d5a..da2215ef65 100644 --- a/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml +++ b/samples/ControlCatalog/Pages/NavigationPage/RetroGamingProfileView.xaml @@ -7,7 +7,6 @@ diff --git a/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml new file mode 100644 index 0000000000..a6213558cb --- /dev/null +++ b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs new file mode 100644 index 0000000000..8f3e6f4b48 --- /dev/null +++ b/samples/ControlCatalog/Pages/PlatformSettingsPage.xaml.cs @@ -0,0 +1,30 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.VisualTree; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public partial class PlatformSettingsPage : ContentPage + { + private readonly PlatformSettingsViewModel _viewModel = new(); + + public PlatformSettingsPage() + { + InitializeComponent(); + DataContext = _viewModel; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _viewModel.Subscribe(this.GetPlatformSettings() ?? Application.Current?.PlatformSettings); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + _viewModel.Unsubscribe(); + base.OnDetachedFromVisualTree(e); + } + } +} diff --git a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml index a905e4efdf..5e25d927ba 100644 --- a/samples/ControlCatalog/Pages/RefreshContainerPage.axaml +++ b/samples/ControlCatalog/Pages/RefreshContainerPage.axaml @@ -27,6 +27,7 @@ +