From 2e682e292ef4ad05da16027e32f74e381d2dfdc9 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Apr 2020 14:22:21 -0300 Subject: [PATCH 001/164] initial work towards updating menus with virtual DOM. --- .../AvaloniaNativeMenuExporter.cs | 406 +++++++----------- src/Avalonia.Native/Mappings.xml | 2 + src/Avalonia.Native/OsxUnicodeKeys.cs | 147 +++++++ 3 files changed, 296 insertions(+), 259 deletions(-) create mode 100644 src/Avalonia.Native/OsxUnicodeKeys.cs diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 95c2aabb3d..8dd54f4423 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -1,96 +1,153 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; -using System.Linq; -using System.Text; -using Avalonia.Collections; using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; -using Avalonia.Input; +using Avalonia.Dialogs; using Avalonia.Native.Interop; using Avalonia.Platform.Interop; using Avalonia.Threading; -using Avalonia.Dialogs; -using Avalonia.Controls.ApplicationLifetimes; -namespace Avalonia.Native +namespace Avalonia.Native.Interop { - enum OsxUnicodeSpecialKey + public partial class IAvnAppMenuItem + { + private IAvnAppMenu _subMenu; + + public NativeMenuItemBase Managed { get; set; } + + public void Update(IAvaloniaNativeFactory factory, NativeMenuItem item) + { + using (var buffer = new Utf8Buffer(item.Header)) + { + Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(item.Gesture.Key))) + { + SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + + SetAction(new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + + if (item.Menu != null) + { + if (_subMenu == null) + { + _subMenu = factory.CreateMenu(); + } + + _subMenu.Update(factory, item.Menu); + } + + if (item.Menu == null && _subMenu != null) + { + // todo remove submenu. + + // needs implementing on native side also. + } + } + } + + public partial class IAvnAppMenu { - NSUpArrowFunctionKey = 0xF700, - NSDownArrowFunctionKey = 0xF701, - NSLeftArrowFunctionKey = 0xF702, - NSRightArrowFunctionKey = 0xF703, - NSF1FunctionKey = 0xF704, - NSF2FunctionKey = 0xF705, - NSF3FunctionKey = 0xF706, - NSF4FunctionKey = 0xF707, - NSF5FunctionKey = 0xF708, - NSF6FunctionKey = 0xF709, - NSF7FunctionKey = 0xF70A, - NSF8FunctionKey = 0xF70B, - NSF9FunctionKey = 0xF70C, - NSF10FunctionKey = 0xF70D, - NSF11FunctionKey = 0xF70E, - NSF12FunctionKey = 0xF70F, - NSF13FunctionKey = 0xF710, - NSF14FunctionKey = 0xF711, - NSF15FunctionKey = 0xF712, - NSF16FunctionKey = 0xF713, - NSF17FunctionKey = 0xF714, - NSF18FunctionKey = 0xF715, - NSF19FunctionKey = 0xF716, - NSF20FunctionKey = 0xF717, - NSF21FunctionKey = 0xF718, - NSF22FunctionKey = 0xF719, - NSF23FunctionKey = 0xF71A, - NSF24FunctionKey = 0xF71B, - NSF25FunctionKey = 0xF71C, - NSF26FunctionKey = 0xF71D, - NSF27FunctionKey = 0xF71E, - NSF28FunctionKey = 0xF71F, - NSF29FunctionKey = 0xF720, - NSF30FunctionKey = 0xF721, - NSF31FunctionKey = 0xF722, - NSF32FunctionKey = 0xF723, - NSF33FunctionKey = 0xF724, - NSF34FunctionKey = 0xF725, - NSF35FunctionKey = 0xF726, - NSInsertFunctionKey = 0xF727, - NSDeleteFunctionKey = 0xF728, - NSHomeFunctionKey = 0xF729, - NSBeginFunctionKey = 0xF72A, - NSEndFunctionKey = 0xF72B, - NSPageUpFunctionKey = 0xF72C, - NSPageDownFunctionKey = 0xF72D, - NSPrintScreenFunctionKey = 0xF72E, - NSScrollLockFunctionKey = 0xF72F, - NSPauseFunctionKey = 0xF730, - NSSysReqFunctionKey = 0xF731, - NSBreakFunctionKey = 0xF732, - NSResetFunctionKey = 0xF733, - NSStopFunctionKey = 0xF734, - NSMenuFunctionKey = 0xF735, - NSUserFunctionKey = 0xF736, - NSSystemFunctionKey = 0xF737, - NSPrintFunctionKey = 0xF738, - NSClearLineFunctionKey = 0xF739, - NSClearDisplayFunctionKey = 0xF73A, - NSInsertLineFunctionKey = 0xF73B, - NSDeleteLineFunctionKey = 0xF73C, - NSInsertCharFunctionKey = 0xF73D, - NSDeleteCharFunctionKey = 0xF73E, - NSPrevFunctionKey = 0xF73F, - NSNextFunctionKey = 0xF740, - NSSelectFunctionKey = 0xF741, - NSExecuteFunctionKey = 0xF742, - NSUndoFunctionKey = 0xF743, - NSRedoFunctionKey = 0xF744, - NSFindFunctionKey = 0xF745, - NSHelpFunctionKey = 0xF746, - NSModeSwitchFunctionKey = 0xF747 + private NativeMenu _menu; + private List _menuItems = new List(); + private Dictionary _menuItemLookup = new Dictionary(); + + private void Remove(IAvnAppMenuItem item) + { + _menuItemLookup.Remove(item.Managed); + _menuItems.Remove(item); + + RemoveItem(item); + } + + private void InsertAt(int index, IAvnAppMenuItem item) + { + if (item.Managed == null) + { + throw new InvalidOperationException("Cannot insert item that with Managed link null"); + } + + _menuItemLookup.Add(item.Managed, item); + _menuItems.Insert(index, item); + + AddItem(item); // todo change to insertatimpl + } + + private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) + { + var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem(); + nativeItem.Managed = item; + + return nativeItem; + } + + public void Update(IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") + { + if (_menu == null) + { + _menu = menu; + } + else if (_menu != menu) + { + throw new Exception("Cannot update a menu from another instance"); + } + + if (!string.IsNullOrWhiteSpace(title)) + { + using (var buffer = new Utf8Buffer(title)) + { + Title = buffer.DangerousGetHandle(); + } + } + + for (int i = 0; i < menu.Items.Count; i++) + { + IAvnAppMenuItem nativeItem = null; + if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].Managed) + { + if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) + { + Remove(nativeItem); + InsertAt(i, nativeItem); + } + else + { + nativeItem = CreateNew(factory, menu.Items[i]); + InsertAt(i, nativeItem); + } + } + + if (menu.Items[i] is NativeMenuItem nmi) + { + nativeItem.Update(factory, nmi); + } + } + + for (int i = menu.Items.Count; i < _menuItems.Count; i++) + { + _menuItems.Remove(_menuItems[i]); + } + } } +} +namespace Avalonia.Native +{ public class MenuActionCallback : CallbackBase, IAvnActionCallback { private Action _action; @@ -130,57 +187,6 @@ namespace Avalonia.Native private IAvnWindow _nativeWindow; private List _menuItems = new List(); - private static Dictionary osxKeys = new Dictionary - { - {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, - {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, - {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, - {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, - { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, - { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, - { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, - { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, - { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, - { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, - { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, - { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, - { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, - { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, - { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, - { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, - { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, - { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, - { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, - { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, - { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, - { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, - { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, - { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, - { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, - { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, - { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, - { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, - { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, - { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, - { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, - //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, - { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, - { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, - { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, - { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, - { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, - //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, - //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, - //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, - //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, - //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, - //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, - //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, - { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, - //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, - //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, - }; - public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { _factory = factory; @@ -194,6 +200,7 @@ namespace Avalonia.Native _factory = factory; _menu = NativeMenu.GetMenu(Application.Current); + DoLayoutReset(); } @@ -255,15 +262,15 @@ namespace Avalonia.Native i.PropertyChanged -= OnItemPropertyChanged; if (i.Menu != null) ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; - } + } _menuItems.Clear(); - if(_nativeWindow is null) + if (_nativeWindow is null) { _menu = NativeMenu.GetMenu(Application.Current); - if(_menu != null) + if (_menu != null) { SetMenu(_menu); } @@ -292,7 +299,7 @@ namespace Avalonia.Native { var menu = _factory.CreateMenu(); - SetChildren(menu, children); + //SetChildren(menu, children); return menu; } @@ -305,128 +312,9 @@ namespace Avalonia.Native } } - private static string ConvertOSXSpecialKeyCodes(Key key) - { - if (osxKeys.ContainsKey(key)) - { - return ((char)osxKeys[key]).ToString(); - } - else - { - return key.ToString().ToLower(); - } - } - - private void SetChildren(IAvnAppMenu menu, ICollection children) - { - foreach (var i in children) - { - if (i is NativeMenuItem item) - { - AddMenuItem(item); - - var menuItem = _factory.CreateMenuItem(); - - using (var buffer = new Utf8Buffer(item.Header)) - { - menuItem.Title = buffer.DangerousGetHandle(); - } - - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(ConvertOSXSpecialKeyCodes(item.Gesture.Key))) - { - menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); - } - } - - menuItem.SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) - { - return item.Enabled; - } - - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); - menu.AddItem(menuItem); - - if (item.Menu?.Items?.Count >= 0) - { - var submenu = _factory.CreateMenu(); - - using (var buffer = new Utf8Buffer(item.Header)) - { - submenu.Title = buffer.DangerousGetHandle(); - } - - menuItem.SetSubMenu(submenu); - - AddItemsToMenu(submenu, item.Menu?.Items); - } - } - else if (i is NativeMenuItemSeperator seperator) - { - menu.AddItem(_factory.CreateMenuItemSeperator()); - } - } - } - - private void AddItemsToMenu(IAvnAppMenu menu, ICollection items, bool isMainMenu = false) - { - foreach (var i in items) - { - if (i is NativeMenuItem item) - { - var menuItem = _factory.CreateMenuItem(); - - AddMenuItem(item); - menuItem.SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) - { - return item.Enabled; - } - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); - if (item.Menu?.Items.Count >= 0 || isMainMenu) - { - var subMenu = CreateSubmenu(item.Menu?.Items); - - menuItem.SetSubMenu(subMenu); - - using (var buffer = new Utf8Buffer(item.Header)) - { - subMenu.Title = buffer.DangerousGetHandle(); - } - } - else - { - using (var buffer = new Utf8Buffer(item.Header)) - { - menuItem.Title = buffer.DangerousGetHandle(); - } - - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower())) - { - menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); - } - } - } - - menu.AddItem(menuItem); - } - else if(i is NativeMenuItemSeperator seperator) - { - menu.AddItem(_factory.CreateMenuItemSeperator()); - } - } - } private void SetMenu(NativeMenu menu) { @@ -439,15 +327,15 @@ namespace Avalonia.Native var menuItem = menu.Parent; - if(menu.Parent is null) + if (menu.Parent is null) { menuItem = new NativeMenuItem(); } menuItem.Menu = menu; - appMenu.Clear(); - AddItemsToMenu(appMenu, new List { menuItem }); + //appMenu.Clear(); + //AddItemsToMenu(appMenu, new List { menuItem }); _factory.SetAppMenu(appMenu); } @@ -466,8 +354,8 @@ namespace Avalonia.Native appMenu = _factory.CreateMenu(); } - appMenu.Clear(); - AddItemsToMenu(appMenu, menuItems); + //appMenu.Clear(); + //AddItemsToMenu(appMenu, menuItems); avnWindow.SetMainMenu(appMenu); } diff --git a/src/Avalonia.Native/Mappings.xml b/src/Avalonia.Native/Mappings.xml index 7ac6377f78..fcaa31a249 100644 --- a/src/Avalonia.Native/Mappings.xml +++ b/src/Avalonia.Native/Mappings.xml @@ -19,5 +19,7 @@ + + diff --git a/src/Avalonia.Native/OsxUnicodeKeys.cs b/src/Avalonia.Native/OsxUnicodeKeys.cs new file mode 100644 index 0000000000..b4056c8cd8 --- /dev/null +++ b/src/Avalonia.Native/OsxUnicodeKeys.cs @@ -0,0 +1,147 @@ +using System.Collections.Generic; +using Avalonia.Input; + +namespace Avalonia.Native.Interop +{ + internal static class OsxUnicodeKeys + { + enum OsxUnicodeSpecialKey + { + NSUpArrowFunctionKey = 0xF700, + NSDownArrowFunctionKey = 0xF701, + NSLeftArrowFunctionKey = 0xF702, + NSRightArrowFunctionKey = 0xF703, + NSF1FunctionKey = 0xF704, + NSF2FunctionKey = 0xF705, + NSF3FunctionKey = 0xF706, + NSF4FunctionKey = 0xF707, + NSF5FunctionKey = 0xF708, + NSF6FunctionKey = 0xF709, + NSF7FunctionKey = 0xF70A, + NSF8FunctionKey = 0xF70B, + NSF9FunctionKey = 0xF70C, + NSF10FunctionKey = 0xF70D, + NSF11FunctionKey = 0xF70E, + NSF12FunctionKey = 0xF70F, + NSF13FunctionKey = 0xF710, + NSF14FunctionKey = 0xF711, + NSF15FunctionKey = 0xF712, + NSF16FunctionKey = 0xF713, + NSF17FunctionKey = 0xF714, + NSF18FunctionKey = 0xF715, + NSF19FunctionKey = 0xF716, + NSF20FunctionKey = 0xF717, + NSF21FunctionKey = 0xF718, + NSF22FunctionKey = 0xF719, + NSF23FunctionKey = 0xF71A, + NSF24FunctionKey = 0xF71B, + NSF25FunctionKey = 0xF71C, + NSF26FunctionKey = 0xF71D, + NSF27FunctionKey = 0xF71E, + NSF28FunctionKey = 0xF71F, + NSF29FunctionKey = 0xF720, + NSF30FunctionKey = 0xF721, + NSF31FunctionKey = 0xF722, + NSF32FunctionKey = 0xF723, + NSF33FunctionKey = 0xF724, + NSF34FunctionKey = 0xF725, + NSF35FunctionKey = 0xF726, + NSInsertFunctionKey = 0xF727, + NSDeleteFunctionKey = 0xF728, + NSHomeFunctionKey = 0xF729, + NSBeginFunctionKey = 0xF72A, + NSEndFunctionKey = 0xF72B, + NSPageUpFunctionKey = 0xF72C, + NSPageDownFunctionKey = 0xF72D, + NSPrintScreenFunctionKey = 0xF72E, + NSScrollLockFunctionKey = 0xF72F, + NSPauseFunctionKey = 0xF730, + NSSysReqFunctionKey = 0xF731, + NSBreakFunctionKey = 0xF732, + NSResetFunctionKey = 0xF733, + NSStopFunctionKey = 0xF734, + NSMenuFunctionKey = 0xF735, + NSUserFunctionKey = 0xF736, + NSSystemFunctionKey = 0xF737, + NSPrintFunctionKey = 0xF738, + NSClearLineFunctionKey = 0xF739, + NSClearDisplayFunctionKey = 0xF73A, + NSInsertLineFunctionKey = 0xF73B, + NSDeleteLineFunctionKey = 0xF73C, + NSInsertCharFunctionKey = 0xF73D, + NSDeleteCharFunctionKey = 0xF73E, + NSPrevFunctionKey = 0xF73F, + NSNextFunctionKey = 0xF740, + NSSelectFunctionKey = 0xF741, + NSExecuteFunctionKey = 0xF742, + NSUndoFunctionKey = 0xF743, + NSRedoFunctionKey = 0xF744, + NSFindFunctionKey = 0xF745, + NSHelpFunctionKey = 0xF746, + NSModeSwitchFunctionKey = 0xF747 + } + + private static Dictionary s_osxKeys = new Dictionary + { + {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey }, + {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey }, + {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey }, + {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey }, + { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey }, + { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey }, + { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey }, + { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey }, + { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey }, + { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey }, + { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey }, + { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey }, + { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey }, + { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey }, + { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey }, + { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey }, + { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey }, + { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey }, + { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey }, + { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey }, + { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey }, + { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey }, + { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey }, + { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey }, + { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey }, + { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey }, + { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey }, + { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey }, + { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey }, + { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey }, + { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey }, + //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey }, + { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey }, + { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey }, + { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey }, + { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey }, + { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey }, + //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey }, + //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey }, + //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey }, + //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey }, + //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey }, + //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey }, + //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey }, + { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey }, + //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey }, + //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey }, + }; + + public static string ConvertOSXSpecialKeyCodes(Key key) + { + if (s_osxKeys.ContainsKey(key)) + { + return ((char)s_osxKeys[key]).ToString(); + } + else + { + return key.ToString().ToLower(); + } + } + } +} From aa94dc476c9ff8f2edc4bec0ddf9bb16735a6f7c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Apr 2020 15:08:03 -0300 Subject: [PATCH 002/164] subscript to events and notify of changes. --- .../AvaloniaNativeMenuExporter.cs | 143 +++++++----------- 1 file changed, 57 insertions(+), 86 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 8dd54f4423..14eb4b03ce 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -14,11 +14,18 @@ namespace Avalonia.Native.Interop public partial class IAvnAppMenuItem { private IAvnAppMenu _subMenu; + private AvaloniaNativeMenuExporter _exporter; public NativeMenuItemBase Managed { get; set; } - public void Update(IAvaloniaNativeFactory factory, NativeMenuItem item) + internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) { + _exporter = exporter; + + Managed = item; + + Managed.PropertyChanged += Item_PropertyChanged; + using (var buffer = new Utf8Buffer(item.Header)) { Title = buffer.DangerousGetHandle(); @@ -49,20 +56,35 @@ namespace Avalonia.Native.Interop _subMenu = factory.CreateMenu(); } - _subMenu.Update(factory, item.Menu); + _subMenu.Update(exporter, factory, item.Menu); } if (item.Menu == null && _subMenu != null) { + _subMenu.Remove(); + // todo remove submenu. // needs implementing on native side also. } } + + private void Item_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + _exporter.QueueReset(); + } + + internal void Remove() + { + _exporter = null; + Managed = null; + Managed.PropertyChanged -= Item_PropertyChanged; + } } public partial class IAvnAppMenu { + private AvaloniaNativeMenuExporter _exporter; private NativeMenu _menu; private List _menuItems = new List(); private Dictionary _menuItemLookup = new Dictionary(); @@ -71,10 +93,18 @@ namespace Avalonia.Native.Interop { _menuItemLookup.Remove(item.Managed); _menuItems.Remove(item); + item.Remove(); RemoveItem(item); } + internal void Remove() + { + ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= IAvnAppMenu_CollectionChanged; + _exporter = null; + _menu = null; + } + private void InsertAt(int index, IAvnAppMenuItem item) { if (item.Managed == null) @@ -96,7 +126,7 @@ namespace Avalonia.Native.Interop return nativeItem; } - public void Update(IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") + internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") { if (_menu == null) { @@ -107,6 +137,10 @@ namespace Avalonia.Native.Interop throw new Exception("Cannot update a menu from another instance"); } + _exporter = exporter; + + ((INotifyCollectionChanged)_menu.Items).CollectionChanged += IAvnAppMenu_CollectionChanged; + if (!string.IsNullOrWhiteSpace(title)) { using (var buffer = new Utf8Buffer(title)) @@ -134,7 +168,7 @@ namespace Avalonia.Native.Interop if (menu.Items[i] is NativeMenuItem nmi) { - nativeItem.Update(factory, nmi); + nativeItem.Update(exporter, factory, nmi); } } @@ -143,6 +177,11 @@ namespace Avalonia.Native.Interop _menuItems.Remove(_menuItems[i]); } } + + private void IAvnAppMenu_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _exporter.QueueReset(); + } } } @@ -181,11 +220,10 @@ namespace Avalonia.Native class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { private IAvaloniaNativeFactory _factory; - private NativeMenu _menu; private bool _resetQueued; private bool _exported = false; private IAvnWindow _nativeWindow; - private List _menuItems = new List(); + private NativeMenu _menu; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -199,8 +237,6 @@ namespace Avalonia.Native { _factory = factory; - _menu = NativeMenu.GetMenu(Application.Current); - DoLayoutReset(); } @@ -210,13 +246,8 @@ namespace Avalonia.Native public void SetNativeMenu(NativeMenu menu) { - if (menu == null) - menu = new NativeMenu(); - - if (_menu != null) - ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged; - _menu = menu; - ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged; + if (_menu == null) + _menu = new NativeMenu(); DoLayoutReset(); } @@ -244,50 +275,29 @@ namespace Avalonia.Native return result; } - private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - QueueReset(); - } - - private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) - { - QueueReset(); - } - void DoLayoutReset() { _resetQueued = false; - foreach (var i in _menuItems) - { - i.PropertyChanged -= OnItemPropertyChanged; - if (i.Menu != null) - ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged; - } - - _menuItems.Clear(); if (_nativeWindow is null) { - _menu = NativeMenu.GetMenu(Application.Current); + var appMenu = NativeMenu.GetMenu(Application.Current); - if (_menu != null) - { - SetMenu(_menu); - } - else + if (appMenu == null) { - SetMenu(CreateDefaultAppMenu()); + appMenu = CreateDefaultAppMenu(); + SetMenu(appMenu); } } else { - SetMenu(_nativeWindow, _menu?.Items); + SetMenu(_nativeWindow, _menu); } _exported = true; } - private void QueueReset() + internal void QueueReset() { if (_resetQueued) return; @@ -295,27 +305,6 @@ namespace Avalonia.Native Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); } - private IAvnAppMenu CreateSubmenu(ICollection children) - { - var menu = _factory.CreateMenu(); - - //SetChildren(menu, children); - - return menu; - } - - private void AddMenuItem(NativeMenuItem item) - { - if (item.Menu?.Items != null) - { - ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; - } - } - - - - - private void SetMenu(NativeMenu menu) { var appMenu = _factory.ObtainAppMenu(); @@ -323,41 +312,23 @@ namespace Avalonia.Native if (appMenu is null) { appMenu = _factory.CreateMenu(); + _factory.SetAppMenu(appMenu); } - var menuItem = menu.Parent; - - if (menu.Parent is null) - { - menuItem = new NativeMenuItem(); - } - - menuItem.Menu = menu; - - //appMenu.Clear(); - //AddItemsToMenu(appMenu, new List { menuItem }); - - _factory.SetAppMenu(appMenu); + appMenu.Update(this, _factory, menu); } - private void SetMenu(IAvnWindow avnWindow, ICollection menuItems) + private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) { - if (menuItems is null) - { - menuItems = new List(); - } - var appMenu = avnWindow.ObtainMainMenu(); if (appMenu is null) { appMenu = _factory.CreateMenu(); + avnWindow.SetMainMenu(appMenu); } - //appMenu.Clear(); - //AddItemsToMenu(appMenu, menuItems); - - avnWindow.SetMainMenu(appMenu); + appMenu.Update(this, _factory, menu); } } } From 0f35059055bfa7def57950cc2a778464e2e78703 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Apr 2020 15:25:08 -0300 Subject: [PATCH 003/164] move code to own files. --- .../AvaloniaNativeMenuExporter.cs | 212 +----------------- src/Avalonia.Native/IAvnAppMenu.cs | 118 ++++++++++ src/Avalonia.Native/IAvnAppMenuItem.cs | 83 +++++++ src/Avalonia.Native/MenuActionCallback.cs | 20 ++ src/Avalonia.Native/PredicateCallback.cs | 20 ++ 5 files changed, 242 insertions(+), 211 deletions(-) create mode 100644 src/Avalonia.Native/IAvnAppMenu.cs create mode 100644 src/Avalonia.Native/IAvnAppMenuItem.cs create mode 100644 src/Avalonia.Native/MenuActionCallback.cs create mode 100644 src/Avalonia.Native/PredicateCallback.cs diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 14eb4b03ce..a074092792 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -1,222 +1,13 @@ using System; -using System.Collections.Generic; -using System.Collections.Specialized; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Platform; using Avalonia.Dialogs; using Avalonia.Native.Interop; -using Avalonia.Platform.Interop; using Avalonia.Threading; -namespace Avalonia.Native.Interop -{ - public partial class IAvnAppMenuItem - { - private IAvnAppMenu _subMenu; - private AvaloniaNativeMenuExporter _exporter; - - public NativeMenuItemBase Managed { get; set; } - - internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) - { - _exporter = exporter; - - Managed = item; - - Managed.PropertyChanged += Item_PropertyChanged; - - using (var buffer = new Utf8Buffer(item.Header)) - { - Title = buffer.DangerousGetHandle(); - } - - if (item.Gesture != null) - { - using (var buffer = new Utf8Buffer(OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(item.Gesture.Key))) - { - SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); - } - } - - SetAction(new PredicateCallback(() => - { - if (item.Command != null || item.HasClickHandlers) - { - return item.Enabled; - } - - return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); - - if (item.Menu != null) - { - if (_subMenu == null) - { - _subMenu = factory.CreateMenu(); - } - - _subMenu.Update(exporter, factory, item.Menu); - } - - if (item.Menu == null && _subMenu != null) - { - _subMenu.Remove(); - - // todo remove submenu. - - // needs implementing on native side also. - } - } - - private void Item_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - _exporter.QueueReset(); - } - - internal void Remove() - { - _exporter = null; - Managed = null; - Managed.PropertyChanged -= Item_PropertyChanged; - } - } - - public partial class IAvnAppMenu - { - private AvaloniaNativeMenuExporter _exporter; - private NativeMenu _menu; - private List _menuItems = new List(); - private Dictionary _menuItemLookup = new Dictionary(); - - private void Remove(IAvnAppMenuItem item) - { - _menuItemLookup.Remove(item.Managed); - _menuItems.Remove(item); - item.Remove(); - - RemoveItem(item); - } - - internal void Remove() - { - ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= IAvnAppMenu_CollectionChanged; - _exporter = null; - _menu = null; - } - - private void InsertAt(int index, IAvnAppMenuItem item) - { - if (item.Managed == null) - { - throw new InvalidOperationException("Cannot insert item that with Managed link null"); - } - - _menuItemLookup.Add(item.Managed, item); - _menuItems.Insert(index, item); - - AddItem(item); // todo change to insertatimpl - } - - private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) - { - var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem(); - nativeItem.Managed = item; - - return nativeItem; - } - - internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") - { - if (_menu == null) - { - _menu = menu; - } - else if (_menu != menu) - { - throw new Exception("Cannot update a menu from another instance"); - } - - _exporter = exporter; - - ((INotifyCollectionChanged)_menu.Items).CollectionChanged += IAvnAppMenu_CollectionChanged; - - if (!string.IsNullOrWhiteSpace(title)) - { - using (var buffer = new Utf8Buffer(title)) - { - Title = buffer.DangerousGetHandle(); - } - } - - for (int i = 0; i < menu.Items.Count; i++) - { - IAvnAppMenuItem nativeItem = null; - if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].Managed) - { - if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) - { - Remove(nativeItem); - InsertAt(i, nativeItem); - } - else - { - nativeItem = CreateNew(factory, menu.Items[i]); - InsertAt(i, nativeItem); - } - } - - if (menu.Items[i] is NativeMenuItem nmi) - { - nativeItem.Update(exporter, factory, nmi); - } - } - - for (int i = menu.Items.Count; i < _menuItems.Count; i++) - { - _menuItems.Remove(_menuItems[i]); - } - } - - private void IAvnAppMenu_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) - { - _exporter.QueueReset(); - } - } -} - namespace Avalonia.Native { - public class MenuActionCallback : CallbackBase, IAvnActionCallback - { - private Action _action; - - public MenuActionCallback(Action action) - { - _action = action; - } - - void IAvnActionCallback.Run() - { - _action?.Invoke(); - } - } - - public class PredicateCallback : CallbackBase, IAvnPredicateCallback - { - private Func _predicate; - - public PredicateCallback(Func predicate) - { - _predicate = predicate; - } - - bool IAvnPredicateCallback.Evaluate() - { - return _predicate(); - } - } - class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter { private IAvaloniaNativeFactory _factory; @@ -246,8 +37,7 @@ namespace Avalonia.Native public void SetNativeMenu(NativeMenu menu) { - if (_menu == null) - _menu = new NativeMenu(); + _menu = menu == null ? new NativeMenu() : menu; DoLayoutReset(); } diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs new file mode 100644 index 0000000000..8ec6e8d5fa --- /dev/null +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using Avalonia.Controls; +using Avalonia.Platform.Interop; + +namespace Avalonia.Native.Interop +{ + public partial class IAvnAppMenu + { + private AvaloniaNativeMenuExporter _exporter; + private NativeMenu _menu; + private List _menuItems = new List(); + private Dictionary _menuItemLookup = new Dictionary(); + + private void Remove(IAvnAppMenuItem item) + { + _menuItemLookup.Remove(item.Managed); + _menuItems.Remove(item); + + RemoveItem(item); + } + + internal void Cleanup() + { + foreach(var item in _menuItems) + { + item.Cleanup(); + } + + ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= IAvnAppMenu_CollectionChanged; + _exporter = null; + _menu = null; + } + + private void InsertAt(int index, IAvnAppMenuItem item) + { + if (item.Managed == null) + { + throw new InvalidOperationException("Cannot insert item that with Managed link null"); + } + + _menuItemLookup.Add(item.Managed, item); + _menuItems.Insert(index, item); + + AddItem(item); // todo change to insertatimpl + } + + private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) + { + var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem(); + nativeItem.Managed = item; + + return nativeItem; + } + + internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") + { + if (_menu == null) + { + _menu = menu; + } + else if (_menu != menu) + { + Cleanup(); + _menu = menu; + } + + _exporter = exporter; + + ((INotifyCollectionChanged)_menu.Items).CollectionChanged += IAvnAppMenu_CollectionChanged; + + if (!string.IsNullOrWhiteSpace(title)) + { + using (var buffer = new Utf8Buffer(title)) + { + Title = buffer.DangerousGetHandle(); + } + } + + for (int i = 0; i < menu.Items.Count; i++) + { + IAvnAppMenuItem nativeItem = null; + + if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].Managed) + { + if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) + { + Remove(nativeItem); + InsertAt(i, nativeItem); + } + else + { + nativeItem = CreateNew(factory, menu.Items[i]); + InsertAt(i, nativeItem); + } + } + + if (menu.Items[i] is NativeMenuItem nmi) + { + nativeItem.Update(exporter, factory, nmi); + } + } + + for (int i = menu.Items.Count; i < _menuItems.Count; i++) + { + Remove(_menuItems[i]); + + _menuItems[i].Cleanup(); + } + } + + private void IAvnAppMenu_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _exporter.QueueReset(); + } + } +} diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs new file mode 100644 index 0000000000..f349c59470 --- /dev/null +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -0,0 +1,83 @@ +using Avalonia.Controls; +using Avalonia.Platform.Interop; + +namespace Avalonia.Native.Interop +{ + public partial class IAvnAppMenuItem + { + private IAvnAppMenu _subMenu; + private AvaloniaNativeMenuExporter _exporter; + + public NativeMenuItemBase Managed { get; set; } + + internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) + { + _exporter = exporter; + + Managed = item; + + Managed.PropertyChanged += Item_PropertyChanged; + + using (var buffer = new Utf8Buffer(item.Header)) + { + Title = buffer.DangerousGetHandle(); + } + + if (item.Gesture != null) + { + using (var buffer = new Utf8Buffer(OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(item.Gesture.Key))) + { + SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); + } + } + + SetAction(new PredicateCallback(() => + { + if (item.Command != null || item.HasClickHandlers) + { + return item.Enabled; + } + + return false; + }), new MenuActionCallback(() => { item.RaiseClick(); })); + + if (item.Menu != null) + { + if (_subMenu == null) + { + _subMenu = factory.CreateMenu(); + } + + _subMenu.Update(exporter, factory, item.Menu); + } + + if (item.Menu == null && _subMenu != null) + { + _subMenu.Cleanup(); + + // todo remove submenu. + + // needs implementing on native side also. + } + } + + private void Item_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + _exporter.QueueReset(); + } + + internal void Cleanup() + { + Managed.PropertyChanged -= Item_PropertyChanged; + + if (_subMenu != null) + { + _subMenu.Cleanup(); + } + + _subMenu = null; + _exporter = null; + Managed = null; + } + } +} diff --git a/src/Avalonia.Native/MenuActionCallback.cs b/src/Avalonia.Native/MenuActionCallback.cs new file mode 100644 index 0000000000..5318195f30 --- /dev/null +++ b/src/Avalonia.Native/MenuActionCallback.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + public class MenuActionCallback : CallbackBase, IAvnActionCallback + { + private Action _action; + + public MenuActionCallback(Action action) + { + _action = action; + } + + void IAvnActionCallback.Run() + { + _action?.Invoke(); + } + } +} diff --git a/src/Avalonia.Native/PredicateCallback.cs b/src/Avalonia.Native/PredicateCallback.cs new file mode 100644 index 0000000000..1ed2ae36af --- /dev/null +++ b/src/Avalonia.Native/PredicateCallback.cs @@ -0,0 +1,20 @@ +using System; +using Avalonia.Native.Interop; + +namespace Avalonia.Native +{ + public class PredicateCallback : CallbackBase, IAvnPredicateCallback + { + private Func _predicate; + + public PredicateCallback(Func predicate) + { + _predicate = predicate; + } + + bool IAvnPredicateCallback.Evaluate() + { + return _predicate(); + } + } +} From 5a3c984b4da18ec27ae15ad35df6397556ca8402 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 8 Apr 2020 16:23:00 -0300 Subject: [PATCH 004/164] allow DoLayoutReset to be called when menu is still null. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index a074092792..1f137c3ee5 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -81,7 +81,10 @@ namespace Avalonia.Native } else { - SetMenu(_nativeWindow, _menu); + if(_menu != null) + { + SetMenu(_nativeWindow, _menu); + } } _exported = true; From 5cc02c2d09a103e12f709cadac49494f640c3c84 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 10:52:34 -0300 Subject: [PATCH 005/164] create dummy menu root. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index a074092792..5eda69979e 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -102,10 +102,22 @@ namespace Avalonia.Native if (appMenu is null) { appMenu = _factory.CreateMenu(); + _factory.SetAppMenu(appMenu); } - appMenu.Update(this, _factory, menu); + var menuItem = menu.Parent; + + if (menu.Parent is null) + { + menuItem = new NativeMenuItem(); + + menuItem.Parent = new NativeMenu(); + } + + menuItem.Menu = menu; + + appMenu.Update(this, _factory, menuItem.Parent); } private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) From 06f57f7e4414f3f7082067dc1e1c32d078d5620b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 10:53:56 -0300 Subject: [PATCH 006/164] Insert Native Menus by index instead of just adding them in order. --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/menu.h | 2 +- native/Avalonia.Native/src/OSX/menu.mm | 4 ++-- src/Avalonia.Native/IAvnAppMenu.cs | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 3475eff654..ade2afe014 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -390,7 +390,7 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown AVNCOM(IAvnAppMenu, 17) : IUnknown { - virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT InsertItem (int index, IAvnAppMenuItem* item) = 0; virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT Clear () = 0; diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index befbe6a7e0..f66abb930b 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -66,7 +66,7 @@ public: AvnMenu* GetNative(); - virtual HRESULT AddItem (IAvnAppMenuItem* item) override; + virtual HRESULT InsertItem (int index, IAvnAppMenuItem* item) override; virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 1d2f075ccb..d5c783f7d4 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -145,13 +145,13 @@ AvnMenu* AvnAppMenu::GetNative() return _native; } -HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) +HRESULT AvnAppMenu::InsertItem(int index, IAvnAppMenuItem *item) { auto avnMenuItem = dynamic_cast(item); if(avnMenuItem != nullptr) { - [_native addItem: avnMenuItem->GetNative()]; + [_native insertItem: avnMenuItem->GetNative() atIndex:index]; } return S_OK; diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 8ec6e8d5fa..d7e3b0054f 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -43,7 +43,7 @@ namespace Avalonia.Native.Interop _menuItemLookup.Add(item.Managed, item); _menuItems.Insert(index, item); - AddItem(item); // todo change to insertatimpl + InsertItem(index, item); // todo change to insertatimpl } private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) @@ -87,12 +87,10 @@ namespace Avalonia.Native.Interop if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) { Remove(nativeItem); - InsertAt(i, nativeItem); } else { nativeItem = CreateNew(factory, menu.Items[i]); - InsertAt(i, nativeItem); } } @@ -100,6 +98,8 @@ namespace Avalonia.Native.Interop { nativeItem.Update(exporter, factory, nmi); } + + InsertAt(i, nativeItem); } for (int i = menu.Items.Count; i < _menuItems.Count; i++) From 30c0b1f0ac91ee53ecbed5a8b583da9062697b39 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 10:54:08 -0300 Subject: [PATCH 007/164] update osx native proj file. --- .../xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme index 1a665d3ea5..5d20a135b9 100644 --- a/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme +++ b/native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme @@ -29,8 +29,6 @@ shouldUseLaunchSchemeArgsEnv = "YES"> - - - - Date: Thu, 9 Apr 2020 11:31:36 -0300 Subject: [PATCH 008/164] somewhat working updating menus. --- .../AvaloniaNativeMenuExporter.cs | 20 ++++++++++++++----- src/Avalonia.Native/IAvnAppMenuItem.cs | 10 +++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index b299e72759..7eb36b2a57 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -105,22 +105,30 @@ namespace Avalonia.Native if (appMenu is null) { appMenu = _factory.CreateMenu(); - - _factory.SetAppMenu(appMenu); } + var menuItem = menu.Parent; + var appMenuHolder = menuItem?.Parent; + if (menu.Parent is null) { menuItem = new NativeMenuItem(); + } - menuItem.Parent = new NativeMenu(); + if(appMenuHolder is null) + { + appMenuHolder = new NativeMenu(); + + appMenuHolder.Add(menuItem); } menuItem.Menu = menu; - appMenu.Update(this, _factory, menuItem.Parent); + appMenu.Update(this, _factory, appMenuHolder); + + _factory.SetAppMenu(appMenu); } private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) @@ -130,10 +138,12 @@ namespace Avalonia.Native if (appMenu is null) { appMenu = _factory.CreateMenu(); - avnWindow.SetMainMenu(appMenu); + } appMenu.Update(this, _factory, menu); + + avnWindow.SetMainMenu(appMenu); } } } diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index f349c59470..4d4d254d8d 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -18,9 +18,12 @@ namespace Avalonia.Native.Interop Managed.PropertyChanged += Item_PropertyChanged; - using (var buffer = new Utf8Buffer(item.Header)) + if(!string.IsNullOrWhiteSpace(item.Header)) { - Title = buffer.DangerousGetHandle(); + using (var buffer = new Utf8Buffer(item.Header)) + { + Title = buffer.DangerousGetHandle(); + } } if (item.Gesture != null) @@ -48,7 +51,8 @@ namespace Avalonia.Native.Interop _subMenu = factory.CreateMenu(); } - _subMenu.Update(exporter, factory, item.Menu); + SetSubMenu(_subMenu); + _subMenu.Update(exporter, factory, item.Menu, item.Header); } if (item.Menu == null && _subMenu != null) From 0d52c8b37b448382e7dee5b4815a3fa13dcea272 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 12:12:25 -0300 Subject: [PATCH 009/164] whitespace. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 7eb36b2a57..914b57addc 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -107,7 +107,6 @@ namespace Avalonia.Native appMenu = _factory.CreateMenu(); } - var menuItem = menu.Parent; var appMenuHolder = menuItem?.Parent; From 0cf079b47f31318daaf22098451c2b19907e004d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 12:18:58 -0300 Subject: [PATCH 010/164] only obtain object from managed side once. --- .../AvaloniaNativeMenuExporter.cs | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 914b57addc..86fce67ed0 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -15,6 +15,7 @@ namespace Avalonia.Native private bool _exported = false; private IAvnWindow _nativeWindow; private NativeMenu _menu; + private IAvnAppMenu _nativeMenu; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -81,7 +82,7 @@ namespace Avalonia.Native } else { - if(_menu != null) + if (_menu != null) { SetMenu(_nativeWindow, _menu); } @@ -100,11 +101,14 @@ namespace Avalonia.Native private void SetMenu(NativeMenu menu) { - var appMenu = _factory.ObtainAppMenu(); - - if (appMenu is null) + if (_nativeMenu is null) { - appMenu = _factory.CreateMenu(); + _nativeMenu = _factory.ObtainAppMenu(); + + if (_nativeMenu is null) + { + _nativeMenu = _factory.CreateMenu(); + } } var menuItem = menu.Parent; @@ -116,7 +120,7 @@ namespace Avalonia.Native menuItem = new NativeMenuItem(); } - if(appMenuHolder is null) + if (appMenuHolder is null) { appMenuHolder = new NativeMenu(); @@ -125,24 +129,26 @@ namespace Avalonia.Native menuItem.Menu = menu; - appMenu.Update(this, _factory, appMenuHolder); + _nativeMenu.Update(this, _factory, appMenuHolder); - _factory.SetAppMenu(appMenu); + _factory.SetAppMenu(_nativeMenu); } private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) { - var appMenu = avnWindow.ObtainMainMenu(); - - if (appMenu is null) + if (_nativeMenu is null) { - appMenu = _factory.CreateMenu(); - + _nativeMenu = avnWindow.ObtainMainMenu(); + + if (_nativeMenu is null) + { + _nativeMenu = _factory.CreateMenu(); + } } - appMenu.Update(this, _factory, menu); + _nativeMenu.Update(this, _factory, menu); - avnWindow.SetMainMenu(appMenu); + avnWindow.SetMainMenu(_nativeMenu); } } } From 00812297bcf75aad26658157335534ff943cfe2b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 12:29:21 -0300 Subject: [PATCH 011/164] fix updating menu item. --- src/Avalonia.Native/IAvnAppMenu.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index d7e3b0054f..06115eadde 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -93,6 +93,10 @@ namespace Avalonia.Native.Interop nativeItem = CreateNew(factory, menu.Items[i]); } } + else + { + nativeItem = _menuItems[i]; + } if (menu.Items[i] is NativeMenuItem nmi) { From e23e9a277498e25f45b5d78b3559e6636752cc82 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 12:34:45 -0300 Subject: [PATCH 012/164] working update of menuitems. --- src/Avalonia.Native/IAvnAppMenu.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 06115eadde..ad69428fd8 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -96,6 +96,7 @@ namespace Avalonia.Native.Interop else { nativeItem = _menuItems[i]; + Remove(nativeItem); } if (menu.Items[i] is NativeMenuItem nmi) From a210267a2b107836dd1fb1c39d9cc8e84d4eea8d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 13:21:09 -0300 Subject: [PATCH 013/164] remove cleanup methods. --- src/Avalonia.Native/IAvnAppMenu.cs | 17 +---------------- src/Avalonia.Native/IAvnAppMenuItem.cs | 16 ---------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index ad69428fd8..c44100ae2a 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -21,18 +21,6 @@ namespace Avalonia.Native.Interop RemoveItem(item); } - internal void Cleanup() - { - foreach(var item in _menuItems) - { - item.Cleanup(); - } - - ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= IAvnAppMenu_CollectionChanged; - _exporter = null; - _menu = null; - } - private void InsertAt(int index, IAvnAppMenuItem item) { if (item.Managed == null) @@ -61,8 +49,7 @@ namespace Avalonia.Native.Interop _menu = menu; } else if (_menu != menu) - { - Cleanup(); + { _menu = menu; } @@ -110,8 +97,6 @@ namespace Avalonia.Native.Interop for (int i = menu.Items.Count; i < _menuItems.Count; i++) { Remove(_menuItems[i]); - - _menuItems[i].Cleanup(); } } diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 4d4d254d8d..c35a796b59 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -57,8 +57,6 @@ namespace Avalonia.Native.Interop if (item.Menu == null && _subMenu != null) { - _subMenu.Cleanup(); - // todo remove submenu. // needs implementing on native side also. @@ -69,19 +67,5 @@ namespace Avalonia.Native.Interop { _exporter.QueueReset(); } - - internal void Cleanup() - { - Managed.PropertyChanged -= Item_PropertyChanged; - - if (_subMenu != null) - { - _subMenu.Cleanup(); - } - - _subMenu = null; - _exporter = null; - Managed = null; - } } } From 6895ea9dd0eb11511e819d10a36c86ea422468fc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 13:28:15 -0300 Subject: [PATCH 014/164] clean event subscriptions before updating menus. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 7 +++++-- src/Avalonia.Native/IAvnAppMenu.cs | 11 +++++++++-- src/Avalonia.Native/IAvnAppMenuItem.cs | 15 ++++++++++++--- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 86fce67ed0..c071456cf4 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -16,6 +16,7 @@ namespace Avalonia.Native private IAvnWindow _nativeWindow; private NativeMenu _menu; private IAvnAppMenu _nativeMenu; + private IDisposable _disposable; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -129,7 +130,8 @@ namespace Avalonia.Native menuItem.Menu = menu; - _nativeMenu.Update(this, _factory, appMenuHolder); + _disposable?.Dispose(); + _disposable = _nativeMenu.Update(this, _factory, appMenuHolder); _factory.SetAppMenu(_nativeMenu); } @@ -146,7 +148,8 @@ namespace Avalonia.Native } } - _nativeMenu.Update(this, _factory, menu); + _disposable?.Dispose(); + _disposable = _nativeMenu.Update(this, _factory, menu); avnWindow.SetMainMenu(_nativeMenu); } diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index c44100ae2a..eb75c6f86b 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Reactive.Disposables; using Avalonia.Controls; using Avalonia.Platform.Interop; @@ -42,8 +43,10 @@ namespace Avalonia.Native.Interop return nativeItem; } - internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") + internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") { + var disposables = new CompositeDisposable(); + if (_menu == null) { _menu = menu; @@ -57,6 +60,8 @@ namespace Avalonia.Native.Interop ((INotifyCollectionChanged)_menu.Items).CollectionChanged += IAvnAppMenu_CollectionChanged; + disposables.Add(Disposable.Create(() => ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= IAvnAppMenu_CollectionChanged)); + if (!string.IsNullOrWhiteSpace(title)) { using (var buffer = new Utf8Buffer(title)) @@ -88,7 +93,7 @@ namespace Avalonia.Native.Interop if (menu.Items[i] is NativeMenuItem nmi) { - nativeItem.Update(exporter, factory, nmi); + disposables.Add(nativeItem.Update(exporter, factory, nmi)); } InsertAt(i, nativeItem); @@ -98,6 +103,8 @@ namespace Avalonia.Native.Interop { Remove(_menuItems[i]); } + + return disposables; } private void IAvnAppMenu_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index c35a796b59..55797449f1 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -1,4 +1,6 @@ -using Avalonia.Controls; +using System; +using System.Reactive.Disposables; +using Avalonia.Controls; using Avalonia.Platform.Interop; namespace Avalonia.Native.Interop @@ -10,14 +12,18 @@ namespace Avalonia.Native.Interop public NativeMenuItemBase Managed { get; set; } - internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) + internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) { + var disposables = new CompositeDisposable(); + _exporter = exporter; Managed = item; Managed.PropertyChanged += Item_PropertyChanged; + disposables.Add(Disposable.Create(() => Managed.PropertyChanged -= Item_PropertyChanged)); + if(!string.IsNullOrWhiteSpace(item.Header)) { using (var buffer = new Utf8Buffer(item.Header)) @@ -52,7 +58,8 @@ namespace Avalonia.Native.Interop } SetSubMenu(_subMenu); - _subMenu.Update(exporter, factory, item.Menu, item.Header); + + disposables.Add(_subMenu.Update(exporter, factory, item.Menu, item.Header)); } if (item.Menu == null && _subMenu != null) @@ -61,6 +68,8 @@ namespace Avalonia.Native.Interop // needs implementing on native side also. } + + return disposables; } private void Item_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) From f2031cddabf2f36813ddbd3227a0a0198cbdd394 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 13:31:52 -0300 Subject: [PATCH 015/164] rename some variables. --- src/Avalonia.Native/IAvnAppMenu.cs | 29 +++++++++++++------------- src/Avalonia.Native/IAvnAppMenuItem.cs | 10 ++++----- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index eb75c6f86b..85bde6845a 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -9,14 +9,15 @@ namespace Avalonia.Native.Interop { public partial class IAvnAppMenu { - private AvaloniaNativeMenuExporter _exporter; - private NativeMenu _menu; + private AvaloniaNativeMenuExporter _exporter; private List _menuItems = new List(); private Dictionary _menuItemLookup = new Dictionary(); + internal NativeMenu ManagedMenu { get; private set; } + private void Remove(IAvnAppMenuItem item) { - _menuItemLookup.Remove(item.Managed); + _menuItemLookup.Remove(item.ManagedMenuItem); _menuItems.Remove(item); RemoveItem(item); @@ -24,12 +25,12 @@ namespace Avalonia.Native.Interop private void InsertAt(int index, IAvnAppMenuItem item) { - if (item.Managed == null) + if (item.ManagedMenuItem == null) { throw new InvalidOperationException("Cannot insert item that with Managed link null"); } - _menuItemLookup.Add(item.Managed, item); + _menuItemLookup.Add(item.ManagedMenuItem, item); _menuItems.Insert(index, item); InsertItem(index, item); // todo change to insertatimpl @@ -38,7 +39,7 @@ namespace Avalonia.Native.Interop private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) { var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem(); - nativeItem.Managed = item; + nativeItem.ManagedMenuItem = item; return nativeItem; } @@ -47,20 +48,20 @@ namespace Avalonia.Native.Interop { var disposables = new CompositeDisposable(); - if (_menu == null) + if (ManagedMenu == null) { - _menu = menu; + ManagedMenu = menu; } - else if (_menu != menu) + else if (ManagedMenu != menu) { - _menu = menu; + ManagedMenu = menu; } _exporter = exporter; - ((INotifyCollectionChanged)_menu.Items).CollectionChanged += IAvnAppMenu_CollectionChanged; + ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged += OnMenuItemsChanged; - disposables.Add(Disposable.Create(() => ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= IAvnAppMenu_CollectionChanged)); + disposables.Add(Disposable.Create(() => ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged)); if (!string.IsNullOrWhiteSpace(title)) { @@ -74,7 +75,7 @@ namespace Avalonia.Native.Interop { IAvnAppMenuItem nativeItem = null; - if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].Managed) + if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].ManagedMenuItem) { if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) { @@ -107,7 +108,7 @@ namespace Avalonia.Native.Interop return disposables; } - private void IAvnAppMenu_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { _exporter.QueueReset(); } diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 55797449f1..7c53b38322 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -10,7 +10,7 @@ namespace Avalonia.Native.Interop private IAvnAppMenu _subMenu; private AvaloniaNativeMenuExporter _exporter; - public NativeMenuItemBase Managed { get; set; } + public NativeMenuItemBase ManagedMenuItem { get; set; } internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) { @@ -18,11 +18,11 @@ namespace Avalonia.Native.Interop _exporter = exporter; - Managed = item; + ManagedMenuItem = item; - Managed.PropertyChanged += Item_PropertyChanged; + ManagedMenuItem.PropertyChanged += OnMenuItemPropertyChanged; - disposables.Add(Disposable.Create(() => Managed.PropertyChanged -= Item_PropertyChanged)); + disposables.Add(Disposable.Create(() => ManagedMenuItem.PropertyChanged -= OnMenuItemPropertyChanged)); if(!string.IsNullOrWhiteSpace(item.Header)) { @@ -72,7 +72,7 @@ namespace Avalonia.Native.Interop return disposables; } - private void Item_PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + private void OnMenuItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) { _exporter.QueueReset(); } From 53e14acb9d42f0c1cd26ede85a8f31bf7d505a53 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 13:35:57 -0300 Subject: [PATCH 016/164] only call SetMenu once or if the menu has actually changed. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index c071456cf4..ea1c5b3c08 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -130,10 +130,15 @@ namespace Avalonia.Native menuItem.Menu = menu; + var setMenu = _nativeMenu.ManagedMenu != appMenuHolder; + _disposable?.Dispose(); _disposable = _nativeMenu.Update(this, _factory, appMenuHolder); - _factory.SetAppMenu(_nativeMenu); + if (setMenu) + { + _factory.SetAppMenu(_nativeMenu); + } } private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) @@ -148,10 +153,15 @@ namespace Avalonia.Native } } + var setMenu = _nativeMenu.ManagedMenu != menu; + _disposable?.Dispose(); _disposable = _nativeMenu.Update(this, _factory, menu); - avnWindow.SetMainMenu(_nativeMenu); + if (setMenu) + { + avnWindow.SetMainMenu(_nativeMenu); + } } } } From b17c69b4c10109fea76760076392bd0b039283ca Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 13:40:33 -0300 Subject: [PATCH 017/164] temporary test. --- samples/ControlCatalog/MainWindow.xaml.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index b40fdb4a17..ab9d763bb9 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -5,6 +5,7 @@ using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Markup.Xaml; +using Avalonia.Threading; using ControlCatalog.ViewModels; namespace ControlCatalog @@ -13,7 +14,7 @@ namespace ControlCatalog { private WindowNotificationManager _notificationArea; private NativeMenu _recentMenu; - + private int seconds = 0; public MainWindow() { this.InitializeComponent(); @@ -29,6 +30,14 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; + + var timer = new DispatcherTimer(); + timer.Interval = TimeSpan.FromSeconds(1); + timer.Tick += (sender, e) => + { + ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Header = $"Recent {seconds++}"; + }; + timer.Start(); var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; } From 75652d094e3fc4c364c828ff60eca2c266378b93 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 14:33:42 -0300 Subject: [PATCH 018/164] fix direct property implementations on nativemenu item. --- src/Avalonia.Controls/NativeMenu.cs | 2 - src/Avalonia.Controls/NativeMenuItem.cs | 61 ++++++++++++------------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 54aa2b5e3d..7c06111a01 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -3,8 +3,6 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using Avalonia.Collections; -using Avalonia.Data; -using Avalonia.LogicalTree; using Avalonia.Metadata; namespace Avalonia.Controls diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index c1144d45b2..1033a0bfe6 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -10,6 +10,7 @@ namespace Avalonia.Controls private string _header; private KeyGesture _gesture; private bool _enabled = true; + private ICommand _command; private NativeMenu _menu; @@ -55,13 +56,7 @@ namespace Avalonia.Controls } public static readonly DirectProperty MenuProperty = - AvaloniaProperty.RegisterDirect(nameof(Menu), o => o._menu, - (o, v) => - { - if (v.Parent != null && v.Parent != o) - throw new InvalidOperationException("NativeMenu already has a parent"); - o._menu = v; - }); + AvaloniaProperty.RegisterDirect(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v); public NativeMenu Menu { @@ -75,38 +70,26 @@ namespace Avalonia.Controls } public static readonly DirectProperty HeaderProperty = - AvaloniaProperty.RegisterDirect(nameof(Header), o => o._header, (o, v) => o._header = v); + AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); public string Header { - get => GetValue(HeaderProperty); - set => SetValue(HeaderProperty, value); + get => _header; + set => SetAndRaise(HeaderProperty, ref _header, value); } public static readonly DirectProperty GestureProperty = - AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v); + AvaloniaProperty.RegisterDirect(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v); public KeyGesture Gesture { - get => GetValue(GestureProperty); - set => SetValue(GestureProperty, value); - } - - private ICommand _command; + get => _gesture; + set => SetAndRaise(GestureProperty, ref _gesture, value); + } public static readonly DirectProperty CommandProperty = AvaloniaProperty.RegisterDirect(nameof(Command), - o => o._command, (o, v) => - { - if (o._command != null) - WeakSubscriptionManager.Unsubscribe(o._command, - nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); - o._command = v; - if (o._command != null) - WeakSubscriptionManager.Subscribe(o._command, - nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber); - o.CanExecuteChanged(); - }); + o => o.Command, (o, v) => o.Command = v); /// /// Defines the property. @@ -115,13 +98,12 @@ namespace Avalonia.Controls Button.CommandParameterProperty.AddOwner(); public static readonly DirectProperty EnabledProperty = - AvaloniaProperty.RegisterDirect(nameof(Enabled), o => o._enabled, - (o, v) => o._enabled = v, true); + AvaloniaProperty.RegisterDirect(nameof(Enabled), o => o.Enabled, (o, v) => o.Enabled = v, true); public bool Enabled { - get => GetValue(EnabledProperty); - set => SetValue(EnabledProperty, value); + get => _enabled; + set => SetAndRaise(EnabledProperty, ref _enabled, value); } void CanExecuteChanged() @@ -133,8 +115,21 @@ namespace Avalonia.Controls public ICommand Command { - get => GetValue(CommandProperty); - set => SetValue(CommandProperty, value); + get => _command; + set + { + if (_command != null) + WeakSubscriptionManager.Unsubscribe(_command, + nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + + SetAndRaise(CommandProperty, ref _command, value); + + if (_command != null) + WeakSubscriptionManager.Subscribe(_command, + nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); + + CanExecuteChanged(); + } } /// From 149a7c6b900453147a464214c5b42914eb1f9010 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 14:58:51 -0300 Subject: [PATCH 019/164] always set main menu so it can be reparented in the osx backend to include the app menu. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index ea1c5b3c08..1c36510817 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -153,15 +153,10 @@ namespace Avalonia.Native } } - var setMenu = _nativeMenu.ManagedMenu != menu; - _disposable?.Dispose(); _disposable = _nativeMenu.Update(this, _factory, menu); - if (setMenu) - { - avnWindow.SetMainMenu(_nativeMenu); - } + avnWindow.SetMainMenu(_nativeMenu); } } } From 53d8dec1a68335986124596db475a92e9f586f02 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 15:22:05 -0300 Subject: [PATCH 020/164] Add owner on CommandProperty. --- src/Avalonia.Controls/NativeMenuItem.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 1033a0bfe6..4a19417de3 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -85,11 +85,13 @@ namespace Avalonia.Controls { get => _gesture; set => SetAndRaise(GestureProperty, ref _gesture, value); - } + } public static readonly DirectProperty CommandProperty = - AvaloniaProperty.RegisterDirect(nameof(Command), - o => o.Command, (o, v) => o.Command = v); + Button.CommandProperty.AddOwner( + menuItem => menuItem.Command, + (menuItem, command) => menuItem.Command = command, + enableDataValidation: true); /// /// Defines the property. From a5c3b3e2b5bd8d064cbf936b5b5556904bb0c49a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 15:28:15 -0300 Subject: [PATCH 021/164] remove debug code. --- samples/ControlCatalog/MainWindow.xaml.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index ab9d763bb9..2a33e04c98 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -5,7 +5,6 @@ using Avalonia.Controls; using Avalonia.Controls.Notifications; using Avalonia.Input; using Avalonia.Markup.Xaml; -using Avalonia.Threading; using ControlCatalog.ViewModels; namespace ControlCatalog @@ -14,7 +13,6 @@ namespace ControlCatalog { private WindowNotificationManager _notificationArea; private NativeMenu _recentMenu; - private int seconds = 0; public MainWindow() { this.InitializeComponent(); @@ -31,13 +29,6 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; - var timer = new DispatcherTimer(); - timer.Interval = TimeSpan.FromSeconds(1); - timer.Tick += (sender, e) => - { - ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Header = $"Recent {seconds++}"; - }; - timer.Start(); var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; } From 8b07ba1121d3c9f2d13e9f28a0eb3e0b45c33cfa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 15:29:00 -0300 Subject: [PATCH 022/164] whitespace --- samples/ControlCatalog/MainWindow.xaml.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 2a33e04c98..96cc05b84d 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -28,7 +28,6 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; - var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; } From 52398648f7b29b234849f42574a4aa2039074690 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 15:30:46 -0300 Subject: [PATCH 023/164] remove comment. --- src/Avalonia.Native/IAvnAppMenu.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 85bde6845a..50485456db 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -33,7 +33,7 @@ namespace Avalonia.Native.Interop _menuItemLookup.Add(item.ManagedMenuItem, item); _menuItems.Insert(index, item); - InsertItem(index, item); // todo change to insertatimpl + InsertItem(index, item); } private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) From 3bf3c64951877f937719aa59524db0094209dd45 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 15:33:33 -0300 Subject: [PATCH 024/164] whitespace. --- samples/ControlCatalog/MainWindow.xaml.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 96cc05b84d..b40fdb4a17 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -13,6 +13,7 @@ namespace ControlCatalog { private WindowNotificationManager _notificationArea; private NativeMenu _recentMenu; + public MainWindow() { this.InitializeComponent(); From 38f8dc73b30d668b3489c621c0d9828842e880d6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 15:57:56 -0300 Subject: [PATCH 025/164] use a copy of lists as we will be removing from the list inside the loop. --- src/Avalonia.Native/IAvnAppMenu.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 50485456db..d31ee24b26 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -71,11 +71,13 @@ namespace Avalonia.Native.Interop } } + var menuItems = _menuItems.ToList(); + for (int i = 0; i < menu.Items.Count; i++) { IAvnAppMenuItem nativeItem = null; - if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].ManagedMenuItem) + if (i >= menuItems.Count || menu.Items[i] != menuItems[i].ManagedMenuItem) { if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) { @@ -88,7 +90,7 @@ namespace Avalonia.Native.Interop } else { - nativeItem = _menuItems[i]; + nativeItem = menuItems[i]; Remove(nativeItem); } @@ -100,9 +102,9 @@ namespace Avalonia.Native.Interop InsertAt(i, nativeItem); } - for (int i = menu.Items.Count; i < _menuItems.Count; i++) + for (int i = menu.Items.Count; i < menuItems.Count; i++) { - Remove(_menuItems[i]); + Remove(menuItems[i]); } return disposables; From cd0ebbdd505d7e507a386a73218b20f2895c528d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 16:06:34 -0300 Subject: [PATCH 026/164] Revert "use a copy of lists as we will be removing from the list inside the loop." This reverts commit 38f8dc73b30d668b3489c621c0d9828842e880d6. --- src/Avalonia.Native/IAvnAppMenu.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index d31ee24b26..50485456db 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -71,13 +71,11 @@ namespace Avalonia.Native.Interop } } - var menuItems = _menuItems.ToList(); - for (int i = 0; i < menu.Items.Count; i++) { IAvnAppMenuItem nativeItem = null; - if (i >= menuItems.Count || menu.Items[i] != menuItems[i].ManagedMenuItem) + if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].ManagedMenuItem) { if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) { @@ -90,7 +88,7 @@ namespace Avalonia.Native.Interop } else { - nativeItem = menuItems[i]; + nativeItem = _menuItems[i]; Remove(nativeItem); } @@ -102,9 +100,9 @@ namespace Avalonia.Native.Interop InsertAt(i, nativeItem); } - for (int i = menu.Items.Count; i < menuItems.Count; i++) + for (int i = menu.Items.Count; i < _menuItems.Count; i++) { - Remove(menuItems[i]); + Remove(_menuItems[i]); } return disposables; From c91b75b0b75dccd59eade5364e13e6b751e9719b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 9 Apr 2020 16:25:59 -0300 Subject: [PATCH 027/164] make menu re-order loop easier to follow. --- src/Avalonia.Native/IAvnAppMenu.cs | 57 +++++++++++++++++------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 50485456db..0d4c4f55e4 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -9,27 +9,34 @@ namespace Avalonia.Native.Interop { public partial class IAvnAppMenu { - private AvaloniaNativeMenuExporter _exporter; + private AvaloniaNativeMenuExporter _exporter; private List _menuItems = new List(); private Dictionary _menuItemLookup = new Dictionary(); internal NativeMenu ManagedMenu { get; private set; } - private void Remove(IAvnAppMenuItem item) + private void RemoveAndDispose(IAvnAppMenuItem item) { _menuItemLookup.Remove(item.ManagedMenuItem); - _menuItems.Remove(item); + _menuItems.Remove(item); + item.Update(null, null, null); RemoveItem(item); + + item.Dispose(); } - private void InsertAt(int index, IAvnAppMenuItem item) + private void MoveExistingTo(int index, IAvnAppMenuItem item) { - if (item.ManagedMenuItem == null) - { - throw new InvalidOperationException("Cannot insert item that with Managed link null"); - } + _menuItems.Remove(item); + _menuItems.Insert(index, item); + RemoveItem(item); + InsertItem(index, item); + } + + private void InsertNewAt(int index, IAvnAppMenuItem item) + { _menuItemLookup.Add(item.ManagedMenuItem, item); _menuItems.Insert(index, item); @@ -53,7 +60,7 @@ namespace Avalonia.Native.Interop ManagedMenu = menu; } else if (ManagedMenu != menu) - { + { ManagedMenu = menu; } @@ -75,34 +82,36 @@ namespace Avalonia.Native.Interop { IAvnAppMenuItem nativeItem = null; - if (i >= _menuItems.Count || menu.Items[i] != _menuItems[i].ManagedMenuItem) + if (i >= _menuItems.Count) { - if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) - { - Remove(nativeItem); - } - else - { - nativeItem = CreateNew(factory, menu.Items[i]); - } + nativeItem = CreateNew(factory, menu.Items[i]); + + InsertNewAt(i, nativeItem); } - else + else if (menu.Items[i] == _menuItems[i].ManagedMenuItem) { nativeItem = _menuItems[i]; - Remove(nativeItem); + } + else if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem)) + { + MoveExistingTo(i, nativeItem); + } + else + { + nativeItem = CreateNew(factory, menu.Items[i]); + + InsertNewAt(i, nativeItem); } if (menu.Items[i] is NativeMenuItem nmi) { disposables.Add(nativeItem.Update(exporter, factory, nmi)); } - - InsertAt(i, nativeItem); } - for (int i = menu.Items.Count; i < _menuItems.Count; i++) + while (_menuItems.Count > menu.Items.Count) { - Remove(_menuItems[i]); + RemoveAndDispose(_menuItems[_menuItems.Count - 1]); } return disposables; From 85e15ef9dafcfae705663e6f6e9fa8eb6cbcfffa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Apr 2020 09:39:39 -0300 Subject: [PATCH 028/164] menu items subscribes to events itself. --- src/Avalonia.Native/IAvnAppMenuItem.cs | 70 +++++++++++++++----------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 7c53b38322..281edc26cf 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -9,36 +9,31 @@ namespace Avalonia.Native.Interop { private IAvnAppMenu _subMenu; private AvaloniaNativeMenuExporter _exporter; + private CompositeDisposable _propertyDisposables = new CompositeDisposable(); + private PredicateCallback _currentAction; public NativeMenuItemBase ManagedMenuItem { get; set; } - internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) + private void UpdateTitle(string title) { - var disposables = new CompositeDisposable(); - - _exporter = exporter; - - ManagedMenuItem = item; - - ManagedMenuItem.PropertyChanged += OnMenuItemPropertyChanged; - - disposables.Add(Disposable.Create(() => ManagedMenuItem.PropertyChanged -= OnMenuItemPropertyChanged)); - - if(!string.IsNullOrWhiteSpace(item.Header)) + using (var buffer = new Utf8Buffer(string.IsNullOrWhiteSpace(title) ? "" : title)) { - using (var buffer = new Utf8Buffer(item.Header)) - { - Title = buffer.DangerousGetHandle(); - } + Title = buffer.DangerousGetHandle(); } + } - if (item.Gesture != null) + private void UpdateGesture(Input.KeyGesture gesture) + { + // todo ensure backend can cope with setting null gesture. + using (var buffer = new Utf8Buffer(gesture == null ? "" : OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(gesture.Key))) { - using (var buffer = new Utf8Buffer(OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(item.Gesture.Key))) - { - SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers); - } - } + SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)gesture.KeyModifiers); + } + } + + private void UpdateAction (NativeMenuItem item) + { + _currentAction?.Dispose(); SetAction(new PredicateCallback(() => { @@ -49,6 +44,30 @@ namespace Avalonia.Native.Interop return false; }), new MenuActionCallback(() => { item.RaiseClick(); })); + } + + internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) + { + var disposables = new CompositeDisposable(); + + _exporter = exporter; + + ManagedMenuItem = item; + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) + .Subscribe(x => UpdateTitle(x)))); + + UpdateTitle(item.Header); + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) + .Subscribe(x => UpdateGesture(x)))); + + UpdateGesture(item.Gesture); + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) + .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)))); + + UpdateAction(ManagedMenuItem as NativeMenuItem); if (item.Menu != null) { @@ -58,7 +77,7 @@ namespace Avalonia.Native.Interop } SetSubMenu(_subMenu); - + disposables.Add(_subMenu.Update(exporter, factory, item.Menu, item.Header)); } @@ -71,10 +90,5 @@ namespace Avalonia.Native.Interop return disposables; } - - private void OnMenuItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - _exporter.QueueReset(); - } } } From ef0b8a55b20e9b63fa8b22228f436d3b8a2795b4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 10 Apr 2020 09:52:42 -0300 Subject: [PATCH 029/164] first stage implementation of deinitialisation of menu items. --- src/Avalonia.Native/IAvnAppMenu.cs | 24 +++++++----- src/Avalonia.Native/IAvnAppMenuItem.cs | 51 +++++++++++++++++++------- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 0d4c4f55e4..412980c2c4 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -23,6 +23,8 @@ namespace Avalonia.Native.Interop item.Update(null, null, null); RemoveItem(item); + item.Deinitialise(); + item.Dispose(); } @@ -35,12 +37,18 @@ namespace Avalonia.Native.Interop InsertItem(index, item); } - private void InsertNewAt(int index, IAvnAppMenuItem item) + private IAvnAppMenuItem CreateNewAt(IAvaloniaNativeFactory factory, int index, NativeMenuItemBase item) { - _menuItemLookup.Add(item.ManagedMenuItem, item); - _menuItems.Insert(index, item); + var result = CreateNew(factory, item); - InsertItem(index, item); + result.Initialise(); + + _menuItemLookup.Add(result.ManagedMenuItem, result); + _menuItems.Insert(index, result); + + InsertItem(index, result); + + return result; } private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) @@ -84,9 +92,7 @@ namespace Avalonia.Native.Interop if (i >= _menuItems.Count) { - nativeItem = CreateNew(factory, menu.Items[i]); - - InsertNewAt(i, nativeItem); + nativeItem = CreateNewAt(factory, i, menu.Items[i]); } else if (menu.Items[i] == _menuItems[i].ManagedMenuItem) { @@ -98,9 +104,7 @@ namespace Avalonia.Native.Interop } else { - nativeItem = CreateNew(factory, menu.Items[i]); - - InsertNewAt(i, nativeItem); + nativeItem = CreateNewAt(factory, i, menu.Items[i]); } if (menu.Items[i] is NativeMenuItem nmi) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 281edc26cf..1dff5ff875 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -10,7 +10,7 @@ namespace Avalonia.Native.Interop private IAvnAppMenu _subMenu; private AvaloniaNativeMenuExporter _exporter; private CompositeDisposable _propertyDisposables = new CompositeDisposable(); - private PredicateCallback _currentAction; + private IDisposable _currentActionDisposable; public NativeMenuItemBase ManagedMenuItem { get; set; } @@ -33,9 +33,9 @@ namespace Avalonia.Native.Interop private void UpdateAction (NativeMenuItem item) { - _currentAction?.Dispose(); + _currentActionDisposable?.Dispose(); - SetAction(new PredicateCallback(() => + var action = new PredicateCallback(() => { if (item.Command != null || item.HasClickHandlers) { @@ -43,7 +43,41 @@ namespace Avalonia.Native.Interop } return false; - }), new MenuActionCallback(() => { item.RaiseClick(); })); + }); + + var callback = new MenuActionCallback(() => { item.RaiseClick(); }); + + _currentActionDisposable = Disposable.Create(() => + { + action.Dispose(); + callback.Dispose(); + }); + + SetAction(action, callback); + } + + internal void Initialise() + { + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) + .Subscribe(x => UpdateTitle(x)))); + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) + .Subscribe(x => UpdateGesture(x)))); + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) + .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)))); + } + + internal void Deinitialise () + { + if(_subMenu != null) + { + _subMenu.Update(null, null, null); + _subMenu = null; + } + + _propertyDisposables?.Dispose(); + _currentActionDisposable?.Dispose(); } internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) @@ -54,19 +88,10 @@ namespace Avalonia.Native.Interop ManagedMenuItem = item; - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) - .Subscribe(x => UpdateTitle(x)))); - UpdateTitle(item.Header); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) - .Subscribe(x => UpdateGesture(x)))); - UpdateGesture(item.Gesture); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) - .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)))); - UpdateAction(ManagedMenuItem as NativeMenuItem); if (item.Menu != null) From bc62352b76a955de9a093aa39f5ab510b8e8fe9e Mon Sep 17 00:00:00 2001 From: Michael Babienco Date: Thu, 12 Mar 2020 16:32:28 -0400 Subject: [PATCH 030/164] Add SetIsChecked to AvnAppMenuItem --- native/Avalonia.Native/inc/avalonia-native.h | 1 + native/Avalonia.Native/src/OSX/menu.h | 2 ++ native/Avalonia.Native/src/OSX/menu.mm | 6 ++++++ 3 files changed, 9 insertions(+) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index ade2afe014..584a457539 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -407,6 +407,7 @@ AVNCOM(IAvnAppMenuItem, 19) : IUnknown virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; + virtual HRESULT SetIsChecked (bool isChecked) = 0; }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index f66abb930b..cbfd94d8b6 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -46,6 +46,8 @@ public: virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override; + virtual HRESULT SetIsChecked (bool isChecked) override; + bool EvaluateItemEnabled(); void RaiseOnClicked(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index d5c783f7d4..2abee42e11 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -110,6 +110,12 @@ HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionC return S_OK; } +HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) +{ + [_native setState:(isChecked ? NSOnState : NSOffState)]; + return S_OK; +} + bool AvnAppMenuItem::EvaluateItemEnabled() { if(_predicate != nullptr) From 780723194721992916f8cfbe2159f53e677bd0e6 Mon Sep 17 00:00:00 2001 From: Michael Babienco Date: Thu, 12 Mar 2020 17:16:54 -0400 Subject: [PATCH 031/164] Add IsChecked to NativeMenuItem # Conflicts: # src/Avalonia.Controls/NativeMenuItem.cs --- src/Avalonia.Controls/NativeMenuItem.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 4a19417de3..a5c93a99a5 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -11,6 +11,7 @@ namespace Avalonia.Controls private KeyGesture _gesture; private bool _enabled = true; private ICommand _command; + private bool _isChecked = false; private NativeMenu _menu; @@ -86,6 +87,24 @@ namespace Avalonia.Controls get => _gesture; set => SetAndRaise(GestureProperty, ref _gesture, value); } + get => GetValue(GestureProperty); + set => SetValue(GestureProperty, value); + } + + public static readonly DirectProperty IsCheckedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsChecked), + o => o._isChecked, + (o, v) => o._isChecked = v, + defaultBindingMode: Data.BindingMode.TwoWay); + + public bool IsChecked + { + get => GetValue(IsCheckedProperty); + set => SetValue(IsCheckedProperty, value); + } + + private ICommand _command; public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( From f8756416be21a8afd8fa8f9e873a86a305c98816 Mon Sep 17 00:00:00 2001 From: Michael Babienco Date: Fri, 13 Mar 2020 10:03:44 -0400 Subject: [PATCH 032/164] Update sample for macOS checked menu item --- samples/ControlCatalog/MainWindow.xaml | 9 +++++++++ .../ViewModels/MainWindowViewModel.cs | 15 +++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index d25de9c1f5..1f48350448 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -36,6 +36,15 @@ + + + + + + + diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 89e7653618..b6aa3e92cd 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -10,6 +10,8 @@ namespace ControlCatalog.ViewModels { private IManagedNotificationManager _notificationManager; + private bool _isMenuItemChecked = true; + public MainWindowViewModel(IManagedNotificationManager notificationManager) { _notificationManager = notificationManager; @@ -42,6 +44,11 @@ namespace ControlCatalog.ViewModels { (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown(); }); + + ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() => + { + IsMenuItemChecked = !IsMenuItemChecked; + }); } public IManagedNotificationManager NotificationManager @@ -50,6 +57,12 @@ namespace ControlCatalog.ViewModels set { this.RaiseAndSetIfChanged(ref _notificationManager, value); } } + public bool IsMenuItemChecked + { + get { return _isMenuItemChecked; } + set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); } + } + public ReactiveCommand ShowCustomManagedNotificationCommand { get; } public ReactiveCommand ShowManagedNotificationCommand { get; } @@ -59,5 +72,7 @@ namespace ControlCatalog.ViewModels public ReactiveCommand AboutCommand { get; } public ReactiveCommand ExitCommand { get; } + + public ReactiveCommand ToggleMenuItemCheckedCommand { get; } } } From cf0fb8069605d2e0409393f41c815f7b9d610c42 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 09:58:55 -0300 Subject: [PATCH 033/164] Fix IsCheckedProperty on managed side. --- src/Avalonia.Controls/NativeMenuItem.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index a5c93a99a5..8e3ea1fb29 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -86,25 +86,19 @@ namespace Avalonia.Controls { get => _gesture; set => SetAndRaise(GestureProperty, ref _gesture, value); - } - get => GetValue(GestureProperty); - set => SetValue(GestureProperty, value); } public static readonly DirectProperty IsCheckedProperty = AvaloniaProperty.RegisterDirect( nameof(IsChecked), - o => o._isChecked, - (o, v) => o._isChecked = v, - defaultBindingMode: Data.BindingMode.TwoWay); + o => o.IsChecked, + (o, v) => o.IsChecked = v); public bool IsChecked { - get => GetValue(IsCheckedProperty); - set => SetValue(IsCheckedProperty, value); - } - - private ICommand _command; + get => _isChecked; + set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); + } public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( From 64d76ce5621547cca73bdf6a150a2a10ea76bc89 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 10:04:34 -0300 Subject: [PATCH 034/164] exporter sets IsChecked property. --- src/Avalonia.Native/IAvnAppMenuItem.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 1dff5ff875..66dde71a80 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -22,6 +22,11 @@ namespace Avalonia.Native.Interop } } + private void UpdateIsChecked (bool isChecked) + { + IsChecked = isChecked; + } + private void UpdateGesture(Input.KeyGesture gesture) { // todo ensure backend can cope with setting null gesture. @@ -66,6 +71,9 @@ namespace Avalonia.Native.Interop _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)))); + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) + .Subscribe(x => UpdateIsChecked(x)))); } internal void Deinitialise () @@ -94,6 +102,8 @@ namespace Avalonia.Native.Interop UpdateAction(ManagedMenuItem as NativeMenuItem); + UpdateIsChecked(item.IsChecked); + if (item.Menu != null) { if (_subMenu == null) From 2f13cbff5f6f91bd69cb48256f509982d3bf4efa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 10:50:39 -0300 Subject: [PATCH 035/164] dont return disposables, dispose as items are removed. --- .../AvaloniaNativeMenuExporter.cs | 17 ++-- src/Avalonia.Native/IAvnAppMenu.cs | 51 ++++++------ src/Avalonia.Native/IAvnAppMenuItem.cs | 79 ++++++++++--------- 3 files changed, 77 insertions(+), 70 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 1c36510817..281af31bc2 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -15,8 +15,7 @@ namespace Avalonia.Native private bool _exported = false; private IAvnWindow _nativeWindow; private NativeMenu _menu; - private IAvnAppMenu _nativeMenu; - private IDisposable _disposable; + private IAvnAppMenu _nativeMenu; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -109,6 +108,8 @@ namespace Avalonia.Native if (_nativeMenu is null) { _nativeMenu = _factory.CreateMenu(); + + _nativeMenu.Initialise(menu, ""); } } @@ -131,9 +132,8 @@ namespace Avalonia.Native menuItem.Menu = menu; var setMenu = _nativeMenu.ManagedMenu != appMenuHolder; - - _disposable?.Dispose(); - _disposable = _nativeMenu.Update(this, _factory, appMenuHolder); + + _nativeMenu.Update(_factory, appMenuHolder); if (setMenu) { @@ -150,11 +150,12 @@ namespace Avalonia.Native if (_nativeMenu is null) { _nativeMenu = _factory.CreateMenu(); + + _nativeMenu.Initialise(menu, ""); } } - - _disposable?.Dispose(); - _disposable = _nativeMenu.Update(this, _factory, menu); + + _nativeMenu.Update(_factory, menu); avnWindow.SetMainMenu(_nativeMenu); } diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 412980c2c4..94e48cdff1 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -9,9 +9,9 @@ namespace Avalonia.Native.Interop { public partial class IAvnAppMenu { - private AvaloniaNativeMenuExporter _exporter; private List _menuItems = new List(); private Dictionary _menuItemLookup = new Dictionary(); + private CompositeDisposable _propertyDisposables = new CompositeDisposable(); internal NativeMenu ManagedMenu { get; private set; } @@ -19,12 +19,9 @@ namespace Avalonia.Native.Interop { _menuItemLookup.Remove(item.ManagedMenuItem); _menuItems.Remove(item); - - item.Update(null, null, null); RemoveItem(item); item.Deinitialise(); - item.Dispose(); } @@ -41,7 +38,7 @@ namespace Avalonia.Native.Interop { var result = CreateNew(factory, item); - result.Initialise(); + result.Initialise(item); _menuItemLookup.Add(result.ManagedMenuItem, result); _menuItems.Insert(index, result); @@ -59,25 +56,12 @@ namespace Avalonia.Native.Interop return nativeItem; } - internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenu menu, string title = "") + internal void Initialise(NativeMenu managedMenu, string title) { - var disposables = new CompositeDisposable(); - - if (ManagedMenu == null) - { - ManagedMenu = menu; - } - else if (ManagedMenu != menu) - { - ManagedMenu = menu; - } - - _exporter = exporter; + ManagedMenu = managedMenu; ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged += OnMenuItemsChanged; - disposables.Add(Disposable.Create(() => ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged)); - if (!string.IsNullOrWhiteSpace(title)) { using (var buffer = new Utf8Buffer(title)) @@ -85,10 +69,29 @@ namespace Avalonia.Native.Interop Title = buffer.DangerousGetHandle(); } } + } + + internal void Deinitialise() + { + ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged; + + foreach(var item in _menuItems) + { + item.Deinitialise(); + item.Dispose(); + } + } + + internal void Update(IAvaloniaNativeFactory factory, NativeMenu menu) + { + if(menu != ManagedMenu) + { + throw new ArgumentException("The menu being updated does not match.", nameof(menu)); + } for (int i = 0; i < menu.Items.Count; i++) { - IAvnAppMenuItem nativeItem = null; + IAvnAppMenuItem nativeItem; if (i >= _menuItems.Count) { @@ -109,7 +112,7 @@ namespace Avalonia.Native.Interop if (menu.Items[i] is NativeMenuItem nmi) { - disposables.Add(nativeItem.Update(exporter, factory, nmi)); + nativeItem.Update(factory, nmi); } } @@ -117,13 +120,11 @@ namespace Avalonia.Native.Interop { RemoveAndDispose(_menuItems[_menuItems.Count - 1]); } - - return disposables; } private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { - _exporter.QueueReset(); + // update menu items. } } } diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 66dde71a80..699d02caa5 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -7,8 +7,7 @@ namespace Avalonia.Native.Interop { public partial class IAvnAppMenuItem { - private IAvnAppMenu _subMenu; - private AvaloniaNativeMenuExporter _exporter; + private IAvnAppMenu _subMenu; private CompositeDisposable _propertyDisposables = new CompositeDisposable(); private IDisposable _currentActionDisposable; @@ -22,7 +21,7 @@ namespace Avalonia.Native.Interop } } - private void UpdateIsChecked (bool isChecked) + private void UpdateIsChecked(bool isChecked) { IsChecked = isChecked; } @@ -33,10 +32,10 @@ namespace Avalonia.Native.Interop using (var buffer = new Utf8Buffer(gesture == null ? "" : OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(gesture.Key))) { SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)gesture.KeyModifiers); - } + } } - private void UpdateAction (NativeMenuItem item) + private void UpdateAction(NativeMenuItem item) { _currentActionDisposable?.Dispose(); @@ -61,26 +60,40 @@ namespace Avalonia.Native.Interop SetAction(action, callback); } - internal void Initialise() + internal void Initialise(NativeMenuItemBase nativeMenuItem) { - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) - .Subscribe(x => UpdateTitle(x)))); + ManagedMenuItem = nativeMenuItem; + + if (ManagedMenuItem is NativeMenuItem item) + { + UpdateTitle(item.Header); + + UpdateGesture(item.Gesture); + + UpdateAction(ManagedMenuItem as NativeMenuItem); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) - .Subscribe(x => UpdateGesture(x)))); + UpdateIsChecked(item.IsChecked); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) - .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)))); + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) + .Subscribe(x => UpdateTitle(x)))); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) - .Subscribe(x => UpdateIsChecked(x)))); + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) + .Subscribe(x => UpdateGesture(x)))); + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) + .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)))); + + _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) + .Subscribe(x => UpdateIsChecked(x)))); + } } - internal void Deinitialise () + internal void Deinitialise() { - if(_subMenu != null) + if (_subMenu != null) { - _subMenu.Update(null, null, null); + _subMenu.Deinitialise(); + _subMenu.Dispose(); _subMenu = null; } @@ -88,42 +101,34 @@ namespace Avalonia.Native.Interop _currentActionDisposable?.Dispose(); } - internal IDisposable Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) - { - var disposables = new CompositeDisposable(); - - _exporter = exporter; - - ManagedMenuItem = item; - - UpdateTitle(item.Header); - - UpdateGesture(item.Gesture); - - UpdateAction(ManagedMenuItem as NativeMenuItem); - - UpdateIsChecked(item.IsChecked); + internal void Update(IAvaloniaNativeFactory factory, NativeMenuItem item) + { + if(item != ManagedMenuItem) + { + throw new ArgumentException("The item does not match the menuitem being updated.", nameof(item)); + } if (item.Menu != null) { if (_subMenu == null) { _subMenu = factory.CreateMenu(); + + _subMenu.Initialise(item.Menu, item.Header); } SetSubMenu(_subMenu); - disposables.Add(_subMenu.Update(exporter, factory, item.Menu, item.Header)); + _subMenu.Update(factory, item.Menu); } if (item.Menu == null && _subMenu != null) { - // todo remove submenu. + _subMenu.Deinitialise(); + _subMenu.Dispose(); - // needs implementing on native side also. + SetSubMenu(null); } - - return disposables; } } } From fa2b813054543a4dfc2b9f67e99b142e12ea4ce2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 10:54:45 -0300 Subject: [PATCH 036/164] queue full tree updates when menu items change. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 4 ++-- src/Avalonia.Native/IAvnAppMenu.cs | 12 +++++++----- src/Avalonia.Native/IAvnAppMenuItem.cs | 4 ++-- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 281af31bc2..6c375b8ffb 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -109,7 +109,7 @@ namespace Avalonia.Native { _nativeMenu = _factory.CreateMenu(); - _nativeMenu.Initialise(menu, ""); + _nativeMenu.Initialise(this, menu, ""); } } @@ -151,7 +151,7 @@ namespace Avalonia.Native { _nativeMenu = _factory.CreateMenu(); - _nativeMenu.Initialise(menu, ""); + _nativeMenu.Initialise(this, menu, ""); } } diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnAppMenu.cs index 94e48cdff1..f45fcb6004 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnAppMenu.cs @@ -9,6 +9,7 @@ namespace Avalonia.Native.Interop { public partial class IAvnAppMenu { + private AvaloniaNativeMenuExporter _exporter; private List _menuItems = new List(); private Dictionary _menuItemLookup = new Dictionary(); private CompositeDisposable _propertyDisposables = new CompositeDisposable(); @@ -56,8 +57,9 @@ namespace Avalonia.Native.Interop return nativeItem; } - internal void Initialise(NativeMenu managedMenu, string title) + internal void Initialise(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title) { + _exporter = exporter; ManagedMenu = managedMenu; ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged += OnMenuItemsChanged; @@ -75,7 +77,7 @@ namespace Avalonia.Native.Interop { ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged; - foreach(var item in _menuItems) + foreach (var item in _menuItems) { item.Deinitialise(); item.Dispose(); @@ -84,7 +86,7 @@ namespace Avalonia.Native.Interop internal void Update(IAvaloniaNativeFactory factory, NativeMenu menu) { - if(menu != ManagedMenu) + if (menu != ManagedMenu) { throw new ArgumentException("The menu being updated does not match.", nameof(menu)); } @@ -112,7 +114,7 @@ namespace Avalonia.Native.Interop if (menu.Items[i] is NativeMenuItem nmi) { - nativeItem.Update(factory, nmi); + nativeItem.Update(_exporter, factory, nmi); } } @@ -124,7 +126,7 @@ namespace Avalonia.Native.Interop private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { - // update menu items. + _exporter.QueueReset(); } } } diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 699d02caa5..1a031bd6dd 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -101,7 +101,7 @@ namespace Avalonia.Native.Interop _currentActionDisposable?.Dispose(); } - internal void Update(IAvaloniaNativeFactory factory, NativeMenuItem item) + internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) { if(item != ManagedMenuItem) { @@ -114,7 +114,7 @@ namespace Avalonia.Native.Interop { _subMenu = factory.CreateMenu(); - _subMenu.Initialise(item.Menu, item.Header); + _subMenu.Initialise(exporter, item.Menu, item.Header); } SetSubMenu(_subMenu); From ed371aa0eef3fb6ddd89afb51fcd9ce793397d8a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 11:22:22 -0300 Subject: [PATCH 037/164] ensure app menu holds its stub. --- .../AvaloniaNativeMenuExporter.cs | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 6c375b8ffb..853ed0fb5a 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -101,18 +101,6 @@ namespace Avalonia.Native private void SetMenu(NativeMenu menu) { - if (_nativeMenu is null) - { - _nativeMenu = _factory.ObtainAppMenu(); - - if (_nativeMenu is null) - { - _nativeMenu = _factory.CreateMenu(); - - _nativeMenu.Initialise(this, menu, ""); - } - } - var menuItem = menu.Parent; var appMenuHolder = menuItem?.Parent; @@ -131,6 +119,18 @@ namespace Avalonia.Native menuItem.Menu = menu; + if (_nativeMenu is null) + { + _nativeMenu = _factory.ObtainAppMenu(); + + if (_nativeMenu is null) + { + _nativeMenu = _factory.CreateMenu(); + + _nativeMenu.Initialise(this, appMenuHolder, ""); + } + } + var setMenu = _nativeMenu.ManagedMenu != appMenuHolder; _nativeMenu.Update(_factory, appMenuHolder); From cd5c8cedce154e85b93132b8be4b9cee3f47ce84 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 11:41:46 -0300 Subject: [PATCH 038/164] correctly set appmenu --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 853ed0fb5a..addbd756e6 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -119,6 +119,8 @@ namespace Avalonia.Native menuItem.Menu = menu; + var setMenu = false; + if (_nativeMenu is null) { _nativeMenu = _factory.ObtainAppMenu(); @@ -128,10 +130,10 @@ namespace Avalonia.Native _nativeMenu = _factory.CreateMenu(); _nativeMenu.Initialise(this, appMenuHolder, ""); + + setMenu = true; } } - - var setMenu = _nativeMenu.ManagedMenu != appMenuHolder; _nativeMenu.Update(_factory, appMenuHolder); From a05a613ff3d1c18417d677dee2e7569d7d23a707 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 11:41:55 -0300 Subject: [PATCH 039/164] fix setting gestures --- src/Avalonia.Native/IAvnAppMenuItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 1a031bd6dd..81f12f3866 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -31,7 +31,8 @@ namespace Avalonia.Native.Interop // todo ensure backend can cope with setting null gesture. using (var buffer = new Utf8Buffer(gesture == null ? "" : OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(gesture.Key))) { - SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)gesture.KeyModifiers); + var modifiers = gesture == null ? AvnInputModifiers.AvnInputModifiersNone : (AvnInputModifiers)gesture.KeyModifiers; + SetGesture(buffer.DangerousGetHandle(), modifiers); } } From 2ee180b12c11da20d5051ba60e889795e9c61d92 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 12:00:39 -0300 Subject: [PATCH 040/164] fix menu property updates. --- src/Avalonia.Native/IAvnAppMenuItem.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 81f12f3866..042ad52b5a 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -75,17 +75,17 @@ namespace Avalonia.Native.Interop UpdateIsChecked(item.IsChecked); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) - .Subscribe(x => UpdateTitle(x)))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) + .Subscribe(x => UpdateTitle(x))); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) - .Subscribe(x => UpdateGesture(x)))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty) + .Subscribe(x => UpdateGesture(x))); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) - .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) + .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem))); - _propertyDisposables.Add(Disposable.Create(() => ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) - .Subscribe(x => UpdateIsChecked(x)))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) + .Subscribe(x => UpdateIsChecked(x))); } } From 5765c869c5cbca4cf69a669d1b76e7971d4239a1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 12:09:01 -0300 Subject: [PATCH 041/164] allow setting submenu to null. --- native/Avalonia.Native/src/OSX/menu.mm | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 2abee42e11..2e94de7207 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -67,9 +67,16 @@ NSMenuItem* AvnAppMenuItem::GetNative() HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) { - auto nsMenu = dynamic_cast(menu)->GetNative(); - - [_native setSubmenu: nsMenu]; + if(menu != nullptr) + { + auto nsMenu = dynamic_cast(menu)->GetNative(); + + [_native setSubmenu: nsMenu]; + } + else + { + [_native setSubmenu: nullptr]; + } return S_OK; } From 87a5f3962ae55f899cb6a70c0bd0e65cddc28740 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 12:10:24 -0300 Subject: [PATCH 042/164] set submenu to null when its removed. --- src/Avalonia.Native/IAvnAppMenuItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index 042ad52b5a..eb270abe8a 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -93,6 +93,7 @@ namespace Avalonia.Native.Interop { if (_subMenu != null) { + SetSubMenu(null); _subMenu.Deinitialise(); _subMenu.Dispose(); _subMenu = null; From f1d23fd4c46eba6fd3706bda2ac1ce5ddb4d13aa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 12:13:42 -0300 Subject: [PATCH 043/164] only set submenu once. --- src/Avalonia.Native/IAvnAppMenuItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnAppMenuItem.cs index eb270abe8a..268a70a751 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnAppMenuItem.cs @@ -117,9 +117,9 @@ namespace Avalonia.Native.Interop _subMenu = factory.CreateMenu(); _subMenu.Initialise(exporter, item.Menu, item.Header); - } - SetSubMenu(_subMenu); + SetSubMenu(_subMenu); + } _subMenu.Update(factory, item.Menu); } From ad645d40374f72bc14add43b4e841c6b6e847b1c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 13 Apr 2020 12:52:29 -0300 Subject: [PATCH 044/164] attempt to add NSMenuDelegate --- native/Avalonia.Native/src/OSX/menu.h | 3 +-- native/Avalonia.Native/src/OSX/menu.mm | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index cbfd94d8b6..3c68cc222b 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -14,8 +14,7 @@ class AvnAppMenuItem; class AvnAppMenu; -@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain -- (void)setMenu:(NSMenu*) menu; +@interface AvnMenu : NSMenu @end @interface AvnMenuItem : NSMenuItem diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 2e94de7207..55a6f385d8 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -4,6 +4,20 @@ #include "window.h" @implementation AvnMenu +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + printf("TEST"); +} + +- (void)menuWillOpen:(NSMenu *)menu +{ + +} + +- (void)menuDidClose:(NSMenu *)menu +{ + +} @end @implementation AvnMenuItem @@ -146,11 +160,13 @@ void AvnAppMenuItem::RaiseOnClicked() AvnAppMenu::AvnAppMenu() { _native = [AvnMenu new]; + [_native setDelegate:_native]; } AvnAppMenu::AvnAppMenu(AvnMenu* native) { _native = native; + [_native setDelegate:_native]; } AvnMenu* AvnAppMenu::GetNative() From 30975465984ea8be8e9d105587295fe4257b5ca1 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 19 Apr 2020 16:53:29 +0300 Subject: [PATCH 045/164] What the actual duck --- native/Avalonia.Native/src/OSX/menu.h | 11 +++-- native/Avalonia.Native/src/OSX/menu.mm | 57 ++++++++++++++++++-------- 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 3c68cc222b..cb13713a54 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -14,7 +14,8 @@ class AvnAppMenuItem; class AvnAppMenu; -@interface AvnMenu : NSMenu +@interface AvnMenu : NSMenu +- (id) initWithDelegate: (NSObject*) del; @end @interface AvnMenuItem : NSMenuItem @@ -62,9 +63,7 @@ public: FORWARD_IUNKNOWN() AvnAppMenu(); - - AvnAppMenu(AvnMenu* native); - + AvnMenu* GetNative(); virtual HRESULT InsertItem (int index, IAvnAppMenuItem* item) override; @@ -77,5 +76,9 @@ public: }; +@interface AvnMenuDelegate : NSObject +- (id) initWithParent: (AvnAppMenu*) parent; +@end + #endif diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 55a6f385d8..5eed5a88e4 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -4,20 +4,17 @@ #include "window.h" @implementation AvnMenu -- (void)menuNeedsUpdate:(NSMenu *)menu { - printf("TEST"); + NSObject* _wtf; } - -- (void)menuWillOpen:(NSMenu *)menu +- (id) initWithDelegate: (NSObject*)del { - + self = [super init]; + self.delegate = del; + _wtf = del; + return self; } -- (void)menuDidClose:(NSMenu *)menu -{ - -} @end @implementation AvnMenuItem @@ -159,15 +156,10 @@ void AvnAppMenuItem::RaiseOnClicked() AvnAppMenu::AvnAppMenu() { - _native = [AvnMenu new]; - [_native setDelegate:_native]; + id del = [[AvnMenuDelegate alloc] initWithParent: this]; + _native = [[AvnMenu alloc] initWithDelegate: del]; } -AvnAppMenu::AvnAppMenu(AvnMenu* native) -{ - _native = native; - [_native setDelegate:_native]; -} AvnMenu* AvnAppMenu::GetNative() { @@ -214,12 +206,41 @@ HRESULT AvnAppMenu::Clear() return S_OK; } +@implementation AvnMenuDelegate +{ + ComPtr _parent; +} +- (id) initWithParent:(AvnAppMenu *)parent +{ + self = [super init]; + _parent = parent; + return self; +} +- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel +{ + if(shouldCancel) + return NO; + return YES; +} + +- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu +{ + return [menu numberOfItems]; +} + +- (void)menuNeedsUpdate:(NSMenu *)menu +{ + printf("NEEDSUPDATE\n"); +} + + +@end + extern IAvnAppMenu* CreateAppMenu() { @autoreleasepool { - id menuBar = [NSMenu new]; - return new AvnAppMenu(menuBar); + return new AvnAppMenu(); } } From 4aa10242cc801ccb978408ed76eec2de9a83f291 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 11:50:28 -0300 Subject: [PATCH 046/164] rename interfaces and fix most native code warnings. --- native/Avalonia.Native/inc/avalonia-native.h | 37 ++++++++++++------- native/Avalonia.Native/src/OSX/app.mm | 3 +- native/Avalonia.Native/src/OSX/common.h | 10 ++--- native/Avalonia.Native/src/OSX/main.mm | 13 +++---- native/Avalonia.Native/src/OSX/menu.h | 10 ++--- native/Avalonia.Native/src/OSX/menu.mm | 18 ++++----- .../src/OSX/platformthreading.mm | 4 +- native/Avalonia.Native/src/OSX/window.h | 2 +- native/Avalonia.Native/src/OSX/window.mm | 6 +-- .../AvaloniaNativeMenuExporter.cs | 2 +- .../{IAvnAppMenu.cs => IAvnMenu.cs} | 16 ++++---- .../{IAvnAppMenuItem.cs => IAvnMenuItem.cs} | 4 +- 12 files changed, 68 insertions(+), 57 deletions(-) rename src/Avalonia.Native/{IAvnAppMenu.cs => IAvnMenu.cs} (84%) rename src/Avalonia.Native/{IAvnAppMenuItem.cs => IAvnMenuItem.cs} (97%) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 584a457539..55592aba51 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -19,8 +19,9 @@ struct IAvnGlContext; struct IAvnGlDisplay; struct IAvnGlSurfaceRenderTarget; struct IAvnGlSurfaceRenderingSession; -struct IAvnAppMenu; -struct IAvnAppMenuItem; +struct IAvnMenu; +struct IAvnMenuItem; +struct IAvnMenuEvents; enum SystemDecorations { SystemDecorationsNone = 0, @@ -188,11 +189,11 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; - virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0; - virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0; - virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0; - virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0; - virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0; + virtual HRESULT ObtainAppMenu(IAvnMenu** retOut) = 0; + virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0; + virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0; + virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0; }; AVNCOM(IAvnString, 17) : IUnknown @@ -222,8 +223,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetTopMost (bool value) = 0; virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; - virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0; - virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0; + virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0; + virtual HRESULT ObtainMainMenu(IAvnMenu** retOut) = 0; virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0; virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; @@ -388,10 +389,10 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown virtual HRESULT GetScaling(double* ret) = 0; }; -AVNCOM(IAvnAppMenu, 17) : IUnknown +AVNCOM(IAvnMenu, 17) : IUnknown { - virtual HRESULT InsertItem (int index, IAvnAppMenuItem* item) = 0; - virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0; + virtual HRESULT InsertItem (int index, IAvnMenuItem* item) = 0; + virtual HRESULT RemoveItem (IAvnMenuItem* item) = 0; virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT Clear () = 0; }; @@ -401,13 +402,21 @@ AVNCOM(IAvnPredicateCallback, 18) : IUnknown virtual bool Evaluate() = 0; }; -AVNCOM(IAvnAppMenuItem, 19) : IUnknown +AVNCOM(IAvnMenuItem, 19) : IUnknown { - virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0; + virtual HRESULT SetSubMenu (IAvnMenu* menu) = 0; virtual HRESULT SetTitle (void* utf8String) = 0; virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; virtual HRESULT SetIsChecked (bool isChecked) = 0; }; +AVNCOM(IAvnMenuEvents, 1A) : IUnknown +{ + /** + * NeedsUpdate + */ + virtual bool NeedUpdate () = 0; +}; + extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/app.mm b/native/Avalonia.Native/src/OSX/app.mm index 5c50aad4cc..c30c2c3d18 100644 --- a/native/Avalonia.Native/src/OSX/app.mm +++ b/native/Avalonia.Native/src/OSX/app.mm @@ -2,7 +2,8 @@ @interface AvnAppDelegate : NSObject @end -extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; +NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular; + @implementation AvnAppDelegate - (void)applicationWillFinishLaunching:(NSNotification *)notification { diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 85403abfe7..591f0a2e46 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -15,11 +15,11 @@ extern IAvnScreens* CreateScreens(); extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); -extern IAvnAppMenu* CreateAppMenu(); -extern IAvnAppMenuItem* CreateAppMenuItem(); -extern IAvnAppMenuItem* CreateAppMenuItemSeperator(); -extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu); -extern IAvnAppMenu* GetAppMenu (); +extern IAvnMenu* CreateAppMenu(); +extern IAvnMenuItem* CreateAppMenuItem(); +extern IAvnMenuItem* CreateAppMenuItemSeperator(); +extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); +extern IAvnMenu* GetAppMenu (); extern NSMenuItem* GetAppMenuItem (); extern void InitializeAvnApp(); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index a2134de6c1..08d71c286d 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -92,12 +92,11 @@ void SetProcessName(NSString* appTitle) { PrivateLSASN asn = ls_get_current_application_asn_func(); // Constant used by WebKit; what exactly it means is unknown. const int magic_session_constant = -2; - OSErr err = + ls_set_application_information_item_func(magic_session_constant, asn, ls_display_name_key, process_name, NULL /* optional out param */); - //LOG_IF(ERROR, err) << "Call to set process name failed, err " << err; } class MacOptions : public ComSingleObject @@ -228,31 +227,31 @@ public: return S_OK; } - virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { *ppv = ::CreateAppMenu(); return S_OK; } - virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override + virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override { *ppv = ::CreateAppMenuItem(); return S_OK; } - virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override + virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override { *ppv = ::CreateAppMenuItemSeperator(); return S_OK; } - virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override + virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override { ::SetAppMenu(s_appTitle, appMenu); return S_OK; } - virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override + virtual HRESULT ObtainAppMenu(IAvnMenu** retOut) override { if(retOut == nullptr) { diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index cb13713a54..82e60addcb 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -23,7 +23,7 @@ class AvnAppMenu; - (void)didSelectItem:(id)sender; @end -class AvnAppMenuItem : public ComSingleObject +class AvnAppMenuItem : public ComSingleObject { private: NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem @@ -38,7 +38,7 @@ public: NSMenuItem* GetNative(); - virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override; + virtual HRESULT SetSubMenu (IAvnMenu* menu) override; virtual HRESULT SetTitle (void* utf8String) override; @@ -54,7 +54,7 @@ public: }; -class AvnAppMenu : public ComSingleObject +class AvnAppMenu : public ComSingleObject { private: AvnMenu* _native; @@ -66,9 +66,9 @@ public: AvnMenu* GetNative(); - virtual HRESULT InsertItem (int index, IAvnAppMenuItem* item) override; + virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override; - virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override; + virtual HRESULT RemoveItem (IAvnMenuItem* item) override; virtual HRESULT SetTitle (void* utf8String) override; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 5eed5a88e4..6d1ba24f5f 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -76,7 +76,7 @@ NSMenuItem* AvnAppMenuItem::GetNative() return _native; } -HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) +HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) { if(menu != nullptr) { @@ -166,7 +166,7 @@ AvnMenu* AvnAppMenu::GetNative() return _native; } -HRESULT AvnAppMenu::InsertItem(int index, IAvnAppMenuItem *item) +HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { auto avnMenuItem = dynamic_cast(item); @@ -178,7 +178,7 @@ HRESULT AvnAppMenu::InsertItem(int index, IAvnAppMenuItem *item) return S_OK; } -HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item) +HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) { auto avnMenuItem = dynamic_cast(item); @@ -236,7 +236,7 @@ HRESULT AvnAppMenu::Clear() @end -extern IAvnAppMenu* CreateAppMenu() +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb) { @autoreleasepool { @@ -244,7 +244,7 @@ extern IAvnAppMenu* CreateAppMenu() } } -extern IAvnAppMenuItem* CreateAppMenuItem() +extern IAvnMenuItem* CreateAppMenuItem() { @autoreleasepool { @@ -252,7 +252,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem() } } -extern IAvnAppMenuItem* CreateAppMenuItemSeperator() +extern IAvnMenuItem* CreateAppMenuItemSeperator() { @autoreleasepool { @@ -260,10 +260,10 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator() } } -static IAvnAppMenu* s_appMenu = nullptr; +static IAvnMenu* s_appMenu = nullptr; static NSMenuItem* s_appMenuItem = nullptr; -extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) +extern void SetAppMenu (NSString* appName, IAvnMenu* menu) { s_appMenu = menu; @@ -344,7 +344,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) } } -extern IAvnAppMenu* GetAppMenu () +extern IAvnMenu* GetAppMenu () { return s_appMenu; } diff --git a/native/Avalonia.Native/src/OSX/platformthreading.mm b/native/Avalonia.Native/src/OSX/platformthreading.mm index 2d72226faf..f93436d157 100644 --- a/native/Avalonia.Native/src/OSX/platformthreading.mm +++ b/native/Avalonia.Native/src/OSX/platformthreading.mm @@ -54,9 +54,11 @@ private: { public: FORWARD_IUNKNOWN() + bool Running = false; bool Cancelled = false; - virtual void Cancel() + + virtual void Cancel() override { Cancelled = true; if(Running) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 5c85a2f423..a16ce524f0 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -19,7 +19,7 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(void) applyMenu:(NSMenu *)menu; +-(void) applyMenu:(NSMenu *_Nullable)menu; -(double) getScaling; @end diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 6298118c10..ce66a6c327 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -27,7 +27,7 @@ public: NSObject* renderTarget; AvnPoint lastPositionSet; NSString* _lastTitle; - IAvnAppMenu* _mainMenu; + IAvnMenu* _mainMenu; bool _shown; WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl) @@ -234,7 +234,7 @@ public: } } - virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override + virtual HRESULT SetMainMenu(IAvnMenu* menu) override { _mainMenu = menu; @@ -247,7 +247,7 @@ public: return S_OK; } - virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override + virtual HRESULT ObtainMainMenu(IAvnMenu** ret) override { if(ret == nullptr) { diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index addbd756e6..f44e18daf6 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -15,7 +15,7 @@ namespace Avalonia.Native private bool _exported = false; private IAvnWindow _nativeWindow; private NativeMenu _menu; - private IAvnAppMenu _nativeMenu; + private IAvnMenu _nativeMenu; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { diff --git a/src/Avalonia.Native/IAvnAppMenu.cs b/src/Avalonia.Native/IAvnMenu.cs similarity index 84% rename from src/Avalonia.Native/IAvnAppMenu.cs rename to src/Avalonia.Native/IAvnMenu.cs index f45fcb6004..1070053d14 100644 --- a/src/Avalonia.Native/IAvnAppMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -7,16 +7,16 @@ using Avalonia.Platform.Interop; namespace Avalonia.Native.Interop { - public partial class IAvnAppMenu + public partial class IAvnMenu { private AvaloniaNativeMenuExporter _exporter; - private List _menuItems = new List(); - private Dictionary _menuItemLookup = new Dictionary(); + private List _menuItems = new List(); + private Dictionary _menuItemLookup = new Dictionary(); private CompositeDisposable _propertyDisposables = new CompositeDisposable(); internal NativeMenu ManagedMenu { get; private set; } - private void RemoveAndDispose(IAvnAppMenuItem item) + private void RemoveAndDispose(IAvnMenuItem item) { _menuItemLookup.Remove(item.ManagedMenuItem); _menuItems.Remove(item); @@ -26,7 +26,7 @@ namespace Avalonia.Native.Interop item.Dispose(); } - private void MoveExistingTo(int index, IAvnAppMenuItem item) + private void MoveExistingTo(int index, IAvnMenuItem item) { _menuItems.Remove(item); _menuItems.Insert(index, item); @@ -35,7 +35,7 @@ namespace Avalonia.Native.Interop InsertItem(index, item); } - private IAvnAppMenuItem CreateNewAt(IAvaloniaNativeFactory factory, int index, NativeMenuItemBase item) + private IAvnMenuItem CreateNewAt(IAvaloniaNativeFactory factory, int index, NativeMenuItemBase item) { var result = CreateNew(factory, item); @@ -49,7 +49,7 @@ namespace Avalonia.Native.Interop return result; } - private IAvnAppMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) + private IAvnMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item) { var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem(); nativeItem.ManagedMenuItem = item; @@ -93,7 +93,7 @@ namespace Avalonia.Native.Interop for (int i = 0; i < menu.Items.Count; i++) { - IAvnAppMenuItem nativeItem; + IAvnMenuItem nativeItem; if (i >= _menuItems.Count) { diff --git a/src/Avalonia.Native/IAvnAppMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs similarity index 97% rename from src/Avalonia.Native/IAvnAppMenuItem.cs rename to src/Avalonia.Native/IAvnMenuItem.cs index 268a70a751..b49387ea9e 100644 --- a/src/Avalonia.Native/IAvnAppMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -5,9 +5,9 @@ using Avalonia.Platform.Interop; namespace Avalonia.Native.Interop { - public partial class IAvnAppMenuItem + public partial class IAvnMenuItem { - private IAvnAppMenu _subMenu; + private IAvnMenu _subMenu; private CompositeDisposable _propertyDisposables = new CompositeDisposable(); private IDisposable _currentActionDisposable; From e0a5aaf3138a03a199ee2672603bbc0be37bddd6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 11:54:34 -0300 Subject: [PATCH 047/164] allow creating menu with events. --- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 2 +- native/Avalonia.Native/src/OSX/menu.h | 3 ++- native/Avalonia.Native/src/OSX/menu.mm | 5 +++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 591f0a2e46..7a433bfd9f 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -15,7 +15,7 @@ extern IAvnScreens* CreateScreens(); extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); -extern IAvnMenu* CreateAppMenu(); +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeperator(); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 08d71c286d..54ba93c06a 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -229,7 +229,7 @@ public: virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { - *ppv = ::CreateAppMenu(); + *ppv = ::CreateAppMenu(cb); return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 82e60addcb..e76605d362 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -58,11 +58,12 @@ class AvnAppMenu : public ComSingleObject { private: AvnMenu* _native; + ComPtr _baseEvents; public: FORWARD_IUNKNOWN() - AvnAppMenu(); + AvnAppMenu(IAvnMenuEvents* events); AvnMenu* GetNative(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 6d1ba24f5f..9a133b6c0c 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -154,8 +154,9 @@ void AvnAppMenuItem::RaiseOnClicked() } } -AvnAppMenu::AvnAppMenu() +AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events) { + _baseEvents = events; id del = [[AvnMenuDelegate alloc] initWithParent: this]; _native = [[AvnMenu alloc] initWithDelegate: del]; } @@ -240,7 +241,7 @@ extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb) { @autoreleasepool { - return new AvnAppMenu(); + return new AvnAppMenu(cb); } } From 24ea90e2efea68545bbd21060f3f6efb05ad2efb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 12:31:35 -0300 Subject: [PATCH 048/164] working updating menus inside needsupdate event. --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/menu.h | 2 + native/Avalonia.Native/src/OSX/menu.mm | 10 ++++- src/Avalonia.Controls/NativeMenu.cs | 7 ++++ .../AvaloniaNativeMenuExporter.cs | 12 +++++- src/Avalonia.Native/IAvnMenu.cs | 42 +++++++++++++++++++ src/Avalonia.Native/IAvnMenuItem.cs | 2 +- 7 files changed, 72 insertions(+), 5 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 55592aba51..d8d65c2fe6 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -416,7 +416,7 @@ AVNCOM(IAvnMenuEvents, 1A) : IUnknown /** * NeedsUpdate */ - virtual bool NeedUpdate () = 0; + virtual void NeedsUpdate () = 0; }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative(); diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index e76605d362..e4353d10ac 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -67,6 +67,8 @@ public: AvnMenu* GetNative(); + void RaiseNeedsUpdate (); + virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override; virtual HRESULT RemoveItem (IAvnMenuItem* item) override; diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 9a133b6c0c..66f4944226 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -167,6 +167,14 @@ AvnMenu* AvnAppMenu::GetNative() return _native; } +void AvnAppMenu::RaiseNeedsUpdate() +{ + if(_baseEvents != nullptr) + { + _baseEvents->NeedsUpdate(); + } +} + HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { auto avnMenuItem = dynamic_cast(item); @@ -231,7 +239,7 @@ HRESULT AvnAppMenu::Clear() - (void)menuNeedsUpdate:(NSMenu *)menu { - printf("NEEDSUPDATE\n"); + _parent->RaiseNeedsUpdate(); } diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 7c06111a01..91c11f1a58 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -15,12 +15,19 @@ namespace Avalonia.Controls [Content] public IList Items => _items; + public event EventHandler NeedsUpdate; + public NativeMenu() { _items.Validate = Validator; _items.CollectionChanged += ItemsChanged; } + public void RaiseNeedsUpdate () + { + NeedsUpdate?.Invoke(this, EventArgs.Empty); + } + private void Validator(NativeMenuItemBase obj) { if (obj.Parent != null) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index f44e18daf6..ce6b4c7263 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -43,6 +43,14 @@ namespace Avalonia.Native DoLayoutReset(); } + internal void InvalidateMenu () + { + if(_resetQueued) + { + DoLayoutReset(); + } + } + private static NativeMenu CreateDefaultAppMenu() { var result = new NativeMenu(); @@ -127,7 +135,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = _factory.CreateMenu(); + _nativeMenu = IAvnMenu.Create(_factory); _nativeMenu.Initialise(this, appMenuHolder, ""); @@ -151,7 +159,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = _factory.CreateMenu(); + _nativeMenu = IAvnMenu.Create(_factory); _nativeMenu.Initialise(this, menu, ""); } diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 1070053d14..354bf6a6b6 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -7,15 +7,57 @@ using Avalonia.Platform.Interop; namespace Avalonia.Native.Interop { + class MenuEvents : CallbackBase, IAvnMenuEvents + { + private IAvnMenu _parent; + + public void Initialise(IAvnMenu parent) + { + _parent = parent; + } + + public void NeedsUpdate() + { + _parent?.RaiseNeedsUpdate(); + } + } + public partial class IAvnMenu { + private MenuEvents _events; private AvaloniaNativeMenuExporter _exporter; private List _menuItems = new List(); private Dictionary _menuItemLookup = new Dictionary(); private CompositeDisposable _propertyDisposables = new CompositeDisposable(); + internal void RaiseNeedsUpdate () + { + ManagedMenu.RaiseNeedsUpdate(); + + _exporter.InvalidateMenu(); + } + internal NativeMenu ManagedMenu { get; private set; } + public static IAvnMenu Create (IAvaloniaNativeFactory factory) + { + var events = new MenuEvents(); + + var menu = factory.CreateMenu(events); + + events.Initialise(menu); + + return menu; + } + + protected override void Dispose(bool disposing) + { + if(disposing) + { + _events.Dispose(); + } + } + private void RemoveAndDispose(IAvnMenuItem item) { _menuItemLookup.Remove(item.ManagedMenuItem); diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index b49387ea9e..dbe8a77ba6 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -114,7 +114,7 @@ namespace Avalonia.Native.Interop { if (_subMenu == null) { - _subMenu = factory.CreateMenu(); + _subMenu = IAvnMenu.Create(factory); _subMenu.Initialise(exporter, item.Menu, item.Header); From 17d17ffe821e23812b73f280dbb3a5cc01e2f0c4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 12:31:44 -0300 Subject: [PATCH 049/164] add example of updating inside click --- samples/ControlCatalog/MainWindow.xaml.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index b40fdb4a17..b83d1eb5d3 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -29,6 +29,14 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; + + var fileMenu = (NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu; + + fileMenu.NeedsUpdate += (sender, e)=> + { + fileMenu.Items.Add(new NativeMenuItem("Test 1")); + }; + var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; } From 1bbea47c65a5abd6ac080b4ffb168cd5cbf9b5c2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 12:35:03 -0300 Subject: [PATCH 050/164] fix disposing native menus / events --- samples/ControlCatalog/MainWindow.xaml.cs | 1 + src/Avalonia.Native/IAvnMenu.cs | 2 ++ 2 files changed, 3 insertions(+) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index b83d1eb5d3..6619d3600c 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -34,6 +34,7 @@ namespace ControlCatalog fileMenu.NeedsUpdate += (sender, e)=> { + fileMenu.Items.Clear(); fileMenu.Items.Add(new NativeMenuItem("Test 1")); }; diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 354bf6a6b6..0610e67c6a 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -47,6 +47,8 @@ namespace Avalonia.Native.Interop events.Initialise(menu); + menu._events = events; + return menu; } From 2b0573f24ccb7878919d7b9d177e6ecb2c7b67df Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 12:39:02 -0300 Subject: [PATCH 051/164] Hide RaiseNeedsUpdate method. --- .../INativeMenuExporterEventsImplBridge.cs | 7 +++++++ src/Avalonia.Controls/NativeMenu.cs | 4 ++-- src/Avalonia.Native/IAvnMenu.cs | 2 +- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs diff --git a/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs new file mode 100644 index 0000000000..672d5c1a13 --- /dev/null +++ b/src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Controls +{ + public interface INativeMenuExporterEventsImplBridge + { + void RaiseNeedsUpdate (); + } +} diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 91c11f1a58..1c81a61de7 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -7,7 +7,7 @@ using Avalonia.Metadata; namespace Avalonia.Controls { - public partial class NativeMenu : AvaloniaObject, IEnumerable + public partial class NativeMenu : AvaloniaObject, IEnumerable, INativeMenuExporterEventsImplBridge { private readonly AvaloniaList _items = new AvaloniaList { ResetBehavior = ResetBehavior.Remove }; @@ -23,7 +23,7 @@ namespace Avalonia.Controls _items.CollectionChanged += ItemsChanged; } - public void RaiseNeedsUpdate () + void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate () { NeedsUpdate?.Invoke(this, EventArgs.Empty); } diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 0610e67c6a..9abc818e9b 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -32,7 +32,7 @@ namespace Avalonia.Native.Interop internal void RaiseNeedsUpdate () { - ManagedMenu.RaiseNeedsUpdate(); + (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseNeedsUpdate(); _exporter.InvalidateMenu(); } From b142fbbba4eba23926956e04418ade9cf62162de Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 12:41:41 -0300 Subject: [PATCH 052/164] add documentation for NeedsUpdate event. --- src/Avalonia.Controls/NativeMenu.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 1c81a61de7..47a8d1909d 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -15,6 +15,9 @@ namespace Avalonia.Controls [Content] public IList Items => _items; + /// + /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically. + /// public event EventHandler NeedsUpdate; public NativeMenu() From 68197f88586e4fea7089f2b5f15f06dc976bc7d5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 13:02:07 -0300 Subject: [PATCH 053/164] Rename NativeMenuItem.Enabled to IsEnabled, and NeedsUpdateEvent to Opening event. --- samples/ControlCatalog/MainWindow.xaml.cs | 2 +- src/Avalonia.Controls/NativeMenu.cs | 4 ++-- src/Avalonia.Controls/NativeMenuItem.cs | 14 +++++++------- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 4 ++-- src/Avalonia.Native/IAvnMenuItem.cs | 2 +- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 6619d3600c..81e5f82f76 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -32,7 +32,7 @@ namespace ControlCatalog var fileMenu = (NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu; - fileMenu.NeedsUpdate += (sender, e)=> + fileMenu.Opening += (sender, e)=> { fileMenu.Items.Clear(); fileMenu.Items.Add(new NativeMenuItem("Test 1")); diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 47a8d1909d..58406a38b1 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls /// /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically. /// - public event EventHandler NeedsUpdate; + public event EventHandler Opening; public NativeMenu() { @@ -28,7 +28,7 @@ namespace Avalonia.Controls void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate () { - NeedsUpdate?.Invoke(this, EventArgs.Empty); + Opening?.Invoke(this, EventArgs.Empty); } private void Validator(NativeMenuItemBase obj) diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 8e3ea1fb29..af87745600 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls { private string _header; private KeyGesture _gesture; - private bool _enabled = true; + private bool _isEnabled = true; private ICommand _command; private bool _isChecked = false; @@ -112,18 +112,18 @@ namespace Avalonia.Controls public static readonly StyledProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); - public static readonly DirectProperty EnabledProperty = - AvaloniaProperty.RegisterDirect(nameof(Enabled), o => o.Enabled, (o, v) => o.Enabled = v, true); + public static readonly DirectProperty IsEnabledProperty = + AvaloniaProperty.RegisterDirect(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true); - public bool Enabled + public bool IsEnabled { - get => _enabled; - set => SetAndRaise(EnabledProperty, ref _enabled, value); + get => _isEnabled; + set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value); } void CanExecuteChanged() { - Enabled = _command?.CanExecute(null) ?? true; + IsEnabled = _command?.CanExecute(null) ?? true; } public bool HasClickHandlers => Clicked != null; diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 90239b5a49..06bbc97dac 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -210,7 +210,7 @@ namespace Avalonia.FreeDesktop return null; if (item.Menu != null && item.Menu.Items.Count == 0) return false; - if (item.Enabled == false) + if (item.IsEnabled == false) return false; return null; } @@ -321,7 +321,7 @@ namespace Avalonia.FreeDesktop if (item is NativeMenuItem menuItem) { - if (menuItem?.Enabled == true) + if (menuItem?.IsEnabled == true) menuItem.RaiseClick(); } } diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index dbe8a77ba6..6c1177f9cf 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -44,7 +44,7 @@ namespace Avalonia.Native.Interop { if (item.Command != null || item.HasClickHandlers) { - return item.Enabled; + return item.IsEnabled; } return false; From a57253c09b2d31900e3a7686736f7ffaa478ea2f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 14:46:54 -0300 Subject: [PATCH 054/164] whitespace --- src/Avalonia.Controls/NativeMenu.cs | 8 ++++---- src/Avalonia.Controls/NativeMenuItem.cs | 8 ++++---- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 10 +++++----- src/Avalonia.Native/IAvnMenu.cs | 6 +++--- src/Avalonia.Native/IAvnMenuItem.cs | 6 +++--- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls/NativeMenu.cs b/src/Avalonia.Controls/NativeMenu.cs index 58406a38b1..38a9f03d29 100644 --- a/src/Avalonia.Controls/NativeMenu.cs +++ b/src/Avalonia.Controls/NativeMenu.cs @@ -26,7 +26,7 @@ namespace Avalonia.Controls _items.CollectionChanged += ItemsChanged; } - void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate () + void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate() { Opening?.Invoke(this, EventArgs.Empty); } @@ -39,10 +39,10 @@ namespace Avalonia.Controls private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e) { - if(e.OldItems!=null) + if (e.OldItems != null) foreach (NativeMenuItemBase i in e.OldItems) i.Parent = null; - if(e.NewItems!=null) + if (e.NewItems != null) foreach (NativeMenuItemBase i in e.NewItems) i.Parent = this; } @@ -57,7 +57,7 @@ namespace Avalonia.Controls } public void Add(NativeMenuItemBase item) => _items.Add(item); - + public IEnumerator GetEnumerator() => _items.GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index af87745600..2f2eca539b 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -98,7 +98,7 @@ namespace Avalonia.Controls { get => _isChecked; set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); - } + } public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( @@ -138,12 +138,12 @@ namespace Avalonia.Controls nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); SetAndRaise(CommandProperty, ref _command, value); - + if (_command != null) WeakSubscriptionManager.Subscribe(_command, nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber); - - CanExecuteChanged(); + + CanExecuteChanged(); } } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index ce6b4c7263..a9438d33b7 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -15,7 +15,7 @@ namespace Avalonia.Native private bool _exported = false; private IAvnWindow _nativeWindow; private NativeMenu _menu; - private IAvnMenu _nativeMenu; + private IAvnMenu _nativeMenu; public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) { @@ -43,9 +43,9 @@ namespace Avalonia.Native DoLayoutReset(); } - internal void InvalidateMenu () + internal void InvalidateMenu() { - if(_resetQueued) + if (_resetQueued) { DoLayoutReset(); } @@ -142,7 +142,7 @@ namespace Avalonia.Native setMenu = true; } } - + _nativeMenu.Update(_factory, appMenuHolder); if (setMenu) @@ -164,7 +164,7 @@ namespace Avalonia.Native _nativeMenu.Initialise(this, menu, ""); } } - + _nativeMenu.Update(_factory, menu); avnWindow.SetMainMenu(_nativeMenu); diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 9abc818e9b..b2141cc1a8 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -30,7 +30,7 @@ namespace Avalonia.Native.Interop private Dictionary _menuItemLookup = new Dictionary(); private CompositeDisposable _propertyDisposables = new CompositeDisposable(); - internal void RaiseNeedsUpdate () + internal void RaiseNeedsUpdate() { (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseNeedsUpdate(); @@ -39,7 +39,7 @@ namespace Avalonia.Native.Interop internal NativeMenu ManagedMenu { get; private set; } - public static IAvnMenu Create (IAvaloniaNativeFactory factory) + public static IAvnMenu Create(IAvaloniaNativeFactory factory) { var events = new MenuEvents(); @@ -54,7 +54,7 @@ namespace Avalonia.Native.Interop protected override void Dispose(bool disposing) { - if(disposing) + if (disposing) { _events.Dispose(); } diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index 6c1177f9cf..a41e461e37 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -7,7 +7,7 @@ namespace Avalonia.Native.Interop { public partial class IAvnMenuItem { - private IAvnMenu _subMenu; + private IAvnMenu _subMenu; private CompositeDisposable _propertyDisposables = new CompositeDisposable(); private IDisposable _currentActionDisposable; @@ -104,8 +104,8 @@ namespace Avalonia.Native.Interop } internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item) - { - if(item != ManagedMenuItem) + { + if (item != ManagedMenuItem) { throw new ArgumentException("The item does not match the menuitem being updated.", nameof(item)); } From 4341caef1a615a3dc407a2dabc6981aa7c46b8aa Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 21 Apr 2020 15:36:25 -0300 Subject: [PATCH 055/164] wrap in autoreleasepool --- native/Avalonia.Native/src/OSX/menu.mm | 129 +++++++++++++++---------- 1 file changed, 78 insertions(+), 51 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 66f4944226..29bed72980 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -78,60 +78,75 @@ NSMenuItem* AvnAppMenuItem::GetNative() HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu) { - if(menu != nullptr) + @autoreleasepool { - auto nsMenu = dynamic_cast(menu)->GetNative(); + if(menu != nullptr) + { + auto nsMenu = dynamic_cast(menu)->GetNative(); + + [_native setSubmenu: nsMenu]; + } + else + { + [_native setSubmenu: nullptr]; + } - [_native setSubmenu: nsMenu]; + return S_OK; } - else - { - [_native setSubmenu: nullptr]; - } - - return S_OK; } HRESULT AvnAppMenuItem::SetTitle (void* utf8String) { - if (utf8String != nullptr) + @autoreleasepool { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers) { - NSEventModifierFlags flags = 0; - - if (modifiers & Control) - flags |= NSEventModifierFlagControl; - if (modifiers & Shift) - flags |= NSEventModifierFlagShift; - if (modifiers & Alt) - flags |= NSEventModifierFlagOption; - if (modifiers & Windows) - flags |= NSEventModifierFlagCommand; - - [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; - [_native setKeyEquivalentModifierMask:flags]; - - return S_OK; + @autoreleasepool + { + NSEventModifierFlags flags = 0; + + if (modifiers & Control) + flags |= NSEventModifierFlagControl; + if (modifiers & Shift) + flags |= NSEventModifierFlagShift; + if (modifiers & Alt) + flags |= NSEventModifierFlagOption; + if (modifiers & Windows) + flags |= NSEventModifierFlagCommand; + + [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; + [_native setKeyEquivalentModifierMask:flags]; + + return S_OK; + } } HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) { - _predicate = predicate; - _callback = callback; - return S_OK; + @autoreleasepool + { + _predicate = predicate; + _callback = callback; + return S_OK; + } } HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) { - [_native setState:(isChecked ? NSOnState : NSOffState)]; - return S_OK; + @autoreleasepool + { + [_native setState:(isChecked ? NSOnState : NSOffState)]; + return S_OK; + } } bool AvnAppMenuItem::EvaluateItemEnabled() @@ -177,42 +192,54 @@ void AvnAppMenu::RaiseNeedsUpdate() HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { - auto avnMenuItem = dynamic_cast(item); - - if(avnMenuItem != nullptr) + @autoreleasepool { - [_native insertItem: avnMenuItem->GetNative() atIndex:index]; + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native insertItem: avnMenuItem->GetNative() atIndex:index]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item) { - auto avnMenuItem = dynamic_cast(item); - - if(avnMenuItem != nullptr) + @autoreleasepool { - [_native removeItem:avnMenuItem->GetNative()]; + auto avnMenuItem = dynamic_cast(item); + + if(avnMenuItem != nullptr) + { + [_native removeItem:avnMenuItem->GetNative()]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenu::SetTitle (void* utf8String) { - if (utf8String != nullptr) + @autoreleasepool { - [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + if (utf8String != nullptr) + { + [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]]; + } + + return S_OK; } - - return S_OK; } HRESULT AvnAppMenu::Clear() { - [_native removeAllItems]; - return S_OK; + @autoreleasepool + { + [_native removeAllItems]; + return S_OK; + } } @implementation AvnMenuDelegate From df35f61906c94403c0475448c93690a275fa5a22 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 23 Apr 2020 14:44:53 +0300 Subject: [PATCH 056/164] add failing tests for parsing points in xaml --- .../PointsListTypeConverterTests.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs new file mode 100644 index 0000000000..1e8153044a --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; +using Avalonia.Controls.Shapes; +using Avalonia.Markup.Xaml.Converters; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Converters +{ + public class PointsListTypeConverterTests + { + [Theory] + [InlineData("1,2 3,4")] + [InlineData("1 2 3 4")] + [InlineData("1 2,3 4")] + [InlineData("1,2,3,4")] + public void Should_Parse_Points_in_Xaml(string input) + { + var xaml = $""; + var loader = new AvaloniaXamlLoader(); + var polygon = (Polygon)loader.Load(xaml); + + var points = polygon.Points; + + Assert.Equal(2, points.Count); + Assert.Equal(new Point(1, 2), points[0]); + Assert.Equal(new Point(3, 4), points[1]); + } + } +} From fb1da608f88989ad9eaf00c97f5318cb005dbd4b Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 23 Apr 2020 14:45:38 +0300 Subject: [PATCH 057/164] add failing tests for point parsing --- .../Converters/PointsListTypeConverterTests.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs index 1e8153044a..b060905f38 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs @@ -7,6 +7,22 @@ namespace Avalonia.Markup.Xaml.UnitTests.Converters { public class PointsListTypeConverterTests { + [Theory] + [InlineData("1,2 3,4")] + [InlineData("1 2 3 4")] + [InlineData("1 2,3 4")] + [InlineData("1,2,3,4")] + public void TypeConverter_Should_Parse(string input) + { + var conv = new PointsListTypeConverter(); + + var points = (IList)conv.ConvertFrom(input); + + Assert.Equal(2, points.Count); + Assert.Equal(new Point(1, 2), points[0]); + Assert.Equal(new Point(3, 4), points[1]); + } + [Theory] [InlineData("1,2 3,4")] [InlineData("1 2 3 4")] From 28448b1041ec23169e277b97f93cd759e1716814 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 23 Apr 2020 14:47:14 +0300 Subject: [PATCH 058/164] improve points parsing in PointsListTypeConverter - fixes #3813 --- .../Converters/PointsListTypeConverter.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs index 871ab77cac..7246d8c583 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs @@ -4,7 +4,7 @@ using System.Globalization; namespace Avalonia.Markup.Xaml.Converters { - using System.ComponentModel; + using System.ComponentModel; public class PointsListTypeConverter : TypeConverter { @@ -16,11 +16,11 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { string strValue = (string)value; - string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - var result = new List(pointStrs.Length); - foreach (var pointStr in pointStrs) + string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n', ',' }, StringSplitOptions.RemoveEmptyEntries); + var result = new List(pointStrs.Length / 2); + for (int i = 0; i < pointStrs.Length; i += 2) { - result.Add(Point.Parse(pointStr)); + result.Add(Point.Parse($"{pointStrs[i]} {pointStrs[i + 1]}")); } return result; From 703cb790490ef74b1f106c12ffdffbb29bb33469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Komosi=C5=84ski?= Date: Thu, 23 Apr 2020 18:44:09 +0200 Subject: [PATCH 059/164] Revert "Collection pooling for DeferredRenderer" --- .../Rendering/SceneGraph/Scene.cs | 17 ++------ .../Rendering/SceneGraph/SceneLayers.cs | 20 ++------- .../Rendering/SceneGraph/VisualNode.cs | 43 ++++--------------- 3 files changed, 17 insertions(+), 63 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index b2e827fa26..b4bf4c799a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// public class Scene : IDisposable { - private readonly Dictionary _index; + private Dictionary _index; /// /// Initializes a new instance of the class. @@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned scene. public Scene CloneScene() { - var index = new Dictionary(_index.Count); + var index = new Dictionary(); var root = Clone((VisualNode)Root, null, index); var result = new Scene(root, index, Layers.Clone(), Generation + 1) @@ -162,18 +162,9 @@ namespace Avalonia.Rendering.SceneGraph index.Add(result.Visual, result); - int childCount = source.Children.Count; - - if (childCount > 0) + foreach (var child in source.Children) { - Span children = result.AddChildrenSpan(childCount); - - for (var i = 0; i < childCount; i++) - { - var child = source.Children[i]; - - children[i] = Clone((VisualNode)child, result, index); - } + result.AddChild(Clone((VisualNode)child, result, index)); } return result; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs index 25f7383a1a..5960b4f560 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs @@ -11,28 +11,16 @@ namespace Avalonia.Rendering.SceneGraph public class SceneLayers : IEnumerable { private readonly IVisual _root; - private readonly List _inner; - private readonly Dictionary _index; + private readonly List _inner = new List(); + private readonly Dictionary _index = new Dictionary(); /// /// Initializes a new instance of the class. /// /// The scene's root visual. - public SceneLayers(IVisual root) : this(root, 0) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The scene's root visual. - /// Initial layer capacity. - public SceneLayers(IVisual root, int capacity) + public SceneLayers(IVisual root) { _root = root; - - _inner = new List(capacity); - _index = new Dictionary(capacity); } /// @@ -96,7 +84,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned layers. public SceneLayers Clone() { - var result = new SceneLayers(_root, Count); + var result = new SceneLayers(_root); foreach (var src in _inner) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 8cd1a47795..82444a0c29 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; -using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -20,8 +19,8 @@ namespace Avalonia.Rendering.SceneGraph private Rect? _bounds; private double _opacity; - private PooledList _children; - private PooledList> _drawOperations; + private List _children; + private List> _drawOperations; private IRef _drawOperationsRefCounter; private bool _drawOperationsCloned; private Matrix transformRestore; @@ -350,18 +349,6 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = transformRestore; } - /// - /// Inserts default constructed children into collection and returns a span for the newly created range. - /// - /// Count of children that will be added. - /// - internal Span AddChildrenSpan(int count) - { - EnsureChildrenCreated(count); - - return _children.AddSpan(count); - } - private Rect CalculateBounds() { var result = new Rect(); @@ -375,11 +362,11 @@ namespace Avalonia.Rendering.SceneGraph return result; } - private void EnsureChildrenCreated(int capacity = 0) + private void EnsureChildrenCreated() { if (_children == null) { - _children = new PooledList(capacity); + _children = new List(); } } @@ -390,21 +377,13 @@ namespace Avalonia.Rendering.SceneGraph { if (_drawOperations == null) { - _drawOperations = new PooledList>(); + _drawOperations = new List>(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; } else if (_drawOperationsCloned) { - var oldDrawOperations = _drawOperations; - - _drawOperations = new PooledList>(oldDrawOperations.Count); - - foreach (var drawOperation in oldDrawOperations) - { - _drawOperations.Add(drawOperation.Clone()); - } - + _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); _drawOperationsRefCounter.Dispose(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; @@ -418,16 +397,14 @@ namespace Avalonia.Rendering.SceneGraph /// /// Draw operations that need to be disposed. /// Disposable for given draw operations. - private static IDisposable CreateDisposeDrawOperations(PooledList> drawOperations) + private static IDisposable CreateDisposeDrawOperations(List> drawOperations) { - return Disposable.Create(drawOperations, operations => + return Disposable.Create(() => { - foreach (var operation in operations) + foreach (var operation in drawOperations) { operation.Dispose(); } - - operations.Dispose(); }); } @@ -437,8 +414,6 @@ namespace Avalonia.Rendering.SceneGraph { _drawOperationsRefCounter?.Dispose(); - _children?.Dispose(); - Disposed = true; } } From 43a0f50866a9a2b45fc1a34b9a0b6f2de9a03a46 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Thu, 23 Apr 2020 23:34:07 +0200 Subject: [PATCH 060/164] Limit reallocations in the deferred renderer. --- .../Rendering/SceneGraph/Scene.cs | 18 ++++++++++--- .../Rendering/SceneGraph/SceneLayers.cs | 20 ++++++++++++--- .../Rendering/SceneGraph/VisualNode.cs | 25 ++++++++++++++----- 3 files changed, 49 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index b4bf4c799a..0f6001516d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// public class Scene : IDisposable { - private Dictionary _index; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. @@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned scene. public Scene CloneScene() { - var index = new Dictionary(); + var index = new Dictionary(_index.Count); var root = Clone((VisualNode)Root, null, index); var result = new Scene(root, index, Layers.Clone(), Generation + 1) @@ -162,9 +162,19 @@ namespace Avalonia.Rendering.SceneGraph index.Add(result.Visual, result); - foreach (var child in source.Children) + var children = source.Children; + var childrenCount = children.Count; + + if (childrenCount > 0) { - result.AddChild(Clone((VisualNode)child, result, index)); + result.TryPreallocateChildren(childrenCount); + + for (var i = 0; i < childrenCount; i++) + { + var child = children[i]; + + result.AddChild(Clone((VisualNode)child, result, index)); + } } return result; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs index 5960b4f560..25f7383a1a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs @@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph public class SceneLayers : IEnumerable { private readonly IVisual _root; - private readonly List _inner = new List(); - private readonly Dictionary _index = new Dictionary(); + private readonly List _inner; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. /// /// The scene's root visual. - public SceneLayers(IVisual root) + public SceneLayers(IVisual root) : this(root, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The scene's root visual. + /// Initial layer capacity. + public SceneLayers(IVisual root, int capacity) { _root = root; + + _inner = new List(capacity); + _index = new Dictionary(capacity); } /// @@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned layers. public SceneLayers Clone() { - var result = new SceneLayers(_root); + var result = new SceneLayers(_root, Count); foreach (var src in _inner) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 82444a0c29..6f566ff6d6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Reactive.Disposables; +using Avalonia.Collections; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -349,6 +349,11 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = transformRestore; } + internal void TryPreallocateChildren(int count) + { + EnsureChildrenCreated(count); + } + private Rect CalculateBounds() { var result = new Rect(); @@ -362,11 +367,11 @@ namespace Avalonia.Rendering.SceneGraph return result; } - private void EnsureChildrenCreated() + private void EnsureChildrenCreated(int capacity = 0) { if (_children == null) { - _children = new List(); + _children = new List(capacity); } } @@ -383,7 +388,15 @@ namespace Avalonia.Rendering.SceneGraph } else if (_drawOperationsCloned) { - _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); + var oldDrawOperations = _drawOperations; + + _drawOperations = new List>(oldDrawOperations.Count); + + foreach (var drawOperation in oldDrawOperations) + { + _drawOperations.Add(drawOperation.Clone()); + } + _drawOperationsRefCounter.Dispose(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; @@ -399,9 +412,9 @@ namespace Avalonia.Rendering.SceneGraph /// Disposable for given draw operations. private static IDisposable CreateDisposeDrawOperations(List> drawOperations) { - return Disposable.Create(() => + return Disposable.Create(drawOperations, operations => { - foreach (var operation in drawOperations) + foreach (var operation in operations) { operation.Dispose(); } From 9de3afc834aeaa6139ec562364ddc9acbf003fd6 Mon Sep 17 00:00:00 2001 From: El Mostafa Idrassi Date: Fri, 24 Apr 2020 08:53:16 +0000 Subject: [PATCH 061/164] macOS: Fix title not showing up in SelectFolderDialog, OpenFileDialog and SaveFileDialog (#3819) macOS: Fix title not showing up in SelectFolderDialog, OpenFileDialog and SaveFileDialog --- native/Avalonia.Native/src/OSX/SystemDialogs.mm | 3 +++ 1 file changed, 3 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/SystemDialogs.mm b/native/Avalonia.Native/src/OSX/SystemDialogs.mm index 3e0911a4aa..a47221056b 100644 --- a/native/Avalonia.Native/src/OSX/SystemDialogs.mm +++ b/native/Avalonia.Native/src/OSX/SystemDialogs.mm @@ -20,6 +20,7 @@ public: if(title != nullptr) { + panel.message = [NSString stringWithUTF8String:title]; panel.title = [NSString stringWithUTF8String:title]; } @@ -94,6 +95,7 @@ public: if(title != nullptr) { + panel.message = [NSString stringWithUTF8String:title]; panel.title = [NSString stringWithUTF8String:title]; } @@ -182,6 +184,7 @@ public: if(title != nullptr) { + panel.message = [NSString stringWithUTF8String:title]; panel.title = [NSString stringWithUTF8String:title]; } From 1217a44bc3c477d96657e6a6850c5c1833ad106a Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Fri, 24 Apr 2020 11:58:42 +0300 Subject: [PATCH 062/164] use StringTokenizer to parse PointsList --- .../Converters/PointsListTypeConverter.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs index 7246d8c583..099ffa8c8c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs @@ -5,6 +5,7 @@ using System.Globalization; namespace Avalonia.Markup.Xaml.Converters { using System.ComponentModel; + using Avalonia.Utilities; public class PointsListTypeConverter : TypeConverter { @@ -15,15 +16,17 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - string strValue = (string)value; - string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n', ',' }, StringSplitOptions.RemoveEmptyEntries); - var result = new List(pointStrs.Length / 2); - for (int i = 0; i < pointStrs.Length; i += 2) + var points = new List(); + + using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList.")) { - result.Add(Point.Parse($"{pointStrs[i]} {pointStrs[i + 1]}")); + while (tokenizer.TryReadDouble(out double x)) + { + points.Add(new Point(x, tokenizer.ReadDouble())); + } } - return result; + return points; } } } From 1624e88ba3aee39c040ea5c4c2bab205de97d031 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 25 Apr 2020 21:57:33 +0200 Subject: [PATCH 063/164] Remove Window.ShowDialog variant that takes IWindowImpl. --- .../Platform/ISystemDialogImpl.cs | 5 ++-- src/Avalonia.Controls/SystemDialog.cs | 6 ++-- src/Avalonia.Controls/Window.cs | 28 +++++++------------ src/Avalonia.DesignerSupport/Remote/Stubs.cs | 4 +-- .../ManagedFileDialogExtensions.cs | 12 +++----- src/Avalonia.Native/SystemDialogs.cs | 20 +++++++++---- .../NativeDialogs/GtkNativeFileDialogs.cs | 8 +++--- .../Avalonia.Win32/SystemDialogImpl.cs | 9 +++--- .../WindowTests.cs | 8 +++--- 9 files changed, 47 insertions(+), 53 deletions(-) diff --git a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs b/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs index 6141b6eb19..affec6301b 100644 --- a/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs +++ b/src/Avalonia.Controls/Platform/ISystemDialogImpl.cs @@ -1,5 +1,4 @@ using System.Threading.Tasks; -using Avalonia.Platform; namespace Avalonia.Controls.Platform { @@ -14,8 +13,8 @@ namespace Avalonia.Controls.Platform /// The details of the file dialog to show. /// The parent window. /// A task returning the selected filenames. - Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent); + Task ShowFileDialogAsync(FileDialog dialog, Window parent); - Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent); + Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent); } } diff --git a/src/Avalonia.Controls/SystemDialog.cs b/src/Avalonia.Controls/SystemDialog.cs index 6ccaa3c742..e74b950f23 100644 --- a/src/Avalonia.Controls/SystemDialog.cs +++ b/src/Avalonia.Controls/SystemDialog.cs @@ -32,7 +32,7 @@ namespace Avalonia.Controls if(parent == null) throw new ArgumentNullException(nameof(parent)); return ((await AvaloniaLocator.Current.GetService() - .ShowFileDialogAsync(this, parent?.PlatformImpl)) ?? + .ShowFileDialogAsync(this, parent)) ?? Array.Empty()).FirstOrDefault(); } } @@ -45,7 +45,7 @@ namespace Avalonia.Controls { if(parent == null) throw new ArgumentNullException(nameof(parent)); - return AvaloniaLocator.Current.GetService().ShowFileDialogAsync(this, parent?.PlatformImpl); + return AvaloniaLocator.Current.GetService().ShowFileDialogAsync(this, parent); } } @@ -61,7 +61,7 @@ namespace Avalonia.Controls { if(parent == null) throw new ArgumentNullException(nameof(parent)); - return AvaloniaLocator.Current.GetService().ShowFolderDialogAsync(this, parent?.PlatformImpl); + return AvaloniaLocator.Current.GetService().ShowFolderDialogAsync(this, parent); } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index ee596432f7..dd00b850fe 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -484,22 +484,12 @@ namespace Avalonia.Controls /// . /// A task that can be used to retrieve the result of the dialog when it closes. /// - public Task ShowDialog(Window owner) => ShowDialog(owner.PlatformImpl); - - /// - /// Shows the window as a dialog. - /// - /// - /// The type of the result produced by the dialog. - /// - /// The dialog's owner window. - /// . - /// A task that can be used to retrieve the result of the dialog when it closes. - /// - public Task ShowDialog(IWindowImpl owner) + public Task ShowDialog(Window owner) { if (owner == null) + { throw new ArgumentNullException(nameof(owner)); + } if (IsVisible) { @@ -516,23 +506,25 @@ namespace Avalonia.Controls using (BeginAutoSizing()) { - - PlatformImpl?.ShowDialog(owner); + PlatformImpl?.ShowDialog(owner.PlatformImpl); Renderer?.Start(); + Observable.FromEventPattern( - x => this.Closed += x, - x => this.Closed -= x) + x => Closed += x, + x => Closed -= x) .Take(1) .Subscribe(_ => { owner.Activate(); result.SetResult((TResult)(_dialogResult ?? default(TResult))); }); + OnOpened(EventArgs.Empty); } - SetWindowStartupLocation(owner); + SetWindowStartupLocation(owner.PlatformImpl); + return result.Task; } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index 7bf1d236bd..82950ce53b 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -166,10 +166,10 @@ namespace Avalonia.DesignerSupport.Remote class SystemDialogsStub : ISystemDialogImpl { - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) => + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) => Task.FromResult((string[])null); - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) => + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) => Task.FromResult((string)null); } diff --git a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs index 771d2b1b5e..41f526a513 100644 --- a/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs +++ b/src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs @@ -1,19 +1,15 @@ -using System; using System.Linq; using System.Threading.Tasks; -using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Dialogs; -using Avalonia.Platform; namespace Avalonia.Dialogs { public static class ManagedFileDialogExtensions { - class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() + private class ManagedSystemDialogImpl : ISystemDialogImpl where T : Window, new() { - async Task Show(SystemDialog d, IWindowImpl parent) + async Task Show(SystemDialog d, Window parent) { var model = new ManagedFileChooserViewModel((FileSystemDialog)d); @@ -39,12 +35,12 @@ namespace Avalonia.Dialogs return result; } - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { return await Show(dialog, parent); } - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { return (await Show(dialog, parent))?.FirstOrDefault(); } diff --git a/src/Avalonia.Native/SystemDialogs.cs b/src/Avalonia.Native/SystemDialogs.cs index de355fdf71..d8c2c34f16 100644 --- a/src/Avalonia.Native/SystemDialogs.cs +++ b/src/Avalonia.Native/SystemDialogs.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Native.Interop; -using Avalonia.Platform; namespace Avalonia.Native { @@ -18,13 +17,15 @@ namespace Avalonia.Native _native = native; } - public Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public Task ShowFileDialogAsync(FileDialog dialog, Window parent) { var events = new SystemDialogEvents(); + var nativeParent = GetNativeWindow(parent); + if (dialog is OpenFileDialog ofd) { - _native.OpenFileDialog((parent as WindowImpl)?.Native, + _native.OpenFileDialog(nativeParent, events, ofd.AllowMultiple, ofd.Title ?? "", ofd.InitialDirectory ?? "", @@ -33,7 +34,7 @@ namespace Avalonia.Native } else { - _native.SaveFileDialog((parent as WindowImpl)?.Native, + _native.SaveFileDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? "", @@ -44,14 +45,21 @@ namespace Avalonia.Native return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; }); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { var events = new SystemDialogEvents(); - _native.SelectFolderDialog((parent as WindowImpl)?.Native, events, dialog.Title ?? "", dialog.InitialDirectory ?? ""); + var nativeParent = GetNativeWindow(parent); + + _native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? ""); return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); }); } + + private IAvnWindow GetNativeWindow(Window window) + { + return (window?.PlatformImpl as WindowImpl)?.Native; + } } public class SystemDialogEvents : CallbackBase, IAvnSystemDialogEvents diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 5c9a0c992a..0c80611ada 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -102,23 +102,23 @@ namespace Avalonia.X11.NativeDialogs return tcs.Task; } - public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { await EnsureInitialized(); return await await RunOnGlibThread( - () => ShowDialog(dialog.Title, parent, + () => ShowDialog(dialog.Title, parent?.PlatformImpl, dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, (dialog as OpenFileDialog)?.AllowMultiple ?? false, Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters)); } - public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { await EnsureInitialized(); return await await RunOnGlibThread(async () => { - var res = await ShowDialog(dialog.Title, parent, + var res = await ShowDialog(dialog.Title, parent?.PlatformImpl, GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null); return res?.FirstOrDefault(); }); diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index c452290afc..8bdd4b7bfa 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -5,7 +5,6 @@ using System.Runtime.InteropServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Platform; using Avalonia.Win32.Interop; namespace Avalonia.Win32 @@ -16,9 +15,9 @@ namespace Avalonia.Win32 private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT; - public unsafe Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + public unsafe Task ShowFileDialogAsync(FileDialog dialog, Window parent) { - var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; return Task.Factory.StartNew(() => { var result = Array.Empty(); @@ -98,13 +97,13 @@ namespace Avalonia.Win32 }); } - public Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + public Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { return Task.Factory.StartNew(() => { string result = string.Empty; - var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero; + var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index cf2920998a..29944c6f85 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = Mock.Of(); var renderer = new Mock(); var target = new Window(CreateImpl(renderer)); @@ -171,7 +171,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = Mock.Of(); + var parent = Mock.Of(); var target = new Window(); var raised = false; @@ -203,7 +203,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Mock(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.Scaling).Returns(1); @@ -242,7 +242,7 @@ namespace Avalonia.Controls.UnitTests { using (UnitTestApplication.Start(TestServices.StyledWindow)) { - var parent = new Mock(); + var parent = new Mock(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.Scaling).Returns(1); From b3075f98fdbb5b9f3823a34fcc3c73a9a48bf660 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 25 Apr 2020 22:25:22 +0200 Subject: [PATCH 064/164] Cache PlatformImpl before invoking async code. --- src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 0c80611ada..07da0048d0 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -105,8 +105,11 @@ namespace Avalonia.X11.NativeDialogs public async Task ShowFileDialogAsync(FileDialog dialog, Window parent) { await EnsureInitialized(); + + var platformImpl = parent?.PlatformImpl; + return await await RunOnGlibThread( - () => ShowDialog(dialog.Title, parent?.PlatformImpl, + () => ShowDialog(dialog.Title, platformImpl, dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, (dialog as OpenFileDialog)?.AllowMultiple ?? false, Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, @@ -116,9 +119,12 @@ namespace Avalonia.X11.NativeDialogs public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) { await EnsureInitialized(); + + var platformImpl = parent?.PlatformImpl; + return await await RunOnGlibThread(async () => { - var res = await ShowDialog(dialog.Title, parent?.PlatformImpl, + var res = await ShowDialog(dialog.Title, platformImpl, GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null); return res?.FirstOrDefault(); }); From 0b1b3914b347c616ec63265942f67f3f547f4dba Mon Sep 17 00:00:00 2001 From: Benedikt Schroeder Date: Mon, 27 Apr 2020 12:47:37 +0200 Subject: [PATCH 065/164] More text rendering improvements --- src/Avalonia.Visuals/Media/FontManager.cs | 26 +++- .../Media/Fonts/FamilyNameCollection.cs | 3 +- src/Avalonia.Visuals/Media/Fonts/FontKey.cs | 10 +- .../Media/TextFormatting/TextFormatter.cs | 2 +- .../Unicode/CodepointEnumerator.cs | 2 +- .../Unicode/UnicodeGeneralCategory.cs | 44 ------- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 67 ++++++---- src/Skia/Avalonia.Skia/FontManagerImpl.cs | 46 +++++-- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 3 +- src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 9 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 116 +++++++++--------- .../Avalonia.Skia/SKTypefaceCollection.cs | 4 +- .../SKTypefaceCollectionCache.cs | 2 +- .../Media/FontManagerImpl.cs | 2 +- .../CustomFontManagerImpl.cs | 48 ++++++-- .../FontManagerImplTests.cs | 13 ++ .../TextLayoutTests.cs | 24 +++- .../Avalonia.UnitTests/MockFontManagerImpl.cs | 11 +- .../Media/FontManagerTests.cs | 12 +- 19 files changed, 271 insertions(+), 173 deletions(-) delete mode 100644 src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index 2de629432c..f9410afe6a 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -23,6 +23,11 @@ namespace Avalonia.Media DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); + if (string.IsNullOrEmpty(DefaultFontFamilyName)) + { + throw new InvalidOperationException("Default font family name can't be null or empty."); + } + _defaultFontFamily = new FontFamily(DefaultFontFamilyName); } @@ -39,7 +44,8 @@ namespace Avalonia.Media var fontManagerImpl = AvaloniaLocator.Current.GetService(); - if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered."); + if (fontManagerImpl == null) + throw new InvalidOperationException("No font manager implementation was registered."); current = new FontManager(fontManagerImpl); @@ -87,7 +93,7 @@ namespace Avalonia.Media fontFamily = _defaultFontFamily; } - var key = new FontKey(fontFamily, fontWeight, fontStyle); + var key = new FontKey(fontFamily.Name, fontWeight, fontStyle); if (_typefaceCache.TryGetValue(key, out var typeface)) { @@ -126,9 +132,21 @@ namespace Avalonia.Media FontStyle fontStyle = FontStyle.Normal, FontFamily fontFamily = null, CultureInfo culture = null) { - return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : + foreach (var cachedTypeface in _typefaceCache.Values) + { + // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup. + if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight + && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0) + { + return cachedTypeface; + } + } + + var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) : null; + + return matchedTypeface; } } } diff --git a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs index a9ea322d76..96312a5466 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Linq; using System.Text; using Avalonia.Utilities; @@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts throw new ArgumentNullException(nameof(familyNames)); } - Names = familyNames.Split(',').Select(x => x.Trim()).ToArray(); + Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim()); PrimaryFamilyName = Names[0]; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs index 1f1e9b067d..579a229fd3 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts { public readonly struct FontKey : IEquatable { - public readonly FontFamily FontFamily; + public readonly string FamilyName; public readonly FontStyle Style; public readonly FontWeight Weight; - public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style) + public FontKey(string familyName, FontWeight weight, FontStyle style) { - FontFamily = fontFamily; + FamilyName = familyName; Style = style; Weight = weight; } public override int GetHashCode() { - var hash = FontFamily.GetHashCode(); + var hash = FamilyName.GetHashCode(); hash = hash * 31 + (int)Style; hash = hash * 31 + (int)Weight; @@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts public bool Equals(FontKey other) { - return FontFamily == other.FontFamily && + return FamilyName == other.FamilyName && Style == other.Style && Weight == other.Weight; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index 7956c5f260..7da39dc5dc 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting //ToDo: Fix FontFamily fallback currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style); + FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily); if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 9c20efd867..2ff4952cab 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -2,7 +2,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { - internal ref struct CodepointEnumerator + public ref struct CodepointEnumerator { private ReadOnlySlice _text; diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs deleted file mode 100644 index 3385116f26..0000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace Avalonia.Media.TextFormatting.Unicode -{ - public enum UnicodeGeneralCategory : byte - { - Other, //C# Cc | Cf | Cn | Co | Cs - Control, //Cc - Format, //Cf - Unassigned, //Cn - PrivateUse, //Co - Surrogate, //Cs - Letter, //L# Ll | Lm | Lo | Lt | Lu - CasedLetter, //LC# Ll | Lt | Lu - LowercaseLetter, //Ll - ModifierLetter, //Lm - OtherLetter, //Lo - TitlecaseLetter, //Lt - UppercaseLetter, //Lu - Mark, //M - SpacingMark, //Mc - EnclosingMark, //Me - NonspacingMark, //Mn - Number, //N# Nd | Nl | No - DecimalNumber, //Nd - LetterNumber, //Nl - OtherNumber, //No - Punctuation, //P - ConnectorPunctuation, //Pc - DashPunctuation, //Pd - ClosePunctuation, //Pe - FinalPunctuation, //Pf - InitialPunctuation, //Pi - OtherPunctuation, //Po - OpenPunctuation, //Ps - Symbol, //S# Sc | Sk | Sm | So - CurrencySymbol, //Sc - ModifierSymbol, //Sk - MathSymbol, //Sm - OtherSymbol, //So - Separator, //Z# Zl | Zp | Zs - LineSeparator, //Zl - ParagraphSeparator, //Zp - SpaceSeparator, //Zs - } -} diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 4bec7b3f56..9f99ed3cef 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -30,6 +30,10 @@ namespace Avalonia.Skia private Matrix _currentTransform; private GRContext _grContext; private bool _disposed; + + private readonly SKPaint _strokePaint = new SKPaint(); + private readonly SKPaint _fillPaint = new SKPaint(); + /// /// Context create info. /// @@ -153,7 +157,7 @@ namespace Avalonia.Skia /// public void DrawLine(IPen pen, Point p1, Point p2) { - using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) + using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y)))) { Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint); } @@ -165,8 +169,8 @@ namespace Avalonia.Skia var impl = (GeometryImpl) geometry; var size = geometry.Bounds.Size; - using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper)) - using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper)) + using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper)) + using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper)) { if (fill.Paint != null) { @@ -188,7 +192,7 @@ namespace Avalonia.Skia if (brush != null) { - using (var paint = CreatePaint(brush, rect.Size)) + using (var paint = CreatePaint(_fillPaint, brush, rect.Size)) { if (isRounded) { @@ -204,7 +208,7 @@ namespace Avalonia.Skia if (pen?.Brush != null) { - using (var paint = CreatePaint(pen, rect.Size)) + using (var paint = CreatePaint(_strokePaint, pen, rect.Size)) { if (isRounded) { @@ -222,7 +226,7 @@ namespace Avalonia.Skia /// public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text) { - using (var paint = CreatePaint(foreground, text.Bounds.Size)) + using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size)) { var textImpl = (FormattedTextImpl) text; textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering); @@ -232,14 +236,14 @@ namespace Avalonia.Skia /// public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin) { - using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) { var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; - paint.ApplyTo(glyphRunImpl.Paint); + ConfigureTextRendering(paintWrapper); Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X, - (float)baselineOrigin.Y, glyphRunImpl.Paint); + (float)baselineOrigin.Y, paintWrapper.Paint); } } @@ -323,7 +327,7 @@ namespace Avalonia.Skia var paint = new SKPaint(); Canvas.SaveLayer(paint); - _maskStack.Push(CreatePaint(mask, bounds.Size)); + _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true)); } /// @@ -364,6 +368,15 @@ namespace Avalonia.Skia } } + internal void ConfigureTextRendering(PaintWrapper wrapper) + { + var paint = wrapper.Paint; + + paint.IsEmbeddedBitmapText = true; + paint.SubpixelText = true; + paint.LcdRenderText = _canTextUseLcdRendering; + } + /// /// Configure paint wrapper for using gradient brush. /// @@ -514,17 +527,16 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given brush. /// + /// The paint to wrap. /// Source brush. /// Target size. + /// Optional dispose of the supplied paint. /// Paint wrapper for given brush. - internal PaintWrapper CreatePaint(IBrush brush, Size targetSize) + internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false) { - var paint = new SKPaint - { - IsAntialias = true - }; + var paintWrapper = new PaintWrapper(paint, disposePaint); - var paintWrapper = new PaintWrapper(paint); + paint.IsAntialias = true; double opacity = brush.Opacity * _currentOpacity; @@ -572,10 +584,12 @@ namespace Avalonia.Skia /// /// Creates paint wrapper for given pen. /// + /// The paint to wrap. /// Source pen. /// Target size. + /// Optional dispose of the supplied paint. /// - private PaintWrapper CreatePaint(IPen pen, Size targetSize) + private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false) { // In Skia 0 thickness means - use hairline rendering // and for us it means - there is nothing rendered. @@ -584,8 +598,7 @@ namespace Avalonia.Skia return default; } - var rv = CreatePaint(pen.Brush, targetSize); - var paint = rv.Paint; + var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint); paint.IsStroke = true; paint.StrokeWidth = (float) pen.Thickness; @@ -668,7 +681,7 @@ namespace Avalonia.Skia /// /// Skia cached paint state. /// - private struct PaintState : IDisposable + private readonly struct PaintState : IDisposable { private readonly SKColor _color; private readonly SKShader _shader; @@ -696,14 +709,16 @@ namespace Avalonia.Skia { //We are saving memory allocations there public readonly SKPaint Paint; + private readonly bool _disposePaint; private IDisposable _disposable1; private IDisposable _disposable2; private IDisposable _disposable3; - public PaintWrapper(SKPaint paint) + public PaintWrapper(SKPaint paint, bool disposePaint) { Paint = paint; + _disposePaint = disposePaint; _disposable1 = null; _disposable2 = null; @@ -751,7 +766,15 @@ namespace Avalonia.Skia /// public void Dispose() { - Paint?.Dispose(); + if (_disposePaint) + { + Paint?.Dispose(); + } + else + { + Paint?.Reset(); + } + _disposable1?.Dispose(); _disposable2?.Dispose(); _disposable3?.Dispose(); diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 53aa6a147c..415a89e1c1 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -32,6 +32,27 @@ namespace Avalonia.Skia public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { + SKFontStyle skFontStyle; + + switch (fontWeight) + { + case FontWeight.Normal when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Normal; + break; + case FontWeight.Normal when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.Italic; + break; + case FontWeight.Bold when fontStyle == FontStyle.Normal: + skFontStyle = SKFontStyle.Bold; + break; + case FontWeight.Bold when fontStyle == FontStyle.Italic: + skFontStyle = SKFontStyle.BoldItalic; + break; + default: + skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle); + break; + } + if (culture == null) { culture = CultureInfo.CurrentUICulture; @@ -45,31 +66,32 @@ namespace Avalonia.Skia t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName; t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName; - if (fontFamily != null) + if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks) { - foreach (var familyName in fontFamily.FamilyNames) + var familyNames = fontFamily.FamilyNames; + + for (var i = 1; i < familyNames.Count; i++) { - var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); + var skTypeface = + _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint); if (skTypeface == null) { continue; } - fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); return true; } } else { - var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight, - SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint); + var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint); if (skTypeface != null) { - fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); return true; } @@ -82,7 +104,7 @@ namespace Avalonia.Skia public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) { - var skTypeface = SKTypeface.Default; + SKTypeface skTypeface = null; if (typeface.FontFamily.Key == null) { @@ -109,6 +131,12 @@ namespace Avalonia.Skia skTypeface = fontCollection.Get(typeface); } + if (skTypeface == null) + { + throw new InvalidOperationException( + $"Could not create glyph typeface for: {typeface.FontFamily.Name}."); + } + return new GlyphTypefaceImpl(skTypeface); } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index d0157815a9..6022e7a552 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -266,7 +266,8 @@ namespace Avalonia.Skia if (fb != null) { //TODO: figure out how to get the brush size - currentWrapper = context.CreatePaint(fb, new Size()); + currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb, + new Size()); } else { diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 7c3f718f9e..0fdea5ed40 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -7,17 +7,11 @@ namespace Avalonia.Skia /// public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob) + public GlyphRunImpl(SKTextBlob textBlob) { - Paint = paint; TextBlob = textBlob; } - /// - /// Gets the paint to draw with. - /// - public SKPaint Paint { get; } - /// /// Gets the text blob to draw. /// @@ -26,7 +20,6 @@ namespace Avalonia.Skia void IDisposable.Dispose() { TextBlob.Dispose(); - Paint.Dispose(); } } } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index f1df6a804e..46872f903e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -149,6 +149,16 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format); } + private static readonly SKPaint s_paint = new SKPaint + { + TextEncoding = SKTextEncoding.GlyphId, + IsAntialias = true, + IsStroke = false, + SubpixelText = true + }; + + private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder(); + /// public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { @@ -158,92 +168,84 @@ namespace Avalonia.Skia var typeface = glyphTypeface.Typeface; - var paint = new SKPaint - { - TextSize = (float)glyphRun.FontRenderingEmSize, - Typeface = typeface, - TextEncoding = SKTextEncoding.GlyphId, - IsAntialias = true, - IsStroke = false, - SubpixelText = true - }; - - using (var textBlobBuilder = new SKTextBlobBuilder()) - { - SKTextBlob textBlob; + s_paint.TextSize = (float)glyphRun.FontRenderingEmSize; + s_paint.Typeface = typeface; - width = 0; - var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); + SKTextBlob textBlob; - if (glyphRun.GlyphOffsets.IsEmpty) - { - if (glyphTypeface.IsFixedPitch) - { - textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); + width = 0; - textBlob = textBlobBuilder.Build(); + var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight); - width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; - } - else - { - var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0); - - var positions = buffer.GetPositionSpan(); - - for (var i = 0; i < count; i++) - { - positions[i] = (float)width; - - if (glyphRun.GlyphAdvances.IsEmpty) - { - width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; - } - else - { - width += glyphRun.GlyphAdvances[i]; - } - } + if (glyphRun.GlyphOffsets.IsEmpty) + { + if (glyphTypeface.IsFixedPitch) + { + s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); - buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + textBlob = s_textBlobBuilder.Build(); - textBlob = textBlobBuilder.Build(); - } + width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length; } else { - var buffer = textBlobBuilder.AllocatePositionedRun(paint, count); + var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0); - var glyphPositions = buffer.GetPositionSpan(); - - var currentX = 0.0; + var positions = buffer.GetPositionSpan(); for (var i = 0; i < count; i++) { - var glyphOffset = glyphRun.GlyphOffsets[i]; - - glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); + positions[i] = (float)width; if (glyphRun.GlyphAdvances.IsEmpty) { - currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; } else { - currentX += glyphRun.GlyphAdvances[i]; + width += glyphRun.GlyphAdvances[i]; } } buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); - width = currentX; + textBlob = s_textBlobBuilder.Build(); + } + } + else + { + var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count); + + var glyphPositions = buffer.GetPositionSpan(); + + var currentX = 0.0; + + for (var i = 0; i < count; i++) + { + var glyphOffset = glyphRun.GlyphOffsets[i]; + + glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y); - textBlob = textBlobBuilder.Build(); + if (glyphRun.GlyphAdvances.IsEmpty) + { + currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale; + } + else + { + currentX += glyphRun.GlyphAdvances[i]; + } } - return new GlyphRunImpl(paint, textBlob); + buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span); + + width = currentX; + + textBlob = s_textBlobBuilder.Build(); } + + return new GlyphRunImpl(textBlob); + } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index ce82835968..7aea90e61e 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -19,7 +19,7 @@ namespace Avalonia.Skia public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style); + var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style); return GetNearestMatch(_typefaces, key); } @@ -49,7 +49,7 @@ namespace Avalonia.Skia if (keys.Length == 0) { - return SKTypeface.Default; + return null; } key = keys[0]; diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index 4f04d25dee..a9aed80a04 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -54,7 +54,7 @@ namespace Avalonia.Skia continue; } - var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); + var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 8ba46dcbce..253a373106 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle); + fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle); return true; } diff --git a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs index aef2423f8a..8d64190ebd 100644 --- a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs @@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests public class CustomFontManagerImpl : IFontManagerImpl { private readonly Typeface[] _customTypefaces; + private readonly string _defaultFamilyName; private readonly Typeface _defaultTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono"); - private readonly Typeface _italicTypeface = + private readonly Typeface _italicTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans"); private readonly Typeface _emojiTypeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji"); @@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests public CustomFontManagerImpl() { _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface }; + _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName; } public string GetDefaultFontFamilyName() { - return _defaultTypeface.FontFamily.ToString(); + return _defaultFamilyName; } public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) @@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests return _customTypefaces.Select(x => x.FontFamily.Name); } + private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName }; + public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { foreach (var customTypeface in _customTypefaces) { if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0) + { continue; - fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle); + } + + fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle); return true; } - var fallback = SKFontManager.Default.MatchCharacter(codepoint); + var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight, + SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint); - fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle); return true; } public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface) { + SKTypeface skTypeface; + switch (typeface.FontFamily.Name) { case "Twitter Color Emoji": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } + case "Noto Sans": + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } case "Noto Mono": - var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily); - var skTypeface = typefaceCollection.Get(typeface); - return new GlyphTypefaceImpl(skTypeface); + { + var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily); + skTypeface = typefaceCollection.Get(typeface); + break; + } default: - return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name, - (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style)); + { + skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name, + (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style); + break; + } } + + return new GlyphTypefaceImpl(skTypeface); } } } diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs index dc2a40aeba..8f80d89ac6 100644 --- a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs @@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("Noto Mono", skTypeface.FamilyName); } } + + [Fact] + public void Should_Throw_For_Invalid_Custom_Font() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var fontManager = new FontManagerImpl(); + + Assert.Throws(() => + fontManager.CreateGlyphTypeface( + new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown"))); + } + } } } diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs index 1f89a5833c..627b7c2ead 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs @@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - maxWidth : 200, + maxWidth : 200, maxHeight : 125, textStyleOverrides: spans); @@ -506,10 +506,30 @@ namespace Avalonia.Skia.UnitTests } } + private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。"; + + [Fact] + public void Should_Wrap() + { + using (Start()) + { + for (var i = 0; i < 2000; i++) + { + var layout = new TextLayout( + Text, + Typeface.Default, + 12, + Brushes.Black, + textWrapping: TextWrapping.Wrap, + maxWidth: 50); + } + } + } + public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(null), textShaperImpl: new TextShaperImpl(), fontManagerImpl : new CustomFontManagerImpl())); diff --git a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs index affdc48f5e..55656fcfc0 100644 --- a/tests/Avalonia.UnitTests/MockFontManagerImpl.cs +++ b/tests/Avalonia.UnitTests/MockFontManagerImpl.cs @@ -8,14 +8,21 @@ namespace Avalonia.UnitTests { public class MockFontManagerImpl : IFontManagerImpl { + private readonly string _defaultFamilyName; + + public MockFontManagerImpl(string defaultFamilyName = "Default") + { + _defaultFamilyName = defaultFamilyName; + } + public string GetDefaultFontFamilyName() { - return "Default"; + return _defaultFamilyName; } public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = false) { - return new[] { "Default" }; + return new[] { _defaultFamilyName }; } public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily, diff --git a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs index d35108080b..81a4ca6495 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Media; +using System; +using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Xunit; @@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); } } + + [Fact] + public void Should_Throw_When_Default_FamilyName_Is_Null() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null)))) + { + Assert.Throws(() => FontManager.Current); + } + } } } From 47ee4e3f8e8a0729f3496df126641b9e2b4ab7dc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 11:51:56 -0300 Subject: [PATCH 066/164] remove obtain menu methods. --- native/Avalonia.Native/inc/avalonia-native.h | 2 -- native/Avalonia.Native/src/OSX/main.mm | 12 ----------- native/Avalonia.Native/src/OSX/window.mm | 12 ----------- .../AvaloniaNativeMenuExporter.cs | 20 +++++-------------- 4 files changed, 5 insertions(+), 41 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index d8d65c2fe6..757c0bc85a 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -189,7 +189,6 @@ public: virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0; virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; - virtual HRESULT ObtainAppMenu(IAvnMenu** retOut) = 0; virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0; virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0; @@ -224,7 +223,6 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown virtual HRESULT SetCursor(IAvnCursor* cursor) = 0; virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0; virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0; - virtual HRESULT ObtainMainMenu(IAvnMenu** retOut) = 0; virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0; virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0; virtual HRESULT ObtainNSViewHandle(void** retOut) = 0; diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 54ba93c06a..a63353bc0a 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -250,18 +250,6 @@ public: ::SetAppMenu(s_appTitle, appMenu); return S_OK; } - - virtual HRESULT ObtainAppMenu(IAvnMenu** retOut) override - { - if(retOut == nullptr) - { - return E_POINTER; - } - - *retOut = ::GetAppMenu(); - - return S_OK; - } }; extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index ce66a6c327..1bb4fc62b0 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -247,18 +247,6 @@ public: return S_OK; } - virtual HRESULT ObtainMainMenu(IAvnMenu** ret) override - { - if(ret == nullptr) - { - return E_POINTER; - } - - *ret = _mainMenu; - - return S_OK; - } - virtual HRESULT BeginMoveDrag () override { @autoreleasepool diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index a9438d33b7..077fd624c1 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -131,16 +131,11 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = _factory.ObtainAppMenu(); + _nativeMenu = IAvnMenu.Create(_factory); - if (_nativeMenu is null) - { - _nativeMenu = IAvnMenu.Create(_factory); - - _nativeMenu.Initialise(this, appMenuHolder, ""); + _nativeMenu.Initialise(this, appMenuHolder, ""); - setMenu = true; - } + setMenu = true; } _nativeMenu.Update(_factory, appMenuHolder); @@ -155,14 +150,9 @@ namespace Avalonia.Native { if (_nativeMenu is null) { - _nativeMenu = avnWindow.ObtainMainMenu(); + _nativeMenu = IAvnMenu.Create(_factory); - if (_nativeMenu is null) - { - _nativeMenu = IAvnMenu.Create(_factory); - - _nativeMenu.Initialise(this, menu, ""); - } + _nativeMenu.Initialise(this, menu, ""); } _nativeMenu.Update(_factory, menu); From 964079df4c6c75f92300b2f3bc74b879255aaedb Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 11:59:24 -0300 Subject: [PATCH 067/164] Use MenuItems bridge interface to raise clicks. --- .../INativeMenuItemExporterEventsImplBridge.cs | 0 src/Avalonia.Controls/NativeMenuBar.cs | 2 +- src/Avalonia.Controls/NativeMenuItem.cs | 4 ++-- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 4 ++-- src/Avalonia.Native/IAvnMenuItem.cs | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs diff --git a/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Avalonia.Controls/NativeMenuBar.cs b/src/Avalonia.Controls/NativeMenuBar.cs index 9b96ab9c8c..63bb39108f 100644 --- a/src/Avalonia.Controls/NativeMenuBar.cs +++ b/src/Avalonia.Controls/NativeMenuBar.cs @@ -30,7 +30,7 @@ namespace Avalonia.Controls private static void OnMenuItemClick(object sender, RoutedEventArgs e) { - (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick(); + (((MenuItem)sender).DataContext as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked(); } } } diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 2f2eca539b..6bcf676ddb 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -5,7 +5,7 @@ using Avalonia.Utilities; namespace Avalonia.Controls { - public class NativeMenuItem : NativeMenuItemBase + public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge { private string _header; private KeyGesture _gesture; @@ -159,7 +159,7 @@ namespace Avalonia.Controls public event EventHandler Clicked; - public void RaiseClick() + void INativeMenuItemExporterEventsImplBridge.RaiseClicked() { Clicked?.Invoke(this, new EventArgs()); diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 06bbc97dac..cd56342059 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -319,10 +319,10 @@ namespace Avalonia.FreeDesktop { var item = GetMenu(id).item; - if (item is NativeMenuItem menuItem) + if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge) { if (menuItem?.IsEnabled == true) - menuItem.RaiseClick(); + bridge?.RaiseClicked(); } } } diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index a41e461e37..a9730920f1 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -50,7 +50,7 @@ namespace Avalonia.Native.Interop return false; }); - var callback = new MenuActionCallback(() => { item.RaiseClick(); }); + var callback = new MenuActionCallback(() => { (item as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked(); }); _currentActionDisposable = Disposable.Create(() => { From 704bf58d706119113110e07a930c72fb808c1c85 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 12:12:07 -0300 Subject: [PATCH 068/164] missing file. --- .../INativeMenuItemExporterEventsImplBridge.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs b/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs index e69de29bb2..6cb68d8ddd 100644 --- a/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs +++ b/src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs @@ -0,0 +1,7 @@ +namespace Avalonia.Controls +{ + public interface INativeMenuItemExporterEventsImplBridge + { + void RaiseClicked (); + } +} From 1d0a33cfeb0e6003bf0c6bb715b85173c52e06b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 12:23:07 -0300 Subject: [PATCH 069/164] rename method. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 2 +- src/Avalonia.Native/IAvnMenu.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 077fd624c1..50df814ac8 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -43,7 +43,7 @@ namespace Avalonia.Native DoLayoutReset(); } - internal void InvalidateMenu() + internal void UpdateIfNeeded() { if (_resetQueued) { diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index b2141cc1a8..8a49559a02 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -34,7 +34,7 @@ namespace Avalonia.Native.Interop { (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseNeedsUpdate(); - _exporter.InvalidateMenu(); + _exporter.UpdateIfNeeded(); } internal NativeMenu ManagedMenu { get; private set; } From caaa193902cfa6649e6a545e5a1eea83d2607d56 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 13:34:12 -0300 Subject: [PATCH 070/164] special treatment for toplevel items in nativemenus. --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 4 ++-- native/Avalonia.Native/src/OSX/menu.h | 3 ++- native/Avalonia.Native/src/OSX/menu.mm | 18 +++++++++++++++--- .../AvaloniaNativeMenuExporter.cs | 10 ++++++---- src/Avalonia.Native/IAvnMenu.cs | 4 ++-- src/Avalonia.Native/IAvnMenuItem.cs | 2 +- 8 files changed, 30 insertions(+), 15 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 757c0bc85a..fe6dfbc8b8 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -190,7 +190,7 @@ public: virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; - virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0; + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv, bool isTopLevel) = 0; virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0; virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0; }; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index 7a433bfd9f..ad492264fd 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -15,7 +15,7 @@ extern IAvnScreens* CreateScreens(); extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); -extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events, bool isTopLevel); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeperator(); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index a63353bc0a..216b531e0f 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -227,9 +227,9 @@ public: return S_OK; } - virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv, bool isTopLevel) override { - *ppv = ::CreateAppMenu(cb); + *ppv = ::CreateAppMenu(cb, isTopLevel); return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index e4353d10ac..6af5d4c7b4 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -59,11 +59,12 @@ class AvnAppMenu : public ComSingleObject private: AvnMenu* _native; ComPtr _baseEvents; + bool _isTopLevel; public: FORWARD_IUNKNOWN() - AvnAppMenu(IAvnMenuEvents* events); + AvnAppMenu(IAvnMenuEvents* events, bool isTopLevel); AvnMenu* GetNative(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 29bed72980..da85955019 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -169,11 +169,18 @@ void AvnAppMenuItem::RaiseOnClicked() } } -AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events) +AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events, bool isTopLevel) { + _isTopLevel = isTopLevel; _baseEvents = events; id del = [[AvnMenuDelegate alloc] initWithParent: this]; _native = [[AvnMenu alloc] initWithDelegate: del]; + + + if(_isTopLevel) + { + [_native insertItem: [NSMenuItem new] atIndex:0]; + } } @@ -194,6 +201,11 @@ HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { @autoreleasepool { + if(_isTopLevel) + { + index++; + } + auto avnMenuItem = dynamic_cast(item); if(avnMenuItem != nullptr) @@ -272,11 +284,11 @@ HRESULT AvnAppMenu::Clear() @end -extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb) +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb, bool isTopLevel) { @autoreleasepool { - return new AvnAppMenu(cb); + return new AvnAppMenu(cb, isTopLevel); } } diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 50df814ac8..2b682b37c2 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -84,9 +84,11 @@ namespace Avalonia.Native if (appMenu == null) { - appMenu = CreateDefaultAppMenu(); - SetMenu(appMenu); + appMenu = CreateDefaultAppMenu(); + NativeMenu.SetMenu(Application.Current, appMenu); } + + SetMenu(appMenu); } else { @@ -131,7 +133,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = IAvnMenu.Create(_factory); + _nativeMenu = IAvnMenu.Create(_factory, false); _nativeMenu.Initialise(this, appMenuHolder, ""); @@ -150,7 +152,7 @@ namespace Avalonia.Native { if (_nativeMenu is null) { - _nativeMenu = IAvnMenu.Create(_factory); + _nativeMenu = IAvnMenu.Create(_factory, true); _nativeMenu.Initialise(this, menu, ""); } diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 8a49559a02..555f1074b1 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -39,11 +39,11 @@ namespace Avalonia.Native.Interop internal NativeMenu ManagedMenu { get; private set; } - public static IAvnMenu Create(IAvaloniaNativeFactory factory) + public static IAvnMenu Create(IAvaloniaNativeFactory factory, bool isTopLevel) { var events = new MenuEvents(); - var menu = factory.CreateMenu(events); + var menu = factory.CreateMenu(events, isTopLevel); events.Initialise(menu); diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index a9730920f1..cd15f27eb7 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -114,7 +114,7 @@ namespace Avalonia.Native.Interop { if (_subMenu == null) { - _subMenu = IAvnMenu.Create(factory); + _subMenu = IAvnMenu.Create(factory, false); _subMenu.Initialise(exporter, item.Menu, item.Header); From 3a6b48c89417158533bd0bf9f351b2ad58619d11 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 15:06:03 -0300 Subject: [PATCH 071/164] simplify reparenting logic. --- native/Avalonia.Native/src/OSX/window.h | 3 +- native/Avalonia.Native/src/OSX/window.mm | 63 ++++++++++-------------- 2 files changed, 27 insertions(+), 39 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index a16ce524f0..e6eeed94a1 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -19,7 +19,8 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(void) applyMenu:(NSMenu *_Nullable)menu; +-(void) reparentMenu; +-(void) applyMenu:(NSMenu* _Nullable)menu; -(double) getScaling; @end diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 1bb4fc62b0..9e8a0b30d3 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -244,6 +244,11 @@ public: [Window applyMenu:nsmenu]; + if ([Window isKeyWindow]) + { + [Window reparentMenu]; + } + return S_OK; } @@ -1140,7 +1145,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent bool _canBecomeKeyAndMain; bool _closed; NSMenu* _menu; - bool _isAppMenuApplied; double _lastScaling; } @@ -1177,6 +1181,23 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } +-(void) reparentMenu +{ + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = [appMenuItem menu]; + + [appMenu removeItem:appMenuItem]; + + [[_menu itemAtIndex:0] setSubmenu:appMenu]; + //[_menu insertItem:appMenuItem atIndex:0]; + } + + [NSApp setMenu:_menu]; +} + -(void) applyMenu:(NSMenu *)menu { if(menu == nullptr) @@ -1185,22 +1206,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } _menu = menu; - - if ([self isKeyWindow]) - { - auto appMenu = ::GetAppMenuItem(); - - if(appMenu != nullptr) - { - [[appMenu menu] removeItem:appMenu]; - - [_menu insertItem:appMenu atIndex:0]; - - _isAppMenuApplied = true; - } - - [NSApp setMenu:menu]; - } } -(void) setCanBecomeKeyAndMain @@ -1303,23 +1308,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self activateAppropriateChild: true]) { - if(_menu == nullptr) - { - _menu = [NSMenu new]; - } - - auto appMenu = ::GetAppMenuItem(); - - if(appMenu != nullptr) - { - [[appMenu menu] removeItem:appMenu]; - - [_menu insertItem:appMenu atIndex:0]; - - _isAppMenuApplied = true; - } - - [NSApp setMenu:_menu]; + [self reparentMenu]; _parent->BaseEvents->Activated(); [super becomeKeyWindow]; @@ -1379,9 +1368,9 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent auto nativeAppMenu = dynamic_cast(appMenu); - [[appMenuItem menu] removeItem:appMenuItem]; + //[[appMenuItem menu] removeItem::appMenuItem]; - [nativeAppMenu->GetNative() addItem:appMenuItem]; + //[nativeAppMenu->GetNative() addItem:appMenuItem]; [NSApp setMenu:nativeAppMenu->GetNative()]; } @@ -1390,8 +1379,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [NSApp setMenu:nullptr]; } - // remove window menu items from appmenu? - [super resignKeyWindow]; } From 77674a8f0c745dd6ed56b405fb9aa98d41e483ba Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 15:44:11 -0300 Subject: [PATCH 072/164] simplify switching between appmenu only and appmenu + window menu. --- native/Avalonia.Native/src/OSX/window.h | 3 +- native/Avalonia.Native/src/OSX/window.mm | 61 +++++++++++++----------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index e6eeed94a1..505900d584 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -19,7 +19,8 @@ class WindowBaseImpl; -(void) pollModalSession: (NSModalSession _Nonnull) session; -(void) restoreParentWindow; -(bool) shouldTryToHandleEvents; --(void) reparentMenu; +-(void) showAppMenuOnly; +-(void) showWindowMenuWithAppMenu; -(void) applyMenu:(NSMenu* _Nullable)menu; -(double) getScaling; @end diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 9e8a0b30d3..ece790150c 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -246,7 +246,7 @@ public: if ([Window isKeyWindow]) { - [Window reparentMenu]; + [Window showWindowMenuWithAppMenu]; } return S_OK; @@ -1181,21 +1181,45 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } --(void) reparentMenu +-(void) showWindowMenuWithAppMenu +{ + if(_menu != nullptr) + { + auto appMenuItem = ::GetAppMenuItem(); + + if(appMenuItem != nullptr) + { + auto appMenu = [appMenuItem menu]; + + [appMenu removeItem:appMenuItem]; + + [_menu insertItem:appMenuItem atIndex:0]; + } + + [NSApp setMenu:_menu]; + } +} + +-(void) showAppMenuOnly { auto appMenuItem = ::GetAppMenuItem(); if(appMenuItem != nullptr) { - auto appMenu = [appMenuItem menu]; + auto appMenu = ::GetAppMenu(); + + auto nativeAppMenu = dynamic_cast(appMenu); + + [[appMenuItem menu] removeItem:appMenuItem]; - [appMenu removeItem:appMenuItem]; + [nativeAppMenu->GetNative() addItem:appMenuItem]; - [[_menu itemAtIndex:0] setSubmenu:appMenu]; - //[_menu insertItem:appMenuItem atIndex:0]; + [NSApp setMenu:nativeAppMenu->GetNative()]; + } + else + { + [NSApp setMenu:nullptr]; } - - [NSApp setMenu:_menu]; } -(void) applyMenu:(NSMenu *)menu @@ -1308,7 +1332,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent { if([self activateAppropriateChild: true]) { - [self reparentMenu]; + [self showWindowMenuWithAppMenu]; _parent->BaseEvents->Activated(); [super becomeKeyWindow]; @@ -1360,24 +1384,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(_parent) _parent->BaseEvents->Deactivated(); - auto appMenuItem = ::GetAppMenuItem(); - - if(appMenuItem != nullptr) - { - auto appMenu = ::GetAppMenu(); - - auto nativeAppMenu = dynamic_cast(appMenu); - - //[[appMenuItem menu] removeItem::appMenuItem]; - - //[nativeAppMenu->GetNative() addItem:appMenuItem]; - - [NSApp setMenu:nativeAppMenu->GetNative()]; - } - else - { - [NSApp setMenu:nullptr]; - } + [self showAppMenuOnly]; [super resignKeyWindow]; } From 230b4030aec98f331df86c53d3a2452cb862a36b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 15:44:41 -0300 Subject: [PATCH 073/164] only call SetMenu once OSX. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 2b682b37c2..1e488505d6 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -150,16 +150,23 @@ namespace Avalonia.Native private void SetMenu(IAvnWindow avnWindow, NativeMenu menu) { + var setMenu = false; + if (_nativeMenu is null) { _nativeMenu = IAvnMenu.Create(_factory, true); - _nativeMenu.Initialise(this, menu, ""); + _nativeMenu.Initialise(this, menu, ""); + + setMenu = true; } _nativeMenu.Update(_factory, menu); - avnWindow.SetMainMenu(_nativeMenu); + if(setMenu) + { + avnWindow.SetMainMenu(_nativeMenu); + } } } } From af510bf705fd972ef0b1b6ccf7851c270006a553 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 19:24:42 -0300 Subject: [PATCH 074/164] use a flag to tell if a menu has been reparented. --- native/Avalonia.Native/inc/avalonia-native.h | 2 +- native/Avalonia.Native/src/OSX/common.h | 2 +- native/Avalonia.Native/src/OSX/main.mm | 4 +-- native/Avalonia.Native/src/OSX/menu.h | 5 ++-- native/Avalonia.Native/src/OSX/menu.mm | 28 ++++++++++++-------- native/Avalonia.Native/src/OSX/window.mm | 13 ++++++--- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index fe6dfbc8b8..757c0bc85a 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -190,7 +190,7 @@ public: virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0; virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0; virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0; - virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv, bool isTopLevel) = 0; + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0; virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0; virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0; }; diff --git a/native/Avalonia.Native/src/OSX/common.h b/native/Avalonia.Native/src/OSX/common.h index ad492264fd..7a433bfd9f 100644 --- a/native/Avalonia.Native/src/OSX/common.h +++ b/native/Avalonia.Native/src/OSX/common.h @@ -15,7 +15,7 @@ extern IAvnScreens* CreateScreens(); extern IAvnClipboard* CreateClipboard(); extern IAvnCursorFactory* CreateCursorFactory(); extern IAvnGlDisplay* GetGlDisplay(); -extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events, bool isTopLevel); +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events); extern IAvnMenuItem* CreateAppMenuItem(); extern IAvnMenuItem* CreateAppMenuItemSeperator(); extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu); diff --git a/native/Avalonia.Native/src/OSX/main.mm b/native/Avalonia.Native/src/OSX/main.mm index 216b531e0f..a63353bc0a 100644 --- a/native/Avalonia.Native/src/OSX/main.mm +++ b/native/Avalonia.Native/src/OSX/main.mm @@ -227,9 +227,9 @@ public: return S_OK; } - virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv, bool isTopLevel) override + virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override { - *ppv = ::CreateAppMenu(cb, isTopLevel); + *ppv = ::CreateAppMenu(cb); return S_OK; } diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 6af5d4c7b4..615f24cf85 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -16,6 +16,8 @@ class AvnAppMenu; @interface AvnMenu : NSMenu - (id) initWithDelegate: (NSObject*) del; +- (void) setIsReparented: (bool) value; +- (bool) isReparented; @end @interface AvnMenuItem : NSMenuItem @@ -59,12 +61,11 @@ class AvnAppMenu : public ComSingleObject private: AvnMenu* _native; ComPtr _baseEvents; - bool _isTopLevel; public: FORWARD_IUNKNOWN() - AvnAppMenu(IAvnMenuEvents* events, bool isTopLevel); + AvnAppMenu(IAvnMenuEvents* events); AvnMenu* GetNative(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index da85955019..915e11f6fa 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -5,16 +5,29 @@ @implementation AvnMenu { + bool _isReparented; NSObject* _wtf; } + - (id) initWithDelegate: (NSObject*)del { self = [super init]; self.delegate = del; _wtf = del; + _isReparented = false; return self; } +- (bool)isReparented +{ + return _isReparented; +} + +- (void)setIsReparented:(bool)value +{ + _isReparented = value; +} + @end @implementation AvnMenuItem @@ -169,18 +182,11 @@ void AvnAppMenuItem::RaiseOnClicked() } } -AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events, bool isTopLevel) +AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events) { - _isTopLevel = isTopLevel; _baseEvents = events; id del = [[AvnMenuDelegate alloc] initWithParent: this]; _native = [[AvnMenu alloc] initWithDelegate: del]; - - - if(_isTopLevel) - { - [_native insertItem: [NSMenuItem new] atIndex:0]; - } } @@ -201,7 +207,7 @@ HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { @autoreleasepool { - if(_isTopLevel) + if([_native isReparented]) { index++; } @@ -284,11 +290,11 @@ HRESULT AvnAppMenu::Clear() @end -extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb, bool isTopLevel) +extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb) { @autoreleasepool { - return new AvnAppMenu(cb, isTopLevel); + return new AvnAppMenu(cb); } } diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index ece790150c..fcb4884b7d 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1144,7 +1144,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent ComPtr _parent; bool _canBecomeKeyAndMain; bool _closed; - NSMenu* _menu; + AvnMenu* _menu; double _lastScaling; } @@ -1194,6 +1194,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [appMenu removeItem:appMenuItem]; [_menu insertItem:appMenuItem atIndex:0]; + + [_menu setIsReparented:true]; } [NSApp setMenu:_menu]; @@ -1212,6 +1214,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [[appMenuItem menu] removeItem:appMenuItem]; + if(_menu != nullptr) + { + [_menu setIsReparented:false]; + } + [nativeAppMenu->GetNative() addItem:appMenuItem]; [NSApp setMenu:nativeAppMenu->GetNative()]; @@ -1222,11 +1229,11 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent } } --(void) applyMenu:(NSMenu *)menu +-(void) applyMenu:(AvnMenu *)menu { if(menu == nullptr) { - menu = [NSMenu new]; + menu = [AvnMenu new]; } _menu = menu; From cdc0b1571d587e56c825546db4e6f5461f8915b1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 27 Apr 2020 19:29:30 -0300 Subject: [PATCH 075/164] remove toplevel argument. --- src/Avalonia.Native/AvaloniaNativeMenuExporter.cs | 4 ++-- src/Avalonia.Native/IAvnMenu.cs | 4 ++-- src/Avalonia.Native/IAvnMenuItem.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs index 1e488505d6..0f2551ffeb 100644 --- a/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs +++ b/src/Avalonia.Native/AvaloniaNativeMenuExporter.cs @@ -133,7 +133,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = IAvnMenu.Create(_factory, false); + _nativeMenu = IAvnMenu.Create(_factory); _nativeMenu.Initialise(this, appMenuHolder, ""); @@ -154,7 +154,7 @@ namespace Avalonia.Native if (_nativeMenu is null) { - _nativeMenu = IAvnMenu.Create(_factory, true); + _nativeMenu = IAvnMenu.Create(_factory); _nativeMenu.Initialise(this, menu, ""); diff --git a/src/Avalonia.Native/IAvnMenu.cs b/src/Avalonia.Native/IAvnMenu.cs index 555f1074b1..8a49559a02 100644 --- a/src/Avalonia.Native/IAvnMenu.cs +++ b/src/Avalonia.Native/IAvnMenu.cs @@ -39,11 +39,11 @@ namespace Avalonia.Native.Interop internal NativeMenu ManagedMenu { get; private set; } - public static IAvnMenu Create(IAvaloniaNativeFactory factory, bool isTopLevel) + public static IAvnMenu Create(IAvaloniaNativeFactory factory) { var events = new MenuEvents(); - var menu = factory.CreateMenu(events, isTopLevel); + var menu = factory.CreateMenu(events); events.Initialise(menu); diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index cd15f27eb7..a9730920f1 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -114,7 +114,7 @@ namespace Avalonia.Native.Interop { if (_subMenu == null) { - _subMenu = IAvnMenu.Create(factory, false); + _subMenu = IAvnMenu.Create(factory); _subMenu.Initialise(exporter, item.Menu, item.Header); From 8f7f4b7267f98ed2fd8f062aec221f5a42fdeb7c Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 Apr 2020 14:12:58 +0800 Subject: [PATCH 076/164] Add failing test for #3653 --- .../AnimationIterationTests.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs index f7a8774689..fe718ec32b 100644 --- a/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs +++ b/tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs @@ -14,6 +14,55 @@ namespace Avalonia.Animation.UnitTests { public class AnimationIterationTests { + [Fact] + public void Check_KeyTime_Correctly_Converted_To_Cue() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 100d), + }, + KeyTime = TimeSpan.FromSeconds(0.5) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(Border.WidthProperty, 0d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(1), + Children = + { + keyframe2, + keyframe1 + } + }; + + var border = new Border() + { + Height = 100d, + Width = 100d + }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(border, clock); + + clock.Step(TimeSpan.Zero); + Assert.Equal(border.Width, 0d); + + clock.Step(TimeSpan.FromSeconds(1)); + Assert.Equal(border.Width, 100d); + + } + + [Fact] public void Check_Initial_Inter_and_Trailing_Delay_Values() { From bfa34b796b8dbfe7534940e13fbee3c6607f0caf Mon Sep 17 00:00:00 2001 From: Jumar Macato Date: Tue, 28 Apr 2020 14:14:24 +0800 Subject: [PATCH 077/164] Correctly process KeyTime to Cue by using TimeSpan.TotalSeconds instead of Ticks. --- src/Avalonia.Animation/Animation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index b1d4e0e58f..1ce9f5db06 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -251,7 +251,7 @@ namespace Avalonia.Animation if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) { - cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks); + cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds); } var newKF = new AnimatorKeyFrame(handler, cue); From 8e2d904377a17899712b0bffe60168dbc4c9cddc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 09:14:15 -0300 Subject: [PATCH 078/164] rename property. --- native/Avalonia.Native/src/OSX/menu.h | 4 ++-- native/Avalonia.Native/src/OSX/menu.mm | 6 +++--- native/Avalonia.Native/src/OSX/window.mm | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 615f24cf85..228731cb28 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -16,8 +16,8 @@ class AvnAppMenu; @interface AvnMenu : NSMenu - (id) initWithDelegate: (NSObject*) del; -- (void) setIsReparented: (bool) value; -- (bool) isReparented; +- (void) setHasGlobalMenuItem: (bool) value; +- (bool) hasGlobalMenuItem; @end @interface AvnMenuItem : NSMenuItem diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 915e11f6fa..1cd9cb0644 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -18,12 +18,12 @@ return self; } -- (bool)isReparented +- (bool)hasGlobalMenuItem { return _isReparented; } -- (void)setIsReparented:(bool)value +- (void)setHasGlobalMenuItem:(bool)value { _isReparented = value; } @@ -207,7 +207,7 @@ HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item) { @autoreleasepool { - if([_native isReparented]) + if([_native hasGlobalMenuItem]) { index++; } diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index fcb4884b7d..109ed63e6f 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -1195,7 +1195,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [_menu insertItem:appMenuItem atIndex:0]; - [_menu setIsReparented:true]; + [_menu setHasGlobalMenuItem:true]; } [NSApp setMenu:_menu]; @@ -1216,7 +1216,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent if(_menu != nullptr) { - [_menu setIsReparented:false]; + [_menu setHasGlobalMenuItem:false]; } [nativeAppMenu->GetNative() addItem:appMenuItem]; From 6b84d5def9b5adebd996c9184fb248ee07bf141f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 09:15:01 -0300 Subject: [PATCH 079/164] remove debug code. --- samples/ControlCatalog/MainWindow.xaml.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 81e5f82f76..d97325ef8d 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -30,14 +30,6 @@ namespace ControlCatalog DataContext = new MainWindowViewModel(_notificationArea); _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu; - var fileMenu = (NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu; - - fileMenu.Opening += (sender, e)=> - { - fileMenu.Items.Clear(); - fileMenu.Items.Add(new NativeMenuItem("Test 1")); - }; - var mainMenu = this.FindControl("MainMenu"); mainMenu.AttachedToVisualTree += MenuAttached; } From 5036685c3cd1145dfd521d490658d5c9732ad3f7 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 Apr 2020 17:50:39 +0300 Subject: [PATCH 080/164] Checkmark support for dbus exporter --- samples/ControlCatalog/MainWindow.xaml | 1 + src/Avalonia.Controls/NativeMenuItem.cs | 20 +++++++++++++++++++ src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 21 +++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 1f48350448..e75506624a 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -41,6 +41,7 @@ diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 6bcf676ddb..702bd8c90a 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -12,6 +12,7 @@ namespace Avalonia.Controls private bool _isEnabled = true; private ICommand _command; private bool _isChecked = false; + private NativeMenuItemToggleType _toggleType; private NativeMenu _menu; @@ -99,6 +100,18 @@ namespace Avalonia.Controls get => _isChecked; set => SetAndRaise(IsCheckedProperty, ref _isChecked, value); } + + public static readonly DirectProperty ToggleTypeProperty = + AvaloniaProperty.RegisterDirect( + nameof(ToggleType), + o => o.ToggleType, + (o, v) => o.ToggleType = v); + + public NativeMenuItemToggleType ToggleType + { + get => _toggleType; + set => SetAndRaise(ToggleTypeProperty, ref _toggleType, value); + } public static readonly DirectProperty CommandProperty = Button.CommandProperty.AddOwner( @@ -169,4 +182,11 @@ namespace Avalonia.Controls } } } + + public enum NativeMenuItemToggleType + { + None, + CheckBox, + Radio + } } diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index cd56342059..c60c1d396f 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -184,7 +184,7 @@ namespace Avalonia.FreeDesktop private static string[] AllProperties = new[] { - "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display" + "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state" }; object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) @@ -234,6 +234,25 @@ namespace Avalonia.FreeDesktop return new[] { lst.ToArray() }; } + if (name == "toggle-type") + { + if (item.ToggleType == NativeMenuItemToggleType.CheckBox) + return "checkmark"; + if (item.ToggleType == NativeMenuItemToggleType.Radio) + return "radio"; + // Someone has forgot to set the style + if (item.IsChecked) + return "checkmark"; + } + + if (name == "toggle-state") + { + if (item.IsChecked) + return 1; + if (item.ToggleType != NativeMenuItemToggleType.None) + return 0; + } + if (name == "children-display") return menu != null ? "submenu" : null; } From fbb1ce340b80a12b3341a309d2796637cc9b1ba8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 16:36:36 -0300 Subject: [PATCH 081/164] Add backend framework for radio buttons in nativemenus. --- native/Avalonia.Native/inc/avalonia-native.h | 8 ++++++ native/Avalonia.Native/src/OSX/menu.h | 3 +++ native/Avalonia.Native/src/OSX/menu.mm | 28 +++++++++++++++++++- src/Avalonia.Native/IAvnMenuItem.cs | 5 ++++ 4 files changed, 43 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 757c0bc85a..7fde6b3360 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -176,6 +176,13 @@ enum AvnWindowEdge WindowEdgeSouthEast }; +enum AvnMenuItemToggleType +{ + None, + CheckMark, + Radio +}; + AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: @@ -407,6 +414,7 @@ AVNCOM(IAvnMenuItem, 19) : IUnknown virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0; virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; virtual HRESULT SetIsChecked (bool isChecked) = 0; + virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) = 0; }; AVNCOM(IAvnMenuEvents, 1A) : IUnknown diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 228731cb28..cd464d2169 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -32,6 +32,7 @@ private: IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; bool _isSeperator; + bool _isCheckable; public: FORWARD_IUNKNOWN() @@ -50,6 +51,8 @@ public: virtual HRESULT SetIsChecked (bool isChecked) override; + virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) override; + bool EvaluateItemEnabled(); void RaiseOnClicked(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 1cd9cb0644..6ea7797fa8 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -70,6 +70,7 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) { + _isCheckable = false; _isSeperator = isSeperator; if(isSeperator) @@ -157,7 +158,32 @@ HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) { @autoreleasepool { - [_native setState:(isChecked ? NSOnState : NSOffState)]; + [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)]; + return S_OK; + } +} + +HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) +{ + @autoreleasepool + { + switch(toggleType) + { + case AvnMenuItemToggleType::None: + [_native setOnStateImage:nullptr]; + _isCheckable = false; + break; + + case AvnMenuItemToggleType::CheckMark: + [_native setOnStateImage:nullptr]; + _isCheckable = true; + break; + + case AvnMenuItemToggleType::Radio: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuRadio"]]; + break; + } + return S_OK; } } diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index a9730920f1..2e829832bf 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -26,6 +26,11 @@ namespace Avalonia.Native.Interop IsChecked = isChecked; } + private void UpdateToggleType(NativeMenuItemToggleType toggleType) + { + + } + private void UpdateGesture(Input.KeyGesture gesture) { // todo ensure backend can cope with setting null gesture. From 6426a206667ddf17efcc6d9bd747b0ad3f9c55a7 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 17:12:49 -0300 Subject: [PATCH 082/164] Implement Radio Toggle type on OSX. --- native/Avalonia.Native/src/OSX/menu.h | 1 - native/Avalonia.Native/src/OSX/menu.mm | 9 +++------ src/Avalonia.Native/IAvnMenuItem.cs | 7 ++++++- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index cd464d2169..2d211e8643 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -32,7 +32,6 @@ private: IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; bool _isSeperator; - bool _isCheckable; public: FORWARD_IUNKNOWN() diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 6ea7797fa8..62eaffa5b8 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -70,7 +70,6 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) { - _isCheckable = false; _isSeperator = isSeperator; if(isSeperator) @@ -158,7 +157,7 @@ HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) { @autoreleasepool { - [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)]; + [_native setState:(isChecked ? NSOnState : NSOffState)]; return S_OK; } } @@ -171,16 +170,14 @@ HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) { case AvnMenuItemToggleType::None: [_native setOnStateImage:nullptr]; - _isCheckable = false; break; case AvnMenuItemToggleType::CheckMark: - [_native setOnStateImage:nullptr]; - _isCheckable = true; + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; break; case AvnMenuItemToggleType::Radio: - [_native setOnStateImage: [NSImage imageNamed:@"NSMenuRadio"]]; + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuItemBullet"]]; break; } diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index 2e829832bf..df82f3955a 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -28,7 +28,7 @@ namespace Avalonia.Native.Interop private void UpdateToggleType(NativeMenuItemToggleType toggleType) { - + ToggleType = (AvnMenuItemToggleType)toggleType; } private void UpdateGesture(Input.KeyGesture gesture) @@ -78,6 +78,8 @@ namespace Avalonia.Native.Interop UpdateAction(ManagedMenuItem as NativeMenuItem); + UpdateToggleType(item.ToggleType); + UpdateIsChecked(item.IsChecked); _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) @@ -89,6 +91,9 @@ namespace Avalonia.Native.Interop _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty) .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.ToggleTypeProperty) + .Subscribe(x => UpdateToggleType(x))); + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) .Subscribe(x => UpdateIsChecked(x))); } From f05751e567997ba45a93cf45b324b1709bd0aafc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 17:14:06 -0300 Subject: [PATCH 083/164] improve demo --- samples/ControlCatalog/MainWindow.xaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index e75506624a..6970f7e1be 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -39,10 +39,14 @@ - + From e055474e3e01377d650c5b61ad1efabef8ce48a5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 17:26:35 -0300 Subject: [PATCH 084/164] support checking CheckMark::None --- native/Avalonia.Native/src/OSX/menu.mm | 3 --- samples/ControlCatalog/MainWindow.xaml | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 62eaffa5b8..cf4acb822f 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -169,9 +169,6 @@ HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) switch(toggleType) { case AvnMenuItemToggleType::None: - [_native setOnStateImage:nullptr]; - break; - case AvnMenuItemToggleType::CheckMark: [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; break; diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 6970f7e1be..9ebfd4b7ed 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -39,6 +39,10 @@ + Date: Tue, 28 Apr 2020 17:30:55 -0300 Subject: [PATCH 085/164] fix checkmark demo --- samples/ControlCatalog/MainWindow.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 9ebfd4b7ed..ca20327e1e 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -41,7 +41,7 @@ Date: Tue, 28 Apr 2020 17:42:58 -0300 Subject: [PATCH 086/164] checkmark == none ignores ischecked. --- native/Avalonia.Native/src/OSX/menu.h | 1 + native/Avalonia.Native/src/OSX/menu.mm | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index 2d211e8643..cd464d2169 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -32,6 +32,7 @@ private: IAvnActionCallback* _callback; IAvnPredicateCallback* _predicate; bool _isSeperator; + bool _isCheckable; public: FORWARD_IUNKNOWN() diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index cf4acb822f..11a8f04ae3 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -70,6 +70,7 @@ AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) { + _isCheckable = false; _isSeperator = isSeperator; if(isSeperator) @@ -157,7 +158,7 @@ HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked) { @autoreleasepool { - [_native setState:(isChecked ? NSOnState : NSOffState)]; + [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)]; return S_OK; } } @@ -169,12 +170,21 @@ HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) switch(toggleType) { case AvnMenuItemToggleType::None: + [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; + + _isCheckable = false; + break; + case AvnMenuItemToggleType::CheckMark: [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]]; + + _isCheckable = true; break; case AvnMenuItemToggleType::Radio: [_native setOnStateImage: [NSImage imageNamed:@"NSMenuItemBullet"]]; + + _isCheckable = true; break; } From 8a6ff82fddbff7be4fbfe180b1064405629150bc Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 17:44:56 -0300 Subject: [PATCH 087/164] make dbus behavior consistent with osx. --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index c60c1d396f..3f64c094ab 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -240,9 +240,6 @@ namespace Avalonia.FreeDesktop return "checkmark"; if (item.ToggleType == NativeMenuItemToggleType.Radio) return "radio"; - // Someone has forgot to set the style - if (item.IsChecked) - return "checkmark"; } if (name == "toggle-state") From f52ae127482358ff3d5f1ca6f49cf14311eea87d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 28 Apr 2020 23:58:05 +0300 Subject: [PATCH 088/164] Don't report toggle-state for non-togglable items --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 3f64c094ab..17affe09a3 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -244,10 +244,8 @@ namespace Avalonia.FreeDesktop if (name == "toggle-state") { - if (item.IsChecked) - return 1; if (item.ToggleType != NativeMenuItemToggleType.None) - return 0; + return item.IsChecked ? 1 : 0; } if (name == "children-display") From 332005b0f6b118e9a745e60dde710a827d3cdfc4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 19:26:58 -0300 Subject: [PATCH 089/164] Add osx backend menu item icon implementation. --- native/Avalonia.Native/inc/avalonia-native.h | 2 ++ native/Avalonia.Native/src/OSX/menu.h | 2 ++ native/Avalonia.Native/src/OSX/menu.mm | 28 ++++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 7fde6b3360..b10db08adc 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -1,5 +1,6 @@ #include "com.h" #include "key.h" +#include "stddef.h" #define AVNCOM(name, id) COMINTERFACE(name, 2e2cda0a, 9ae5, 4f1b, 8e, 20, 08, 1a, 04, 27, 9f, id) @@ -415,6 +416,7 @@ AVNCOM(IAvnMenuItem, 19) : IUnknown virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0; virtual HRESULT SetIsChecked (bool isChecked) = 0; virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) = 0; + virtual HRESULT SetIcon (void* data, size_t length) = 0; }; AVNCOM(IAvnMenuEvents, 1A) : IUnknown diff --git a/native/Avalonia.Native/src/OSX/menu.h b/native/Avalonia.Native/src/OSX/menu.h index cd464d2169..bfbc6801f8 100644 --- a/native/Avalonia.Native/src/OSX/menu.h +++ b/native/Avalonia.Native/src/OSX/menu.h @@ -53,6 +53,8 @@ public: virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) override; + virtual HRESULT SetIcon (void* data, size_t length) override; + bool EvaluateItemEnabled(); void RaiseOnClicked(); diff --git a/native/Avalonia.Native/src/OSX/menu.mm b/native/Avalonia.Native/src/OSX/menu.mm index 11a8f04ae3..dc1245cd23 100644 --- a/native/Avalonia.Native/src/OSX/menu.mm +++ b/native/Avalonia.Native/src/OSX/menu.mm @@ -192,6 +192,34 @@ HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType) } } +HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length) +{ + @autoreleasepool + { + if(data != nullptr) + { + NSData *imageData = [NSData dataWithBytes:data length:length]; + NSImage *image = [[NSImage alloc] initWithData:imageData]; + + NSSize originalSize = [image size]; + + NSSize size; + size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333; + + auto scaleFactor = size.height / originalSize.height; + size.width = originalSize.width * scaleFactor; + + [image setSize: size]; + [_native setImage:image]; + } + else + { + [_native setImage:nullptr]; + } + return S_OK; + } +} + bool AvnAppMenuItem::EvaluateItemEnabled() { if(_predicate != nullptr) From 2a568f632fcbfe1085833546b2501b90295e18d6 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 19:27:26 -0300 Subject: [PATCH 090/164] Add a markup converter for IBitmap type. --- .../XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index ebe4035ed6..a1fe6976b7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -100,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); + Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); From 01342b1b6f40bc49aad4c91e6d538132b1406144 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 19:27:43 -0300 Subject: [PATCH 091/164] Add Icon property to nativemenuicon. --- src/Avalonia.Controls/NativeMenuItem.cs | 12 ++++++++++ src/Avalonia.Native/IAvnMenuItem.cs | 29 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 702bd8c90a..4c94d82eb4 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -1,6 +1,7 @@ using System; using System.Windows.Input; using Avalonia.Input; +using Avalonia.Media.Imaging; using Avalonia.Utilities; namespace Avalonia.Controls @@ -13,6 +14,7 @@ namespace Avalonia.Controls private ICommand _command; private bool _isChecked = false; private NativeMenuItemToggleType _toggleType; + private IBitmap _icon; private NativeMenu _menu; @@ -71,6 +73,16 @@ namespace Avalonia.Controls } } + public static readonly DirectProperty IconProperty = + AvaloniaProperty.RegisterDirect(nameof(Icon), o => o.Icon, (o, v) => o.Icon = v); + + + public IBitmap Icon + { + get => _icon; + set => SetAndRaise(IconProperty, ref _icon, value); + } + public static readonly DirectProperty HeaderProperty = AvaloniaProperty.RegisterDirect(nameof(Header), o => o.Header, (o, v) => o.Header = v); diff --git a/src/Avalonia.Native/IAvnMenuItem.cs b/src/Avalonia.Native/IAvnMenuItem.cs index df82f3955a..c8819d1994 100644 --- a/src/Avalonia.Native/IAvnMenuItem.cs +++ b/src/Avalonia.Native/IAvnMenuItem.cs @@ -1,6 +1,8 @@ using System; +using System.IO; using System.Reactive.Disposables; using Avalonia.Controls; +using Avalonia.Media.Imaging; using Avalonia.Platform.Interop; namespace Avalonia.Native.Interop @@ -31,6 +33,28 @@ namespace Avalonia.Native.Interop ToggleType = (AvnMenuItemToggleType)toggleType; } + private unsafe void UpdateIcon (IBitmap icon) + { + if(icon is null) + { + SetIcon(IntPtr.Zero, 0); + } + else + { + using(var ms = new MemoryStream()) + { + icon.Save(ms); + + var imageData = ms.ToArray(); + + fixed(void* ptr = imageData) + { + SetIcon(new IntPtr(ptr), imageData.Length); + } + } + } + } + private void UpdateGesture(Input.KeyGesture gesture) { // todo ensure backend can cope with setting null gesture. @@ -80,6 +104,8 @@ namespace Avalonia.Native.Interop UpdateToggleType(item.ToggleType); + UpdateIcon(item.Icon); + UpdateIsChecked(item.IsChecked); _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty) @@ -96,6 +122,9 @@ namespace Avalonia.Native.Interop _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty) .Subscribe(x => UpdateIsChecked(x))); + + _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IconProperty) + .Subscribe(x => UpdateIcon(x))); } } From eff033589e7cff6fe14e9e5c2ab9f1724e5f77a3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 28 Apr 2020 19:27:56 -0300 Subject: [PATCH 092/164] Add icons to control catalog example. --- samples/ControlCatalog/MainWindow.xaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index ca20327e1e..bea751ad4c 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -14,9 +14,9 @@ - + - + From 8ff277c260fbb6b34fe83822c7f4c106e10fa88d Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 29 Apr 2020 01:35:53 +0300 Subject: [PATCH 093/164] icon-data support for dbusmenu --- src/Avalonia.FreeDesktop/DBusMenuExporter.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs index 17affe09a3..e93ca64d3a 100644 --- a/src/Avalonia.FreeDesktop/DBusMenuExporter.cs +++ b/src/Avalonia.FreeDesktop/DBusMenuExporter.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.IO; using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Controls; @@ -184,7 +185,7 @@ namespace Avalonia.FreeDesktop private static string[] AllProperties = new[] { - "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state" + "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data" }; object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name) @@ -248,6 +249,16 @@ namespace Avalonia.FreeDesktop return item.IsChecked ? 1 : 0; } + if (name == "icon-data") + { + if (item.Icon != null) + { + var ms = new MemoryStream(); + item.Icon.Save(ms); + return ms.ToArray(); + } + } + if (name == "children-display") return menu != null ? "submenu" : null; } From eb8de3433477ef968877d92add1220851f1e18b0 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 29 Apr 2020 07:28:58 -0400 Subject: [PATCH 094/164] Reset DesiredHeight before during measure Sets DataGridCells.DesiredHeight to 0 before measuring the individual cells --- .../Primitives/DataGridCellsPresenter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index b014c699bb..0f513e7f42 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -269,6 +269,9 @@ namespace Avalonia.Controls.Primitives // Since we didn't know the final widths of the columns until we resized, // we waited until now to measure each cell double leftEdge = 0; + if (autoSizeHeight) + DesiredHeight = 0; + foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns()) { DataGridCell cell = OwningRow.Cells[column.Index]; From 7e6f0b82659f529925a8a5df697a57babee048d4 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 10:57:16 -0400 Subject: [PATCH 095/164] Add KeySpline class from WPF source --- src/Avalonia.Animation/KeySpline.cs | 271 ++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 src/Avalonia.Animation/KeySpline.cs diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs new file mode 100644 index 0000000000..34e8d89aae --- /dev/null +++ b/src/Avalonia.Animation/KeySpline.cs @@ -0,0 +1,271 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Text; +using Avalonia; +using Avalonia.Utilities; + +// From: https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs + +namespace Avalonia.Animation +{ + [TypeConverter(typeof(KeySplineTypeConverter))] + public class KeySpline : AvaloniaObject + { + // Control points + private double _controlPointX1; + private double _controlPointY1; + private double _controlPointX2; + private double _controlPointY2; + private bool _isSpecified; + private bool _isDirty; + + // The parameter that corresponds to the most recent time + private double _parameter; + + // Cached coefficients + private double _Bx; // 3*points[0].X + private double _Cx; // 3*points[1].X + private double _Cx_Bx; // 2*(Cx - Bx) + private double _three_Cx; // 3 - Cx + + private double _By; // 3*points[0].Y + private double _Cy; // 3*points[1].Y + + // constants + private const double _accuracy = .001; // 1/3 the desired accuracy in X + private const double _fuzz = .000001; // computational zero + + public KeySpline() + { + _controlPointX1 = 0.0; + _controlPointY1 = 0.0; + _controlPointX2 = 1.0; + _controlPointY2 = 1.0; + _isDirty = true; + } + + public KeySpline(double x1, double y1, double x2, double y2) + { + _controlPointX1 = x1; + _controlPointY1 = y1; + _controlPointX2 = x2; + _controlPointY2 = y2; + _isDirty = true; + } + + public static KeySpline Parse(string value, CultureInfo culture) + { + using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) + { + return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); + } + } + + public double ControlPointX1 + { + get => _controlPointX1; + set => _controlPointX1 = value; + } + + public double ControlPointY1 + { + get => _controlPointY1; + set => _controlPointY1 = value; + } + + public double ControlPointX2 + { + get => _controlPointX2; + set => _controlPointX2 = value; + } + + public double ControlPointY2 + { + get => _controlPointY2; + set => _controlPointY2 = value; + } + + /// + /// Calculates spline progress from a linear progress. + /// + /// the linear progress + /// the spline progress + public double GetSplineProgress(double linearProgress) + { + //ReadPreamble(); + + if (_isDirty) + { + Build(); + } + + if (!_isSpecified) + { + return linearProgress; + } + else + { + SetParameterFromX(linearProgress); + + return GetBezierValue(_By, _Cy, _parameter); + } + } + + /// + /// Compute cached coefficients. + /// + private void Build() + { + if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1) + { + // This KeySpline would have no effect on the progress. + _isSpecified = false; + } + else + { + _isSpecified = true; + + _parameter = 0; + + // X coefficients + _Bx = 3 * _controlPointX1; + _Cx = 3 * _controlPointX2; + _Cx_Bx = 2 * (_Cx - _Bx); + _three_Cx = 3 - _Cx; + + // Y coefficients + _By = 3 * _controlPointY1; + _Cy = 3 * _controlPointY2; + } + + _isDirty = false; + } + + /// + /// Get an X or Y value with the Bezier formula. + /// + /// the second Bezier coefficient + /// the third Bezier coefficient + /// the parameter value to evaluate at + /// the value of the Bezier function at the given parameter + static private double GetBezierValue(double b, double c, double t) + { + double s = 1.0 - t; + double t2 = t * t; + + return b * t * s * s + c * t2 * s + t2 * t; + } + + /// + /// Get X and dX/dt at a given parameter + /// + /// the parameter value to evaluate at + /// the value of x there + /// the value of dx/dt there + private void GetXAndDx(double t, out double x, out double dx) + { + double s = 1.0 - t; + double t2 = t * t; + double s2 = s * s; + + x = _Bx * t * s2 + _Cx * t2 * s + t2 * t; + dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2; + } + + /// + /// Compute the parameter value that corresponds to a given X value, using a modified + /// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make + /// use of some known properties of this particular function: + /// * We are only interested in solutions in the interval [0,1] + /// * X(t) is increasing, so we can assume that if X(t) > time t > solution. We use + /// that to clamp down the search interval with every probe. + /// * The derivative of X and Y are between 0 and 3. + /// + /// the time, scaled to fit in [0,1] + private void SetParameterFromX(double time) + { + // Dynamic search interval to clamp with + double bottom = 0; + double top = 1; + + if (time == 0) + { + _parameter = 0; + } + else if (time == 1) + { + _parameter = 1; + } + else + { + // Loop while improving the guess + while (top - bottom > _fuzz) + { + double x, dx, absdx; + + // Get x and dx/dt at the current parameter + GetXAndDx(_parameter, out x, out dx); + absdx = Math.Abs(dx); + + // Clamp down the search interval, relying on the monotonicity of X(t) + if (x > time) + { + top = _parameter; // because parameter > solution + } + else + { + bottom = _parameter; // because parameter < solution + } + + // The desired accuracy is in ultimately in y, not in x, so the + // accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt). + // But dy/dt <=3, so we omit that + if (Math.Abs(x - time) < _accuracy * absdx) + { + break; // We're there + } + + if (absdx > _fuzz) + { + // Nonzero derivative, use Newton-Raphson to obtain the next guess + double next = _parameter - (x - time) / dx; + + // If next guess is out of the search interval then clamp it in + if (next >= top) + { + _parameter = (_parameter + top) / 2; + } + else if (next <= bottom) + { + _parameter = (_parameter + bottom) / 2; + } + else + { + // Next guess is inside the search interval, accept it + _parameter = next; + } + } + else // Zero derivative, halve the search interval + { + _parameter = (bottom + top) / 2; + } + } + } + } + } + + 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); + } + } +} From 24870351fa79ed925833b4afe11953fa0141ba29 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 11:32:41 -0400 Subject: [PATCH 096/164] Use KeySpline in Animation --- src/Avalonia.Animation/Animation.cs | 2 +- src/Avalonia.Animation/AnimatorKeyFrame.cs | 9 +++++ .../Animators/Animator`1.cs | 3 ++ src/Avalonia.Animation/KeyFrame.cs | 20 +++++++++++ src/Avalonia.Animation/KeySpline.cs | 36 ++++++++++++++++--- 5 files changed, 65 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 1ce9f5db06..ca1d97290e 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -254,7 +254,7 @@ namespace Avalonia.Animation cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds); } - var newKF = new AnimatorKeyFrame(handler, cue); + var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline); subscriptions.Add(newKF.BindSetter(setter, control)); diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 36d15e518e..f6a0c12be4 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -24,11 +24,20 @@ namespace Avalonia.Animation { AnimatorType = animatorType; Cue = cue; + KeySpline = null; + } + + public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline) + { + AnimatorType = animatorType; + Cue = cue; + KeySpline = keySpline; } internal bool isNeutral; public Type AnimatorType { get; } public Cue Cue { get; } + public KeySpline KeySpline { get; } public AvaloniaProperty Property { get; private set; } private object _value; diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index aa5e6aaf14..121ffda564 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators else newValue = (T)lastKeyframe.Value; + if (lastKeyframe.KeySpline != null) // TODO: do we use firstKeyFrame or lastKeyframe?! + progress = lastKeyframe.KeySpline.GetSplineProgress(progress); + return Interpolate(progress, oldValue, newValue); } diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index ec59586584..c2cc1aa051 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -19,6 +19,7 @@ namespace Avalonia.Animation { private TimeSpan _ktimeSpan; private Cue _kCue; + private KeySpline _kKeySpline; public KeyFrame() { @@ -74,6 +75,25 @@ namespace Avalonia.Animation } } + /// + /// Gets or sets the KeySpline of this . + /// + /// The key spline. + public KeySpline KeySpline + { + get + { + return _kKeySpline; + } + set + { + _kKeySpline = value; + if (value != null && !value.IsValid()) + { + throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0."); + } + } + } } diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index 34e8d89aae..dc6fb61744 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -66,7 +66,17 @@ namespace Avalonia.Animation public double ControlPointX1 { get => _controlPointX1; - set => _controlPointX1 = value; + set + { + if (IsValidXValue(value)) + { + _controlPointX1 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0."); + } + } } public double ControlPointY1 @@ -78,7 +88,17 @@ namespace Avalonia.Animation public double ControlPointX2 { get => _controlPointX2; - set => _controlPointX2 = value; + set + { + if (IsValidXValue(value)) + { + _controlPointX2 = value; + } + else + { + throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0."); + } + } } public double ControlPointY2 @@ -94,8 +114,6 @@ namespace Avalonia.Animation /// the spline progress public double GetSplineProgress(double linearProgress) { - //ReadPreamble(); - if (_isDirty) { Build(); @@ -113,6 +131,16 @@ namespace Avalonia.Animation } } + public bool IsValid() + { + return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); + } + + private bool IsValidXValue(double value) + { + return value >= 0.0 && value <= 1.0; + } + /// /// Compute cached coefficients. /// From 30eacab09d5023774231e219fd3edc5bfa42fb05 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 12:10:43 -0400 Subject: [PATCH 097/164] Add some unit tests for KeySpline --- .../KeySplineTests.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/Avalonia.Animation.UnitTests/KeySplineTests.cs diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs new file mode 100644 index 0000000000..a523be2a78 --- /dev/null +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -0,0 +1,48 @@ +using System; +using Avalonia.Controls; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Animation.UnitTests +{ + public class KeySplineTests + { + [Theory] + [InlineData("1,2 3,4")] + [InlineData("1 2 3 4")] + [InlineData("1 2,3 4")] + [InlineData("1,2,3,4")] + public void Can_Parse_KeySpline_Via_TypeConverter(string input) + { + var conv = new KeySplineTypeConverter(); + + var keySpline = (KeySpline)conv.ConvertFrom(input); + + Assert.Equal(1, keySpline.ControlPointX1); + Assert.Equal(2, keySpline.ControlPointY1); + Assert.Equal(3, keySpline.ControlPointX2); + Assert.Equal(4, keySpline.ControlPointY2); + } + + [Theory] + [InlineData(0.00)] + [InlineData(0.50)] + [InlineData(1.00)] + public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input) + { + var keySpline = new KeySpline(); + keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown + keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown + } + + [Theory] + [InlineData(-0.01)] + [InlineData(1.01)] + public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input) + { + var keySpline = new KeySpline(); + Assert.Throws(() => keySpline.ControlPointX1 = input); + Assert.Throws(() => keySpline.ControlPointX2 = input); + } + } +} From 47c0282ae9368d414310aa2ef167fb1e1465ab4a Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 12:21:27 -0400 Subject: [PATCH 098/164] Update documentation --- src/Avalonia.Animation/KeySpline.cs | 49 ++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index dc6fb61744..fc7dc8966e 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -6,10 +6,16 @@ using System.Text; using Avalonia; using Avalonia.Utilities; -// From: https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs +// 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 { + /// + /// Determines how an animation is used based on a cubic bezier curve. + /// X1 and X2 must be between 0.0 and 1.0, inclusive. + /// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline + /// [TypeConverter(typeof(KeySplineTypeConverter))] public class KeySpline : AvaloniaObject { @@ -37,6 +43,9 @@ namespace Avalonia.Animation private const double _accuracy = .001; // 1/3 the desired accuracy in X private const double _fuzz = .000001; // computational zero + /// + /// Create a with X1 = Y1 = 0 and X2 = Y2 = 1. + /// public KeySpline() { _controlPointX1 = 0.0; @@ -46,6 +55,13 @@ namespace Avalonia.Animation _isDirty = true; } + /// + /// Create a with the given parameters + /// + /// X coordinate for the first control point + /// Y coordinate for the first control point + /// X coordinate for the second control point + /// Y coordinate for the second control point public KeySpline(double x1, double y1, double x2, double y2) { _controlPointX1 = x1; @@ -55,6 +71,14 @@ namespace Avalonia.Animation _isDirty = true; } + /// + /// Parse a from a string. The string + /// needs to contain 4 values in it for the 2 control points. + /// + /// string with 4 values in it + /// culture of the string + /// Thrown if the string does not have 4 values + /// 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.")) @@ -63,6 +87,9 @@ namespace Avalonia.Animation } } + /// + /// X coordinate of the first control point + /// public double ControlPointX1 { get => _controlPointX1; @@ -79,12 +106,18 @@ namespace Avalonia.Animation } } + /// + /// Y coordinate of the first control point + /// public double ControlPointY1 { get => _controlPointY1; set => _controlPointY1 = value; } + /// + /// X coordinate of the second control point + /// public double ControlPointX2 { get => _controlPointX2; @@ -101,6 +134,9 @@ namespace Avalonia.Animation } } + /// + /// Y coordinate of the second control point + /// public double ControlPointY2 { get => _controlPointY2; @@ -131,11 +167,22 @@ namespace Avalonia.Animation } } + /// + /// Check to see whether the is valid by looking + /// at its X values. + /// + /// true if the X values for this fall in + /// acceptable range; false otherwise. public bool IsValid() { return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2); } + /// + /// + /// + /// + /// private bool IsValidXValue(double value) { return value >= 0.0 && value <= 1.0; From 4cf4bb953bd7a11dfb80718e61730fec627b97a8 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Wed, 29 Apr 2020 12:39:25 -0400 Subject: [PATCH 099/164] Update docs again (missed KeySplineTypeConverter) --- src/Avalonia.Animation/KeySpline.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index fc7dc8966e..5a4f7a15a3 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -331,6 +331,9 @@ namespace Avalonia.Animation } } + /// + /// Converts string values to values + /// public class KeySplineTypeConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) From d3fcebc716d81ace07b53daa416a100badc35ca2 Mon Sep 17 00:00:00 2001 From: sdoroff Date: Wed, 29 Apr 2020 16:34:10 -0400 Subject: [PATCH 100/164] Fix StringFormat in DataGridBoundColumns Prevent using the default converter when a Binding.StringFormat is present --- src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 09c3d07a41..8e82bf1a38 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -55,7 +55,7 @@ namespace Avalonia.Controls binding.Mode = BindingMode.TwoWay; } - if (binding.Converter == null) + if (binding.Converter == null && string.IsNullOrEmpty(binding.StringFormat)) { binding.Converter = DataGridValueConverter.Instance; } From 7d80492a1268ec021bd66a012e67d62738378025 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:25:11 -0300 Subject: [PATCH 101/164] Add Fullscreen to WindowState enum. --- src/Avalonia.Controls/WindowState.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/WindowState.cs b/src/Avalonia.Controls/WindowState.cs index 4ed30e726e..777b52dc11 100644 --- a/src/Avalonia.Controls/WindowState.cs +++ b/src/Avalonia.Controls/WindowState.cs @@ -19,5 +19,10 @@ namespace Avalonia.Controls /// The window is maximized. /// Maximized, + + /// + /// The window is fullscreen. + /// + FullScreen, } } From 02fa37772871df8958dc89829fc4d78688a35afe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:26:08 -0300 Subject: [PATCH 102/164] add missing window style. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index cbfa1abfb7..5c364c4038 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop WS_SIZEFRAME = 0x40000, WS_SYSMENU = 0x80000, WS_TABSTOP = 0x10000, + WS_THICKFRAME = 0x40000, WS_VISIBLE = 0x10000000, WS_VSCROLL = 0x200000, WS_EX_DLGMODALFRAME = 0x00000001, From dd776df3c7a52b38e5f8bfefecc36cbdf1d54e36 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:27:03 -0300 Subject: [PATCH 103/164] add extension method to convert between RECT and PixelRect. --- src/Windows/Avalonia.Win32/Win32TypeExtensions.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/Windows/Avalonia.Win32/Win32TypeExtensions.cs diff --git a/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs new file mode 100644 index 0000000000..0e9daf3137 --- /dev/null +++ b/src/Windows/Avalonia.Win32/Win32TypeExtensions.cs @@ -0,0 +1,13 @@ +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + public static class Win32TypeExtensions + { + internal static PixelRect ToPixelRect(this RECT rect) + { + return new PixelRect(rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top); + } + } +} From 05a2a648712436f3b4c514938fd50323502e0698 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:29:06 -0300 Subject: [PATCH 104/164] use extension method in screens api. --- src/Windows/Avalonia.Win32/ScreenImpl.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/ScreenImpl.cs b/src/Windows/Avalonia.Win32/ScreenImpl.cs index 963042b249..442794f0f0 100644 --- a/src/Windows/Avalonia.Win32/ScreenImpl.cs +++ b/src/Windows/Avalonia.Win32/ScreenImpl.cs @@ -8,7 +8,7 @@ namespace Avalonia.Win32 { public class ScreenImpl : IScreenImpl { - public int ScreenCount + public int ScreenCount { get => GetSystemMetrics(SystemMetric.SM_CMONITORS); } @@ -33,7 +33,7 @@ namespace Avalonia.Win32 var shcore = LoadLibrary("shcore.dll"); var method = GetProcAddress(shcore, nameof(GetDpiForMonitor)); if (method != IntPtr.Zero) - { + { GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _); dpi = (double)x; } @@ -51,11 +51,8 @@ namespace Avalonia.Win32 RECT bounds = monitorInfo.rcMonitor; RECT workingArea = monitorInfo.rcWork; - PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left, - bounds.bottom - bounds.top); - PixelRect avaloniaWorkArea = - new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left, - workingArea.bottom - workingArea.top); + PixelRect avaloniaBounds = bounds.ToPixelRect(); + PixelRect avaloniaWorkArea = workingArea.ToPixelRect(); screens[index] = new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1, monitor); From 5eee1e04e5d9e009d20139277bedefa735a52a17 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 29 Apr 2020 22:35:37 -0300 Subject: [PATCH 105/164] win32 implementation for fullscreen windowstate. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 118 ++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index e193c72ef7..5f88fc3f0c 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -37,10 +37,21 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; + private struct SavedWindowInfo + { + public WindowStyles Style { get; set; } + public WindowStyles ExStyle { get; set; } + public RECT WindowRect { get; set; } + }; + + private SavedWindowInfo _savedWindowInfo; + private bool _fullScreen; + #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; #endif + private readonly List _disabledBy; private readonly TouchDevice _touchDevice; private readonly MouseDevice _mouseDevice; @@ -82,7 +93,9 @@ namespace Avalonia.Win32 _windowProperties = new WindowProperties { - ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full + ShowInTaskbar = false, + IsResizable = true, + Decorations = SystemDecorations.Full }; _rendererLock = new ManagedDeferredRendererLock(); @@ -205,6 +218,90 @@ namespace Avalonia.Win32 } } + private void MarkFullscreen(bool fullscreen) + { + //if (!task_bar_list_) + //{ + // HRESULT hr = + + + // ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, + // IID_PPV_ARGS(&task_bar_list_)); + // if (SUCCEEDED(hr) && FAILED(task_bar_list_->HrInit())) + // task_bar_list_ = nullptr; + //} + + //// As per MSDN marking the window as fullscreen should ensure that the + //// taskbar is moved to the bottom of the Z-order when the fullscreen window + //// is activated. If the window is not fullscreen, the Shell falls back to + //// heuristics to determine how the window should be treated, which means + //// that it could still consider the window as fullscreen. :( + //if (task_bar_list_) + // task_bar_list_->MarkFullscreenWindow(hwnd_, !!fullscreen); + } + + private void SetFullScreen(bool fullscreen) + { + // Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + //std::unique_ptr visibility; + + // With Aero enabled disabling the visibility causes the window to disappear + // for several frames, which looks worse than doing other updates + // non-atomically. + //if (!ui::win::IsAeroGlassEnabled()) + // visibility = std::make_unique(hwnd_); + + // Save current window state if not already fullscreen. + if (!_fullScreen) + { + _savedWindowInfo.Style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); + _savedWindowInfo.ExStyle = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + GetWindowRect(_hwnd, out var windowRect); + _savedWindowInfo.WindowRect = windowRect; + } + + _fullScreen = fullscreen; + + if (_fullScreen) + { + // Set new window style and size. + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, + (uint)(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME))); + SetWindowLong( + _hwnd, (int)WindowLongParam.GWL_EXSTYLE, + (uint)(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | + WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE))); + + // On expand, if we're given a window_rect, grow to it, otherwise do + // not resize. + MONITORINFO monitor_info = MONITORINFO.Create(); + GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); + + var window_rect = monitor_info.rcMonitor.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + else + { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)_savedWindowInfo.Style); + SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)_savedWindowInfo.ExStyle); + + // On restore, resize to the previous saved rect size. + var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + + MarkFullscreen(fullscreen); + } + public IEnumerable Surfaces => new object[] { Handle, _gl, _framebuffer }; public PixelPoint Position @@ -545,16 +642,35 @@ namespace Avalonia.Win32 switch (state) { case WindowState.Minimized: + if (_fullScreen) + { + SetFullScreen(false); + } + command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: + if (_fullScreen) + { + SetFullScreen(false); + } + command = ShowWindowCommand.Maximize; break; case WindowState.Normal: + if (_fullScreen) + { + SetFullScreen(false); + } + command = ShowWindowCommand.Restore; break; + case WindowState.FullScreen: + SetFullScreen(true); + return; + default: throw new ArgumentException("Invalid WindowState."); } From 996821b60b8b60159d911b9ee6499db262bfc4fb Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 30 Apr 2020 09:06:50 -0400 Subject: [PATCH 106/164] Removed TODO now that behavior is verified with WPF --- src/Avalonia.Animation/Animators/Animator`1.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Animation/Animators/Animator`1.cs b/src/Avalonia.Animation/Animators/Animator`1.cs index 121ffda564..0660440e30 100644 --- a/src/Avalonia.Animation/Animators/Animator`1.cs +++ b/src/Avalonia.Animation/Animators/Animator`1.cs @@ -89,7 +89,7 @@ namespace Avalonia.Animation.Animators else newValue = (T)lastKeyframe.Value; - if (lastKeyframe.KeySpline != null) // TODO: do we use firstKeyFrame or lastKeyframe?! + if (lastKeyframe.KeySpline != null) progress = lastKeyframe.KeySpline.GetSplineProgress(progress); return Interpolate(progress, oldValue, newValue); From dc75bf512a8122cb6c2b168231979e6584da2b4a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 16:42:25 +0200 Subject: [PATCH 107/164] Run sizing tests with both Show and ShowDialog. One test now failing with `ShowDialog` due to #3843. --- .../WindowTests.cs | 313 +++++++++--------- 1 file changed, 165 insertions(+), 148 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 29944c6f85..e99be9cfd2 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -336,210 +336,227 @@ namespace Avalonia.Controls.UnitTests } } - [Fact] - public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual() + public class SizingTests { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual() { - var child = new ChildControl(); - var target = new Window - { - Width = 100, - Height = 50, - SizeToContent = SizeToContent.Manual, - Content = child - }; + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.Manual, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(new Size(100, 50), child.MeasureSizes[0]); + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(100, 50), child.MeasureSizes[0]); + } } - } - [Fact] - public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified() { - var windowImpl = MockWindowingPlatform.CreateWindowMock(); - windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450)); - - var child = new ChildControl(); - var target = new Window(windowImpl.Object) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - SizeToContent = SizeToContent.Manual, - Content = child - }; + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450)); - target.Show(); + var child = new ChildControl(); + var target = new Window(windowImpl.Object) + { + SizeToContent = SizeToContent.Manual, + Content = child + }; - Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(new Size(550, 450), child.MeasureSizes[0]); + Show(target); + + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(new Size(550, 450), child.MeasureSizes[0]); + } } - } - [Fact] - public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() { - var child = new ChildControl(); - var target = new Window + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 100, - Height = 50, - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var child = new ChildControl(); + var target = new Window + { + Width = 100, + Height = 50, + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(1, child.MeasureSizes.Count); - Assert.Equal(Size.Infinity, child.MeasureSizes[0]); + Assert.Equal(1, child.MeasureSizes.Count); + Assert.Equal(Size.Infinity, child.MeasureSizes[0]); + } } - } - [Fact] - public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() - { - // Issue #3784. - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() { - var windowImpl = MockWindowingPlatform.CreateWindowMock(); - var clientSize = new Size(200, 200); - var maxClientSize = new Size(480, 480); - - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size => + // Issue #3784. + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - clientSize = size.Constrain(maxClientSize); - windowImpl.Object.Resized?.Invoke(clientSize); - }); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + var clientSize = new Size(200, 200); + var maxClientSize = new Size(480, 480); - windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size => + { + clientSize = size.Constrain(maxClientSize); + windowImpl.Object.Resized?.Invoke(clientSize); + }); - var child = new Canvas - { - Width = 400, - Height = 800, - }; - var target = new Window(windowImpl.Object) - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); - target.Show(); + var child = new Canvas + { + Width = 400, + Height = 800, + }; + var target = new Window(windowImpl.Object) + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + Show(target); - Assert.Equal(new Size(400, 480), target.Bounds.Size); + Assert.Equal(new Size(400, 480), target.Bounds.Size); - // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no - // parent control to be offset against. - Assert.Equal(new Point(0, 0), target.Bounds.Position); + // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no + // parent control to be offset against. + Assert.Equal(new Point(0, 0), target.Bounds.Position); + } } - } - [Fact] - public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight() { - var child = new Canvas + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var child = new Canvas + { + Width = 400, + Height = 800, + }; - var target = new Window() - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(400, target.Width); - Assert.Equal(800, target.Height); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + } } - } - [Fact] - public void SizeToContent_Should_Not_Be_Lost_On_Show() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void SizeToContent_Should_Not_Be_Lost_On_Show() { - var child = new Canvas + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var child = new Canvas + { + Width = 400, + Height = 800, + }; - var target = new Window() - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } } - } - [Fact] - public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() { - var child = new Canvas + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var child = new Canvas + { + Width = 400, + Height = 800, + }; - var target = new Window() - { - SizeToContent = SizeToContent.WidthAndHeight, - Content = child - }; + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; - target.Show(); + Show(target); - Assert.Equal(400, target.Width); - Assert.Equal(800, target.Height); + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); - child.Width = 410; - target.LayoutManager.ExecuteLayoutPass(); + child.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); - Assert.Equal(410, target.Width); - Assert.Equal(800, target.Height); - Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + Assert.Equal(410, target.Width); + Assert.Equal(800, target.Height); + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } } - } - [Fact] - public void Setting_Width_Should_Resize_WindowImpl() - { - // Issue #3796 - using (UnitTestApplication.Start(TestServices.StyledWindow)) + [Fact] + public void Setting_Width_Should_Resize_WindowImpl() { - var target = new Window() + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - Width = 400, - Height = 800, - }; + var target = new Window() + { + Width = 400, + Height = 800, + }; - target.Show(); + Show(target); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); - Assert.Equal(400, target.Width); - Assert.Equal(800, target.Height); + target.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); - target.Width = 410; - target.LayoutManager.ExecuteLayoutPass(); + var windowImpl = Mock.Get(target.PlatformImpl); + windowImpl.Verify(x => x.Resize(new Size(410, 800))); + Assert.Equal(410, target.Width); + } + } - var windowImpl = Mock.Get(target.PlatformImpl); - windowImpl.Verify(x => x.Resize(new Size(410, 800))); - Assert.Equal(410, target.Width); + protected virtual void Show(Window window) + { + window.Show(); + } + } + + public class DialogSizingTests : SizingTests + { + protected override void Show(Window window) + { + var owner = new Window(); + window.ShowDialog(owner); } } From 4ee9700f08d61625ebc1615129b27d462c3e053a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 16:45:53 +0200 Subject: [PATCH 108/164] Set initial size when showing Window as dialog. Fixes #3843. --- src/Avalonia.Controls/Window.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index dd00b850fe..75f32c862e 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -500,6 +500,19 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; + + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); + + if (initialSize != ClientSize) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(initialSize); + } + } + LayoutManager.ExecuteInitialLayoutPass(this); var result = new TaskCompletionSource(); From d1e4614a3855e1137a8dc6326c54570bee8b689b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 30 Apr 2020 17:13:18 +0200 Subject: [PATCH 109/164] Added ComboBox.PopupClosedOverride. To control whether the event that caused the popup to close is swallowed or passed through. For now just implemented on `ComboBox` - we may want to implement it in future on all controls that display popups if this is something that proves generally useful. Fixes #3845. --- src/Avalonia.Controls/ComboBox.cs | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 1daa6a5630..67ef6cd1e9 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -234,6 +234,23 @@ namespace Avalonia.Controls base.OnTemplateApplied(e); } + /// + /// Called when the ComboBox popup is closed, with the + /// that caused the popup to close. + /// + /// The event args. + /// + /// This method can be overridden to control whether the event that caused the popup to close + /// is swallowed or passed through. + /// + protected virtual void PopupClosedOverride(PopupClosedEventArgs e) + { + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + } + internal void ItemFocused(ComboBoxItem dropDownItem) { if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) @@ -247,10 +264,7 @@ namespace Avalonia.Controls _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; - if (e.CloseEvent is PointerEventArgs pointerEvent) - { - pointerEvent.Handled = true; - } + PopupClosedOverride(e); if (CanFocus(this)) { From 26ad865589f7b8ac83d0b34fc5e209e023a50302 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:39:56 -0300 Subject: [PATCH 110/164] Add Guids for TaskBarList interop. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index 5c364c4038..e9f00a0c7f 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1643,6 +1643,8 @@ namespace Avalonia.Win32.Interop public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B"); public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE"); + public static readonly Guid TaskBarList = Guid.Parse("56FDF344-FD6D-11D0-958A-006097C9A090"); + public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"); } [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] From 1ce8f4f6bad28ac2e523e6b6a097bd040cbbbb6c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:40:45 -0300 Subject: [PATCH 111/164] use helper methods GetStyle / SetStyle. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5f88fc3f0c..6bbcb3dc92 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -254,8 +254,8 @@ namespace Avalonia.Win32 // Save current window state if not already fullscreen. if (!_fullScreen) { - _savedWindowInfo.Style = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE); - _savedWindowInfo.ExStyle = (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE); + _savedWindowInfo.Style = GetStyle(); + _savedWindowInfo.ExStyle = GetExtendedStyle(); GetWindowRect(_hwnd, out var windowRect); _savedWindowInfo.WindowRect = windowRect; } @@ -265,12 +265,8 @@ namespace Avalonia.Win32 if (_fullScreen) { // Set new window style and size. - SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, - (uint)(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME))); - SetWindowLong( - _hwnd, (int)WindowLongParam.GWL_EXSTYLE, - (uint)(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | - WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE))); + SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); + SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); // On expand, if we're given a window_rect, grow to it, otherwise do // not resize. @@ -288,8 +284,8 @@ namespace Avalonia.Win32 // Reset original window style and size. The multiple window size/moves // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be // repainted. Better-looking methods welcome. - SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)_savedWindowInfo.Style); - SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)_savedWindowInfo.ExStyle); + SetStyle(_savedWindowInfo.Style); + SetExtendedStyle(_savedWindowInfo.ExStyle); // On restore, resize to the previous saved rect size. var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); From ecd4cd7bbf4a8eeecd3815c772fddda6fdbb3dc5 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:43:11 -0300 Subject: [PATCH 112/164] Add CoCreateInstance overload that gives intptr. --- src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index e9f00a0c7f..43beb35e45 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1147,7 +1147,10 @@ namespace Avalonia.Win32.Interop internal static extern int CoCreateInstance(ref Guid clsid, IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); - + [DllImport("ole32.dll", PreserveSig = true)] + internal static extern int CoCreateInstance(ref Guid clsid, + IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter); + [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)] internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv); From 93e43951dbb58cacca68d78a1ddfade135de250f Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:43:43 -0300 Subject: [PATCH 113/164] Add delegates and VTables for ITaskBarList2. --- .../Avalonia.Win32/ITaskBarList2VTable.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs diff --git a/src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs b/src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs new file mode 100644 index 0000000000..c187251ee3 --- /dev/null +++ b/src/Windows/Avalonia.Win32/ITaskBarList2VTable.cs @@ -0,0 +1,22 @@ +using System; +using System.Runtime.InteropServices; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen); + delegate HRESULT HrInit(IntPtr This); + + struct ITaskBarList2VTable + { + public IntPtr IUnknown1; + public IntPtr IUnknown2; + public IntPtr IUnknown3; + public IntPtr HrInit; + public IntPtr AddTab; + public IntPtr DeleteTab; + public IntPtr ActivateTab; + public IntPtr SetActiveAlt; + public IntPtr MarkFullscreenWindow; + } +} From cecce99c954be7b7cccd467403ae58500ae171d8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:44:11 -0300 Subject: [PATCH 114/164] SystemDialogs call the correct cocreateinstance overload. --- src/Windows/Avalonia.Win32/SystemDialogImpl.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs index 8bdd4b7bfa..c6164e0868 100644 --- a/src/Windows/Avalonia.Win32/SystemDialogImpl.cs +++ b/src/Windows/Avalonia.Win32/SystemDialogImpl.cs @@ -24,7 +24,7 @@ namespace Avalonia.Win32 Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; var openDialog = dialog as OpenFileDialog; @@ -105,9 +105,9 @@ namespace Avalonia.Win32 var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero; Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog; - Guid iid = UnmanagedMethods.ShellIds.IFileDialog; + Guid iid = UnmanagedMethods.ShellIds.IFileDialog; - UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk); + UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk); var frm = (UnmanagedMethods.IFileDialog)unk; uint options; frm.GetOptions(out options); From 90f386ffc0468df5e2678a777acb80e6e7b1faf8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:44:37 -0300 Subject: [PATCH 115/164] correctly port MarkFullScreen using CoreRT compatible COM. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 58 ++++++++++++++---------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 6bbcb3dc92..ba7f90eb63 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -46,6 +46,7 @@ namespace Avalonia.Win32 private SavedWindowInfo _savedWindowInfo; private bool _fullScreen; + private IntPtr _taskBarList; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -218,39 +219,46 @@ namespace Avalonia.Win32 } } - private void MarkFullscreen(bool fullscreen) + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// Fullscreen state. + private unsafe void MarkFullscreen(bool fullscreen) { - //if (!task_bar_list_) - //{ - // HRESULT hr = + if (_taskBarList == IntPtr.Zero) + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out _taskBarList); + + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + var hrInit = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); - // ::CoCreateInstance(CLSID_TaskbarList, nullptr, CLSCTX_INPROC_SERVER, - // IID_PPV_ARGS(&task_bar_list_)); - // if (SUCCEEDED(hr) && FAILED(task_bar_list_->HrInit())) - // task_bar_list_ = nullptr; - //} + if (hrInit(_taskBarList) != HRESULT.S_OK) + { + _taskBarList = IntPtr.Zero; + } + } + } - //// As per MSDN marking the window as fullscreen should ensure that the - //// taskbar is moved to the bottom of the Z-order when the fullscreen window - //// is activated. If the window is not fullscreen, the Shell falls back to - //// heuristics to determine how the window should be treated, which means - //// that it could still consider the window as fullscreen. :( - //if (task_bar_list_) - // task_bar_list_->MarkFullscreenWindow(hwnd_, !!fullscreen); + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + var markFullscreen = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + markFullscreen(_taskBarList, _hwnd, fullscreen); + } } + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// private void SetFullScreen(bool fullscreen) { - // Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc - //std::unique_ptr visibility; - - // With Aero enabled disabling the visibility causes the window to disappear - // for several frames, which looks worse than doing other updates - // non-atomically. - //if (!ui::win::IsAeroGlassEnabled()) - // visibility = std::make_unique(hwnd_); - // Save current window state if not already fullscreen. if (!_fullScreen) { From 81c4a1e796c6814edfde812fa1ff34464186ac2e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:46:32 -0300 Subject: [PATCH 116/164] move struct. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index ba7f90eb63..f3538a9bfe 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -37,13 +37,6 @@ namespace Avalonia.Win32 { WindowEdge.West, HitTestValues.HTLEFT } }; - private struct SavedWindowInfo - { - public WindowStyles Style { get; set; } - public WindowStyles ExStyle { get; set; } - public RECT WindowRect { get; set; } - }; - private SavedWindowInfo _savedWindowInfo; private bool _fullScreen; private IntPtr _taskBarList; @@ -833,6 +826,13 @@ namespace Avalonia.Win32 IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + private struct SavedWindowInfo + { + public WindowStyles Style { get; set; } + public WindowStyles ExStyle { get; set; } + public RECT WindowRect { get; set; } + }; + private struct WindowProperties { public bool ShowInTaskbar; From 51ad905a2c3e73e832178222a5586a194add9e1b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:48:42 -0300 Subject: [PATCH 117/164] use WindowProperties to hold fullscreen flag. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index f3538a9bfe..9553447623 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -38,7 +38,6 @@ namespace Avalonia.Win32 }; private SavedWindowInfo _savedWindowInfo; - private bool _fullScreen; private IntPtr _taskBarList; #if USE_MANAGED_DRAG @@ -253,7 +252,7 @@ namespace Avalonia.Win32 private void SetFullScreen(bool fullscreen) { // Save current window state if not already fullscreen. - if (!_fullScreen) + if (!_windowProperties.IsFullScreen) { _savedWindowInfo.Style = GetStyle(); _savedWindowInfo.ExStyle = GetExtendedStyle(); @@ -261,9 +260,9 @@ namespace Avalonia.Win32 _savedWindowInfo.WindowRect = windowRect; } - _fullScreen = fullscreen; + _windowProperties.IsFullScreen = fullscreen; - if (_fullScreen) + if (_windowProperties.IsFullScreen) { // Set new window style and size. SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); @@ -639,7 +638,7 @@ namespace Avalonia.Win32 switch (state) { case WindowState.Minimized: - if (_fullScreen) + if (_windowProperties.IsFullScreen) { SetFullScreen(false); } @@ -647,7 +646,7 @@ namespace Avalonia.Win32 command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: - if (_fullScreen) + if (_windowProperties.IsFullScreen) { SetFullScreen(false); } @@ -656,7 +655,7 @@ namespace Avalonia.Win32 break; case WindowState.Normal: - if (_fullScreen) + if (_windowProperties.IsFullScreen) { SetFullScreen(false); } @@ -838,6 +837,7 @@ namespace Avalonia.Win32 public bool ShowInTaskbar; public bool IsResizable; public SystemDecorations Decorations; + public bool IsFullScreen; } } } From fe954a0398baf00fd60277841847480f379d4850 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 13:51:01 -0300 Subject: [PATCH 118/164] move private methods. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 174 +++++++++++------------ 1 file changed, 87 insertions(+), 87 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 9553447623..842bf3dc72 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -211,93 +211,6 @@ namespace Avalonia.Win32 } } - /// - /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc - /// - /// Fullscreen state. - private unsafe void MarkFullscreen(bool fullscreen) - { - if (_taskBarList == IntPtr.Zero) - { - Guid clsid = ShellIds.TaskBarList; - Guid iid = ShellIds.ITaskBarList2; - - int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out _taskBarList); - - if (_taskBarList != IntPtr.Zero) - { - var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); - - var hrInit = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); - - if (hrInit(_taskBarList) != HRESULT.S_OK) - { - _taskBarList = IntPtr.Zero; - } - } - } - - if (_taskBarList != IntPtr.Zero) - { - var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); - var markFullscreen = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); - markFullscreen(_taskBarList, _hwnd, fullscreen); - } - } - - /// - /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc - /// - /// - private void SetFullScreen(bool fullscreen) - { - // Save current window state if not already fullscreen. - if (!_windowProperties.IsFullScreen) - { - _savedWindowInfo.Style = GetStyle(); - _savedWindowInfo.ExStyle = GetExtendedStyle(); - GetWindowRect(_hwnd, out var windowRect); - _savedWindowInfo.WindowRect = windowRect; - } - - _windowProperties.IsFullScreen = fullscreen; - - if (_windowProperties.IsFullScreen) - { - // Set new window style and size. - SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); - SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); - - // On expand, if we're given a window_rect, grow to it, otherwise do - // not resize. - MONITORINFO monitor_info = MONITORINFO.Create(); - GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); - - var window_rect = monitor_info.rcMonitor.ToPixelRect(); - - SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, - window_rect.Width, window_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); - } - else - { - // Reset original window style and size. The multiple window size/moves - // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be - // repainted. Better-looking methods welcome. - SetStyle(_savedWindowInfo.Style); - SetExtendedStyle(_savedWindowInfo.ExStyle); - - // On restore, resize to the previous saved rect size. - var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); - - SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, - new_rect.Height, - SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); - } - - MarkFullscreen(fullscreen); - } - public IEnumerable Surfaces => new object[] { Handle, _gl, _framebuffer }; public PixelPoint Position @@ -631,6 +544,93 @@ namespace Avalonia.Win32 } } + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// Fullscreen state. + private unsafe void MarkFullscreen(bool fullscreen) + { + if (_taskBarList == IntPtr.Zero) + { + Guid clsid = ShellIds.TaskBarList; + Guid iid = ShellIds.ITaskBarList2; + + int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out _taskBarList); + + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + + var hrInit = Marshal.GetDelegateForFunctionPointer((*ptr)->HrInit); + + if (hrInit(_taskBarList) != HRESULT.S_OK) + { + _taskBarList = IntPtr.Zero; + } + } + } + + if (_taskBarList != IntPtr.Zero) + { + var ptr = (ITaskBarList2VTable**)_taskBarList.ToPointer(); + var markFullscreen = Marshal.GetDelegateForFunctionPointer((*ptr)->MarkFullscreenWindow); + markFullscreen(_taskBarList, _hwnd, fullscreen); + } + } + + /// + /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// + /// + private void SetFullScreen(bool fullscreen) + { + // Save current window state if not already fullscreen. + if (!_windowProperties.IsFullScreen) + { + _savedWindowInfo.Style = GetStyle(); + _savedWindowInfo.ExStyle = GetExtendedStyle(); + GetWindowRect(_hwnd, out var windowRect); + _savedWindowInfo.WindowRect = windowRect; + } + + _windowProperties.IsFullScreen = fullscreen; + + if (_windowProperties.IsFullScreen) + { + // Set new window style and size. + SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); + SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); + + // On expand, if we're given a window_rect, grow to it, otherwise do + // not resize. + MONITORINFO monitor_info = MONITORINFO.Create(); + GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info); + + var window_rect = monitor_info.rcMonitor.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y, + window_rect.Width, window_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + else + { + // Reset original window style and size. The multiple window size/moves + // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be + // repainted. Better-looking methods welcome. + SetStyle(_savedWindowInfo.Style); + SetExtendedStyle(_savedWindowInfo.ExStyle); + + // On restore, resize to the previous saved rect size. + var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); + + SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, + new_rect.Height, + SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); + } + + MarkFullscreen(fullscreen); + } + private void ShowWindow(WindowState state) { ShowWindowCommand command; From a41162dce6fd27794cde937c57c0785f2d5d0d38 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 14:05:06 -0300 Subject: [PATCH 119/164] use UpdateWindowProperties to MarkFullScreen. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 47 ++++++++++++------------ 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 842bf3dc72..c2f8eb3dbf 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -580,23 +580,19 @@ namespace Avalonia.Win32 /// /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc + /// Method must only be called from inside UpdateWindowProperties. /// /// private void SetFullScreen(bool fullscreen) { - // Save current window state if not already fullscreen. - if (!_windowProperties.IsFullScreen) + if (fullscreen) { + // Save current window state if not already fullscreen. _savedWindowInfo.Style = GetStyle(); _savedWindowInfo.ExStyle = GetExtendedStyle(); GetWindowRect(_hwnd, out var windowRect); _savedWindowInfo.WindowRect = windowRect; - } - - _windowProperties.IsFullScreen = fullscreen; - if (_windowProperties.IsFullScreen) - { // Set new window style and size. SetStyle(_savedWindowInfo.Style & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME)); SetExtendedStyle(_savedWindowInfo.ExStyle & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE)); @@ -635,42 +631,40 @@ namespace Avalonia.Win32 { ShowWindowCommand command; + var newWindowProperties = _windowProperties; + switch (state) { case WindowState.Minimized: - if (_windowProperties.IsFullScreen) - { - SetFullScreen(false); - } - + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Minimize; break; case WindowState.Maximized: - if (_windowProperties.IsFullScreen) - { - SetFullScreen(false); - } - + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Maximize; break; case WindowState.Normal: - if (_windowProperties.IsFullScreen) - { - SetFullScreen(false); - } - + newWindowProperties.IsFullScreen = false; command = ShowWindowCommand.Restore; break; case WindowState.FullScreen: - SetFullScreen(true); - return; + newWindowProperties.IsFullScreen = true; + command = ShowWindowCommand.Maximize; + break; default: throw new ArgumentException("Invalid WindowState."); } + UpdateWindowProperties(newWindowProperties); + + if (newWindowProperties.IsFullScreen) + { + return; + } + UnmanagedMethods.ShowWindow(_hwnd, command); if (state == WindowState.Maximized) @@ -800,6 +794,11 @@ namespace Avalonia.Win32 SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } + + if (oldProperties.IsFullScreen != newProperties.IsFullScreen) + { + SetFullScreen(newProperties.IsFullScreen); + } } #if USE_MANAGED_DRAG From bfa96382f7b7ac617c1f55b14463c8f30a3bc9d2 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 14:06:12 -0300 Subject: [PATCH 120/164] cleaner code. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index c2f8eb3dbf..97ec32cfb5 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -651,8 +651,8 @@ namespace Avalonia.Win32 case WindowState.FullScreen: newWindowProperties.IsFullScreen = true; - command = ShowWindowCommand.Maximize; - break; + UpdateWindowProperties(newWindowProperties); + return; default: throw new ArgumentException("Invalid WindowState."); @@ -660,11 +660,6 @@ namespace Avalonia.Win32 UpdateWindowProperties(newWindowProperties); - if (newWindowProperties.IsFullScreen) - { - return; - } - UnmanagedMethods.ShowWindow(_hwnd, command); if (state == WindowState.Maximized) From 62b8da3cdf2dd4741078b7b82af972b95185e0ca Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 30 Apr 2020 15:46:07 -0400 Subject: [PATCH 121/164] Added a test for KeySpline animation --- .../KeySplineTests.cs | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs index a523be2a78..b98564a03c 100644 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -1,6 +1,7 @@ using System; -using Avalonia.Controls; -using Avalonia.UnitTests; +using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.Styling; using Xunit; namespace Avalonia.Animation.UnitTests @@ -44,5 +45,76 @@ namespace Avalonia.Animation.UnitTests Assert.Throws(() => keySpline.ControlPointX1 = input); Assert.Throws(() => keySpline.ControlPointX2 = input); } + + [Fact] + public void Check_KeySpline_Handled_properly() + { + var keyframe1 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, -2.5d), + }, + KeyTime = TimeSpan.FromSeconds(0) + }; + + var keyframe2 = new KeyFrame() + { + Setters = + { + new Setter(RotateTransform.AngleProperty, 2.5d), + }, + KeyTime = TimeSpan.FromSeconds(5), + KeySpline = new KeySpline(0.1123555056179775, + 0.657303370786517, + 0.8370786516853934, + 0.499999999999999999) + }; + + var animation = new Animation() + { + Duration = TimeSpan.FromSeconds(5), + Children = + { + keyframe1, + keyframe2 + }, + IterationCount = new IterationCount(5), + PlaybackDirection = PlaybackDirection.Alternate + }; + + var rotateTransform = new RotateTransform(-2.5); + var rect = new Rectangle() + { + RenderTransform = rotateTransform + }; + + var clock = new TestClock(); + var animationRun = animation.RunAsync(rect, clock); + + // position is what you'd expect at end and beginning + clock.Step(TimeSpan.Zero); + Assert.Equal(rotateTransform.Angle, -2.5); + clock.Step(TimeSpan.FromSeconds(5)); + Assert.Equal(rotateTransform.Angle, 2.5); + + // test some points in between end and beginning + var tolerance = 0.01; + clock.Step(TimeSpan.Parse("00:00:10.0153932")); + var expected = -2.4122350198982545; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:11.2655407")); + expected = -0.37153223002125113; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:12.6158773")); + expected = 0.3967885416786294; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + + clock.Step(TimeSpan.Parse("00:00:14.6495256")); + expected = 1.8016358493761722; + Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance); + } } } From c38c7e2d65ddeb84504819412dbe8671725d09fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 16:47:15 -0300 Subject: [PATCH 122/164] [OSX] implement FullScreen and fix sync of window states. --- native/Avalonia.Native/inc/avalonia-native.h | 1 + native/Avalonia.Native/src/OSX/window.h | 2 + native/Avalonia.Native/src/OSX/window.mm | 126 ++++++++++++++++--- 3 files changed, 114 insertions(+), 15 deletions(-) diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index b10db08adc..b0937d35ba 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -135,6 +135,7 @@ enum AvnWindowState Normal, Minimized, Maximized, + FullScreen, }; enum AvnStandardCursorType diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index 505900d584..449751b06e 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -33,6 +33,8 @@ struct INSWindowHolder struct IWindowStateChanged { virtual void WindowStateChanged () = 0; + virtual void StartStateTransition () = 0; + virtual void EndStateTransition () = 0; }; #endif /* window_h */ diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 109ed63e6f..d7e4680cce 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -408,6 +408,8 @@ private: SystemDecorations _hasDecorations = SystemDecorationsFull; CGRect _lastUndecoratedFrame; AvnWindowState _lastWindowState; + bool _inSetWindowState; + bool _transitioningWindowState; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -421,6 +423,8 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _transitioningWindowState = false; + _inSetWindowState = false; _lastWindowState = Normal; WindowEvents = events; [Window setCanBecomeKeyAndMain]; @@ -457,11 +461,29 @@ private: } } + void StartStateTransition () override + { + _transitioningWindowState = true; + } + + void EndStateTransition () override + { + _transitioningWindowState = false; + } + void WindowStateChanged () override { - AvnWindowState state; - GetWindowState(&state); - WindowEvents->WindowStateChanged(state); + if(!_inSetWindowState && !_transitioningWindowState) + { + AvnWindowState state; + GetWindowState(&state); + + if(_lastWindowState != state) + { + _lastWindowState = state; + WindowEvents->WindowStateChanged(state); + } + } } bool UndecoratedIsMaximized () @@ -586,6 +608,12 @@ private: return E_POINTER; } + if(([Window styleMask] & NSFullScreenWindowMask) == NSFullScreenWindowMask) + { + *ret = FullScreen; + return S_OK; + } + if([Window isMiniaturized]) { *ret = Minimized; @@ -608,12 +636,25 @@ private: { @autoreleasepool { + if(_lastWindowState == state) + { + return S_OK; + } + + _inSetWindowState = true; + + auto currentState = _lastWindowState; _lastWindowState = state; if(_shown) { switch (state) { case Maximized: + if(currentState == FullScreen) + { + [Window toggleFullScreen:nullptr]; + } + lastPositionSet.X = 0; lastPositionSet.Y = 0; @@ -629,15 +670,29 @@ private: break; case Minimized: + if(currentState == FullScreen) + { + [Window toggleFullScreen:nullptr]; + } + [Window miniaturize:Window]; break; - default: + case FullScreen: + [Window toggleFullScreen:nullptr]; + break; + + case Normal: if([Window isMiniaturized]) { [Window deminiaturize:Window]; } + if(currentState == FullScreen) + { + [Window toggleFullScreen:nullptr]; + } + if(IsZoomed()) { DoZoom(); @@ -646,23 +701,17 @@ private: } } + _inSetWindowState = false; + return S_OK; } } virtual void OnResized () override { - if(_shown) + if(_shown && !_inSetWindowState && !_transitioningWindowState) { - auto windowState = [Window isMiniaturized] ? Minimized - : (IsZoomed() ? Maximized : Normal); - - if (windowState != _lastWindowState) - { - _lastWindowState = windowState; - - WindowEvents->WindowStateChanged(windowState); - } + WindowStateChanged(); } } @@ -1378,7 +1427,54 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent - (void)windowDidResize:(NSNotification *)notification { - _parent->OnResized(); + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->WindowStateChanged(); + } +} + +- (void)windowWillExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidExitFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); + } +} + +- (void)windowWillEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->StartStateTransition(); + } +} + +- (void)windowDidEnterFullScreen:(NSNotification *)notification +{ + auto parent = dynamic_cast(_parent.operator->()); + + if(parent != nullptr) + { + parent->EndStateTransition(); + parent->WindowStateChanged(); + } } - (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame From 00ef2fe59000cb1cd5ce1ec381441da8e708da2c Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 30 Apr 2020 16:53:03 -0300 Subject: [PATCH 123/164] deminiturize when entering fullscreen if needed. --- native/Avalonia.Native/src/OSX/window.mm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index d7e4680cce..349db9f1cd 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -679,6 +679,11 @@ private: break; case FullScreen: + if([Window isMiniaturized]) + { + [Window deminiaturize:Window]; + } + [Window toggleFullScreen:nullptr]; break; From 7ba1c9661bd5d3db6b91b898d7e222e447fde841 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 30 Apr 2020 15:58:21 -0400 Subject: [PATCH 124/164] Add some notes on how I got the KeySpline test values --- .../KeySplineTests.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs index b98564a03c..df7c0693e1 100644 --- a/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs +++ b/tests/Avalonia.Animation.UnitTests/KeySplineTests.cs @@ -46,6 +46,31 @@ namespace Avalonia.Animation.UnitTests Assert.Throws(() => keySpline.ControlPointX2 = input); } + /* + To get the test values for the KeySpline test, you can: + 1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations + 2) Add the following xaml somewhere: +