diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index bbfc28aa9f..f8fbf64e83 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -279,8 +279,9 @@ partial class Build : NukeBuild if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, new NumergeNukeLogger())) throw new Exception("Package merge failed"); - RefAssemblyGenerator.GenerateRefAsmsInPackage(Parameters.NugetRoot / "Avalonia." + - Parameters.Version + ".nupkg"); + RefAssemblyGenerator.GenerateRefAsmsInPackage( + Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.nupkg", + Parameters.NugetRoot / $"Avalonia.{Parameters.Version}.snupkg"); }); Target ValidateApiDiff => _ => _ diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index 54e428c442..e93070e2f0 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -1,8 +1,10 @@ +#nullable enable + +using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; -using ILRepacking; using Mono.Cecil; using Mono.Cecil.Cil; @@ -10,8 +12,8 @@ public class RefAssemblyGenerator { class Resolver : DefaultAssemblyResolver, IAssemblyResolver { - private readonly string _dir; - Dictionary _cache = new(); + readonly string _dir; + readonly Dictionary _cache = new(); public Resolver(string dir) { @@ -31,17 +33,17 @@ public class RefAssemblyGenerator public static void PatchRefAssembly(string file) { - var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk"); + var reader = typeof(RefAssemblyGenerator).Assembly.GetManifestResourceStream("avalonia.snk")!; var snk = new byte[reader.Length]; - reader.Read(snk, 0, snk.Length); + reader.ReadExactly(snk, 0, snk.Length); var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters { ReadWrite = true, InMemory = true, ReadSymbols = true, - SymbolReaderProvider = new DefaultSymbolReaderProvider(false), - AssemblyResolver = new Resolver(Path.GetDirectoryName(file)) + SymbolReaderProvider = new DefaultSymbolReaderProvider(throwIfNoSymbol: true), + AssemblyResolver = new Resolver(Path.GetDirectoryName(file)!) }); var obsoleteAttribute = def.MainModule.ImportReference(new TypeReference("System", "ObsoleteAttribute", def.MainModule, @@ -58,7 +60,7 @@ public class RefAssemblyGenerator { StrongNameKeyBlob = snk, WriteSymbols = def.MainModule.HasSymbols, - SymbolWriterProvider = new EmbeddedPortablePdbWriterProvider(), + SymbolWriterProvider = new PortablePdbWriterProvider(), DeterministicMvid = def.MainModule.HasSymbols }); } @@ -146,7 +148,7 @@ public class RefAssemblyGenerator m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; } - static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute) + static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute? unstableAttribute) { if (def.CustomAttributes.Any(a => a.AttributeType.FullName == "System.ObsoleteAttribute")) return; @@ -172,43 +174,66 @@ public class RefAssemblyGenerator }); } - public static void GenerateRefAsmsInPackage(string packagePath) + public static void GenerateRefAsmsInPackage(string mainPackagePath, string symbolsPackagePath) { - using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), - ZipArchiveMode.Update)) + using var mainArchive = OpenPackage(mainPackagePath); + using var symbolsArchive = OpenPackage(symbolsPackagePath); + + foreach (var entry in mainArchive.Entries + .Where(e => e.FullName.StartsWith("ref/", StringComparison.Ordinal)) + .ToArray()) { - foreach (var entry in archive.Entries.ToList()) - { - if (entry.FullName.StartsWith("ref/")) - entry.Delete(); - } - - foreach (var entry in archive.Entries.ToList()) + entry.Delete(); + } + + foreach (var libEntry in GetLibEntries(mainArchive, ".xml")) + { + var refEntry = mainArchive.CreateEntry("ref/" + libEntry.FullName.Substring(4), CompressionLevel.Optimal); + using var src = libEntry.Open(); + using var dst = refEntry.Open(); + src.CopyTo(dst); + } + + var pdbEntries = GetLibEntries(symbolsArchive, ".pdb").ToDictionary(e => e.FullName); + + var libs = GetLibEntries(mainArchive, ".dll") + .Select(e => (NameParts: e.FullName.Split('/'), Entry: e)) + .Select(e => ( + Tfm: e.NameParts[1], + DllName: e.NameParts[2], + DllEntry: e.Entry, + PdbName: Path.ChangeExtension(e.NameParts[2], ".pdb"), + PdbEntry: pdbEntries.TryGetValue(Path.ChangeExtension(e.Entry.FullName, ".pdb"), out var pdbEntry) ? + pdbEntry : + throw new InvalidOperationException($"Missing symbols for {e.Entry.FullName}"))) + .GroupBy(e => e.Tfm); + + foreach (var tfm in libs) + { + using var _ = Helpers.UseTempDir(out var temp); + + foreach (var lib in tfm) { - if (entry.FullName.StartsWith("lib/") && entry.Name.EndsWith(".xml")) - { - var newEntry = archive.CreateEntry("ref/" + entry.FullName.Substring(4), - CompressionLevel.Optimal); - using (var src = entry.Open()) - using (var dst = newEntry.Open()) - src.CopyTo(dst); - } - } + var extractedDllPath = Path.Combine(temp, lib.DllName); + var extractedPdbPath = Path.Combine(temp, lib.PdbName); + + lib.DllEntry.ExtractToFile(extractedDllPath); + lib.PdbEntry.ExtractToFile(extractedPdbPath); - var libs = archive.Entries.Where(e => e.FullName.StartsWith("lib/") && e.FullName.EndsWith(".dll")) - .Select((e => new { s = e.FullName.Split('/'), e = e })) - .Select(e => new { Tfm = e.s[1], Name = e.s[2], Entry = e.e }) - .GroupBy(x => x.Tfm); - foreach(var tfm in libs) - using (Helpers.UseTempDir(out var temp)) - { - foreach (var l in tfm) - l.Entry.ExtractToFile(Path.Combine(temp, l.Name)); - foreach (var l in tfm) - PatchRefAssembly(Path.Combine(temp, l.Name)); - foreach (var l in tfm) - archive.CreateEntryFromFile(Path.Combine(temp, l.Name), $"ref/{l.Tfm}/{l.Name}"); - } + PatchRefAssembly(extractedDllPath); + + mainArchive.CreateEntryFromFile(extractedDllPath, $"ref/{lib.Tfm}/{lib.DllName}"); + symbolsArchive.CreateEntryFromFile(extractedPdbPath, $"ref/{lib.Tfm}/{lib.PdbName}"); + } } + + static ZipArchive OpenPackage(string packagePath) + => new(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), ZipArchiveMode.Update); + + static ZipArchiveEntry[] GetLibEntries(ZipArchive archive, string extension) + => archive.Entries + .Where(e => e.FullName.StartsWith("lib/", StringComparison.Ordinal) + && e.FullName.EndsWith(extension, StringComparison.Ordinal)) + .ToArray(); } } diff --git a/src/Android/Avalonia.Android/AndroidInputMethod.cs b/src/Android/Avalonia.Android/AndroidInputMethod.cs index 7d5130cf5d..f708d6936c 100644 --- a/src/Android/Avalonia.Android/AndroidInputMethod.cs +++ b/src/Android/Avalonia.Android/AndroidInputMethod.cs @@ -113,6 +113,11 @@ namespace Avalonia.Android private void OnSelectionChanged() { + if (Client is null) + { + return; + } + var selection = Client.Selection; _imm.UpdateSelection(_host, selection.Start, selection.End, selection.Start, selection.End); diff --git a/src/Avalonia.Base/Animation/CrossFade.cs b/src/Avalonia.Base/Animation/CrossFade.cs index 640d6456a3..d598db870f 100644 --- a/src/Avalonia.Base/Animation/CrossFade.cs +++ b/src/Avalonia.Base/Animation/CrossFade.cs @@ -35,6 +35,18 @@ namespace Avalonia.Animation { Children = { + new KeyFrame() + { + Setters = + { + new Setter + { + Property = Visual.OpacityProperty, + Value = 1d + } + }, + Cue = new Cue(0d) + }, new KeyFrame() { Setters = @@ -54,6 +66,18 @@ namespace Avalonia.Animation { Children = { + new KeyFrame() + { + Setters = + { + new Setter + { + Property = Visual.OpacityProperty, + Value = 0d + } + }, + Cue = new Cue(0d) + }, new KeyFrame() { Setters = @@ -117,11 +141,13 @@ namespace Avalonia.Animation if (from != null) { + from.Opacity = 0f; tasks.Add(_fadeOutAnimation.RunAsync(from, null, cancellationToken)); } if (to != null) { + to.Opacity = 1f; to.IsVisible = true; tasks.Add(_fadeInAnimation.RunAsync(to, null, cancellationToken)); } diff --git a/src/Avalonia.Controls/HotkeyManager.cs b/src/Avalonia.Controls/HotkeyManager.cs index de753f0bd0..6ad4a8cc76 100644 --- a/src/Avalonia.Controls/HotkeyManager.cs +++ b/src/Avalonia.Controls/HotkeyManager.cs @@ -149,10 +149,10 @@ namespace Avalonia.Controls return; var control = args.Sender as Control; - if (control is not IClickableControl) + if (control is not IClickableControl and not ICommandSource) { Logging.Logger.TryGet(Logging.LogEventLevel.Warning, Logging.LogArea.Control)?.Log(control, - $"The element {args.Sender.GetType().Name} does not implement IClickableControl and does not support binding a HotKey ({args.NewValue})."); + $"The element {args.Sender.GetType().Name} does not implement IClickableControl nor ICommandSource and does not support binding a HotKey ({args.NewValue})."); return; } diff --git a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 30f9d8f380..84772e7789 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -126,6 +126,12 @@ namespace Avalonia.Controls public static readonly StyledProperty VerticalContentAlignmentProperty = ContentControl.VerticalContentAlignmentProperty.AddOwner(); + /// + /// Defines the property + /// + public static readonly StyledProperty TextAlignmentProperty = + TextBox.TextAlignmentProperty.AddOwner(); + private IDisposable? _textBoxTextChangedSubscription; private bool _internalValueSet; @@ -299,6 +305,15 @@ namespace Avalonia.Controls set => SetValue(VerticalContentAlignmentProperty, value); } + /// + /// Gets or sets the of the + /// + public Media.TextAlignment TextAlignment + { + get => GetValue(TextAlignmentProperty); + set => SetValue(TextAlignmentProperty, value); + } + /// /// Initializes new instance of class. /// diff --git a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs index 9ce6604594..b897d52204 100644 --- a/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs +++ b/src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs @@ -62,9 +62,15 @@ namespace Avalonia.FreeDesktop.DBusIme foreach (var name in _knownNames) { var dbus = new OrgFreedesktopDBus(Connection, "org.freedesktop.DBus", "/org/freedesktop/DBus"); - _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); - var nameOwner = await dbus.GetNameOwnerAsync(name); - OnNameChange(null, (name, null, nameOwner)); + try + { + _disposables.Add(await dbus.WatchNameOwnerChangedAsync(OnNameChange)); + var nameOwner = await dbus.GetNameOwnerAsync(name); + OnNameChange(null, (name, null, nameOwner)); + } + catch (DBusException) + { + } } } diff --git a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml index 1f84ee664c..a470eb1d3b 100644 --- a/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml @@ -57,6 +57,7 @@ VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" Text="{TemplateBinding Text}" + TextAlignment="{TemplateBinding TextAlignment}" AcceptsReturn="False" TextWrapping="NoWrap" /> diff --git a/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml b/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml index 4ce6a20dc6..ff4be12fda 100644 --- a/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Simple/Controls/NumericUpDown.xaml @@ -31,6 +31,7 @@ DataValidationErrors.Errors="{TemplateBinding (DataValidationErrors.Errors)}" IsReadOnly="{TemplateBinding IsReadOnly}" Text="{TemplateBinding Text}" + TextAlignment="{TemplateBinding TextAlignment}" TextWrapping="NoWrap" Watermark="{TemplateBinding Watermark}" /> diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index be2d754819..a2019c276b 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -17,7 +17,6 @@ using Avalonia.OpenGL; using Avalonia.OpenGL.Egl; using Avalonia.Platform; using Avalonia.Platform.Storage; -using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Threading; using Avalonia.X11.Glx; @@ -36,6 +35,7 @@ namespace Avalonia.X11 { private readonly AvaloniaX11Platform _platform; private readonly bool _popup; + private readonly bool _overrideRedirect; private readonly X11Info _x11; private XConfigureEvent? _configure; private PixelPoint? _configurePoint; @@ -72,10 +72,11 @@ namespace Avalonia.X11 WaitPaint } - public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent) + public X11Window(AvaloniaX11Platform platform, IWindowImpl? popupParent, bool overrideRedirect = false) { _platform = platform; _popup = popupParent != null; + _overrideRedirect = _popup || overrideRedirect; _x11 = platform.Info; _mouse = new MouseDevice(); _touch = new TouchDevice(); @@ -92,7 +93,7 @@ namespace Avalonia.X11 | SetWindowValuemask.BackPixmap | SetWindowValuemask.BackingStore | SetWindowValuemask.BitGravity | SetWindowValuemask.WinGravity; - if (_popup) + if (_overrideRedirect) { attr.override_redirect = 1; valueMask |= SetWindowValuemask.OverrideRedirect; @@ -155,7 +156,7 @@ namespace Avalonia.X11 else _renderHandle = _handle; - Handle = new SurfacePlatformHandle(this); + Handle = new PlatformHandle(_handle, "XID"); _realSize = new PixelSize(defaultWidth, defaultHeight); platform.Windows[_handle] = OnEvent; XEventMask ignoredMask = XEventMask.SubstructureRedirectMask @@ -165,15 +166,18 @@ namespace Avalonia.X11 ignoredMask |= platform.XI2.AddWindow(_handle, this); var mask = new IntPtr(0xffffff ^ (int)ignoredMask); XSelectInput(_x11.Display, _handle, mask); - var protocols = new[] + if (!_overrideRedirect) { - _x11.Atoms.WM_DELETE_WINDOW - }; - XSetWMProtocols(_x11.Display, _handle, protocols, protocols.Length); - XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM, - 32, PropertyMode.Replace, new[] {_x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL}, 1); + var protocols = new[] + { + _x11.Atoms.WM_DELETE_WINDOW + }; + XSetWMProtocols(_x11.Display, _handle, protocols, protocols.Length); + XChangeProperty(_x11.Display, _handle, _x11.Atoms._NET_WM_WINDOW_TYPE, _x11.Atoms.XA_ATOM, + 32, PropertyMode.Replace, new[] { _x11.Atoms._NET_WM_WINDOW_TYPE_NORMAL }, 1); - SetWmClass(_platform.Options.WmClass); + SetWmClass(_platform.Options.WmClass); + } var surfaces = new List { @@ -187,7 +191,7 @@ namespace Avalonia.X11 if (glx != null) surfaces.Insert(0, new GlxGlPlatformSurface(new SurfaceInfo(this, _x11.DeferredDisplay, _handle, _renderHandle))); - surfaces.Add(Handle); + surfaces.Add(new SurfacePlatformHandle(this)); Surfaces = surfaces.ToArray(); UpdateMotifHints(); @@ -257,6 +261,8 @@ namespace Avalonia.X11 private void UpdateMotifHints() { + if(_overrideRedirect) + return; var functions = MotifFunctions.Move | MotifFunctions.Close | MotifFunctions.Resize | MotifFunctions.Minimize | MotifFunctions.Maximize; var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border | @@ -286,6 +292,8 @@ namespace Avalonia.X11 private void UpdateSizeHints(PixelSize? preResize) { + if(_overrideRedirect) + return; var min = _minMaxSize.minSize; var max = _minMaxSize.maxSize; @@ -507,7 +515,7 @@ namespace Avalonia.X11 } UpdateImePosition(); - if (changedSize && !updatedSizeViaScaling && !_popup) + if (changedSize && !updatedSizeViaScaling && !_overrideRedirect) Resized?.Invoke(ClientSize, WindowResizeReason.Unspecified); }, DispatcherPriority.AsyncRenderTargetResize); @@ -984,7 +992,7 @@ namespace Avalonia.X11 XConfigureResizeWindow(_x11.Display, _renderHandle, pixelSize); XFlush(_x11.Display); - if (force || !_wasMappedAtLeastOnce || (_popup && needImmediatePopupResize)) + if (force || !_wasMappedAtLeastOnce || (_overrideRedirect && needImmediatePopupResize)) { _realSize = pixelSize; Resized?.Invoke(ClientSize, reason); diff --git a/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs b/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs new file mode 100644 index 0000000000..55a3f0d5d4 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/HotKeyedControlsTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Windows.Input; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.LogicalTree; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + internal class HotKeyedTextBox : TextBox, ICommandSource + { + private class DelegateCommand : ICommand + { + private readonly Action _action; + public DelegateCommand(Action action) => _action = action; + public event EventHandler CanExecuteChanged { add { } remove { } } + public bool CanExecute(object parameter) => true; + public void Execute(object parameter) => _action(); + } + + public static readonly StyledProperty HotKeyProperty = + HotKeyManager.HotKeyProperty.AddOwner(); + + private KeyGesture _hotkey; + + public KeyGesture HotKey + { + get => GetValue(HotKeyProperty); + set => SetValue(HotKeyProperty, value); + } + + protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_hotkey != null) + { + this.SetValue(HotKeyProperty, _hotkey); + } + + base.OnAttachedToLogicalTree(e); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (this.HotKey != null) + { + _hotkey = this.HotKey; + this.SetValue(HotKeyProperty, null); + } + + base.OnDetachedFromLogicalTree(e); + } + + public void CanExecuteChanged(object sender, EventArgs e) + { + } + + protected override Type StyleKeyOverride => typeof(TextBox); + + public ICommand Command => _command; + + public object CommandParameter => null; + + private readonly DelegateCommand _command; + + public HotKeyedTextBox() + { + _command = new DelegateCommand(() => Focus()); + } + } + + public class HotKeyedControlsTests + { + private static Window PreparedWindow(object content = null) + { + var platform = AvaloniaLocator.Current.GetRequiredService(); + var windowImpl = Mock.Get(platform.CreateWindow()); + windowImpl.Setup(x => x.Compositor).Returns(RendererMocks.CreateDummyCompositor()); + var w = new Window(windowImpl.Object) { Content = content }; + w.ApplyTemplate(); + return w; + } + + private static IDisposable CreateServicesWithFocus() + { + return UnitTestApplication.Start( + TestServices.StyledWindow.With( + windowingPlatform: new MockWindowingPlatform( + null, + window => MockWindowingPlatform.CreatePopupMock(window).Object), + focusManager: new FocusManager(), + keyboardDevice: () => new KeyboardDevice())); + } + + [Fact] + public void HotKeyedTextBox_Focus_Performed_On_Hotkey() + { + using var _ = CreateServicesWithFocus(); + + var keyboardDevice = new KeyboardDevice(); + var hotKeyedTextBox = new HotKeyedTextBox { HotKey = new KeyGesture(Key.F, KeyModifiers.Control) }; + var root = PreparedWindow(); + root.Content = hotKeyedTextBox; + root.Show(); + + Assert.False(hotKeyedTextBox.IsFocused); + + keyboardDevice.ProcessRawEvent( + new RawKeyEventArgs( + keyboardDevice, + 0, + root, + RawKeyEventType.KeyDown, + Key.F, + RawInputModifiers.Control)); + + Assert.True(hotKeyedTextBox.IsFocused); + } + } +}