diff --git a/build.sh b/build.sh index 40b1c225a6..a40e00f815 100755 --- a/build.sh +++ b/build.sh @@ -67,6 +67,8 @@ else fi fi +export PATH=$DOTNET_DIRECTORY:$PATH + echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)" "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]} diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index b86c679397..f9bfaf0b47 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -387,6 +387,9 @@ AVNCOM(IAvnClipboard, 0f) : IUnknown virtual HRESULT SetText (char* type, void* utf8Text) = 0; virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0; virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0; + virtual HRESULT SetBytes(char* type, void* utf8Text, int len) = 0; + virtual HRESULT GetBytes(char* type, IAvnString**ppv) = 0; + virtual HRESULT Clear() = 0; }; diff --git a/native/Avalonia.Native/src/OSX/AvnString.h b/native/Avalonia.Native/src/OSX/AvnString.h index 88bc4e6963..5d299374e5 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.h +++ b/native/Avalonia.Native/src/OSX/AvnString.h @@ -12,4 +12,5 @@ extern IAvnString* CreateAvnString(NSString* string); extern IAvnStringArray* CreateAvnStringArray(NSArray* array); extern IAvnStringArray* CreateAvnStringArray(NSString* string); +extern IAvnString* CreateByteArray(void* data, int len); #endif /* AvnString_h */ diff --git a/native/Avalonia.Native/src/OSX/AvnString.mm b/native/Avalonia.Native/src/OSX/AvnString.mm index 6445a9fef1..00b748ef63 100644 --- a/native/Avalonia.Native/src/OSX/AvnString.mm +++ b/native/Avalonia.Native/src/OSX/AvnString.mm @@ -29,6 +29,13 @@ public: memcpy((void*)_cstring, (void*)cstring, _length); } + AvnStringImpl(void*ptr, int len) + { + _length = len; + _cstring = (const char*)malloc(_length); + memcpy((void*)_cstring, ptr, len); + } + virtual ~AvnStringImpl() { free((void*)_cstring); @@ -114,3 +121,8 @@ IAvnStringArray* CreateAvnStringArray(NSString* string) { return new AvnStringArrayImpl(string); } + +IAvnString* CreateByteArray(void* data, int len) +{ + return new AvnStringImpl(data, len); +} diff --git a/native/Avalonia.Native/src/OSX/clipboard.mm b/native/Avalonia.Native/src/OSX/clipboard.mm index 18d60d3853..116a08670e 100644 --- a/native/Avalonia.Native/src/OSX/clipboard.mm +++ b/native/Avalonia.Native/src/OSX/clipboard.mm @@ -82,6 +82,40 @@ public: return S_OK; } + + virtual HRESULT SetBytes(char* type, void* bytes, int len) override + { + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + auto data = [NSData dataWithBytes:bytes length:len]; + if(_item == nil) + [_pb setData:data forType:typeString]; + else + [_item setData:data forType:typeString]; + return S_OK; + } + + virtual HRESULT GetBytes(char* type, IAvnString**ppv) override + { + *ppv = nil; + auto typeString = [NSString stringWithUTF8String:(const char*)type]; + NSData*data; + @try + { + if(_item) + data = [_item dataForType:typeString]; + else + data = [_pb dataForType:typeString]; + if(data == nil) + return E_FAIL; + } + @catch(NSException* e) + { + return E_FAIL; + } + *ppv = CreateByteArray((void*)data.bytes, (int)data.length); + return S_OK; + } + virtual HRESULT Clear() override { diff --git a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs index 51e0a1e799..7802f336fb 100644 --- a/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs +++ b/src/Android/Avalonia.Android/Platform/ClipboardImpl.cs @@ -43,5 +43,11 @@ namespace Avalonia.Android.Platform return Task.FromResult(null); } + + public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); + + public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); } } diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index f2ed86d2aa..84ef0fb695 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -1,7 +1,5 @@ using System; using System.ComponentModel; -using System.Linq; -using System.Reactive.Linq; using System.Reflection; using Avalonia.Utilities; @@ -31,7 +29,11 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(propertyName != null); reference.TryGetTarget(out object instance); - var p = instance.GetType().GetRuntimeProperties().FirstOrDefault(x => x.Name == propertyName); + + const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | + BindingFlags.Static | BindingFlags.Instance; + + var p = instance.GetType().GetProperty(propertyName, bindingFlags); if (p != null) { diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index c19ee8dba7..5d694f4cf9 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -1,13 +1,16 @@ using System; -using System.Linq; +using System.Collections.Generic; +using System.Linq.Expressions; using System.Reflection; namespace Avalonia.Data.Core.Plugins { - class MethodAccessorPlugin : IPropertyAccessorPlugin + public class MethodAccessorPlugin : IPropertyAccessorPlugin { - public bool Match(object obj, string methodName) - => obj.GetType().GetRuntimeMethods().Any(x => x.Name == methodName); + private readonly Dictionary<(Type, string), MethodInfo> _methodLookup = + new Dictionary<(Type, string), MethodInfo>(); + + public bool Match(object obj, string methodName) => GetFirstMethodWithName(obj.GetType(), methodName) != null; public IPropertyAccessor Start(WeakReference reference, string methodName) { @@ -15,17 +18,22 @@ namespace Avalonia.Data.Core.Plugins Contract.Requires(methodName != null); reference.TryGetTarget(out object instance); - var method = instance.GetType().GetRuntimeMethods().FirstOrDefault(x => x.Name == methodName); + + var method = GetFirstMethodWithName(instance.GetType(), methodName); if (method != null) { - if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) + var parameters = method.GetParameters(); + + if (parameters.Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8) { - var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName)); + var exception = new ArgumentException( + "Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", + nameof(methodName)); return new PropertyError(new BindingNotification(exception, BindingErrorType.Error)); } - return new Accessor(reference, method); + return new Accessor(reference, method, parameters); } else { @@ -35,31 +43,72 @@ namespace Avalonia.Data.Core.Plugins } } + private MethodInfo GetFirstMethodWithName(Type type, string methodName) + { + var key = (type, methodName); + + if (!_methodLookup.TryGetValue(key, out MethodInfo methodInfo)) + { + methodInfo = TryFindAndCacheMethod(type, methodName); + } + + return methodInfo; + } + + private MethodInfo TryFindAndCacheMethod(Type type, string methodName) + { + MethodInfo found = null; + + const BindingFlags bindingFlags = + BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance; + + var methods = type.GetMethods(bindingFlags); + + foreach (MethodInfo methodInfo in methods) + { + if (methodInfo.Name == methodName) + { + found = methodInfo; + + break; + } + } + + _methodLookup.Add((type, methodName), found); + + return found; + } + private sealed class Accessor : PropertyAccessorBase { - public Accessor(WeakReference reference, MethodInfo method) + public Accessor(WeakReference reference, MethodInfo method, ParameterInfo[] parameters) { Contract.Requires(reference != null); Contract.Requires(method != null); - var paramTypes = method.GetParameters().Select(param => param.ParameterType).ToArray(); var returnType = method.ReturnType; - - if (returnType == typeof(void)) + bool hasReturn = returnType != typeof(void); + + var signatureTypeCount = (hasReturn ? 1 : 0) + parameters.Length; + + var paramTypes = new Type[signatureTypeCount]; + + for (var i = 0; i < parameters.Length; i++) { - if (paramTypes.Length == 0) - { - PropertyType = typeof(Action); - } - else - { - PropertyType = Type.GetType($"System.Action`{paramTypes.Length}").MakeGenericType(paramTypes); - } + ParameterInfo parameter = parameters[i]; + + paramTypes[i] = parameter.ParameterType; + } + + if (hasReturn) + { + paramTypes[paramTypes.Length - 1] = returnType; + + PropertyType = Expression.GetFuncType(paramTypes); } else { - var genericTypeParameters = paramTypes.Concat(new[] { returnType }).ToArray(); - PropertyType = Type.GetType($"System.Func`{genericTypeParameters.Length}").MakeGenericType(genericTypeParameters); + PropertyType = Expression.GetActionType(paramTypes); } if (method.IsStatic) diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 2f11746891..b458b15c64 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -204,13 +204,15 @@ namespace Avalonia.Controls private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e) { var control = (Control)e.Sender; + var newValue = (bool)e.NewValue; + ToolTip toolTip; - if ((bool)e.NewValue) + if (newValue) { var tip = GetTip(control); if (tip == null) return; - var toolTip = control.GetValue(ToolTipProperty); + toolTip = control.GetValue(ToolTipProperty); if (toolTip == null || (tip != toolTip && tip != toolTip.Content)) { toolTip?.Close(); @@ -223,9 +225,11 @@ namespace Avalonia.Controls } else { - var toolTip = control.GetValue(ToolTipProperty); + toolTip = control.GetValue(ToolTipProperty); toolTip?.Close(); } + + toolTip?.UpdatePseudoClasses(newValue); } private void Open(Control control) @@ -238,6 +242,9 @@ namespace Avalonia.Controls _popup.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); + + WindowManagerAddShadowHintChanged(_popup, false); + _popup.Show(); } @@ -250,5 +257,18 @@ namespace Avalonia.Controls _popup = null; } } + + private void WindowManagerAddShadowHintChanged(IPopupHost host, bool hint) + { + if (host is PopupRoot pr) + { + pr.PlatformImpl.SetWindowManagerAddShadowHint(hint); + } + } + + private void UpdatePseudoClasses(bool newValue) + { + PseudoClasses.Set(":open", newValue); + } } } diff --git a/src/Avalonia.DesignerSupport/Remote/Stubs.cs b/src/Avalonia.DesignerSupport/Remote/Stubs.cs index c2565cc59c..64b3af4ea2 100644 --- a/src/Avalonia.DesignerSupport/Remote/Stubs.cs +++ b/src/Avalonia.DesignerSupport/Remote/Stubs.cs @@ -160,6 +160,10 @@ namespace Avalonia.DesignerSupport.Remote public Task SetTextAsync(string text) => Task.CompletedTask; public Task ClearAsync() => Task.CompletedTask; + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + public Task GetFormatsAsync() => Task.FromResult(new string[0]); + + public Task GetDataAsync(string format) => Task.FromResult((object)null); } class CursorFactoryStub : IStandardCursorFactory diff --git a/src/Avalonia.Input/Platform/IClipboard.cs b/src/Avalonia.Input/Platform/IClipboard.cs index b195050fa8..eb880904eb 100644 --- a/src/Avalonia.Input/Platform/IClipboard.cs +++ b/src/Avalonia.Input/Platform/IClipboard.cs @@ -9,5 +9,11 @@ namespace Avalonia.Input.Platform Task SetTextAsync(string text); Task ClearAsync(); + + Task SetDataObjectAsync(IDataObject data); + + Task GetFormatsAsync(); + + Task GetDataAsync(string format); } } diff --git a/src/Avalonia.Native/AvnString.cs b/src/Avalonia.Native/AvnString.cs index ba427b6aac..11b1a33276 100644 --- a/src/Avalonia.Native/AvnString.cs +++ b/src/Avalonia.Native/AvnString.cs @@ -5,6 +5,7 @@ namespace Avalonia.Native.Interop unsafe partial class IAvnString { private string _managed; + private byte[] _bytes; public string String { @@ -22,6 +23,20 @@ namespace Avalonia.Native.Interop } } + public byte[] Bytes + { + get + { + if (_bytes == null) + { + _bytes = new byte[Length()]; + Marshal.Copy(Pointer(), _bytes, 0, _bytes.Length); + } + + return _bytes; + } + } + public override string ToString() => String; } diff --git a/src/Avalonia.Native/ClipboardImpl.cs b/src/Avalonia.Native/ClipboardImpl.cs index 2a24869513..554e7a497a 100644 --- a/src/Avalonia.Native/ClipboardImpl.cs +++ b/src/Avalonia.Native/ClipboardImpl.cs @@ -82,6 +82,38 @@ namespace Avalonia.Native using (var strings = _native.GetStrings(NSFilenamesPboardType)) return strings.ToStringArray(); } + + public unsafe Task SetDataObjectAsync(IDataObject data) + { + _native.Clear(); + foreach (var fmt in data.GetDataFormats()) + { + var o = data.Get(fmt); + if(o is string s) + using (var b = new Utf8Buffer(s)) + _native.SetText(fmt, b.DangerousGetHandle()); + else if(o is byte[] bytes) + fixed (byte* pbytes = bytes) + _native.SetBytes(fmt, new IntPtr(pbytes), bytes.Length); + } + return Task.CompletedTask; + } + + public Task GetFormatsAsync() + { + using (var n = _native.ObtainFormats()) + return Task.FromResult(n.ToStringArray()); + } + + public async Task GetDataAsync(string format) + { + if (format == DataFormats.Text) + return await GetTextAsync(); + if (format == DataFormats.FileNames) + return GetFileNames(); + using (var n = _native.GetBytes(format)) + return n.Bytes; + } } class ClipboardDataObject : IDataObject, IDisposable diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index 23913c51e0..d2aa31cc31 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -337,5 +337,15 @@ + + + 12 + 1 + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index f3c19a3ba9..b3f666010b 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -338,5 +338,15 @@ + + + 12 + 1 + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 599177526d..2f4a571bf4 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -393,5 +393,16 @@ + + + 12 + 1 + + + + + + + 8,5,8,7 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index ff43508f8c..6a28f2c0fa 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -393,5 +393,16 @@ + + + 12 + 1 + + + + + + + 8,5,8,7 diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml index 1fc0202dd3..cf6f32f9bc 100644 --- a/src/Avalonia.Themes.Fluent/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml @@ -1,17 +1,78 @@ - \ No newline at end of file + + + + + Hover Here + + + + + + ToolTip + A control which pops up a hint when a control is hovered + + + ToolTip bottom placement + + + + + + + + + + + + diff --git a/src/Avalonia.X11/X11Atoms.cs b/src/Avalonia.X11/X11Atoms.cs index dd16fae386..32f1822e51 100644 --- a/src/Avalonia.X11/X11Atoms.cs +++ b/src/Avalonia.X11/X11Atoms.cs @@ -22,6 +22,7 @@ // using System; +using System.Collections.Generic; using System.Linq; using static Avalonia.X11.XLib; // ReSharper disable FieldCanBeMadeReadOnly.Global @@ -40,8 +41,9 @@ namespace Avalonia.X11 internal class X11Atoms { + private readonly IntPtr _display; -// Our atoms + // Our atoms public readonly IntPtr AnyPropertyType = (IntPtr)0; public readonly IntPtr XA_PRIMARY = (IntPtr)1; public readonly IntPtr XA_SECONDARY = (IntPtr)2; @@ -187,10 +189,13 @@ namespace Avalonia.X11 public readonly IntPtr ATOM_PAIR; public readonly IntPtr MANAGER; public readonly IntPtr _KDE_NET_WM_BLUR_BEHIND_REGION; + public readonly IntPtr INCR; - + private readonly Dictionary _namesToAtoms = new Dictionary(); + private readonly Dictionary _atomsToNames = new Dictionary(); public X11Atoms(IntPtr display) { + _display = display; // make sure this array stays in sync with the statements below @@ -204,7 +209,33 @@ namespace Avalonia.X11 XInternAtoms(display, atomNames, atomNames.Length, true, atoms); for (var c = 0; c < fields.Length; c++) + { + _namesToAtoms[fields[c].Name] = atoms[c]; + _atomsToNames[atoms[c]] = fields[c].Name; fields[c].SetValue(this, atoms[c]); + } + } + + public IntPtr GetAtom(string name) + { + if (_namesToAtoms.TryGetValue(name, out var rv)) + return rv; + var atom = XInternAtom(_display, name, false); + _namesToAtoms[name] = atom; + _atomsToNames[atom] = name; + return atom; + } + + public string GetAtomName(IntPtr atom) + { + if (_atomsToNames.TryGetValue(atom, out var rv)) + return rv; + var name = XLib.GetAtomName(_display, atom); + if (name == null) + return null; + _atomsToNames[atom] = name; + _namesToAtoms[name] = atom; + return name; } } } diff --git a/src/Avalonia.X11/X11Clipboard.cs b/src/Avalonia.X11/X11Clipboard.cs index a431ffcc1a..7023bb3ef3 100644 --- a/src/Avalonia.X11/X11Clipboard.cs +++ b/src/Avalonia.X11/X11Clipboard.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading.Tasks; +using Avalonia.Input; using Avalonia.Input.Platform; using static Avalonia.X11.XLib; namespace Avalonia.X11 @@ -10,12 +12,14 @@ namespace Avalonia.X11 class X11Clipboard : IClipboard { private readonly X11Info _x11; - private string _storedString; + private IDataObject _storedDataObject; private IntPtr _handle; private TaskCompletionSource _requestedFormatsTcs; - private TaskCompletionSource _requestedTextTcs; + private TaskCompletionSource _requestedDataTcs; private readonly IntPtr[] _textAtoms; private readonly IntPtr _avaloniaSaveTargetsAtom; + private readonly Dictionary _formatAtoms = new Dictionary(); + private readonly Dictionary _atomFormats = new Dictionary(); public X11Clipboard(AvaloniaX11Platform platform) { @@ -31,6 +35,11 @@ namespace Avalonia.X11 }.Where(a => a != IntPtr.Zero).ToArray(); } + bool IsStringAtom(IntPtr atom) + { + return _textAtoms.Contains(atom); + } + Encoding GetStringEncoding(IntPtr atom) { return (atom == _x11.Atoms.XA_STRING @@ -75,21 +84,31 @@ namespace Avalonia.X11 Encoding textEnc; if (target == _x11.Atoms.TARGETS) { - var atoms = _textAtoms; - atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE}) - .ToArray(); + var atoms = new HashSet { _x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE }; + foreach (var fmt in _storedDataObject.GetDataFormats()) + { + if (fmt == DataFormats.Text) + foreach (var ta in _textAtoms) + atoms.Add(ta); + else + atoms.Add(_x11.Atoms.GetAtom(fmt)); + } + XChangeProperty(_x11.Display, window, property, - _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length); + _x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms.ToArray(), atoms.Count); return property; } else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero) { return property; } - else if ((textEnc = GetStringEncoding(target)) != null) + else if ((textEnc = GetStringEncoding(target)) != null + && _storedDataObject?.Contains(DataFormats.Text) == true) { - - var data = textEnc.GetBytes(_storedString ?? ""); + var text = _storedDataObject.GetText(); + if(text == null) + return IntPtr.Zero; + var data = textEnc.GetBytes(text); fixed (void* pdata = data) XChangeProperty(_x11.Display, window, property, target, 8, PropertyMode.Replace, @@ -121,6 +140,23 @@ namespace Avalonia.X11 return property; } + else if(_storedDataObject?.Contains(_x11.Atoms.GetAtomName(target)) == true) + { + var objValue = _storedDataObject.Get(_x11.Atoms.GetAtomName(target)); + + if(!(objValue is byte[] bytes)) + { + if (objValue is string s) + bytes = Encoding.UTF8.GetBytes(s); + else + return IntPtr.Zero; + } + + XChangeProperty(_x11.Display, window, property, target, 8, + PropertyMode.Replace, + bytes, bytes.Length); + return property; + } else return IntPtr.Zero; } @@ -131,15 +167,15 @@ namespace Avalonia.X11 if (sel.property == IntPtr.Zero) { _requestedFormatsTcs?.TrySetResult(null); - _requestedTextTcs?.TrySetResult(null); + _requestedDataTcs?.TrySetResult(null); } XGetWindowProperty(_x11.Display, _handle, sel.property, IntPtr.Zero, new IntPtr (0x7fffffff), true, (IntPtr)Atom.AnyPropertyType, - out var actualAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop); + out var actualTypeAtom, out var actualFormat, out var nitems, out var bytes_after, out var prop); Encoding textEnc = null; if (nitems == IntPtr.Zero) { _requestedFormatsTcs?.TrySetResult(null); - _requestedTextTcs?.TrySetResult(null); + _requestedDataTcs?.TrySetResult(null); } else { @@ -154,10 +190,24 @@ namespace Avalonia.X11 _requestedFormatsTcs?.TrySetResult(formats); } } - else if ((textEnc = GetStringEncoding(sel.property)) != null) + else if ((textEnc = GetStringEncoding(actualTypeAtom)) != null) { var text = textEnc.GetString((byte*)prop.ToPointer(), nitems.ToInt32()); - _requestedTextTcs?.TrySetResult(text); + _requestedDataTcs?.TrySetResult(text); + } + else + { + if (actualTypeAtom == _x11.Atoms.INCR) + { + // TODO: Actually implement that monstrosity + _requestedDataTcs.TrySetResult(null); + } + else + { + var data = new byte[(int)nitems * (actualFormat / 8)]; + Marshal.Copy(prop, data, 0, data.Length); + _requestedDataTcs?.TrySetResult(data); + } } } @@ -174,17 +224,19 @@ namespace Avalonia.X11 return _requestedFormatsTcs.Task; } - Task SendTextRequest(IntPtr format) + Task SendDataRequest(IntPtr format) { - if (_requestedTextTcs == null || _requestedFormatsTcs.Task.IsCompleted) - _requestedTextTcs = new TaskCompletionSource(); + if (_requestedDataTcs == null || _requestedFormatsTcs.Task.IsCompleted) + _requestedDataTcs = new TaskCompletionSource(); XConvertSelection(_x11.Display, _x11.Atoms.CLIPBOARD, format, format, _handle, IntPtr.Zero); - return _requestedTextTcs.Task; + return _requestedDataTcs.Task; } + + bool HasOwner => XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) != IntPtr.Zero; public async Task GetTextAsync() { - if (XGetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD) == IntPtr.Zero) + if (!HasOwner) return null; var res = await SendFormatRequest(); var target = _x11.Atoms.UTF8_STRING; @@ -199,7 +251,7 @@ namespace Avalonia.X11 } } - return await SendTextRequest(target); + return (string)await SendDataRequest(target); } void StoreAtomsInClipboardManager(IntPtr[] atoms) @@ -220,15 +272,52 @@ namespace Avalonia.X11 public Task SetTextAsync(string text) { - _storedString = text; + var data = new DataObject(); + data.Set(DataFormats.Text, text); + return SetDataObjectAsync(data); + } + + public Task ClearAsync() + { + return SetTextAsync(null); + } + + public Task SetDataObjectAsync(IDataObject data) + { + _storedDataObject = data; XSetSelectionOwner(_x11.Display, _x11.Atoms.CLIPBOARD, _handle, IntPtr.Zero); StoreAtomsInClipboardManager(_textAtoms); return Task.CompletedTask; } - public Task ClearAsync() + public async Task GetFormatsAsync() { - return SetTextAsync(null); + if (!HasOwner) + return null; + var res = await SendFormatRequest(); + if (res == null) + return null; + var rv = new List(); + if (_textAtoms.Any(res.Contains)) + rv.Add(DataFormats.Text); + foreach (var t in res) + rv.Add(_x11.Atoms.GetAtomName(t)); + return rv.ToArray(); + } + + public async Task GetDataAsync(string format) + { + if (!HasOwner) + return null; + if (format == DataFormats.Text) + return await GetTextAsync(); + + var formatAtom = _x11.Atoms.GetAtom(format); + var res = await SendFormatRequest(); + if (!res.Contains(formatAtom)) + return null; + + return await SendDataRequest(formatAtom); } } } diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 3c41f7bdde..85aa4862b7 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -236,6 +236,10 @@ namespace Avalonia.X11 public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, PropertyMode mode, ref IntPtr value, int nelements); + [DllImport(libX11)] + public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, + int format, PropertyMode mode, byte[] data, int nelements); + [DllImport(libX11)] public static extern int XChangeProperty(IntPtr display, IntPtr window, IntPtr property, IntPtr type, int format, PropertyMode mode, uint[] data, int nelements); diff --git a/src/Windows/Avalonia.Win32/ClipboardImpl.cs b/src/Windows/Avalonia.Win32/ClipboardImpl.cs index 1c94d7a446..7d9e0a8bd2 100644 --- a/src/Windows/Avalonia.Win32/ClipboardImpl.cs +++ b/src/Windows/Avalonia.Win32/ClipboardImpl.cs @@ -1,25 +1,30 @@ using System; +using System.Linq; +using System.Reactive.Disposables; using System.Runtime.InteropServices; using System.Threading.Tasks; +using Avalonia.Input; using Avalonia.Input.Platform; +using Avalonia.Threading; using Avalonia.Win32.Interop; namespace Avalonia.Win32 { internal class ClipboardImpl : IClipboard { - private async Task OpenClipboard() + private async Task OpenClipboard() { while (!UnmanagedMethods.OpenClipboard(IntPtr.Zero)) { await Task.Delay(100); } + + return Disposable.Create(() => UnmanagedMethods.CloseClipboard()); } public async Task GetTextAsync() { - await OpenClipboard(); - try + using(await OpenClipboard()) { IntPtr hText = UnmanagedMethods.GetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT); if (hText == IntPtr.Zero) @@ -37,10 +42,6 @@ namespace Avalonia.Win32 UnmanagedMethods.GlobalUnlock(hText); return rv; } - finally - { - UnmanagedMethods.CloseClipboard(); - } } public async Task SetTextAsync(string text) @@ -50,31 +51,66 @@ namespace Avalonia.Win32 throw new ArgumentNullException(nameof(text)); } - await OpenClipboard(); - - UnmanagedMethods.EmptyClipboard(); - - try + using(await OpenClipboard()) { + UnmanagedMethods.EmptyClipboard(); + var hGlobal = Marshal.StringToHGlobalUni(text); UnmanagedMethods.SetClipboardData(UnmanagedMethods.ClipboardFormat.CF_UNICODETEXT, hGlobal); } - finally - { - UnmanagedMethods.CloseClipboard(); - } } public async Task ClearAsync() { - await OpenClipboard(); - try + using(await OpenClipboard()) { UnmanagedMethods.EmptyClipboard(); } - finally + } + + public async Task SetDataObjectAsync(IDataObject data) + { + Dispatcher.UIThread.VerifyAccess(); + var wrapper = new DataObject(data); + while (true) { - UnmanagedMethods.CloseClipboard(); + if (UnmanagedMethods.OleSetClipboard(wrapper) == 0) + break; + await Task.Delay(100); + } + } + + public async Task GetFormatsAsync() + { + Dispatcher.UIThread.VerifyAccess(); + while (true) + { + if (UnmanagedMethods.OleGetClipboard(out var dataObject) == 0) + { + var wrapper = new OleDataObject(dataObject); + var formats = wrapper.GetDataFormats().ToArray(); + Marshal.ReleaseComObject(dataObject); + return formats; + } + + await Task.Delay(100); + } + } + + public async Task GetDataAsync(string format) + { + Dispatcher.UIThread.VerifyAccess(); + while (true) + { + if (UnmanagedMethods.OleGetClipboard(out var dataObject) == 0) + { + var wrapper = new OleDataObject(dataObject); + var rv = wrapper.Get(format); + Marshal.ReleaseComObject(dataObject); + return rv; + } + + await Task.Delay(100); } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index da52c3116e..ba3775200b 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1169,6 +1169,12 @@ namespace Avalonia.Win32.Interop [DllImport("user32.dll")] public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem); + [DllImport("ole32.dll", PreserveSig = false)] + public static extern int OleGetClipboard(out IOleDataObject dataObject); + + [DllImport("ole32.dll", PreserveSig = true)] + public static extern int OleSetClipboard(IOleDataObject dataObject); + [DllImport("kernel32.dll", ExactSpelling = true)] public static extern IntPtr GlobalLock(IntPtr handle); diff --git a/src/iOS/Avalonia.iOS/Clipboard.cs b/src/iOS/Avalonia.iOS/Clipboard.cs index c66b379453..2deb49473f 100644 --- a/src/iOS/Avalonia.iOS/Clipboard.cs +++ b/src/iOS/Avalonia.iOS/Clipboard.cs @@ -22,5 +22,11 @@ namespace Avalonia.iOS UIPasteboard.General.String = ""; return Task.FromResult(0); } + + public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException(); + + public Task GetFormatsAsync() => throw new PlatformNotSupportedException(); + + public Task GetDataAsync(string format) => throw new PlatformNotSupportedException(); } } diff --git a/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs new file mode 100644 index 0000000000..3d5a4029bb --- /dev/null +++ b/tests/Avalonia.Benchmarks/Data/PropertyAccessorBenchmarks.cs @@ -0,0 +1,88 @@ +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using Avalonia.Data.Core.Plugins; +using BenchmarkDotNet.Attributes; +using JetBrains.Annotations; + +namespace Avalonia.Benchmarks.Data +{ + [MemoryDiagnoser, InProcess] + public class PropertyAccessorBenchmarks + { + private readonly InpcPropertyAccessorPlugin _inpcPlugin = new InpcPropertyAccessorPlugin(); + private readonly MethodAccessorPlugin _methodPlugin = new MethodAccessorPlugin(); + private readonly TestObject _targetStrongRef = new TestObject(); + private readonly WeakReference _targetWeakRef; + + public PropertyAccessorBenchmarks() + { + _targetWeakRef = new WeakReference(_targetStrongRef); + } + + [Benchmark] + public void InpcAccessorMatch() + { + _inpcPlugin.Match(_targetWeakRef, nameof(TestObject.Test)); + } + + [Benchmark] + public void InpcAccessorStart() + { + _inpcPlugin.Start(_targetWeakRef, nameof(TestObject.Test)); + } + + [Benchmark] + public void MethodAccessorMatch() + { + _methodPlugin.Match(_targetWeakRef, nameof(TestObject.Execute)); + } + + [Benchmark] + public void MethodAccessorStart() + { + _methodPlugin.Start(_targetWeakRef, nameof(TestObject.Execute)); + } + + private class TestObject : INotifyPropertyChanged + { + private string _test; + + public string Test + { + get => _test; + set + { + if (_test == value) + { + return; + } + + _test = value; + + OnPropertyChanged(); + } + } + + public void Execute() + { + } + + public void Execute(object p0) + { + } + + public void Execute(object p0, object p1) + { + } + + public event PropertyChangedEventHandler PropertyChanged; + + [NotifyPropertyChangedInvocator] + protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index d2f62cde04..217dec2d6d 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -654,6 +654,11 @@ namespace Avalonia.Controls.UnitTests public Task SetTextAsync(string text) => Task.CompletedTask; public Task ClearAsync() => Task.CompletedTask; + public Task SetDataObjectAsync(IDataObject data) => Task.CompletedTask; + + public Task GetFormatsAsync() => Task.FromResult(Array.Empty()); + + public Task GetDataAsync(string format) => Task.FromResult((object)null); } } } diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index dc5b574db7..34b37e7635 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -102,5 +102,76 @@ namespace Avalonia.Controls.UnitTests Assert.True(ToolTip.GetIsOpen(target)); } } + + [Fact] + public void Open_Class_Should_Not_Initially_Be_Added() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.Empty(toolTip.Classes); + } + } + + [Fact] + public void Setting_IsOpen_Should_Add_Open_Class() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + ToolTip.SetIsOpen(decorator, true); + + Assert.Equal(new[] { ":open" }, toolTip.Classes); + } + } + + [Fact] + public void Clearing_IsOpen_Should_Remove_Open_Class() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var toolTip = new ToolTip(); + var window = new Window(); + + var decorator = new Decorator() + { + [ToolTip.TipProperty] = toolTip + }; + + window.Content = decorator; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + ToolTip.SetIsOpen(decorator, true); + ToolTip.SetIsOpen(decorator, false); + + Assert.Empty(toolTip.Classes); + } + } } }