[NUI] Update theme system
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / Notification.cs
1 /*
2  * Copyright(c) 2021 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 using System;
18 using System.Collections.Generic;
19 using System.ComponentModel;
20 using Tizen.NUI.BaseComponents;
21
22 namespace Tizen.NUI.Components
23 {
24     /// <summary>
25     /// Notification helps to raise a notification window with a content View.
26     /// </summary>
27     /// <since_tizen> 8 </since_tizen>
28     public class Notification : Disposable
29     {
30         /// <summary>
31         /// Toast will appear at the top of the screen.
32         /// </summary>
33         [EditorBrowsable(EditorBrowsableState.Never)]
34         public static readonly Position ToastTop = ParentOrigin.TopCenter;
35
36         /// <summary>
37         /// Toast will appear at the center of the screen.
38         /// </summary>
39         [EditorBrowsable(EditorBrowsableState.Never)]
40         public static readonly Position ToastCenter = ParentOrigin.Center;
41
42         /// <summary>
43         /// Toast will appear at the bottom of the screen.
44         /// </summary>
45         [EditorBrowsable(EditorBrowsableState.Never)]
46         public static readonly Position ToastBottom = ParentOrigin.BottomCenter;
47
48         /// <summary>
49         /// Show the notification for a short period of time.
50         /// </summary>
51         [EditorBrowsable(EditorBrowsableState.Never)]
52         public static readonly uint ToastShort = 2000;
53
54         /// <summary>
55         /// Show the notification for a long period of time.
56         /// </summary>
57         [EditorBrowsable(EditorBrowsableState.Never)]
58         public static readonly uint ToastLong = ToastShort * 2;
59
60         private static HashSet<Notification> instanceSet;
61
62         private Window notificationWindow;
63
64         private Timer timer;
65
66         private NotificationLevel level = NotificationLevel.Base;
67
68         private Rectangle positionSize;
69
70         private bool dismissOnTouch = false;
71
72         private Animation onPostAnimation;
73
74         private Animation onDismissAnimation;
75
76         private NotificationState state = NotificationState.Ready;
77
78         static Notification()
79         {
80             ThemeManager.AddPackageTheme(DefaultThemeCreator.Instance);
81         }
82
83         /// <summary>
84         /// Create a notification with a content View.
85         /// </summary>
86         /// <param name="contentView">The content view instance to display in the notification window.</param>
87         /// <exception cref="NotSupportedException">Thrown when the device does not support a notification feature.</exception>
88         /// <exception cref="ArgumentNullException">Thrown when a given contentView is null.</exception>
89         /// <remark>Since the notification creates a new window, the system should support a multi-window feature. Otherwise it will throw a NotSupportedException.</remark>
90         /// <since_tizen> 8 </since_tizen>
91         public Notification(View contentView) : base()
92         {
93             if (!Window.IsSupportedMultiWindow())
94             {
95                 throw new NotSupportedException("This device does not support multi-window. Notification can not be created. ");
96             }
97             ContentView = contentView ?? throw new ArgumentNullException(nameof(contentView));
98         }
99
100         private enum NotificationState
101         {
102             Ready,
103             Post,
104             Dismiss,
105         }
106
107         /// <summary>
108         /// The content view received in a constructor.
109         /// </summary>
110         /// <since_tizen> 8 </since_tizen>
111         public View ContentView { get; private set; }
112
113         private Window NotificationWindow
114         {
115             get
116             {
117                 if (notificationWindow == null)
118                 {
119                     notificationWindow = new Window(null, true)
120                     {
121                         Type = WindowType.Notification,
122                         BackgroundColor = Color.Transparent,
123                     };
124                     notificationWindow.Show();
125                 }
126
127                 return notificationWindow;
128             }
129             set => notificationWindow = value;
130         }
131
132         private Timer Timer
133         {
134             get
135             {
136                 if (timer == null)
137                 {
138                     timer = new Timer(0);
139                     timer.Tick += OnTimeOut;
140                 }
141
142                 return timer;
143             }
144             set
145             {
146                 if (timer != null)
147                 {
148                     timer.Stop();
149                     timer.Tick -= OnTimeOut;
150
151                     if (value == null)
152                     {
153                         timer.Dispose();
154                     }
155                 }
156
157                 timer = value;
158             }
159         }
160
161         /// <summary>
162         /// Create a simple text notification called toast.
163         /// </summary>
164         /// <param name="text">The string content.</param>
165         /// <param name="gravity">The location at which the toast should appear. It's one of the notification constants: ToastTop, ToastCenter and ToastBottom.</param>
166         /// <returns>The created Notification instance.</returns>
167         /// <exception cref="NotSupportedException">Thrown when the device does not support a notification feature.</exception>
168         /// <exception cref="ArgumentNullException">Thrown when the given text or gravity is null.</exception>
169         /// <remark>Application need to set http://tizen.org/privilege/window.priority.set to post a notification.</remark>
170         /// <remark>Since the notification creates a new window, the system should support a multi-window feature. Otherwise it will throw a NotSupportedException.</remark>
171         /// <example>
172         /// The following example demonstrates how to make a toast at the bottom and show it for a short period time.
173         /// <code>
174         /// Notification.MakeToast("Hello World!", Notification.ToastBottom).Post(Notification.ToastShort);
175         /// </code>
176         /// </example>
177         /// <since_tizen> 9 </since_tizen>
178         public static Notification MakeToast(string text, Position gravity)
179         {
180             if (!Window.IsSupportedMultiWindow())
181             {
182                 throw new NotSupportedException("This device does not support multi-window. Notification can not be created. ");
183             }
184
185             var textLabel = new TextLabel(text ?? throw new ArgumentNullException(nameof(text)))
186             {
187                 Opacity = 0.0f
188             };
189
190             if (gravity == null) throw new ArgumentNullException(nameof(gravity));
191
192             var style = ThemeManager.GetInitialStyleWithoutClone("NotificationToast");
193             if (style != null)
194             {
195                 textLabel.ApplyStyle(style);
196             }
197
198             textLabel.ParentOrigin = gravity;
199             textLabel.PivotPoint = gravity;
200
201             if (gravity == ToastCenter)
202             {
203                 textLabel.PositionY = 0;
204             }
205             else if (gravity == ToastBottom)
206             {
207                 textLabel.PositionY = -textLabel.PositionY;
208             }
209
210             var postAnimation = new Animation(700);
211             postAnimation.AnimateTo(textLabel, "Opacity", 1.0f);
212
213             var dismissAnimation = new Animation(500);
214             dismissAnimation.AnimateTo(textLabel, "Opacity", 0.0f);
215
216             return new Notification(textLabel).SetAnimationOnPost(postAnimation).SetAnimationOnDismiss(dismissAnimation);
217         }
218
219         /// <summary>
220         /// Post a notification window with the content view.
221         /// </summary>
222         /// <param name="duration">Dismiss the notification window after given time in millisecond. The value 0 won't dismiss the notification.</param>
223         /// <returns>The current Notification instance.</returns>
224         /// <privilege>http://tizen.org/privilege/window.priority.set</privilege>
225         /// <exception cref="UnauthorizedAccessException">Thrown when the application does not have proper privilege.</exception>
226         /// <since_tizen> 8 </since_tizen>
227         public void Post(uint duration = 0)
228         {
229             if (state != NotificationState.Ready)
230             {
231                 return;
232             }
233
234             var applyLevelResult = ApplyLevel(level);
235
236             if (applyLevelResult == Window.OperationResult.PermissionDenied)
237             {
238                 throw new UnauthorizedAccessException("Cannot post a Notification: Permission Denied. The privilege http://tizen.org/privilege/window.priority.set is needed.");
239             }
240
241             if (applyLevelResult != Window.OperationResult.Succeed)
242             {
243                 Tizen.Log.Info("NUI", "The notification window may not have proper notification level.");
244             }
245
246             ApplyPositionSize(positionSize);
247
248             ApplyDismissOnTouch(dismissOnTouch);
249
250             NotificationWindow.Add(ContentView);
251
252             if (duration > 0)
253             {
254                 Timer.Interval = duration;
255             }
256
257             state = NotificationState.Post;
258
259             onPostAnimation?.Play();
260
261             RegisterInstance(this);
262         }
263
264         /// <summary>
265         /// Sets a priority level for the specified notification window.
266         /// The default level is NotificationLevel.Base.
267         /// </summary>
268         /// <param name="level">The notification window level.</param>
269         /// <returns>The current Notification instance.</returns>
270         /// <privilege>http://tizen.org/privilege/window.priority.set</privilege>
271         /// <exception cref="UnauthorizedAccessException">Thrown when the application does not have proper privilege.</exception>
272         /// <since_tizen> 8 </since_tizen>
273         public Notification SetLevel(NotificationLevel level)
274         {
275             this.level = level;
276
277             if (state == NotificationState.Post)
278             {
279                 var result = ApplyLevel(level);
280
281                 if (result == Window.OperationResult.PermissionDenied)
282                 {
283                     throw new UnauthorizedAccessException("Cannot set notification level: Permission Denied. The privilege http://tizen.org/privilege/window.priority.set is needed.");
284                 }
285
286                 if (result != Window.OperationResult.Succeed)
287                 {
288                     Tizen.Log.Info("NUI", "Cannot set notification level: Unknown reason. The notification window may not have proper notification level.");
289                 }
290             }
291
292             return this;
293         }
294
295         /// <summary>
296         /// Sets position and size of the notification window.
297         /// </summary>
298         /// <param name="positionSize">The position and size information in rectangle.</param>
299         /// <returns>The current Notification instance.</returns>
300         /// <exception cref="ArgumentException">Thrown when a given positionSize is invalid.</exception>
301         /// <since_tizen> 8 </since_tizen>
302         public Notification SetPositionSize(Rectangle positionSize)
303         {
304             this.positionSize = positionSize ?? throw (new ArgumentException("Input positionSize should not be null."));
305
306             if (state == NotificationState.Post || state == NotificationState.Dismiss)
307             {
308                 ApplyPositionSize(positionSize);
309             }
310
311             return this;
312         }
313
314         /// <summary>
315         /// Sets whether listen to touch event to dismiss notification window.
316         /// </summary>
317         /// <param name="dismissOnTouch">Dismiss notification window on touch if the value is true.</param>
318         /// <returns>The current Notification instance.</returns>
319         [EditorBrowsable(EditorBrowsableState.Never)]
320         public Notification SetDismissOnTouch(bool dismissOnTouch)
321         {
322             if (this.dismissOnTouch == dismissOnTouch)
323             {
324                 return this;
325             }
326
327             this.dismissOnTouch = dismissOnTouch;
328
329             if (state == NotificationState.Post)
330             {
331                 ApplyDismissOnTouch(dismissOnTouch);
332             }
333
334             return this;
335         }
336
337         /// <summary>
338         /// Sets a user-defined animation to play when posting the notification.
339         /// The Notification will play the given animation right after the notification window pops up.
340         /// </summary>
341         /// <param name="animation">The animation to play.</param>
342         /// <since_tizen> 8 </since_tizen>
343         public Notification SetAnimationOnPost(Animation animation)
344         {
345             this.onPostAnimation = animation;
346
347             return this;
348         }
349
350         /// <summary>
351         /// Sets a user-defined animation to play when dismiss the notification.
352         /// On dismiss, the given animation is played, and after the playback is completed the notification window is undisplayed.
353         /// </summary>
354         /// <param name="animation">The animation to play.</param>
355         /// <since_tizen> 8 </since_tizen>
356         public Notification SetAnimationOnDismiss(Animation animation)
357         {
358             this.onDismissAnimation = animation;
359
360             return this;
361         }
362
363         /// <summary>
364         /// Dismiss the notification window.
365         /// </summary>
366         /// <since_tizen> 8 </since_tizen>
367         public void Dismiss()
368         {
369             if (state != NotificationState.Post)
370             {
371                 return;
372             }
373
374             state = NotificationState.Dismiss;
375
376             if (onDismissAnimation != null)
377             {
378                 onDismissAnimation.Finished += OnAnimationEnd;
379
380                 onDismissAnimation.Play();
381
382                 Timer = null;
383
384                 ApplyDismissOnTouch(false);
385
386                 return;
387             }
388
389             ClearAll();
390         }
391
392         /// <summary>
393         /// Dismiss the notification window directly without waiting the onDismissAnimation finished.
394         /// </summary>
395         /// <since_tizen> 8 </since_tizen>
396         public void ForceQuit()
397         {
398             if (state != NotificationState.Post && state != NotificationState.Dismiss)
399             {
400                 return;
401             }
402
403             ClearAll();
404         }
405
406         /// <inheritdoc/>
407         /// <since_tizen> 8 </since_tizen>
408         protected override void Dispose(DisposeTypes type)
409         {
410             if (disposed)
411             {
412                 return;
413             }
414
415             if (type == DisposeTypes.Explicit)
416             {
417                 ClearAll();
418
419                 positionSize?.Dispose();
420                 onPostAnimation?.Dispose();
421                 onDismissAnimation?.Dispose();
422             }
423
424             base.Dispose(type);
425         }
426
427         private static void RegisterInstance(Notification instance)
428         {
429             if (instanceSet == null)
430             {
431                 instanceSet = new HashSet<Notification>();
432             }
433
434             instanceSet.Add(instance);
435         }
436
437         private static void UnregisterInstance(Notification instance)
438         {
439             if (instanceSet == null)
440             {
441                 return;
442             }
443
444             instanceSet.Remove(instance);
445
446             if (instanceSet.Count == 0)
447             {
448                 instanceSet = null;
449             }
450         }
451
452         private void DestroyNotificationWindow()
453         {
454             notificationWindow.Hide();
455
456             notificationWindow.Dispose();
457
458             notificationWindow = null;
459         }
460
461         private Window.OperationResult ApplyLevel(NotificationLevel level)
462         {
463             var ret = (Window.OperationResult)Interop.Window.SetNotificationLevel(NotificationWindow.SwigCPtr, (int)level);
464             if (NDalicPINVOKE.SWIGPendingException.Pending) throw NDalicPINVOKE.SWIGPendingException.Retrieve();
465             return ret;
466         }
467
468         private void ApplyPositionSize(Rectangle positionSize)
469         {
470             if (positionSize != null)
471             {
472                 NotificationWindow.SetPositionSize(positionSize);
473             }
474         }
475
476         private void ApplyDismissOnTouch(bool dismissOnTouch)
477         {
478             if (dismissOnTouch)
479             {
480                 NotificationWindow.TouchEvent += OnWindowTouch;
481             }
482             else
483             {
484                 NotificationWindow.TouchEvent -= OnWindowTouch;
485             }
486         }
487
488         private void ClearAll()
489         {
490             if (onDismissAnimation != null)
491             {
492                 onDismissAnimation.Finished -= OnAnimationEnd;
493
494                 onDismissAnimation.Stop();
495             }
496
497             notificationWindow.Remove(ContentView);
498
499             notificationWindow.TouchEvent -= OnWindowTouch;
500
501             Timer = null;
502
503             DestroyNotificationWindow();
504
505             state = NotificationState.Ready;
506
507             UnregisterInstance(this);
508         }
509
510         private void OnWindowTouch(object target, Window.TouchEventArgs args)
511         {
512             Dismiss();
513         }
514
515         private bool OnTimeOut(object target, Timer.TickEventArgs args)
516         {
517             Dismiss();
518
519             return false;
520         }
521
522         private void OnAnimationEnd(object target, EventArgs args)
523         {
524             ClearAll();
525         }
526     }
527 }