f73cc2dfd5584c2a891f1fc340545b12ad91401a
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI / src / public / BaseComponents / LottieAnimationView.cs
1 /*
2  * Copyright(c) 2020 Samsung Electronics Co., Ltd.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  *
16  */
17
18 using global::System;
19 using global::System.Runtime.InteropServices;
20 using System.ComponentModel;
21 using System.Collections.Generic;
22 using System.Globalization;
23
24 namespace Tizen.NUI.BaseComponents
25 {
26     /// <summary>
27     /// LottieAnimationView renders an animated vector image (Lottie file).
28     /// </summary>
29     /// <since_tizen> 7 </since_tizen>
30     public partial class LottieAnimationView : ImageView
31     {
32
33         #region Constructor, Destructor, Dispose
34         /// <summary>
35         /// LottieAnimationView constructor
36         /// </summary>
37         /// <param name="scale">The factor of scaling image, default : 1.0f</param>
38         /// <param name="shown">false : not displayed (hidden), true : displayed (shown), default : true</param>
39         /// <remarks>
40         /// If the shown parameter is false, the animation is not visible even if the LottieAnimationView instance is created.
41         /// </remarks>
42         /// <example>
43         /// <code>
44         /// LottieAnimationView myLottie = new LottieAnimationView();
45         /// LottieAnimationView myLottie2 = new LottieAnimationView(2.0f);
46         /// LottieAnimationView myLottie3 = new LottieAnimationView(1.0f, false);
47         /// </code>
48         /// </example>
49         /// <since_tizen> 7 </since_tizen>
50         public LottieAnimationView(float scale = 1.0f, bool shown = true) : base()
51         {
52             NUILog.Debug($"< constructor GetId={GetId()} >");
53             currentStates.url = "";
54             currentStates.loopCount = 1;
55             currentStates.loopMode = LoopingModeType.Restart;
56             currentStates.stopEndAction = StopBehaviorType.CurrentFrame;
57             currentStates.framePlayRangeMin = -1;
58             currentStates.framePlayRangeMax = -1;
59             currentStates.totalFrame = -1;
60             currentStates.scale = scale;
61             currentStates.redrawInScalingDown = true;
62
63             // Set changed flag as true when initalized state.
64             // After some properties change, LottieAnimationView.UpdateImage will apply these inital values.
65             currentStates.changed = true;
66             SetVisible(shown);
67         }
68
69         /// <summary>
70         /// Dispose(DisposeTypes type)
71         /// </summary>
72         /// <param name="type"></param>
73         // This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
74         [EditorBrowsable(EditorBrowsableState.Never)]
75         protected override void Dispose(DisposeTypes type)
76         {
77             if (disposed)
78             {
79                 return;
80             }
81
82             CleanCallbackDictionaries();
83
84             //Release your own unmanaged resources here.
85             //You should not access any managed member here except static instance.
86             //because the execution order of Finalizes is non-deterministic.
87
88             //disconnect event signal
89             if (finishedEventHandler != null && visualEventSignalCallback != null)
90             {
91                 using VisualEventSignal visualEvent = VisualEventSignal();
92                 visualEvent.Disconnect(visualEventSignalCallback);
93                 finishedEventHandler = null;
94                 NUILog.Debug($"disconnect event signal");
95             }
96
97             base.Dispose(type);
98         }
99
100         // This is used for internal purpose. hidden API.
101         [EditorBrowsable(EditorBrowsableState.Never)]
102         protected override void Dispose(bool disposing)
103         {
104             CleanCallbackDictionaries();
105             base.Dispose(disposing);
106         }
107         #endregion Constructor, Destructor, Dispose
108
109
110         #region Property
111         /// <summary>
112         /// Set or Get resource URL of Lottie file.
113         /// </summary>
114         /// <since_tizen> 7 </since_tizen>
115         public string URL
116         {
117             get
118             {
119                 return GetValue(URLProperty) as string;
120             }
121             set
122             {
123                 SetValue(URLProperty, value);
124                 NotifyPropertyChanged();
125             }
126         }
127
128         private string InternalURL
129         {
130             set
131             {
132                 // Reset cached infomations.
133                 currentStates.contentInfo = null;
134                 currentStates.mark1 = null;
135                 currentStates.mark2 = null;
136                 currentStates.framePlayRangeMin = -1;
137                 currentStates.framePlayRangeMax = -1;
138                 currentStates.totalFrame = -1;
139
140                 string ret = (value == null ? "" : value);
141                 currentStates.url = ret;
142
143                 NUILog.Debug($"<[{GetId()}]SET url={currentStates.url}");
144
145                 // TODO : Could create new Image without additional creation?
146                 using PropertyMap map = new PropertyMap();
147                 using PropertyValue type = new PropertyValue((int)Visual.Type.AnimatedVectorImage);
148                 using PropertyValue url = new PropertyValue(currentStates.url);
149                 using PropertyValue loopCnt = new PropertyValue(currentStates.loopCount);
150                 using PropertyValue stopAction = new PropertyValue((int)currentStates.stopEndAction);
151                 using PropertyValue loopMode = new PropertyValue((int)currentStates.loopMode);
152                 using PropertyValue redrawInScalingDown = new PropertyValue(currentStates.redrawInScalingDown);
153
154                 map.Add(Visual.Property.Type, type)
155                     .Add(ImageVisualProperty.URL, url)
156                     .Add(ImageVisualProperty.LoopCount, loopCnt)
157                     .Add(ImageVisualProperty.StopBehavior, stopAction)
158                     .Add(ImageVisualProperty.LoopingMode, loopMode)
159                     .Add(ImageVisualProperty.RedrawInScalingDown, redrawInScalingDown);
160                 Image = map;
161
162                 // All states applied well.
163                 currentStates.changed = false;
164
165                 if (currentStates.scale != 1.0f)
166                 {
167                     Scale = new Vector3(currentStates.scale, currentStates.scale, currentStates.scale);
168                 }
169                 NUILog.Debug($"<[{GetId()}]>");
170             }
171             get
172             {
173                 string ret = currentStates.url;
174                 NUILog.Debug($"<[{GetId()}] GET");
175                 NUILog.Debug($"gotten url={ret} >");
176                 return ret;
177             }
178         }
179
180         /// <summary>
181         /// Gets the playing state
182         /// </summary>
183         /// <since_tizen> 7 </since_tizen>
184         public PlayStateType PlayState
185         {
186             get
187             {
188                 NUILog.Debug($"< Get!");
189
190                 int ret = 0;
191                 Interop.View.InternalRetrievingVisualPropertyInt(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.PlayState, out ret);
192
193                 currentStates.playState = (PlayStateType)ret;
194                 NUILog.Debug($"gotten play state={currentStates.playState} >");
195
196                 return currentStates.playState;
197             }
198         }
199
200         /// <summary>
201         /// Get the number of total frames
202         /// </summary>
203         /// <since_tizen> 7 </since_tizen>
204         public int TotalFrame
205         {
206             get
207             {
208                 int ret = currentStates.totalFrame;
209                 if (ret == -1)
210                 {
211                     Interop.View.InternalRetrievingVisualPropertyInt(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.TotalFrameNumber, out ret);
212
213                     currentStates.totalFrame = ret;
214                     NUILog.Debug($"TotalFrameNumber get! ret={ret}");
215                 }
216                 return ret;
217             }
218         }
219
220         /// <summary>
221         /// Set or get the current frame. When setting a specific frame, it is displayed as a still image.
222         /// </summary>
223         /// <remarks>
224         /// Gets the value set by a user. If the setting value is out-ranged, it is reset as a minimum frame or a maximum frame.
225         /// </remarks>
226         /// <example>
227         /// We assume that the animation in myLottie.json file has 100 frames originally. If so, its frame index will be 0 - 99.
228         /// <code>
229         /// LottieAnimationView myLottie = new LottieAnimationView();
230         /// myLottie.URL = Tizen.Applications.Application.Current.DirectoryInfo.Resource + "myLottie.json"; //myLottie.json's total frame is 100 (frame: 0~99)
231         /// NUIApplication.GetDefaultWindow().GetDefaultLayer().Add(myLottie);
232         /// myLottie.CurrentFrame = 200; //display 99 frame
233         /// myLottie.SetMinMaxFrame(10, 20);
234         /// myLottie.CurrentFrame = 15; //display 15 frame
235         /// myLottie.CurrentFrame = 50; //display 20 frame, because the MinMax is set (10,20) above
236         /// </code>
237         /// </example>
238         /// <since_tizen> 7 </since_tizen>
239         public int CurrentFrame
240         {
241             get
242             {
243                 return (int)GetValue(CurrentFrameProperty);
244             }
245             set
246             {
247                 SetValue(CurrentFrameProperty, value);
248                 NotifyPropertyChanged();
249             }
250         }
251
252         private int InternalCurrentFrame
253         {
254             set
255             {
256                 NUILog.Debug($"<[{GetId()}]SET frame={value}>");
257
258                 Interop.View.DoActionWithSingleIntAttributes(this.SwigCPtr, ImageView.Property.IMAGE, ActionJumpTo, value);
259             }
260             get
261             {
262                 int ret = 0;
263
264                 Interop.View.InternalRetrievingVisualPropertyInt(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.CurrentFrameNumber, out ret);
265
266                 NUILog.Debug($"CurrentFrameNumber get! val={ret}");
267                 return ret;
268             }
269         }
270
271         /// <summary>
272         /// Sets or gets the looping mode of Lottie animation.
273         /// </summary>
274         /// <since_tizen> 7 </since_tizen>
275         public LoopingModeType LoopingMode
276         {
277             get
278             {
279                 return (LoopingModeType)GetValue(LoopingModeProperty);
280             }
281             set
282             {
283                 SetValue(LoopingModeProperty, value);
284                 NotifyPropertyChanged();
285             }
286         }
287
288         private LoopingModeType InternalLoopingMode
289         {
290             set
291             {
292                 if (currentStates.loopMode != (LoopingModeType)value)
293                 {
294                     currentStates.changed = true;
295                     currentStates.loopMode = (LoopingModeType)value;
296
297                     NUILog.Debug($"<[{GetId()}] SET loopMode={currentStates.loopMode}>");
298
299                     Interop.View.InternalUpdateVisualPropertyInt(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.LoopingMode, (int)currentStates.loopMode);
300                 }
301             }
302             get
303             {
304                 NUILog.Debug($"LoopMode get! {currentStates.loopMode}");
305                 return currentStates.loopMode;
306             }
307         }
308
309         /// <summary>
310         /// Sets or gets the loop count.
311         /// </summary>
312         /// <remarks>
313         /// The minus value means the infinite loop count.
314         /// </remarks>
315         /// <example>
316         /// <code>
317         /// LottieAnimationView myLottie = new LottieAnimationView();
318         /// myLottie.URL = Tizen.Applications.Application.Current.DirectoryInfo.Resource + "myLottie.json"; //myLottie.json's total frame is 100 (frame: 0~99)
319         /// NUIApplication.GetDefaultWindow().GetDefaultLayer().Add(myLottie);
320         /// myLottie.LoopCount = -1; //infinite loop
321         /// myLottie.Play();
322         /// myLottie.Stop(); //it plays continuously unless Stop() is called
323         /// myLottie.LoopCount = 2;
324         /// myLottie.Play(); //it plays only 2 times and stops automatically
325         /// </code>
326         /// </example>
327         /// <since_tizen> 7 </since_tizen>
328         public int LoopCount
329         {
330             get
331             {
332                 return (int)GetValue(LoopCountProperty);
333             }
334             set
335             {
336                 SetValue(LoopCountProperty, value);
337                 NotifyPropertyChanged();
338             }
339         }
340
341         private int InternalLoopCount
342         {
343             set
344             {
345                 if (currentStates.loopCount != value)
346                 {
347                     currentStates.changed = true;
348                     currentStates.loopCount = value;
349
350                     NUILog.Debug($"<[{GetId()}]SET currentStates.loopCount={currentStates.loopCount}>");
351
352                     Interop.View.InternalUpdateVisualPropertyInt(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.LoopCount, currentStates.loopCount);
353                 }
354             }
355             get
356             {
357                 NUILog.Debug($"LoopCount get! {currentStates.loopCount}");
358                 return currentStates.loopCount;
359             }
360         }
361
362         /// <summary>
363         /// Sets or gets the stop behavior.
364         /// </summary>
365         /// <since_tizen> 7 </since_tizen>
366         public StopBehaviorType StopBehavior
367         {
368             get
369             {
370                 return (StopBehaviorType)GetValue(StopBehaviorProperty);
371             }
372             set
373             {
374                 SetValue(StopBehaviorProperty, value);
375                 NotifyPropertyChanged();
376             }
377         }
378
379         private StopBehaviorType InternalStopBehavior
380         {
381             set
382             {
383                 if (currentStates.stopEndAction != (StopBehaviorType)value)
384                 {
385                     currentStates.changed = true;
386                     currentStates.stopEndAction = (StopBehaviorType)value;
387
388                     NUILog.Debug($"<[{GetId()}]SET val={currentStates.stopEndAction}>");
389
390                     Interop.View.InternalUpdateVisualPropertyInt(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.StopBehavior, (int)currentStates.stopEndAction);
391                 }
392             }
393             get
394             {
395                 NUILog.Debug($"StopBehavior get! {currentStates.stopEndAction}");
396                 return currentStates.stopEndAction;
397             }
398         }
399
400         /// <summary>
401         /// Whether to redraw the image when the visual is scaled down.
402         /// </summary>
403         /// <remarks>
404         /// Inhouse API.
405         /// It is used in the AnimatedVectorImageVisual.The default is true.
406         /// </remarks>
407         [EditorBrowsable(EditorBrowsableState.Never)]
408         public bool RedrawInScalingDown
409         {
410             get
411             {
412                 return (bool)GetValue(RedrawInScalingDownProperty);
413             }
414             set
415             {
416                 SetValue(RedrawInScalingDownProperty, value);
417                 NotifyPropertyChanged();
418             }
419         }
420
421         private bool InternalRedrawInScalingDown
422         {
423             set
424             {
425                 if (currentStates.redrawInScalingDown != value)
426                 {
427                     currentStates.changed = true;
428                     currentStates.redrawInScalingDown = value;
429
430                     NUILog.Debug($"<[{GetId()}]SET currentStates.redrawInScalingDown={currentStates.redrawInScalingDown}>");
431
432                     Interop.View.InternalUpdateVisualPropertyBool(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.RedrawInScalingDown, currentStates.redrawInScalingDown);
433                 }
434             }
435             get
436             {
437                 NUILog.Debug($"RedrawInScalingDown get! {currentStates.redrawInScalingDown}");
438                 return currentStates.redrawInScalingDown;
439             }
440         }
441         #endregion Property
442
443
444         #region Method
445         /// <summary>
446         /// Set the minimum and the maximum frame.
447         /// </summary>
448         /// <param name="minFrame">minimum frame</param>
449         /// <param name="maxFrame">maximum frame</param>
450         /// <since_tizen> 7 </since_tizen>
451         public void SetMinMaxFrame(int minFrame, int maxFrame)
452         {
453             if (currentStates.framePlayRangeMin != minFrame || currentStates.framePlayRangeMax != maxFrame)
454             {
455                 NUILog.Debug($"< [{GetId()}] SetPlayRange({minFrame}, {maxFrame})");
456                 currentStates.changed = true;
457                 currentStates.framePlayRangeMin = minFrame;
458                 currentStates.framePlayRangeMax = maxFrame;
459
460                 Interop.View.InternalUpdateVisualPropertyIntPair(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.PlayRange, currentStates.framePlayRangeMin, currentStates.framePlayRangeMax);
461
462                 NUILog.Debug($"  [{GetId()}] currentStates.min:({currentStates.framePlayRangeMin}, max:{currentStates.framePlayRangeMax})>");
463             }
464         }
465
466         /// <summary>
467         /// Play Animation.
468         /// </summary>
469         /// <since_tizen> 7 </since_tizen>
470         public new void Play()
471         {
472             NUILog.Debug($"<[{GetId()}] Play()");
473             debugPrint();
474             base.Play();
475             NUILog.Debug($"[{GetId()}]>");
476         }
477
478         /// <summary>
479         /// Pause Animation.
480         /// </summary>
481         /// <since_tizen> 7 </since_tizen>
482         public new void Pause()
483         {
484             NUILog.Debug($"<[{GetId()}] Pause()>");
485             debugPrint();
486             base.Pause();
487             NUILog.Debug($"[{GetId()}]>");
488         }
489
490         /// <summary>
491         /// Stop Animation.
492         /// </summary>
493         /// <since_tizen> 7 </since_tizen>
494         public new void Stop()
495         {
496             NUILog.Debug($"<[{GetId()}] Stop()");
497             debugPrint();
498             base.Stop();
499             NUILog.Debug($"[{GetId()}]>");
500         }
501
502         /// <summary>
503         /// Get the list of layers' information such as the start frame and the end frame in the Lottie file.
504         /// </summary>
505         /// <returns>List of Tuple (string of layer name, integer of start frame, integer of end frame)</returns>
506         /// <since_tizen> 7 </since_tizen>
507         public List<Tuple<string, int, int>> GetContentInfo()
508         {
509             if (currentStates.contentInfo != null)
510             {
511                 return currentStates.contentInfo;
512             }
513
514             NUILog.Debug($"<");
515
516             PropertyMap imageMap = base.Image;
517             if (imageMap != null)
518             {
519                 PropertyValue val = imageMap.Find(ImageVisualProperty.ContentInfo);
520                 PropertyMap contentMap = new PropertyMap();
521                 if (val?.Get(ref contentMap) == true)
522                 {
523                     currentStates.contentInfo = new List<Tuple<string, int, int>>();
524                     for (uint i = 0; i < contentMap.Count(); i++)
525                     {
526                         using PropertyKey propertyKey = contentMap.GetKeyAt(i);
527                         string key = propertyKey.StringKey;
528
529                         using PropertyValue arrVal = contentMap.GetValue(i);
530                         using PropertyArray arr = new PropertyArray();
531                         if (arrVal.Get(arr))
532                         {
533                             int startFrame = -1;
534                             using PropertyValue start = arr.GetElementAt(0);
535                             start?.Get(out startFrame);
536
537                             int endFrame = -1;
538                             using PropertyValue end = arr.GetElementAt(1);
539                             end?.Get(out endFrame);
540
541                             NUILog.Debug($"[{i}] layer name={key}, startFrame={startFrame}, endFrame={endFrame}");
542
543                             Tuple<string, int, int> item = new Tuple<string, int, int>(key, startFrame, endFrame);
544
545                             currentStates.contentInfo?.Add(item);
546                         }
547                     }
548                 }
549                 contentMap.Dispose();
550                 val?.Dispose();
551             }
552             NUILog.Debug($">");
553
554             return currentStates.contentInfo;
555         }
556
557         /// <summary>
558         /// A marker has its start frame and end frame.
559         /// Animation will play between the start frame and the end frame of the marker if one marker is specified.
560         /// Or animation will play between the start frame of the first marker and the end frame of the second marker if two markers are specified.   *
561         /// </summary>
562         /// <param name="marker1">First marker</param>
563         /// <param name="marker2">Second marker</param>
564         // This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
565         [EditorBrowsable(EditorBrowsableState.Never)]
566         public void SetMinMaxFrameByMarker(string marker1, string marker2 = null)
567         {
568             if (currentStates.mark1 != marker1 || currentStates.mark2 != marker2)
569             {
570                 NUILog.Debug($"< [{GetId()}] SetMinMaxFrameByMarker({marker1}, {marker2})");
571
572                 currentStates.changed = true;
573                 currentStates.mark1 = marker1;
574                 currentStates.mark2 = marker2;
575
576                 if (string.IsNullOrEmpty(currentStates.mark2))
577                 {
578                     Interop.View.InternalUpdateVisualPropertyString(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.PlayRange, currentStates.mark1);
579                 }
580                 else
581                 {
582                     Interop.View.InternalUpdateVisualPropertyStringPair(this.SwigCPtr, ImageView.Property.IMAGE, ImageVisualProperty.PlayRange, currentStates.mark1, currentStates.mark2);
583                 }
584
585                 NUILog.Debug($"  [{GetId()}] currentStates.mark1:{currentStates.mark1}, mark2:{currentStates.mark2} >");
586             }
587         }
588
589         /// <summary>
590         /// Get MinMax Frame
591         /// </summary>
592         /// <returns>Tuple of Min and Max frames</returns>
593         // This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
594         [EditorBrowsable(EditorBrowsableState.Never)]
595         public Tuple<int, int> GetMinMaxFrame()
596         {
597             NUILog.Debug($"< [{GetId()}] GetMinMaxFrame()! total frame={currentStates.totalFrame}");
598
599             using PropertyMap map = Image;
600             if (map != null)
601             {
602                 using PropertyValue val = map.Find(ImageVisualProperty.PlayRange);
603                 if (val != null)
604                 {
605                     using PropertyArray array = new PropertyArray();
606                     if (val.Get(array))
607                     {
608                         uint cnt = array.Count();
609                         int item1 = -1, item2 = -1;
610                         for (uint i = 0; i < cnt; i++)
611                         {
612                             using PropertyValue v = array.GetElementAt(i);
613                             int intRet;
614                             if (v.Get(out intRet))
615                             {
616                                 NUILog.Debug($"Got play range of string [{i}]: {intRet}");
617                                 if (i == 0)
618                                 {
619                                     item1 = intRet;
620                                 }
621                                 else if (i == 1)
622                                 {
623                                     item2 = intRet;
624                                 }
625                             }
626                             else
627                             {
628                                 Tizen.Log.Error("NUI", $"[ERR] fail to get play range from dali! case#1");
629                             }
630                         }
631                         NUILog.Debug($"  [{GetId()}] GetMinMaxFrame(min:{item1}, max:{item2})! >");
632                         return new Tuple<int, int>(item1, item2);
633                     }
634                 }
635             }
636             Tizen.Log.Error("NUI", $"[ERR] fail to get play range from dali! case#2");
637             return new Tuple<int, int>(-1, -1);
638         }
639
640         // This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
641         [EditorBrowsable(EditorBrowsableState.Never)]
642         public void DoActionExtension(LottieAnimationViewDynamicProperty info)
643         {
644             dynamicPropertyCallbackId++;
645
646             weakReferencesOfLottie?.Add(dynamicPropertyCallbackId, new WeakReference<LottieAnimationView>(this));
647             InternalSavedDynamicPropertyCallbacks?.Add(dynamicPropertyCallbackId, info.Callback);
648
649             Interop.View.DoActionExtension(SwigCPtr, ImageView.Property.IMAGE, ActionSetDynamicProperty, dynamicPropertyCallbackId, info.KeyPath, (int)info.Property, Marshal.GetFunctionPointerForDelegate<System.Delegate>(rootCallback));
650
651             if (NDalicPINVOKE.SWIGPendingException.Pending) throw NDalicPINVOKE.SWIGPendingException.Retrieve();
652         }
653
654         private void CleanCallbackDictionaries()
655         {
656             if (weakReferencesOfLottie?.Count > 0 && InternalSavedDynamicPropertyCallbacks != null)
657             {
658                 foreach (var key in InternalSavedDynamicPropertyCallbacks?.Keys)
659                 {
660                     if (weakReferencesOfLottie.ContainsKey(key))
661                     {
662                         weakReferencesOfLottie.Remove(key);
663                     }
664                 }
665             }
666             InternalSavedDynamicPropertyCallbacks?.Clear();
667             InternalSavedDynamicPropertyCallbacks = null;
668         }
669
670         /// <summary>
671         /// Update lottie-image-relative properties synchronously.
672         /// After call this API, All image properties updated.
673         /// </summary>
674         [EditorBrowsable(EditorBrowsableState.Never)]
675         protected override void UpdateImage()
676         {
677             if (!imagePropertyUpdatedFlag) return;
678
679             // Update currentStates properties to cachedImagePropertyMap
680             if(currentStates.changed)
681             {
682                 UpdateImage(ImageVisualProperty.LoopCount, new PropertyValue(currentStates.loopCount), false);
683                 UpdateImage(ImageVisualProperty.StopBehavior, new PropertyValue((int)currentStates.stopEndAction), false);
684                 UpdateImage(ImageVisualProperty.LoopingMode, new PropertyValue((int)currentStates.loopMode), false);
685                 UpdateImage(ImageVisualProperty.RedrawInScalingDown, new PropertyValue(currentStates.redrawInScalingDown), false);
686
687                 // Do not cache PlayRange and TotalFrameNumber into cachedImagePropertyMap.
688                 // (To keep legacy implements behaviour)
689                 currentStates.changed = false;
690             }
691
692             base.UpdateImage();
693         }
694
695         /// <summary>
696         /// Update NUI cached animated image visual property map by inputed property map.
697         /// And call base.MergeCachedImageVisualProperty()
698         /// </summary>
699         /// <remarks>
700         /// For performance issue, we will collect only "cachedLottieAnimationPropertyKeyList" hold in this class.
701         /// </remarks>
702         [EditorBrowsable(EditorBrowsableState.Never)]
703         protected override void MergeCachedImageVisualProperty(PropertyMap map)
704         {
705             if (map == null) return;
706             if (cachedImagePropertyMap == null)
707             {
708                 cachedImagePropertyMap = new PropertyMap();
709             }
710             foreach (var key in cachedLottieAnimationPropertyKeyList)
711             {
712                 PropertyValue value = map.Find(key);
713                 if (value != null)
714                 {
715                     // Update-or-Insert new value
716                     cachedImagePropertyMap[key] = value;
717                 }
718             }
719             base.MergeCachedImageVisualProperty(map);
720         }
721         #endregion Method
722
723
724         #region Event, Enum, Struct, ETC
725         /// <summary>
726         /// Animation finished event.
727         /// </summary>
728         /// <since_tizen> 7 </since_tizen>
729         public event EventHandler Finished
730         {
731             add
732             {
733                 if (finishedEventHandler == null)
734                 {
735                     NUILog.Debug($"<[{GetId()}] Finished eventhandler added>");
736                     visualEventSignalCallback = onVisualEventSignal;
737                     using VisualEventSignal visualEvent = VisualEventSignal();
738                     visualEvent.Connect(visualEventSignalCallback);
739                 }
740                 finishedEventHandler += value;
741             }
742             remove
743             {
744                 NUILog.Debug($"<[{GetId()}] Finished eventhandler removed>");
745                 finishedEventHandler -= value;
746                 if (finishedEventHandler == null && visualEventSignalCallback != null)
747                 {
748                     using VisualEventSignal visualEvent = VisualEventSignal();
749                     visualEvent.Disconnect(visualEventSignalCallback);
750                     if (visualEvent?.Empty() == true)
751                     {
752                         visualEventSignalCallback = null;
753                     }
754                 }
755             }
756         }
757
758         /// <summary>
759         /// Enumeration for what state the vector animation is in
760         /// </summary>
761         /// <since_tizen> 7 </since_tizen>
762         public enum PlayStateType
763         {
764             /// <summary>
765             /// Invalid
766             /// </summary>
767             /// <since_tizen> 7 </since_tizen>
768             Invalid = -1,
769             /// <summary>
770             /// Vector Animation has stopped
771             /// </summary>
772             /// <since_tizen> 7 </since_tizen>
773             Stopped = 0,
774             /// <summary>
775             /// The vector animation is playing
776             /// </summary>
777             /// <since_tizen> 7 </since_tizen>
778             Playing = 1,
779             /// <summary>
780             /// The vector animation is paused
781             /// </summary>
782             /// <since_tizen> 7 </since_tizen>
783             Paused = 2
784         }
785
786         /// <summary>
787         /// Enumeration for what to do when the animation is stopped.
788         /// </summary>
789         /// <since_tizen> 7 </since_tizen>
790         public enum StopBehaviorType
791         {
792             /// <summary>
793             /// When the animation is stopped, the current frame is shown.
794             /// </summary>
795             /// <since_tizen> 7 </since_tizen>
796             CurrentFrame,
797             /// <summary>
798             /// When the animation is stopped, the min frame (first frame) is shown.
799             /// </summary>
800             /// <since_tizen> 7 </since_tizen>
801             MinimumFrame,
802             /// <summary>
803             /// When the animation is stopped, the max frame (last frame) is shown.
804             /// </summary>
805             /// <since_tizen> 7 </since_tizen>
806             MaximumFrame
807         }
808
809         /// <summary>
810         /// Enumeration for what looping mode is in.
811         /// </summary>
812         /// <since_tizen> 7 </since_tizen>
813         public enum LoopingModeType
814         {
815             /// <summary>
816             /// When the animation arrives at the end in looping mode, the animation restarts from the beginning.
817             /// </summary>
818             /// <since_tizen> 7 </since_tizen>
819             Restart,
820             /// <summary>
821             /// When the animation arrives at the end in looping mode, the animation reverses direction and runs backwards again.
822             /// </summary>
823             /// <since_tizen> 7 </since_tizen>
824             AutoReverse
825         }
826
827         /// <summary>
828         /// Vector Property
829         /// </summary>
830         // This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
831         [EditorBrowsable(EditorBrowsableState.Never)]
832         public enum VectorProperty
833         {
834             /// <summary>
835             /// Fill color of the object, Type of <see cref="Vector3"/>
836             /// </summary>
837             [EditorBrowsable(EditorBrowsableState.Never)]
838             FillColor,
839
840             /// <summary>
841             /// Fill opacity of the object, Type of float
842             /// </summary>
843             [EditorBrowsable(EditorBrowsableState.Never)]
844             FillOpacity,
845
846             /// <summary>
847             /// Stroke color of the object, Type of <see cref="Vector3"/>
848             /// </summary>
849             [EditorBrowsable(EditorBrowsableState.Never)]
850             StrokeColor,
851
852             /// <summary>
853             /// Stroke opacity of the object, Type of float
854             /// </summary>
855             [EditorBrowsable(EditorBrowsableState.Never)]
856             StrokeOpacity,
857
858             /// <summary>
859             /// Stroke width of the object, Type of float
860             /// </summary>
861             [EditorBrowsable(EditorBrowsableState.Never)]
862             StrokeWidth,
863
864             /// <summary>
865             /// Transform anchor of the Layer and Group object, Type of <see cref="Vector2"/>
866             /// </summary>
867             [EditorBrowsable(EditorBrowsableState.Never)]
868             TransformAnchor,
869
870             /// <summary>
871             /// Transform position of the Layer and Group object, Type of <see cref="Vector2"/>
872             /// </summary>
873             [EditorBrowsable(EditorBrowsableState.Never)]
874             TransformPosition,
875
876             /// <summary>
877             /// Transform scale of the Layer and Group object, Type of <see cref="Vector2"/>, Value range of [0..100]
878             /// </summary>
879             [EditorBrowsable(EditorBrowsableState.Never)]
880             TransformScale,
881
882             /// <summary>
883             /// Transform rotation of the Layer and Group object, Type of float, Value range of [0..360] in degrees
884             /// </summary>
885             [EditorBrowsable(EditorBrowsableState.Never)]
886             TransformRotation,
887
888             /// <summary>
889             /// Transform opacity of the Layer and Group object, Type of float
890             /// </summary>
891             [EditorBrowsable(EditorBrowsableState.Never)]
892             TransformOpacity
893         };
894
895         // This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
896         [EditorBrowsable(EditorBrowsableState.Never)]
897         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
898         public delegate PropertyValue DynamicPropertyCallbackType(int returnType, uint frameNumber);
899         #endregion Event, Enum, Struct, ETC
900
901
902         #region Internal
903         /// <summary>
904         /// Actions property value to Jump to the specified frame.
905         /// </summary>
906         internal static readonly int ActionJumpTo = Interop.LottieAnimationView.AnimatedVectorImageVisualActionJumpToGet();
907
908         // This is used for internal purpose.
909         internal static readonly int ActionSetDynamicProperty = Interop.LottieAnimationView.AnimatedVectorImageVisualActionSetDynamicProperty();
910
911         internal class VisualEventSignalArgs : EventArgs
912         {
913             public int VisualIndex
914             {
915                 set;
916                 get;
917             }
918             public int SignalId
919             {
920                 set;
921                 get;
922             }
923         }
924
925         internal event EventHandler<VisualEventSignalArgs> VisualEvent
926         {
927             add
928             {
929                 if (visualEventSignalHandler == null)
930                 {
931                     visualEventSignalCallback = onVisualEventSignal;
932                     using VisualEventSignal visualEvent = VisualEventSignal();
933                     visualEvent?.Connect(visualEventSignalCallback);
934                 }
935                 visualEventSignalHandler += value;
936             }
937             remove
938             {
939                 visualEventSignalHandler -= value;
940                 if (visualEventSignalHandler == null && visualEventSignalCallback != null)
941                 {
942                     using VisualEventSignal visualEvent = VisualEventSignal();
943                     visualEvent?.Disconnect(visualEventSignalCallback);
944                     if (visualEvent?.Empty() == true)
945                     {
946                         visualEventSignalCallback = null;
947                     }
948                 }
949             }
950         }
951
952         internal void EmitVisualEventSignal(int visualIndex, int signalId)
953         {
954             using VisualEventSignal visualEvent = VisualEventSignal();
955             visualEvent?.Emit(this, visualIndex, signalId);
956         }
957
958         internal VisualEventSignal VisualEventSignal()
959         {
960             VisualEventSignal ret = new VisualEventSignal(Interop.VisualEventSignal.NewWithView(View.getCPtr(this)), false);
961             if (NDalicPINVOKE.SWIGPendingException.Pending) throw NDalicPINVOKE.SWIGPendingException.Retrieve();
962             return ret;
963         }
964
965         internal Dictionary<int, DynamicPropertyCallbackType> InternalSavedDynamicPropertyCallbacks = new Dictionary<int, DynamicPropertyCallbackType>();
966
967         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
968         internal delegate void RootCallbackType(int id, int returnType, uint frameNumber, ref float val1, ref float val2, ref float val3);
969
970         internal RootCallbackType rootCallback = RootCallback;
971
972         static internal void RootCallback(int id, int returnType, uint frameNumber, ref float val1, ref float val2, ref float val3)
973         {
974             WeakReference<LottieAnimationView> current = null;
975             LottieAnimationView currentView = null;
976             DynamicPropertyCallbackType currentCallback = null;
977             PropertyValue ret = null;
978
979             if (weakReferencesOfLottie.TryGetValue(id, out current))
980             {
981                 if (current.TryGetTarget(out currentView))
982                 {
983                     if (currentView != null && currentView.InternalSavedDynamicPropertyCallbacks != null &&
984                         currentView.InternalSavedDynamicPropertyCallbacks.TryGetValue(id, out currentCallback))
985                     {
986                         ret = currentCallback?.Invoke(returnType, frameNumber);
987                     }
988                     else
989                     {
990                         Tizen.Log.Error("NUI", "can't find the callback in LottieAnimationView, just return here!");
991                         return;
992                     }
993                 }
994                 else
995                 {
996                     Tizen.Log.Error("NUI", "can't find the callback in LottieAnimationView, just return here!");
997                     return;
998                 }
999             }
1000             else
1001             {
1002                 Tizen.Log.Error("NUI", "can't find LottieAnimationView by id, just return here!");
1003                 return;
1004             }
1005
1006             switch (returnType)
1007             {
1008                 case (int)(VectorProperty.FillColor):
1009                 case (int)(VectorProperty.StrokeColor):
1010                     Vector3 tmpVector3 = new Vector3(-1, -1, -1);
1011                     if ((ret != null) && ret.Get(tmpVector3))
1012                     {
1013                         val1 = tmpVector3.X;
1014                         val2 = tmpVector3.Y;
1015                         val3 = tmpVector3.Z;
1016                     }
1017                     tmpVector3.Dispose();
1018                     break;
1019
1020                 case (int)(VectorProperty.TransformAnchor):
1021                 case (int)(VectorProperty.TransformPosition):
1022                 case (int)(VectorProperty.TransformScale):
1023                     Vector2 tmpVector2 = new Vector2(-1, -1);
1024                     if ((ret != null) && ret.Get(tmpVector2))
1025                     {
1026                         val1 = tmpVector2.X;
1027                         val2 = tmpVector2.Y;
1028                     }
1029                     tmpVector2.Dispose();
1030                     break;
1031
1032                 case (int)(VectorProperty.FillOpacity):
1033                 case (int)(VectorProperty.StrokeOpacity):
1034                 case (int)(VectorProperty.StrokeWidth):
1035                 case (int)(VectorProperty.TransformRotation):
1036                 case (int)(VectorProperty.TransformOpacity):
1037                     float tmpFloat = -1;
1038                     if ((ret != null) && ret.Get(out tmpFloat))
1039                     {
1040                         val1 = tmpFloat;
1041                     }
1042                     break;
1043                 default:
1044                     //do nothing
1045                     break;
1046             }
1047             ret?.Dispose();
1048         }
1049         #endregion Internal
1050
1051
1052         #region Private
1053
1054         // Collection of lottie-image-sensitive properties.
1055         private static readonly List<int> cachedLottieAnimationPropertyKeyList = new List<int> {
1056             ImageVisualProperty.LoopCount,
1057             ImageVisualProperty.StopBehavior,
1058             ImageVisualProperty.LoopingMode,
1059             ImageVisualProperty.RedrawInScalingDown,
1060         };
1061
1062         private struct states
1063         {
1064             internal string url;
1065             internal int loopCount;
1066             internal LoopingModeType loopMode;
1067             internal StopBehaviorType stopEndAction;
1068             internal int framePlayRangeMin;
1069             internal int framePlayRangeMax;
1070             internal int totalFrame;
1071             internal float scale;
1072             internal PlayStateType playState;
1073             internal List<Tuple<string, int, int>> contentInfo;
1074             internal string mark1, mark2;
1075             internal bool redrawInScalingDown;
1076             internal bool changed;
1077         };
1078         private states currentStates;
1079
1080         private const string tag = "NUITEST";
1081         private event EventHandler finishedEventHandler;
1082
1083         private void OnFinished()
1084         {
1085             NUILog.Debug($"<[{GetId()}] OnFinished()>");
1086             finishedEventHandler?.Invoke(this, null);
1087         }
1088
1089         private void onVisualEventSignal(IntPtr targetView, int visualIndex, int signalId)
1090         {
1091             OnFinished();
1092
1093             if (targetView != IntPtr.Zero)
1094             {
1095                 View v = Registry.GetManagedBaseHandleFromNativePtr(targetView) as View;
1096                 if (v != null)
1097                 {
1098                     NUILog.Debug($"targetView is not null! name={v.Name}");
1099                 }
1100                 else
1101                 {
1102                     NUILog.Debug($"target is something created from dali");
1103                 }
1104             }
1105             VisualEventSignalArgs e = new VisualEventSignalArgs();
1106             e.VisualIndex = visualIndex;
1107             e.SignalId = signalId;
1108             visualEventSignalHandler?.Invoke(this, e);
1109
1110             NUILog.Debug($"<[{GetId()}] onVisualEventSignal()! visualIndex={visualIndex}, signalId={signalId}>");
1111         }
1112
1113         [UnmanagedFunctionPointer(CallingConvention.StdCall)]
1114         private delegate void VisualEventSignalCallbackType(IntPtr targetView, int visualIndex, int signalId);
1115
1116         private VisualEventSignalCallbackType visualEventSignalCallback;
1117         private EventHandler<VisualEventSignalArgs> visualEventSignalHandler;
1118
1119         static private int dynamicPropertyCallbackId = 0;
1120         //static private Dictionary<int, DynamicPropertyCallbackType> dynamicPropertyCallbacks = new Dictionary<int, DynamicPropertyCallbackType>();
1121         static private Dictionary<int, WeakReference<LottieAnimationView>> weakReferencesOfLottie = new Dictionary<int, WeakReference<LottieAnimationView>>();
1122
1123         private void debugPrint()
1124         {
1125             NUILog.Debug($"===================================");
1126             NUILog.Debug($"<[{GetId()}] get currentStates : url={currentStates.url}, loopCount={currentStates.loopCount}, \nframePlayRangeMin/Max({currentStates.framePlayRangeMin},{currentStates.framePlayRangeMax}) ");
1127             NUILog.Debug($"  get from Property : StopBehavior={StopBehavior}, LoopMode={LoopingMode}, LoopCount={LoopCount}, PlayState={PlayState}");
1128             NUILog.Debug($"  RedrawInScalingDown={RedrawInScalingDown} >");
1129             NUILog.Debug($"===================================");
1130         }
1131
1132         #endregion Private
1133     }
1134
1135     /// <summary>
1136     /// A class containing frame informations for a LottieAnimationView.
1137     /// </summary>
1138     [EditorBrowsable(EditorBrowsableState.Never)]
1139     public class LottieFrameInfo : ICloneable
1140     {
1141         /// <summary>
1142         /// Creates a new instance with a playing range.
1143         /// </summary>
1144         [EditorBrowsable(EditorBrowsableState.Never)]
1145         public LottieFrameInfo(int startFrame, int endFrame)
1146         {
1147             StartFrame = startFrame;
1148             EndFrame = endFrame;
1149         }
1150
1151         /// <summary>
1152         /// Creates a new instance with a still image frame.
1153         /// </summary>
1154         [EditorBrowsable(EditorBrowsableState.Never)]
1155         public LottieFrameInfo(int stillImageFrame) : this(stillImageFrame, stillImageFrame)
1156         {
1157         }
1158
1159         /// <summary>
1160         /// Create a new instance from a pair notation.
1161         /// </summary>
1162         [EditorBrowsable(EditorBrowsableState.Never)]
1163         public static implicit operator LottieFrameInfo((int, int) pair)
1164         {
1165             return new LottieFrameInfo(pair.Item1, pair.Item2);
1166         }
1167
1168         /// <summary>
1169         /// Create a new instance from an int value.
1170         /// </summary>
1171         [EditorBrowsable(EditorBrowsableState.Never)]
1172         public static implicit operator LottieFrameInfo(int stillImageFrame)
1173         {
1174             return new LottieFrameInfo(stillImageFrame);
1175         }
1176
1177         /// <summary>
1178         /// Create a new instance from string.
1179         /// Possible input : "0, 10", "10"
1180         /// </summary>
1181         [EditorBrowsable(EditorBrowsableState.Never)]
1182         public static implicit operator LottieFrameInfo(string pair)
1183         {
1184             if (pair == null)
1185             {
1186                 return null;
1187             }
1188
1189             string[] parts = pair.Split(',');
1190             if (parts.Length == 1)
1191             {
1192                 return new LottieFrameInfo(Int32.Parse(parts[0].Trim(), CultureInfo.InvariantCulture));
1193             }
1194             else if (parts.Length == 2)
1195             {
1196                 return new LottieFrameInfo(Int32.Parse(parts[0].Trim(), CultureInfo.InvariantCulture), Int32.Parse(parts[1].Trim(), CultureInfo.InvariantCulture));
1197             }
1198
1199             Tizen.Log.Error("NUI", $"Can not convert string {pair} to LottieFrameInfo");
1200             return null;
1201         }
1202
1203         /// <summary>
1204         /// The start frame of the lottie animation.
1205         /// </summary>
1206         [EditorBrowsable(EditorBrowsableState.Never)]
1207         public int StartFrame { get; }
1208
1209         /// <summary>
1210         /// The end frame of the lottie animation.
1211         /// </summary>
1212         [EditorBrowsable(EditorBrowsableState.Never)]
1213         public int EndFrame { get; }
1214
1215         /// <summary>
1216         /// Create LottieFrameInfo struct with animation range information
1217         /// </summary>
1218         [EditorBrowsable(EditorBrowsableState.Never)]
1219         public static LottieFrameInfo CreateAnimationRange(int startFrame, int endFrame)
1220         {
1221             return new LottieFrameInfo(startFrame, endFrame);
1222         }
1223
1224         /// <summary>
1225         /// Create LottieFrameInfo struct with still image information
1226         /// </summary>
1227         [EditorBrowsable(EditorBrowsableState.Never)]
1228         public static LottieFrameInfo CreateStillImage(int stillImageFrame)
1229         {
1230             return new LottieFrameInfo(stillImageFrame, stillImageFrame);
1231         }
1232
1233         /// <summary>
1234         /// Inhouse API.
1235         /// Whether this LottieFrameInfo represents one frame or more.
1236         /// </summary>
1237         [EditorBrowsable(EditorBrowsableState.Never)]
1238         public bool IsStillImage()
1239         {
1240             return StartFrame == EndFrame;
1241         }
1242
1243         /// <summary>
1244         /// Inhouse API.
1245         /// Play specified LottieAnimationView with this frame information.
1246         /// </summary>
1247         /// <param name="lottieView">The target LottieAnimationView to play.</param>
1248         /// <param name="noPlay">Whether go direct to the EndFrame. It is false by default.</param>
1249         [EditorBrowsable(EditorBrowsableState.Never)]
1250         [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1062: Validate arguments of public methods", Justification = "The null checking is done by BeReadyToShow()")]
1251         public void Show(LottieAnimationView lottieView, bool noPlay = false)
1252         {
1253             if (!BeReadyToShow(lottieView))
1254             {
1255                 return;
1256             }
1257
1258             lottieView.SetMinMaxFrame(StartFrame, Math.Min(EndFrame, lottieView.TotalFrame - 1));
1259
1260             if (IsStillImage() || noPlay)
1261             {
1262                 lottieView.CurrentFrame = EndFrame;
1263             }
1264             else
1265             {
1266                 lottieView.CurrentFrame = StartFrame;
1267                 lottieView.Play();
1268             }
1269         }
1270
1271         /// <inheritdoc/>
1272         [EditorBrowsable(EditorBrowsableState.Never)]
1273         public object Clone() => new LottieFrameInfo(StartFrame, EndFrame);
1274
1275         private bool BeReadyToShow(LottieAnimationView lottieView)
1276         {
1277             // Validate input lottieView
1278             if (null == lottieView || lottieView.PlayState == LottieAnimationView.PlayStateType.Invalid)
1279             {
1280                 return false;
1281             }
1282
1283             // Stop if it was playing
1284             if (lottieView.PlayState == LottieAnimationView.PlayStateType.Playing)
1285             {
1286                 lottieView.Stop();
1287             }
1288
1289             return true;
1290         }
1291     }
1292
1293     // This will be public opened after ACR done. (Before ACR, need to be hidden as Inhouse API)
1294     [EditorBrowsable(EditorBrowsableState.Never)]
1295     public struct LottieAnimationViewDynamicProperty : IEquatable<LottieAnimationViewDynamicProperty>
1296     {
1297         [EditorBrowsable(EditorBrowsableState.Never)]
1298         public string KeyPath { get; set; }
1299
1300         [EditorBrowsable(EditorBrowsableState.Never)]
1301         public LottieAnimationView.VectorProperty Property { get; set; }
1302
1303         [EditorBrowsable(EditorBrowsableState.Never)]
1304         public LottieAnimationView.DynamicPropertyCallbackType Callback { get; set; }
1305
1306         public override bool Equals(object obj)
1307         {
1308             if (obj is LottieAnimationViewDynamicProperty target)
1309             {
1310                 if (KeyPath == target.KeyPath && Property == target.Property && Callback == target.Callback)
1311                 {
1312                     return true;
1313                 }
1314             }
1315             return false;
1316         }
1317
1318         public override int GetHashCode()
1319         {
1320             return base.GetHashCode();
1321         }
1322
1323         public static bool operator ==(LottieAnimationViewDynamicProperty left, LottieAnimationViewDynamicProperty right)
1324         {
1325             return left.Equals(right);
1326         }
1327
1328         public static bool operator !=(LottieAnimationViewDynamicProperty left, LottieAnimationViewDynamicProperty right)
1329         {
1330             return !(left == right);
1331         }
1332
1333         public bool Equals(LottieAnimationViewDynamicProperty other)
1334         {
1335             if (other != null)
1336             {
1337                 if (KeyPath == other.KeyPath && Property == other.Property && Callback == other.Callback)
1338                 {
1339                     return true;
1340                 }
1341             }
1342             return false;
1343         }
1344     }
1345
1346 }