diff --git a/Avalonia.sln b/Avalonia.sln index 3a2c619d5b..f6dc039c2f 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -201,9 +201,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Dialogs", "src\Ava EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.FreeDesktop", "src\Avalonia.FreeDesktop\Avalonia.FreeDesktop.csproj", "{4D36CEC8-53F2-40A5-9A37-79AAE356E2DA}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.DataGrid.UnitTests", "tests\Avalonia.Controls.DataGrid.UnitTests\Avalonia.Controls.DataGrid.UnitTests.csproj", "{351337F5-D66F-461B-A957-4EF60BDB4BA6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "samples\interop\NativeEmbedSample\NativeEmbedSample.csproj", "{3C84E04B-36CF-4D0D-B965-C26DD649D1F3}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject @@ -211,8 +211,8 @@ Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 src\Shared\RenderHelpers\RenderHelpers.projitems*{3e908f67-5543-4879-a1dc-08eace79b3cd}*SharedItemsImports = 5 - src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 4 - src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 4 + src\Shared\PlatformSupport\PlatformSupport.projitems*{4488ad85-1495-4809-9aa4-ddfe0a48527e}*SharedItemsImports = 5 + src\Shared\PlatformSupport\PlatformSupport.projitems*{7b92af71-6287-4693-9dcb-bd5b6e927e23}*SharedItemsImports = 5 src\Shared\RenderHelpers\RenderHelpers.projitems*{7d2d3083-71dd-4cc9-8907-39a0d86fb322}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{88060192-33d5-4932-b0f9-8bd2763e857d}*SharedItemsImports = 5 src\Shared\PlatformSupport\PlatformSupport.projitems*{e4d9629c-f168-4224-3f51-a5e482ffbc42}*SharedItemsImports = 13 diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 1cf3bc75b0..9ff6130e5f 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -205,6 +205,15 @@ enum AvnMenuItemToggleType Radio }; +enum AvnExtendClientAreaChromeHints +{ + AvnNoChrome = 0, + AvnSystemChrome = 0x01, + AvnPreferSystemChrome = 0x02, + AvnOSXThickTitleBar = 0x08, + AvnDefaultChrome = AvnSystemChrome, +}; + AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: @@ -278,6 +287,11 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase virtual HRESULT SetTitleBarColor (AvnColor color) = 0; virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT GetWindowState(AvnWindowState*ret) = 0; + virtual HRESULT TakeFocusFromChildren() = 0; + virtual HRESULT SetExtendClientArea (bool enable) = 0; + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) = 0; + virtual HRESULT GetExtendTitleBarHeight (double*ret) = 0; + virtual HRESULT SetExtendTitleBarHeight (double value) = 0; }; AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown @@ -493,8 +507,8 @@ AVNCOM(IAvnNativeControlHostTopLevelAttachment, 21) : IUnknown virtual void* GetParentHandle() = 0; virtual HRESULT InitializeWithChildHandle(void* child) = 0; virtual HRESULT AttachTo(IAvnNativeControlHost* host) = 0; - virtual void MoveTo(float x, float y, float width, float height) = 0; - virtual void Hide() = 0; + virtual void ShowInBounds(float x, float y, float width, float height) = 0; + virtual void HideWithSize(float width, float height) = 0; virtual void ReleaseChild() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/controlhost.mm b/native/Avalonia.Native/src/OSX/controlhost.mm index 315ec2f310..5ee2344ac7 100644 --- a/native/Avalonia.Native/src/OSX/controlhost.mm +++ b/native/Avalonia.Native/src/OSX/controlhost.mm @@ -97,7 +97,7 @@ public: return S_OK; }; - virtual void MoveTo(float x, float y, float width, float height) override + virtual void ShowInBounds(float x, float y, float width, float height) override { if(_child == nil) return; @@ -106,7 +106,7 @@ public: IAvnNativeControlHostTopLevelAttachment* slf = this; slf->AddRef(); dispatch_async(dispatch_get_main_queue(), ^{ - slf->MoveTo(x, y, width, height); + slf->ShowInBounds(x, y, width, height); slf->Release(); }); return; @@ -122,9 +122,24 @@ public: [[_holder superview] setNeedsDisplay:true]; } - virtual void Hide() override + virtual void HideWithSize(float width, float height) override { + if(_child == nil) + return; + if(AvnInsidePotentialDeadlock::IsInside()) + { + IAvnNativeControlHostTopLevelAttachment* slf = this; + slf->AddRef(); + dispatch_async(dispatch_get_main_queue(), ^{ + slf->HideWithSize(width, height); + slf->Release(); + }); + return; + } + + NSRect frame = {0, 0, width, height}; [_holder setHidden: true]; + [_child setFrame: frame]; } virtual void ReleaseChild() override diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index bdf3007a28..b1f64bca88 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,9 +3,6 @@ class WindowBaseImpl; -@interface AutoFitContentVisualEffectView : NSVisualEffectView -@end - @interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; @@ -15,6 +12,14 @@ class WindowBaseImpl; -(AvnPixelSize) getPixelSize; @end +@interface AutoFitContentView : NSView +-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; +-(void) ShowTitleBar: (bool) show; +-(void) SetTitleBarHeightHint: (double) height; +-(void) SetContent: (NSView* _Nonnull) content; +-(void) ShowBlur: (bool) show; +@end + @interface AvnWindow : NSWindow +(void) closeAll; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; @@ -27,6 +32,8 @@ class WindowBaseImpl; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(NSMenu* _Nullable)menu; -(double) getScaling; +-(double) getExtendedTitleBarHeight; +-(void) setIsExtended:(bool)value; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 86b3584681..872269bb26 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -6,8 +6,6 @@ #include #include "rendertarget.h" - - class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder { private: @@ -20,7 +18,7 @@ public: View = NULL; Window = NULL; } - NSVisualEffectView* VisualEffect; + AutoFitContentView* StandardContainer; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -39,6 +37,7 @@ public: _glContext = gl; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; View = [[AvnView alloc] initWithParent:this]; + StandardContainer = [[AutoFitContentView new] initWithContent:View]; Window = [[AvnWindow alloc] initWithParent:this]; @@ -49,12 +48,8 @@ public: [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - VisualEffect = [AutoFitContentVisualEffectView new]; - [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; - [VisualEffect setMaterial:NSVisualEffectMaterialLight]; - [VisualEffect setAutoresizesSubviews:true]; - - [Window setContentView: View]; + [Window setOpaque:false]; + [Window setContentView: StandardContainer]; } virtual HRESULT ObtainNSWindowHandle(void** ret) override @@ -116,10 +111,15 @@ public: { SetPosition(lastPositionSet); UpdateStyle(); - - [Window makeKeyAndOrderFront:Window]; - [NSApp activateIgnoringOtherApps:YES]; - + if(ShouldTakeFocusOnShow()) + { + [Window makeKeyAndOrderFront:Window]; + [NSApp activateIgnoringOtherApps:YES]; + } + else + { + [Window orderFront: Window]; + } [Window setTitle:_lastTitle]; _shown = true; @@ -128,6 +128,11 @@ public: } } + virtual bool ShouldTakeFocusOnShow() + { + return true; + } + virtual HRESULT Hide () override { @autoreleasepool @@ -400,12 +405,7 @@ public: virtual HRESULT SetBlurEnabled (bool enable) override { - [Window setContentView: enable ? VisualEffect : View]; - - if(enable) - { - [VisualEffect addSubview:View]; - } + [StandardContainer ShowBlur:enable]; return S_OK; } @@ -482,6 +482,8 @@ private: bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; + bool _isClientAreaExtended; + AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -495,6 +497,8 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _decorations = SystemDecorationsFull; @@ -513,8 +517,20 @@ private: if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { NSView *titlebarView = [subview subviews][0]; for (id button in titlebarView.subviews) { - if ([button isKindOfClass:[NSButton class]]) { - [button setHidden: (_decorations != SystemDecorationsFull)]; + if ([button isKindOfClass:[NSButton class]]) + { + if(_isClientAreaExtended) + { + auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + [button setHidden: !wantsChrome]; + } + else + { + [button setHidden: (_decorations != SystemDecorationsFull)]; + } + + [button setWantsLayer:true]; } } } @@ -590,6 +606,35 @@ private: if(_lastWindowState != state) { + if(_isClientAreaExtended) + { + if(_lastWindowState == FullScreen) + { + // we exited fs. + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + + [Window setTitlebarAppearsTransparent:true]; + + [StandardContainer setFrameSize: StandardContainer.frame.size]; + } + else if(state == FullScreen) + { + // we entered fs. + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = nullptr; + } + + [Window setTitlebarAppearsTransparent:false]; + + [StandardContainer setFrameSize: StandardContainer.frame.size]; + } + } + _lastWindowState = state; WindowEvents->WindowStateChanged(state); } @@ -646,8 +691,6 @@ private: return S_OK; } - auto currentFrame = [Window frame]; - UpdateStyle(); HideOrShowTrafficLights(); @@ -774,6 +817,90 @@ private: } } + virtual HRESULT TakeFocusFromChildren () override + { + if(Window == nil) + return S_OK; + if([Window isKeyWindow]) + [Window makeFirstResponder: View]; + + return S_OK; + } + + virtual HRESULT SetExtendClientArea (bool enable) override + { + _isClientAreaExtended = enable; + + if(enable) + { + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) + { + [StandardContainer ShowTitleBar:true]; + } + else + { + [StandardContainer ShowTitleBar:false]; + } + + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + else + { + Window.toolbar = nullptr; + } + } + else + { + Window.titleVisibility = NSWindowTitleVisible; + Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; + } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; + } + + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override + { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } + + virtual HRESULT GetExtendTitleBarHeight (double*ret) override + { + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; + } + + virtual HRESULT SetExtendTitleBarHeight (double value) override + { + [StandardContainer SetTitleBarHeightHint:value]; + return S_OK; + } + void EnterFullScreenMode () { _fullScreenActive = true; @@ -783,8 +910,9 @@ private: [Window setTitlebarAppearsTransparent:NO]; [Window setTitle:_lastTitle]; - [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable]; - + Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; + Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView; + [Window toggleFullScreen:nullptr]; } @@ -932,19 +1060,120 @@ protected: { s |= NSWindowStyleMaskMiniaturizable; } + + if(_isClientAreaExtended) + { + s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; + } return s; } }; NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; -@implementation AutoFitContentVisualEffectView +@implementation AutoFitContentView +{ + NSVisualEffectView* _titleBarMaterial; + NSBox* _titleBarUnderline; + NSView* _content; + NSVisualEffectView* _blurBehind; + double _titleBarHeightHint; + bool _settingSize; +} + +-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content +{ + _titleBarHeightHint = -1; + _content = content; + _settingSize = false; + + [self setAutoresizesSubviews:true]; + [self setWantsLayer:true]; + + _titleBarMaterial = [NSVisualEffectView new]; + [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; + [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; + [_titleBarMaterial setWantsLayer:true]; + _titleBarMaterial.hidden = true; + + _titleBarUnderline = [NSBox new]; + _titleBarUnderline.boxType = NSBoxSeparator; + _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; + _titleBarUnderline.hidden = true; + + [self addSubview:_titleBarMaterial]; + [self addSubview:_titleBarUnderline]; + + _blurBehind = [NSVisualEffectView new]; + [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [_blurBehind setMaterial:NSVisualEffectMaterialLight]; + [_blurBehind setWantsLayer:true]; + _blurBehind.hidden = true; + + [self addSubview:_blurBehind]; + [self addSubview:_content]; + + [self setWantsLayer:true]; + return self; +} + +-(void) ShowBlur:(bool)show +{ + _blurBehind.hidden = !show; +} + +-(void) ShowTitleBar: (bool) show +{ + _titleBarMaterial.hidden = !show; + _titleBarUnderline.hidden = !show; +} + +-(void) SetTitleBarHeightHint: (double) height +{ + _titleBarHeightHint = height; + + [self setFrameSize:self.frame.size]; +} + -(void)setFrameSize:(NSSize)newSize { - [super setFrameSize:newSize]; - if([[self subviews] count] == 0) + if(_settingSize) + { return; - [[self subviews][0] setFrameSize: newSize]; + } + + _settingSize = true; + [super setFrameSize:newSize]; + + [_blurBehind setFrameSize:newSize]; + [_content setFrameSize:newSize]; + + auto window = objc_cast([self window]); + + // TODO get actual titlebar size + + double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; + + NSRect tbar; + tbar.origin.x = 0; + tbar.origin.y = newSize.height - height; + tbar.size.width = newSize.width; + tbar.size.height = height; + + [_titleBarMaterial setFrame:tbar]; + tbar.size.height = height < 1 ? 0 : 1; + [_titleBarUnderline setFrame:tbar]; + _settingSize = false; +} + +-(void) SetContent: (NSView* _Nonnull) content +{ + if(content != nullptr) + { + [content removeFromSuperview]; + [self addSubview:content]; + _content = content; + } } @end @@ -1504,15 +1733,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _canBecomeKeyAndMain; bool _closed; bool _isEnabled; + bool _isExtended; AvnMenu* _menu; double _lastScaling; } +-(void) setIsExtended:(bool)value; +{ + _isExtended = value; +} + -(double) getScaling { return _lastScaling; } +-(double) getExtendedTitleBarHeight +{ + if(_isExtended) + { + for (id subview in self.contentView.superview.subviews) + { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) + { + NSView *titlebarView = [subview subviews][0]; + + return (double)titlebarView.frame.size.height; + } + } + + return -1; + } + else + { + return 0; + } +} + +(void)closeAll { NSArray* windows = [NSArray arrayWithArray:[NSApp windows]]; @@ -1631,6 +1888,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; [self invalidateShadow]; + _isExtended = false; return self; } @@ -1858,7 +2116,6 @@ private: WindowEvents = events; [Window setLevel:NSPopUpMenuWindowLevel]; } - protected: virtual NSWindowStyleMask GetStyle() override { @@ -1876,6 +2133,11 @@ protected: return S_OK; } } +public: + virtual bool ShouldTakeFocusOnShow() override + { + return false; + } }; extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl) diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 488062f5b6..ca210300ee 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -29,6 +29,9 @@ ScrollViewer.HorizontalScrollBarVisibility="Disabled"> + + + @@ -54,6 +57,7 @@ + @@ -62,6 +66,7 @@ + diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d6d4a71ad3..b0c205246e 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -58,13 +58,6 @@ namespace ControlCatalog if (VisualRoot is Window window) window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; }; - - var transparencyLevels = this.Find("TransparencyLevels"); - transparencyLevels.SelectionChanged += (sender, e) => - { - if (VisualRoot is Window window) - window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex; - }; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 76422bc130..13fd4a2165 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}"> + ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" + ExtendClientAreaChromeHints="{Binding ChromeHints}" + ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" + TransparencyLevelHint="{Binding TransparencyLevel}" + TransparencyBackgroundFallback="Transparent" + x:Name="MainWindow" + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{x:Null}"> @@ -56,20 +62,30 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml new file mode 100644 index 0000000000..af6b6e8605 --- /dev/null +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -0,0 +1,123 @@ + + + DatePicker and TimePicker + + + + + A simple DatePicker with a header + + + + + + + + + <DatePicker Header="Pick a date" /> + + + + + + + A DatePicker with day formatted and year hidden. + + + + + + + + + <DatePicker DayFormat="d (ddd)" YearVisible="False" /> + + + + + + + + + A simple TimePicker. + + + + + + + + + <TimePicker /> + + + + + + + A TimePicker with a header and minute increments specified. + + + + + + + + + <TimePicker Header="Arrival time" MinuteIncrement="15" /> + + + + + + + A TimePicker using a 12-hour clock. + + + + + + + + + <TimePicker ClockIdentifier="12HourClock" Header="12 hour clock" /> + + + + + + + A TimePicker using a 24-hour clock. + + + + + + + + + <TimePicker ClockIdentifier="24HourClock" Header="24 hour clock" /> + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs new file mode 100644 index 0000000000..6c7ae3437e --- /dev/null +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs @@ -0,0 +1,30 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class DateTimePickerPage : UserControl + { + public DateTimePickerPage() + { + this.InitializeComponent(); + this.FindControl("DatePickerDesc").Text = "Use a DatePicker to let users set a date in your app, " + + "for example to schedule an appointment. The DatePicker displays three controls for month, day, and year. " + + "These controls are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + + "Order of month, day, and year is dynamically set based on user date settings"; + + this.FindControl("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " + + "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " + + "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + + "12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden."; + + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/ProgressBarPage.xaml b/samples/ControlCatalog/Pages/ProgressBarPage.xaml index 13bae59805..2ec0b48c76 100644 --- a/samples/ControlCatalog/Pages/ProgressBarPage.xaml +++ b/samples/ControlCatalog/Pages/ProgressBarPage.xaml @@ -1,29 +1,19 @@ - + ProgressBar A progress bar control - - - + + + - - + - - + - - + + diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index c6f5521e60..ea31ed0050 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -6,11 +6,22 @@ A control that lets the user select from a range of values by moving a Thumb control along a Track. - + + + + + + + + + + + + + + + + + + + Inline + CompactInline + Overlay + CompactOverlay + + + + + SystemControlBackgroundChromeMediumLowBrush + Red + Blue + Green + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs new file mode 100644 index 0000000000..cbf217c94a --- /dev/null +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class SplitViewPage : UserControl + { + public SplitViewPage() + { + this.InitializeComponent(); + DataContext = new SplitViewPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml new file mode 100644 index 0000000000..b90f43c3b6 --- /dev/null +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -0,0 +1,19 @@ + + + + + + + + None + Transparent + Blur + AcrylicBlur + + + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs new file mode 100644 index 0000000000..d8d4f3f371 --- /dev/null +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class WindowCustomizationsPage : UserControl + { + public WindowCustomizationsPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 0257b4ce66..4356a032fa 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Dialogs; +using Avalonia.Platform; +using System; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -14,6 +16,12 @@ namespace ControlCatalog.ViewModels private bool _isMenuItemChecked = true; private WindowState _windowState; private WindowState[] _windowStates; + private int _transparencyLevel; + private ExtendClientAreaChromeHints _chromeHints; + private bool _extendClientAreaEnabled; + private bool _systemTitleBarEnabled; + private bool _preferSystemChromeEnabled; + private double _titleBarHeight; public MainWindowViewModel(IManagedNotificationManager notificationManager) { @@ -62,6 +70,63 @@ namespace ControlCatalog.ViewModels WindowState.Maximized, WindowState.FullScreen, }; + + this.WhenAnyValue(x => x.SystemTitleBarEnabled, x=>x.PreferSystemChromeEnabled) + .Subscribe(x => + { + var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; + + if(x.Item1) + { + hints |= ExtendClientAreaChromeHints.SystemChrome; + } + + if(x.Item2) + { + hints |= ExtendClientAreaChromeHints.PreferSystemChrome; + } + + ChromeHints = hints; + }); + + SystemTitleBarEnabled = true; + TitleBarHeight = -1; + } + + public int TransparencyLevel + { + get { return _transparencyLevel; } + set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); } + } + + public ExtendClientAreaChromeHints ChromeHints + { + get { return _chromeHints; } + set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } + } + + public bool ExtendClientAreaEnabled + { + get { return _extendClientAreaEnabled; } + set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } + } + + public bool SystemTitleBarEnabled + { + get { return _systemTitleBarEnabled; } + set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } + } + + public bool PreferSystemChromeEnabled + { + get { return _preferSystemChromeEnabled; } + set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } + } + + public double TitleBarHeight + { + get { return _titleBarHeight; } + set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); } } public WindowState WindowState diff --git a/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs new file mode 100644 index 0000000000..f27f605a8b --- /dev/null +++ b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs @@ -0,0 +1,46 @@ +using System; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class SplitViewPageViewModel : ReactiveObject + { + private bool _isLeft = true; + private int _displayMode = 3; //CompactOverlay + + public bool IsLeft + { + get => _isLeft; + set + { + this.RaiseAndSetIfChanged(ref _isLeft, value); + this.RaisePropertyChanged(nameof(PanePlacement)); + } + } + + public int DisplayMode + { + get => _displayMode; + set + { + this.RaiseAndSetIfChanged(ref _displayMode, value); + this.RaisePropertyChanged(nameof(CurrentDisplayMode)); + } + } + + public SplitViewPanePlacement PanePlacement => _isLeft ? SplitViewPanePlacement.Left : SplitViewPanePlacement.Right; + + public SplitViewDisplayMode CurrentDisplayMode + { + get + { + if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode)) + { + return (SplitViewDisplayMode)_displayMode; + } + return SplitViewDisplayMode.CompactOverlay; + } + } + } +} diff --git a/samples/interop/NativeEmbedSample/MainWindow.xaml b/samples/interop/NativeEmbedSample/MainWindow.xaml index dcec9035e0..f2161a1bea 100644 --- a/samples/interop/NativeEmbedSample/MainWindow.xaml +++ b/samples/interop/NativeEmbedSample/MainWindow.xaml @@ -20,7 +20,16 @@ + + + + Text + + + Tooltip + + diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index 5b0dea6c60..e006459652 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; namespace Avalonia.Animation.Easings @@ -25,6 +26,11 @@ namespace Avalonia.Animation.Easings /// Returns the instance of the parsed type. public static Easing Parse(string e) { + if (e.Contains(',')) + { + return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); + } + if (_easingTypes == null) { _easingTypes = new Dictionary(); diff --git a/src/Avalonia.Animation/Easing/SplineEasing.cs b/src/Avalonia.Animation/Easing/SplineEasing.cs new file mode 100644 index 0000000000..975fcc4746 --- /dev/null +++ b/src/Avalonia.Animation/Easing/SplineEasing.cs @@ -0,0 +1,85 @@ +namespace Avalonia.Animation.Easings +{ + /// + /// Eases a value + /// using a user-defined cubic bezier curve. + /// Good for custom easing functions that doesn't quite + /// fit with the built-in ones. + /// + public class SplineEasing : Easing + { + /// + /// X coordinate of the first control point + /// + public double X1 + { + get => _internalKeySpline.ControlPointX1; + set + { + _internalKeySpline.ControlPointX1 = value; + } + } + + /// + /// Y coordinate of the first control point + /// + public double Y1 + { + get => _internalKeySpline.ControlPointY1; + set + { + _internalKeySpline.ControlPointY1 = value; + } + } + + /// + /// X coordinate of the second control point + /// + public double X2 + { + get => _internalKeySpline.ControlPointX2; + set + { + _internalKeySpline.ControlPointX2 = value; + } + } + + /// + /// Y coordinate of the second control point + /// + public double Y2 + { + get => _internalKeySpline.ControlPointY2; + set + { + _internalKeySpline.ControlPointY2 = value; + } + } + + private readonly KeySpline _internalKeySpline; + + public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d) + { + _internalKeySpline = new KeySpline(); + + this.X1 = x1; + this.Y1 = y1; + this.X2 = x2; + this.Y1 = y2; + } + + public SplineEasing(KeySpline keySpline) + { + _internalKeySpline = keySpline; + } + + public SplineEasing() + { + _internalKeySpline = new KeySpline(); + } + + /// + public override double Ease(double progress) => + _internalKeySpline.GetSplineProgress(progress); + } +} diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index 5a4f7a15a3..a6e9769186 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -81,7 +81,10 @@ namespace Avalonia.Animation /// A with the appropriate values set public static KeySpline Parse(string value, CultureInfo culture) { - using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) + if (culture is null) + culture = CultureInfo.InvariantCulture; + + using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".")) { return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } @@ -98,6 +101,7 @@ namespace Avalonia.Animation if (IsValidXValue(value)) { _controlPointX1 = value; + _isDirty = true; } else { @@ -112,7 +116,11 @@ namespace Avalonia.Animation public double ControlPointY1 { get => _controlPointY1; - set => _controlPointY1 = value; + set + { + _controlPointY1 = value; + _isDirty = true; + } } /// @@ -126,6 +134,7 @@ namespace Avalonia.Animation if (IsValidXValue(value)) { _controlPointX2 = value; + _isDirty = true; } else { @@ -140,7 +149,11 @@ namespace Avalonia.Animation public double ControlPointY2 { get => _controlPointY2; - set => _controlPointY2 = value; + set + { + _controlPointY2 = value; + _isDirty = true; + } } /// @@ -330,20 +343,4 @@ namespace Avalonia.Animation } } } - - /// - /// Converts string values to values - /// - public class KeySplineTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return KeySpline.Parse((string)value, culture); - } - } } diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs new file mode 100644 index 0000000000..cd7427a37d --- /dev/null +++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +// Ported from WPF open-source code. +// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs + +namespace Avalonia.Animation +{ + /// + /// Converts string values to values + /// + public class KeySplineTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return KeySpline.Parse((string)value, culture); + } + } +} diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index daa7191cc5..09480f2701 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -159,8 +159,6 @@ namespace Avalonia /// internal int Id { get; } - internal bool HasChangedSubscriptions => _changed?.HasObservers ?? false; - /// /// Provides access to a property's binding via the /// indexer. @@ -512,7 +510,7 @@ namespace Avalonia /// /// An if setting the property can be undone, otherwise null. /// - internal abstract IDisposable? RouteSetValue( + internal abstract IDisposable RouteSetValue( IAvaloniaObject o, object value, BindingPriority priority); diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 4a3b104f2a..4cde965400 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -362,7 +362,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void Register(Type type, AvaloniaProperty property) @@ -413,7 +413,7 @@ namespace Avalonia /// The property. /// /// You won't usually want to call this method directly, instead use the - /// + /// /// method. /// public void RegisterAttached(Type type, AvaloniaProperty property) diff --git a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs index d915887e4c..b52829a60f 100644 --- a/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs +++ b/src/Avalonia.Base/Collections/AvaloniaListExtensions.cs @@ -140,6 +140,7 @@ namespace Avalonia.Collections } } + [Obsolete("Causes memory leaks. Use DynamicData or similar instead.")] public static IAvaloniaReadOnlyList CreateDerivedList( this IAvaloniaReadOnlyList collection, Func select) diff --git a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs index 9bc3609dc5..7a233a62ab 100644 --- a/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/IReadOnlyPooledList.cs @@ -13,9 +13,11 @@ namespace Avalonia.Collections.Pooled public interface IReadOnlyPooledList : IReadOnlyList { +#pragma warning disable CS0419 /// /// Gets a for the items currently in the collection. /// +#pragma warning restore CS0419 ReadOnlySpan Span { get; } } } diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index f0d6b292cc..e50e100d32 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -138,7 +138,6 @@ namespace Avalonia.Collections.Pooled /// initially empty, but will have room for the given number of elements /// before any reallocations are required. /// - /// If true, Count of list equals capacity. Depending on ClearMode, rented items may or may not hold dirty values. public PooledList(int capacity, ClearMode clearMode, ArrayPool customPool, bool sizeToCapacity) { if (capacity < 0) @@ -499,11 +498,13 @@ namespace Avalonia.Collections.Pooled public void AddRange(T[] array) => AddRange(array.AsSpan()); +#pragma warning disable CS0419 /// /// Adds the elements of the given to the end of this list. If /// required, the capacity of the list is increased to twice the previous /// capacity or the new size, whichever is larger. /// +#pragma warning restore CS0419 public void AddRange(ReadOnlySpan span) { var newSpan = InsertSpan(_size, span.Length, false); diff --git a/src/Avalonia.Base/Data/BindingValue.cs b/src/Avalonia.Base/Data/BindingValue.cs index 9aac1bacba..6e3c9ae67b 100644 --- a/src/Avalonia.Base/Data/BindingValue.cs +++ b/src/Avalonia.Base/Data/BindingValue.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Utilities; #nullable enable @@ -81,14 +82,14 @@ namespace Avalonia.Data /// public readonly struct BindingValue { - private readonly T _value; + [AllowNull] private readonly T _value; /// /// Initializes a new instance of the struct with a type of /// /// /// The value. - public BindingValue(T value) + public BindingValue([AllowNull] T value) { ValidateValue(value); _value = value; @@ -96,7 +97,7 @@ namespace Avalonia.Data Error = null; } - private BindingValue(BindingValueType type, T value, Exception? error) + private BindingValue(BindingValueType type, [AllowNull] T value, Exception? error) { _value = value; Type = type; @@ -154,7 +155,7 @@ namespace Avalonia.Data BindingValueType.UnsetValue => AvaloniaProperty.UnsetValue, BindingValueType.DoNothing => BindingOperations.DoNothing, BindingValueType.Value => _value, - BindingValueType.BindingError => + BindingValueType.BindingError => new BindingNotification(Error, BindingErrorType.Error), BindingValueType.BindingErrorWithFallback => new BindingNotification(Error, BindingErrorType.Error, Value), @@ -175,7 +176,7 @@ namespace Avalonia.Data /// The binding type is or /// . /// - public BindingValue WithValue(T value) + public BindingValue WithValue([AllowNull] T value) { if (Type == BindingValueType.DoNothing) { @@ -190,6 +191,7 @@ namespace Avalonia.Data /// Gets the value of the binding value if present, otherwise the default value. /// /// The value. + [return: MaybeNull] public T GetValueOrDefault() => HasValue ? _value : default; /// @@ -206,6 +208,7 @@ namespace Avalonia.Data /// The value if present and of the correct type, `default(TResult)` if the value is /// not present or of an incorrect type. /// + [return: MaybeNull] public TResult GetValueOrDefault() { return HasValue ? @@ -222,7 +225,8 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue) + [return: MaybeNull] + public TResult GetValueOrDefault([AllowNull] TResult defaultValue) { return HasValue ? _value is TResult result ? result : default @@ -242,7 +246,7 @@ namespace Avalonia.Data UnsetValueType _ => Unset, DoNothingType _ => DoNothing, BindingNotification n => n.ToBindingValue().Cast(), - _ => (T)value + _ => new BindingValue((T)value) }; } @@ -250,7 +254,7 @@ namespace Avalonia.Data /// Creates a binding value from an instance of the underlying value type. /// /// The value. - public static implicit operator BindingValue(T value) => new BindingValue(value); + public static implicit operator BindingValue([AllowNull] T value) => new BindingValue(value); /// /// Creates a binding value from an . @@ -278,7 +282,7 @@ namespace Avalonia.Data /// The binding error. public static BindingValue BindingError(Exception e) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.BindingError, default, e); } @@ -290,7 +294,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue BindingError(Exception e, T fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.BindingErrorWithFallback, fallbackValue, e); } @@ -303,7 +307,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue BindingError(Exception e, Optional fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? @@ -319,7 +323,7 @@ namespace Avalonia.Data /// The data validation error. public static BindingValue DataValidationError(Exception e) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.DataValidationError, default, e); } @@ -331,7 +335,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue DataValidationError(Exception e, T fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue(BindingValueType.DataValidationErrorWithFallback, fallbackValue, e); } @@ -344,7 +348,7 @@ namespace Avalonia.Data /// The fallback value. public static BindingValue DataValidationError(Exception e, Optional fallbackValue) { - e = e ?? throw new ArgumentNullException("e"); + e = e ?? throw new ArgumentNullException(nameof(e)); return new BindingValue( fallbackValue.HasValue ? @@ -354,7 +358,7 @@ namespace Avalonia.Data e); } - private static void ValidateValue(T value) + private static void ValidateValue([AllowNull] T value) { if (value is UnsetValueType) { diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 7c402f42f6..6d09befeba 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -12,8 +12,19 @@ namespace Avalonia.Data.Core base.NextValueChanged(Negate(value)); } - private static object Negate(object v) + private static object Negate(object value) { + var notification = value as BindingNotification; + var v = BindingNotification.ExtractValue(value); + + BindingNotification GenerateError(Exception e) + { + notification ??= new BindingNotification(AvaloniaProperty.UnsetValue); + notification.AddError(e, BindingErrorType.Error); + notification.ClearValue(); + return notification; + } + if (v != AvaloniaProperty.UnsetValue) { var s = v as string; @@ -28,9 +39,7 @@ namespace Avalonia.Data.Core } else { - return new BindingNotification( - new InvalidCastException($"Unable to convert '{s}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{s}' to bool.")); } } else @@ -38,24 +47,31 @@ namespace Avalonia.Data.Core try { var boolean = Convert.ToBoolean(v, CultureInfo.InvariantCulture); - return !boolean; + + if (notification is object) + { + notification.SetValue(!boolean); + return notification; + } + else + { + return !boolean; + } } catch (InvalidCastException) { // The error message here is "Unable to cast object of type 'System.Object' // to type 'System.IConvertible'" which is kinda useless so provide our own. - return new BindingNotification( - new InvalidCastException($"Unable to convert '{v}' to bool."), - BindingErrorType.Error); + return GenerateError(new InvalidCastException($"Unable to convert '{v}' to bool.")); } catch (Exception e) { - return new BindingNotification(e, BindingErrorType.Error); + return GenerateError(e); } } } - return AvaloniaProperty.UnsetValue; + return notification ?? AvaloniaProperty.UnsetValue; } public object Transform(object value) diff --git a/src/Avalonia.Base/Data/Optional.cs b/src/Avalonia.Base/Data/Optional.cs index dd952c895c..8e044d7896 100644 --- a/src/Avalonia.Base/Data/Optional.cs +++ b/src/Avalonia.Base/Data/Optional.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; #nullable enable @@ -22,13 +23,13 @@ namespace Avalonia.Data /// public readonly struct Optional : IEquatable> { - private readonly T _value; + [AllowNull] private readonly T _value; /// /// Initializes a new instance of the struct with value. /// /// The value. - public Optional(T value) + public Optional([AllowNull] T value) { _value = value; HasValue = true; @@ -48,7 +49,7 @@ namespace Avalonia.Data public T Value => HasValue ? _value : throw new InvalidOperationException("Optional has no value."); /// - public override bool Equals(object obj) => obj is Optional o && this == o; + public override bool Equals(object? obj) => obj is Optional o && this == o; /// public bool Equals(Optional other) => this == other; @@ -69,6 +70,7 @@ namespace Avalonia.Data /// Gets the value if present, otherwise the default value. /// /// The value. + [return: MaybeNull] public T GetValueOrDefault() => HasValue ? _value : default; /// @@ -85,6 +87,7 @@ namespace Avalonia.Data /// The value if present and of the correct type, `default(TResult)` if the value is /// not present or of an incorrect type. /// + [return: MaybeNull] public TResult GetValueOrDefault() { return HasValue ? @@ -101,7 +104,8 @@ namespace Avalonia.Data /// present but not of the correct type or null, or if the /// value is not present. /// - public TResult GetValueOrDefault(TResult defaultValue) + [return: MaybeNull] + public TResult GetValueOrDefault([AllowNull] TResult defaultValue) { return HasValue ? _value is TResult result ? result : default @@ -112,7 +116,7 @@ namespace Avalonia.Data /// Creates an from an instance of the underlying value type. /// /// The value. - public static implicit operator Optional(T value) => new Optional(value); + public static implicit operator Optional([AllowNull] T value) => new Optional(value); /// /// Compares two s for inequality. @@ -128,7 +132,7 @@ namespace Avalonia.Data /// The first value. /// The second value. /// True if the values are equal; otherwise false. - public static bool operator==(Optional x, Optional y) + public static bool operator ==(Optional x, Optional y) { if (!x.HasValue && !y.HasValue) { diff --git a/src/Avalonia.Base/DirectPropertyBase.cs b/src/Avalonia.Base/DirectPropertyBase.cs index 0e65379abd..d42c030245 100644 --- a/src/Avalonia.Base/DirectPropertyBase.cs +++ b/src/Avalonia.Base/DirectPropertyBase.cs @@ -120,7 +120,7 @@ namespace Avalonia return o.GetValue(this); } - internal override object RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) + internal override object? RouteGetBaseValue(IAvaloniaObject o, BindingPriority maxPriority) { return o.GetValue(this); } diff --git a/src/Avalonia.Base/IStyledPropertyMetadata.cs b/src/Avalonia.Base/IStyledPropertyMetadata.cs index f567cd930c..a68b65e5e0 100644 --- a/src/Avalonia.Base/IStyledPropertyMetadata.cs +++ b/src/Avalonia.Base/IStyledPropertyMetadata.cs @@ -1,5 +1,3 @@ -using System; - namespace Avalonia { /// diff --git a/src/Avalonia.Base/Metadata/NullableAttributes.cs b/src/Avalonia.Base/Metadata/NullableAttributes.cs new file mode 100644 index 0000000000..91f5e81863 --- /dev/null +++ b/src/Avalonia.Base/Metadata/NullableAttributes.cs @@ -0,0 +1,140 @@ +#pragma warning disable MA0048 // File name must match type name +#define INTERNAL_NULLABLE_ATTRIBUTES +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +// https://github.com/dotnet/corefx/blob/48363ac826ccf66fbe31a5dcb1dc2aab9a7dd768/src/Common/src/CoreLib/System/Diagnostics/CodeAnalysis/NullableAttributes.cs + +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace System.Diagnostics.CodeAnalysis +{ + /// Specifies that null is allowed as an input even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class AllowNullAttribute : Attribute + { } + + /// Specifies that null is disallowed as an input even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DisallowNullAttribute : Attribute + { } + + /// Specifies that an output may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullAttribute : Attribute + { } + + /// Specifies that an output will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullAttribute : Attribute + { } + + /// Specifies that when a method returns , the parameter may be null even if the corresponding type disallows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class MaybeNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that when a method returns , the parameter will not be null even if the corresponding type allows it. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullWhenAttribute : Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + + /// Specifies that the output will be non-null if the named parameter is non-null. + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class NotNullIfNotNullAttribute : Attribute + { + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } + } + + /// Applied to a method that will never return under any circumstance. + [AttributeUsage(AttributeTargets.Method, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnAttribute : Attribute + { } + + /// Specifies that the method will not return if the associated Boolean parameter is passed the specified value. + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +#if INTERNAL_NULLABLE_ATTRIBUTES + internal +#else + public +#endif + sealed class DoesNotReturnIfAttribute : Attribute + { + /// Initializes the attribute with the specified parameter value. + /// + /// The condition parameter value. Code after the method will be considered unreachable by diagnostics if the argument to + /// the associated parameter matches this value. + /// + public DoesNotReturnIfAttribute(bool parameterValue) => ParameterValue = parameterValue; + + /// Gets the condition parameter value. + public bool ParameterValue { get; } + } +} +#endif diff --git a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs index 6ed6c2ef52..4d82381323 100644 --- a/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/IPriorityValueEntry.cs @@ -1,7 +1,4 @@ -using System; -using Avalonia.Data; - -#nullable enable +#nullable enable namespace Avalonia.PropertyStore { @@ -10,8 +7,6 @@ namespace Avalonia.PropertyStore /// internal interface IPriorityValueEntry : IValue { - BindingPriority Priority { get; } - void Reparent(IValueSink sink); } diff --git a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs index 59c017bc09..859e9ba81c 100644 --- a/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs +++ b/src/Avalonia.Base/PropertyStore/LocalValueEntry.cs @@ -1,4 +1,5 @@ -using Avalonia.Data; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Data; #nullable enable @@ -11,9 +12,9 @@ namespace Avalonia.PropertyStore /// The property type. internal class LocalValueEntry : IValue { - private T _value; + [AllowNull] private T _value; - public LocalValueEntry(T value) => _value = value; + public LocalValueEntry([AllowNull] T value) => _value = value; public BindingPriority Priority => BindingPriority.LocalValue; Optional IValue.GetValue() => new Optional(_value); diff --git a/src/Avalonia.Base/StyledPropertyMetadata`1.cs b/src/Avalonia.Base/StyledPropertyMetadata`1.cs index 300548db0a..cf0a0c34ec 100644 --- a/src/Avalonia.Base/StyledPropertyMetadata`1.cs +++ b/src/Avalonia.Base/StyledPropertyMetadata`1.cs @@ -1,5 +1,4 @@ using System; -using System.Diagnostics; using Avalonia.Data; namespace Avalonia @@ -35,7 +34,7 @@ namespace Avalonia /// /// Gets the value coercion callback, if any. /// - public Func? CoerceValue { get; private set; } + public Func CoerceValue { get; private set; } object IStyledPropertyMetadata.DefaultValue => DefaultValue; diff --git a/src/Avalonia.Base/Utilities/StyleClassParser.cs b/src/Avalonia.Base/Utilities/StyleClassParser.cs new file mode 100644 index 0000000000..2db58f73d9 --- /dev/null +++ b/src/Avalonia.Base/Utilities/StyleClassParser.cs @@ -0,0 +1,45 @@ +using System; +using System.Globalization; + +namespace Avalonia.Utilities +{ +#if !BUILDTASK + public +#endif + static class StyleClassParser + { + public static ReadOnlySpan ParseStyleClass(this ref CharacterReader r) + { + if (IsValidIdentifierStart(r.Peek)) + { + return r.TakeWhile(c => IsValidIdentifierChar(c)); + } + else + { + return ReadOnlySpan.Empty; + } + } + + private static bool IsValidIdentifierStart(char c) + { + return char.IsLetter(c) || c == '_'; + } + + private static bool IsValidIdentifierChar(char c) + { + if (IsValidIdentifierStart(c) || c == '-') + { + return true; + } + else + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark || + cat == UnicodeCategory.ConnectorPunctuation || + cat == UnicodeCategory.Format || + cat == UnicodeCategory.DecimalDigitNumber; + } + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 05e66f2e0a..6b89fcbdb9 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -162,7 +162,7 @@ namespace Avalonia _sink.ValueChanged(new AvaloniaPropertyChangedEventArgs( _owner, property, - old, + new Optional(old), default, BindingPriority.Unset)); } diff --git a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj index e200b48166..449c1b483a 100644 --- a/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj +++ b/src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj @@ -42,7 +42,10 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + diff --git a/src/Avalonia.Controls/Chrome/CaptionButtons.cs b/src/Avalonia.Controls/Chrome/CaptionButtons.cs new file mode 100644 index 0000000000..75d6c366b8 --- /dev/null +++ b/src/Avalonia.Controls/Chrome/CaptionButtons.cs @@ -0,0 +1,86 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Controls.Chrome +{ + /// + /// Draws window minimize / maximize / close buttons in a when managed client decorations are enabled. + /// + public class CaptionButtons : TemplatedControl + { + private CompositeDisposable? _disposables; + private Window? _hostWindow; + + public void Attach(Window hostWindow) + { + if (_disposables == null) + { + _hostWindow = hostWindow; + + _disposables = new CompositeDisposable + { + _hostWindow.GetObservable(Window.WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":minimized", x == WindowState.Minimized); + PseudoClasses.Set(":normal", x == WindowState.Normal); + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }) + }; + } + } + + public void Detach() + { + if (_disposables != null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer?.Children.Remove(this); + + _disposables.Dispose(); + _disposables = null; + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + var closeButton = e.NameScope.Get("PART_CloseButton"); + var restoreButton = e.NameScope.Get("PART_RestoreButton"); + var minimiseButton = e.NameScope.Get("PART_MinimiseButton"); + var fullScreenButton = e.NameScope.Get("PART_FullScreenButton"); + + closeButton.PointerReleased += (sender, e) => _hostWindow?.Close(); + + restoreButton.PointerReleased += (sender, e) => + { + if (_hostWindow != null) + { + _hostWindow.WindowState = _hostWindow.WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; + } + }; + + minimiseButton.PointerReleased += (sender, e) => + { + if (_hostWindow != null) + { + _hostWindow.WindowState = WindowState.Minimized; + } + }; + + fullScreenButton.PointerReleased += (sender, e) => + { + if (_hostWindow != null) + { + _hostWindow.WindowState = _hostWindow.WindowState == WindowState.FullScreen ? WindowState.Normal : WindowState.FullScreen; + } + }; + } + } +} diff --git a/src/Avalonia.Controls/Chrome/TitleBar.cs b/src/Avalonia.Controls/Chrome/TitleBar.cs new file mode 100644 index 0000000000..78b49d2a03 --- /dev/null +++ b/src/Avalonia.Controls/Chrome/TitleBar.cs @@ -0,0 +1,117 @@ +using System; +using System.Reactive.Disposables; +using Avalonia.Controls.Primitives; + +#nullable enable + +namespace Avalonia.Controls.Chrome +{ + /// + /// Draws a titlebar when managed client decorations are enabled. + /// + public class TitleBar : TemplatedControl + { + private CompositeDisposable? _disposables; + private readonly Window? _hostWindow; + private CaptionButtons? _captionButtons; + + public TitleBar(Window hostWindow) + { + _hostWindow = hostWindow; + } + + public TitleBar() + { + + } + + public void Attach() + { + if (_disposables == null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer?.Children.Add(this); + + if (_hostWindow != null) + { + _disposables = new CompositeDisposable + { + _hostWindow.GetObservable(Window.WindowDecorationMarginProperty) + .Subscribe(x => UpdateSize()), + + _hostWindow.GetObservable(Window.ExtendClientAreaTitleBarHeightHintProperty) + .Subscribe(x => UpdateSize()), + + _hostWindow.GetObservable(Window.OffScreenMarginProperty) + .Subscribe(x => UpdateSize()), + + _hostWindow.GetObservable(Window.WindowStateProperty) + .Subscribe(x => + { + PseudoClasses.Set(":minimized", x == WindowState.Minimized); + PseudoClasses.Set(":normal", x == WindowState.Normal); + PseudoClasses.Set(":maximized", x == WindowState.Maximized); + PseudoClasses.Set(":fullscreen", x == WindowState.FullScreen); + }) + }; + + _captionButtons?.Attach(_hostWindow); + } + + UpdateSize(); + } + } + + private void UpdateSize() + { + if (_hostWindow != null) + { + Margin = new Thickness( + _hostWindow.OffScreenMargin.Left, + _hostWindow.OffScreenMargin.Top, + _hostWindow.OffScreenMargin.Right, + _hostWindow.OffScreenMargin.Bottom); + + if (_hostWindow.WindowState != WindowState.FullScreen) + { + Height = _hostWindow.WindowDecorationMargin.Top; + + if (_captionButtons != null) + { + _captionButtons.Height = Height; + } + } + } + } + + public void Detach() + { + if (_disposables != null) + { + var layer = ChromeOverlayLayer.GetOverlayLayer(_hostWindow); + + layer?.Children.Remove(this); + + _disposables.Dispose(); + _disposables = null; + + _captionButtons?.Detach(); + } + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + _captionButtons = e.NameScope.Get("PART_CaptionButtons"); + + if (_hostWindow != null) + { + _captionButtons.Attach(_hostWindow); + } + + UpdateSize(); + } + } +} diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs new file mode 100644 index 0000000000..5d3311e8c6 --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -0,0 +1,412 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Interactivity; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Avalonia.Controls +{ + /// + /// A control to allow the user to select a date + /// + public class DatePicker : TemplatedControl + { + /// + /// Define the Property + /// + public static readonly DirectProperty DayFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(DayFormat), + x => x.DayFormat, (x, v) => x.DayFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty DayVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(DayVisible), + x => x.DayVisible, (x, v) => x.DayVisible = v); + + /// + /// Defines the Property + /// + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); + + /// + /// Defines the Property + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MaxYearProperty = + AvaloniaProperty.RegisterDirect(nameof(MaxYear), + x => x.MaxYear, (x, v) => x.MaxYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MinYearProperty = + AvaloniaProperty.RegisterDirect(nameof(MinYear), + x => x.MinYear, (x, v) => x.MinYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(MonthFormat), + x => x.MonthFormat, (x, v) => x.MonthFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(MonthVisible), + x => x.MonthVisible, (x, v) => x.MonthVisible = v); + + /// + /// Defiens the Property + /// + public static readonly DirectProperty YearFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(YearFormat), + x => x.YearFormat, (x, v) => x.YearFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty YearVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(YearVisible), + x => x.YearVisible, (x, v) => x.YearVisible = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty SelectedDateProperty = + AvaloniaProperty.RegisterDirect(nameof(SelectedDate), + x => x.SelectedDate, (x, v) => x.SelectedDate = v); + + //Template Items + private Button _flyoutButton; + private TextBlock _dayText; + private TextBlock _monthText; + private TextBlock _yearText; + private Grid _container; + private Rectangle _spacer1; + private Rectangle _spacer2; + private Popup _popup; + private DatePickerPresenter _presenter; + + private bool _areControlsAvailable; + + private string _dayFormat = "%d"; + private bool _dayVisible = true; + private DateTimeOffset _maxYear; + private DateTimeOffset _minYear; + private string _monthFormat = "MMMM"; + private bool _monthVisible = true; + private string _yearFormat = "yyyy"; + private bool _yearVisible = true; + private DateTimeOffset? _selectedDate; + + public DatePicker() + { + PseudoClasses.Set(":hasnodate", true); + var now = DateTimeOffset.Now; + _minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset); + _maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset); + } + + public string DayFormat + { + get => _dayFormat; + set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + } + + /// + /// Gets or sets whether the day is visible + /// + public bool DayVisible + { + get => _dayVisible; + set + { + SetAndRaise(DayVisibleProperty, ref _dayVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the DatePicker header + /// + public object Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + /// + /// Gets or sets the header template + /// + public IDataTemplate HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + + /// + /// Gets or sets the maximum year for the picker + /// + public DateTimeOffset MaxYear + { + get => _maxYear; + set + { + if (value < MinYear) + throw new InvalidOperationException("MaxDate cannot be less than MinDate"); + SetAndRaise(MaxYearProperty, ref _maxYear, value); + + if (SelectedDate.HasValue && SelectedDate.Value > value) + SelectedDate = value; + } + } + + /// + /// Gets or sets the minimum year for the picker + /// + public DateTimeOffset MinYear + { + get => _minYear; + set + { + if (value > MaxYear) + throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); + SetAndRaise(MinYearProperty, ref _minYear, value); + + if (SelectedDate.HasValue && SelectedDate.Value < value) + SelectedDate = value; + } + } + + /// + /// Gets or sets the month format + /// + public string MonthFormat + { + get => _monthFormat; + set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + } + + /// + /// Gets or sets whether the month is visible + /// + public bool MonthVisible + { + get => _monthVisible; + set + { + SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the year format + /// + public string YearFormat + { + get => _yearFormat; + set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + } + + /// + /// Gets or sets whether the year is visible + /// + public bool YearVisible + { + get => _yearVisible; + set + { + SetAndRaise(YearVisibleProperty, ref _yearVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the Selected Date for the picker, can be null + /// + public DateTimeOffset? SelectedDate + { + get => _selectedDate; + set + { + var old = _selectedDate; + SetAndRaise(SelectedDateProperty, ref _selectedDate, value); + SetSelectedDateText(); + OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value)); + } + } + + /// + /// Raised when the changes + /// + public event EventHandler SelectedDateChanged; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + _areControlsAvailable = false; + if (_flyoutButton != null) + _flyoutButton.Click -= OnFlyoutButtonClicked; + if (_presenter != null) + { + _presenter.Confirmed -= OnConfirmed; + _presenter.Dismissed -= OnDismissPicker; + } + + base.OnApplyTemplate(e); + _flyoutButton = e.NameScope.Find