[NUI] Enhance button relayout and change styles
[platform/core/csapi/tizenfx.git] / src / Tizen.NUI.Components / Controls / Button.Internal.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
18 using System;
19 using System.ComponentModel;
20 using System.Diagnostics;
21 using Tizen.NUI.BaseComponents;
22 using Tizen.NUI.Components.Extension;
23 using Tizen.NUI.Accessibility; // To use AccessibilityManager
24
25 namespace Tizen.NUI.Components
26 {
27     public partial class Button
28     {
29         private ImageView overlayImage;
30         private TextLabel buttonText;
31         private ImageView buttonIcon;
32         private Vector2 size;
33
34         private EventHandler<StateChangedEventArgs> stateChangeHandler;
35
36         private bool isPressed = false;
37         private bool styleApplied = false;
38
39         /// <summary>
40         /// Get accessibility name.
41         /// </summary>
42         [EditorBrowsable(EditorBrowsableState.Never)]
43         protected override string AccessibilityGetName()
44         {
45             return Text;
46         }
47
48         /// <summary>
49         /// Prevents from showing child widgets in AT-SPI tree.
50         /// </summary>
51         [EditorBrowsable(EditorBrowsableState.Never)]
52         protected override bool AccessibilityShouldReportZeroChildren()
53         {
54             return true;
55         }
56
57         /// <summary>
58         /// The ButtonExtension instance that is injected by ButtonStyle.
59         /// </summary>
60         [EditorBrowsable(EditorBrowsableState.Never)]
61         protected ButtonExtension Extension { get; set; }
62
63         /// <summary>
64         /// Creates Button's text part.
65         /// </summary>
66         /// <return>The created Button's text part.</return>
67         [EditorBrowsable(EditorBrowsableState.Never)]
68         protected virtual TextLabel CreateText()
69         {
70             return new TextLabel
71             {
72                 HorizontalAlignment = HorizontalAlignment.Center,
73                 VerticalAlignment = VerticalAlignment.Center,
74                 AccessibilityHighlightable = false
75             };
76         }
77
78         /// <summary>
79         /// Creates Button's icon part.
80         /// </summary>
81         /// <return>The created Button's icon part.</return>
82         [EditorBrowsable(EditorBrowsableState.Never)]
83         protected virtual ImageView CreateIcon()
84         {
85             return new ImageView
86             {
87                 AccessibilityHighlightable = false
88             };
89         }
90
91         /// <summary>
92         /// Creates Button's overlay image part.
93         /// </summary>
94         /// <return>The created Button's overlay image part.</return>
95         [EditorBrowsable(EditorBrowsableState.Never)]
96         protected virtual ImageView CreateOverlayImage()
97         {
98             return new ImageView
99             {
100                 PositionUsesPivotPoint = true,
101                 ParentOrigin = NUI.ParentOrigin.Center,
102                 PivotPoint = NUI.PivotPoint.Center,
103                 WidthResizePolicy = ResizePolicyType.FillToParent,
104                 HeightResizePolicy = ResizePolicyType.FillToParent,
105                 AccessibilityHighlightable = false
106             };
107         }
108
109         /// <summary>
110         /// Called when the Button is Clicked by a user
111         /// </summary>
112         /// <param name="eventArgs">The click information.</param>
113         [EditorBrowsable(EditorBrowsableState.Never)]
114         protected virtual void OnClicked(ClickedEventArgs eventArgs)
115         {
116         }
117
118         /// <summary>
119         /// Get Button style.
120         /// </summary>
121         /// <returns>The default button style.</returns>
122         /// <since_tizen> 8 </since_tizen>
123         protected override ViewStyle CreateViewStyle()
124         {
125             return new ButtonStyle();
126         }
127
128         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
129         [EditorBrowsable(EditorBrowsableState.Never)]
130         protected override void OnUpdate()
131         {
132             base.OnUpdate();
133             Extension?.OnRelayout(this);
134         }
135
136         /// <inheritdoc/>
137         [EditorBrowsable(EditorBrowsableState.Never)]
138         protected override bool HandleControlStateOnTouch(Touch touch)
139         {
140             if (!IsEnabled || null == touch)
141             {
142                 return false;
143             }
144
145             PointStateType state = touch.GetState(0);
146
147             switch (state)
148             {
149                 case PointStateType.Down:
150                     isPressed = true;
151                     Extension?.SetTouchInfo(touch);
152                     UpdateState();
153                     return true;
154                 case PointStateType.Interrupted:
155                     isPressed = false;
156                     UpdateState();
157                     return true;
158                 case PointStateType.Up:
159                     {
160                         bool clicked = isPressed && IsEnabled;
161
162                         isPressed = false;
163
164                         if (IsSelectable)
165                         {
166                             Extension?.SetTouchInfo(touch);
167                             IsSelected = !IsSelected;
168                         }
169                         else
170                         {
171                             Extension?.SetTouchInfo(touch);
172                             UpdateState();
173                         }
174
175                         if (clicked)
176                         {
177                             ClickedEventArgs eventArgs = new ClickedEventArgs();
178                             OnClickedInternal(eventArgs);
179                         }
180
181                         return true;
182                     }
183                 default:
184                     break;
185             }
186             return base.HandleControlStateOnTouch(touch);
187         }
188
189         /// <summary>
190         /// Update Button State.
191         /// </summary>
192         /// <since_tizen> 6 </since_tizen>
193         /// This will be public opened in tizen_5.5 after ACR done. Before ACR, need to be hidden as inhouse API.
194         [EditorBrowsable(EditorBrowsableState.Never)]
195         protected void UpdateState()
196         {
197             if (!styleApplied) return;
198
199             ControlState sourceState = ControlState;
200             ControlState targetState;
201
202             // Normal, Disabled
203             targetState = IsEnabled ? ControlState.Normal : ControlState.Disabled;
204
205             // Selected, DisabledSelected
206             if (IsSelected) targetState += ControlState.Selected;
207
208             // Pressed, PressedSelected
209             if (isPressed) targetState += ControlState.Pressed;
210
211             // Focused, FocusedPressed, FocusedPressedSelected, DisabledFocused, DisabledSelectedFocused
212             if (IsFocused) targetState += ControlState.Focused;
213
214             if (sourceState != targetState)
215             {
216                 ControlState = targetState;
217                 OnUpdate();
218
219                 StateChangedEventArgs e = new StateChangedEventArgs
220                 {
221                     PreviousState = ControlStatesExtension.FromControlStateClass(sourceState),
222                     CurrentState = ControlStatesExtension.FromControlStateClass(targetState)
223                 };
224                 stateChangeHandler?.Invoke(this, e);
225
226                 Extension?.OnControlStateChanged(this, new ControlStateChangedEventArgs(sourceState, targetState));
227             }
228         }
229
230         /// <summary>
231         /// Dispose Button and all children on it.
232         /// </summary>
233         /// <param name="type">Dispose type.</param>
234         /// <since_tizen> 6 </since_tizen>
235         protected override void Dispose(DisposeTypes type)
236         {
237             if (disposed)
238             {
239                 return;
240             }
241
242             if (type == DisposeTypes.Explicit)
243             {
244                 Extension?.OnDispose(this);
245
246                 if (buttonIcon != null)
247                 {
248                     Utility.Dispose(buttonIcon);
249                 }
250                 if (buttonText != null)
251                 {
252                     Utility.Dispose(buttonText);
253                 }
254                 if (overlayImage != null)
255                 {
256                     Utility.Dispose(overlayImage);
257                 }
258             }
259
260             base.Dispose(type);
261         }
262
263         /// <summary>
264         /// Initializes AT-SPI object.
265         /// </summary>
266         [EditorBrowsable(EditorBrowsableState.Never)]
267         public override void OnInitialize()
268         {
269             base.OnInitialize();
270             SetAccessibilityConstructor(Role.PushButton);
271
272             AccessibilityHighlightable = true;
273             EnableControlStatePropagation = true;
274
275             AccessibilityManager.Instance.SetAccessibilityAttribute(this, AccessibilityManager.AccessibilityAttribute.Trait, "Button");
276
277             buttonText = CreateText();
278             buttonIcon = CreateIcon();
279             LayoutItems();
280
281 #if PROFILE_MOBILE
282             Feedback = true;
283 #endif
284         }
285
286         /// <inheritdoc/>
287         [EditorBrowsable(EditorBrowsableState.Never)]
288         public override void OnRelayout(Vector2 size, RelayoutContainer container)
289         {
290             Debug.Assert(size != null);
291
292             if (size.Equals(this.size))
293             {
294                 return;
295             }
296
297             this.size = new Vector2(size);
298
299             UpdateSizeAndSpacing();
300         }
301
302         /// <inheritdoc/>
303         [EditorBrowsable(EditorBrowsableState.Never)]
304         protected override void OnControlStateChanged(ControlStateChangedEventArgs controlStateChangedInfo)
305         {
306             base.OnControlStateChanged(controlStateChangedInfo);
307
308             var stateEnabled = !controlStateChangedInfo.CurrentState.Contains(ControlState.Disabled);
309
310             if (IsEnabled != stateEnabled)
311             {
312                 IsEnabled = stateEnabled;
313             }
314
315             var statePressed = controlStateChangedInfo.CurrentState.Contains(ControlState.Pressed);
316
317             if (isPressed != statePressed)
318             {
319                 isPressed = statePressed;
320             }
321         }
322
323         /// <summary>
324         /// Put sub items (e.g. buttonText, buttonIcon) to the right place.
325         /// </summary>
326         [EditorBrowsable(EditorBrowsableState.Never)]
327         protected virtual void LayoutItems()
328         {
329             if (buttonIcon == null || buttonText == null)
330             {
331                 return;
332             }
333
334             buttonIcon.Unparent();
335             buttonText.Unparent();
336             overlayImage?.Unparent();
337
338             if (IconRelativeOrientation == IconOrientation.Left)
339             {
340                 Layout = new LinearLayout()
341                 {
342                     LinearOrientation = LinearLayout.Orientation.Horizontal,
343                     LinearAlignment = itemAlignment,
344                     CellPadding = itemSpacing ?? new Size2D(0, 0)
345                 };
346
347                 Add(buttonIcon);
348                 Add(buttonText);
349             }
350             else if (IconRelativeOrientation == IconOrientation.Right)
351             {
352                 Layout = new LinearLayout()
353                 {
354                     LinearOrientation = LinearLayout.Orientation.Horizontal,
355                     LinearAlignment = itemAlignment,
356                     CellPadding = itemSpacing ?? new Size2D(0, 0)
357                 };
358
359                 Add(buttonText);
360                 Add(buttonIcon);
361             }
362             else if (IconRelativeOrientation == IconOrientation.Top)
363             {
364                 Layout = new LinearLayout()
365                 {
366                     LinearOrientation = LinearLayout.Orientation.Vertical,
367                     LinearAlignment = itemAlignment,
368                     CellPadding = itemSpacing ?? new Size2D(0, 0)
369                 };
370
371                 Add(buttonIcon);
372                 Add(buttonText);
373             }
374             else if (IconRelativeOrientation == IconOrientation.Bottom)
375             {
376                 Layout = new LinearLayout()
377                 {
378                     LinearOrientation = LinearLayout.Orientation.Vertical,
379                     LinearAlignment = itemAlignment,
380                     CellPadding = itemSpacing ?? new Size2D(0, 0)
381                 };
382
383                 Add(buttonText);
384                 Add(buttonIcon);
385             }
386
387             if (overlayImage != null)
388             {
389                 overlayImage.ExcludeLayouting = true;
390                 Add(overlayImage);
391             }
392         }
393
394         private void UpdateSizeAndSpacing()
395         {
396             if (size == null || buttonIcon == null || buttonText == null)
397             {
398                 return;
399             }
400
401             LinearLayout layout = Layout as LinearLayout;
402
403             if (layout == null)
404             {
405                 return;
406             }
407
408             float lengthWithoutText = 0;
409             Size2D cellPadding = null;
410             Extents iconMargin = buttonIcon.Margin ?? new Extents(0);
411             Extents textMargin = buttonText.Margin ?? new Extents(0);
412
413             if (buttonIcon.Size.Width != 0 && buttonIcon.Size.Height != 0)
414             {
415                 lengthWithoutText = buttonIcon.Size.Width;
416
417                 if (!String.IsNullOrEmpty(buttonText.Text))
418                 {
419                     cellPadding = itemSpacing;
420
421                     if (iconRelativeOrientation == IconOrientation.Left || iconRelativeOrientation == IconOrientation.Right)
422                     {
423                         lengthWithoutText += (itemSpacing?.Width ?? 0) + iconMargin.Start + iconMargin.End + textMargin.Start + textMargin.End;
424                     }
425                     else
426                     {
427                         lengthWithoutText += (itemSpacing?.Height ?? 0) + iconMargin.Top + iconMargin.Bottom + textMargin.Top + textMargin.Bottom;
428                     }
429                 }
430             }
431
432             layout.CellPadding = cellPadding ?? new Size2D(0, 0);
433
434             // If the button has fixed width and the text is not empty, the text should not exceed button boundary.
435             if (WidthSpecification != LayoutParamPolicies.WrapContent && !String.IsNullOrEmpty(buttonText.Text))
436             {
437                 buttonText.MaximumSize = new Size2D((int)Math.Max(size.Width - lengthWithoutText, Math.Max(buttonText.MinimumSize.Width, 1)), (int)size.Height);
438             }
439         }
440
441         private void OnClickedInternal(ClickedEventArgs eventArgs)
442         {
443             Command?.Execute(CommandParameter);
444             OnClicked(eventArgs);
445             Extension?.OnClicked(this, eventArgs);
446
447             ClickEventArgs nestedEventArgs = new ClickEventArgs();
448             ClickEvent?.Invoke(this, nestedEventArgs);
449             Clicked?.Invoke(this, eventArgs);
450         }
451
452         internal override bool OnAccessibilityActivated()
453         {
454             using (var key = new Key())
455             {
456                 key.State = Key.StateType.Down;
457                 key.KeyPressedName = "Return";
458
459                 // Touch Down
460                 OnKey(key);
461
462                 // Touch Up
463                 key.State = Key.StateType.Up;
464                 OnKey(key);
465             }
466
467             return true;
468         }
469     }
470 }