Browse Source

Implemented custom format support for clipboard

Nikita Tsukanov 5 years ago
parent
commit
996e602bf8

+ 3 - 0
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;
 };
 

+ 1 - 0
native/Avalonia.Native/src/OSX/AvnString.h

@@ -12,4 +12,5 @@
 extern IAvnString* CreateAvnString(NSString* string);
 extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
 extern IAvnStringArray* CreateAvnStringArray(NSString* string);
+extern IAvnString* CreateByteArray(void* data, int len);
 #endif /* AvnString_h */

+ 12 - 0
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);
+}

+ 34 - 0
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
     {

+ 6 - 0
src/Android/Avalonia.Android/Platform/ClipboardImpl.cs

@@ -43,5 +43,11 @@ namespace Avalonia.Android.Platform
 
             return Task.FromResult<object>(null);
         }
+
+        public Task SetDataObjectAsync(IDataObject data) => throw new PlatformNotSupportedException();
+
+        public Task<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException();
+
+        public Task<object> GetFormatAsync(string format) => throw new PlatformNotSupportedException();
     }
 }

+ 4 - 0
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -156,6 +156,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<string[]> GetFormatsAsync() => Task.FromResult(new string[0]);
+
+        public Task<object> GetFormatAsync(string format) => Task.FromResult((object)null);
     }
 
     class CursorFactoryStub : IStandardCursorFactory

+ 6 - 0
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<string[]> GetFormatsAsync();
+        
+        Task<object> GetFormatAsync(string format);
     }
 }

+ 15 - 0
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;
     }
     

+ 32 - 0
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<string[]> GetFormatsAsync()
+        {
+            using (var n = _native.ObtainFormats())
+                return Task.FromResult(n.ToStringArray());
+        }
+
+        public async Task<object> GetFormatAsync(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

+ 33 - 2
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<string, IntPtr> _namesToAtoms  = new Dictionary<string, IntPtr>();
+        private readonly Dictionary<IntPtr, string> _atomsToNames = new Dictionary<IntPtr, string>();
         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;
         }
     }
 }

+ 112 - 23
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<IntPtr[]> _requestedFormatsTcs;
-        private TaskCompletionSource<string> _requestedTextTcs;
+        private TaskCompletionSource<object> _requestedDataTcs;
         private readonly IntPtr[] _textAtoms;
         private readonly IntPtr _avaloniaSaveTargetsAtom;
+        private readonly Dictionary<string, IntPtr> _formatAtoms = new Dictionary<string, IntPtr>();
+        private readonly Dictionary<IntPtr, string> _atomFormats = new Dictionary<IntPtr, string>();
 
         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<IntPtr> { _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<string> SendTextRequest(IntPtr format)
+        Task<object> SendDataRequest(IntPtr format)
         {
-            if (_requestedTextTcs == null || _requestedFormatsTcs.Task.IsCompleted)
-                _requestedTextTcs = new TaskCompletionSource<string>();
+            if (_requestedDataTcs == null || _requestedFormatsTcs.Task.IsCompleted)
+                _requestedDataTcs = new TaskCompletionSource<object>();
             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<string> 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<string[]> GetFormatsAsync()
         {
-            return SetTextAsync(null);
+            if (!HasOwner)
+                return null;
+            var res = await SendFormatRequest();
+            if (res == null)
+                return null;
+            var rv = new List<string>();
+            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<object> GetFormatAsync(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);
         }
     }
 }

+ 4 - 0
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);

+ 56 - 20
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<IDisposable> OpenClipboard()
         {
             while (!UnmanagedMethods.OpenClipboard(IntPtr.Zero))
             {
                 await Task.Delay(100);
             }
+
+            return Disposable.Create(() => UnmanagedMethods.CloseClipboard());
         }
 
         public async Task<string> 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<string[]> 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<object> GetFormatAsync(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);
             }
         }
     }

+ 6 - 0
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);
 

+ 6 - 0
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<string[]> GetFormatsAsync() => throw new PlatformNotSupportedException();
+
+        public Task<object> GetFormatAsync(string format) => throw new PlatformNotSupportedException();
     }
 }

+ 5 - 0
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<string[]> GetFormatsAsync() => Task.FromResult(Array.Empty<string>());
+
+            public Task<object> GetFormatAsync(string format) => Task.FromResult((object)null);
         }
     }
 }