• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (c) 2023-2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 *     http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16import { ButtonOptions } from '@ohos.arkui.advanced.Dialog';
17import { BusinessError, Callback } from '@ohos.base';
18import display from '@ohos.display';
19import hilog from '@ohos.hilog';
20import measure from '@ohos.measure';
21import resourceManager from '@ohos.resourceManager';
22import { CustomColors, CustomTheme, Theme } from '@ohos.arkui.theme';
23import { LengthMetrics, LengthUnit } from '@ohos.arkui.node';
24import common from '@ohos.app.ability.common';
25
26class CustomThemeImpl implements CustomTheme {
27  public colors?: CustomColors;
28
29  constructor(colors: CustomColors) {
30    this.colors = colors;
31  }
32}
33
34const TITLE_MAX_LINES: number = 2;
35const HORIZON_BUTTON_MAX_COUNT: number = 2;
36const VERTICAL_BUTTON_MAX_COUNT: number = 4;
37const BUTTON_LAYOUT_WEIGHT: number = 1;
38const CHECKBOX_CONTAINER_HEIGHT: number = 48;
39const CONTENT_MAX_LINES: number = 2;
40const LOADING_PROGRESS_WIDTH: number = 40;
41const LOADING_PROGRESS_HEIGHT: number = 40;
42const LOADING_MAX_LINES: number = 10;
43const LOADING_MAX_LINES_BIG_FONT: number = 4;
44const LOADING_TEXT_LAYOUT_WEIGHT: number = 1;
45const LOADING_TEXT_MARGIN_LEFT: number = 12;
46const LOADING_MIN_HEIGHT: number = 48;
47const LIST_MIN_HEIGHT: number = 48;
48const CHECKBOX_CONTAINER_LENGTH: number = 20;
49const TEXT_MIN_HEIGHT: number = 48;
50const DEFAULT_IMAGE_SIZE: number = 64;
51const MIN_CONTENT_HEIGHT: number = 100;
52const MAX_CONTENT_HEIGHT: number = 30000;
53const KEYCODE_UP: number = 2012;
54const KEYCODE_DOWN: number = 2013;
55const IGNORE_KEY_EVENT_TYPE: number = 1;
56const FIRST_ITEM_INDEX: number = 0;
57const VERSION_TWELVE: number = 50000012;
58const BUTTON_MIN_FONT_SIZE = 9;
59const MAX_FONT_SCALE: number = 2;
60// 'sys.float.alert_container_max_width'
61const MAX_DIALOG_WIDTH: number = getNumberByResourceId(125831042, 400);
62// 'sys.float.alert_right_padding_horizontal'
63const BUTTON_HORIZONTAL_MARGIN: number = getNumberByResourceId(125831054, 16);
64// 'sys.float.padding_level8'
65const BUTTON_HORIZONTAL_PADDING: number = getNumberByResourceId(125830927, 16);
66// 'sys.float.alert_button_horizontal_space'
67const BUTTON_HORIZONTAL_SPACE: number = getNumberByResourceId(125831051, 8);
68// 'sys.float.padding_level4'
69const CHECK_BOX_MARGIN_END: number = getNumberByResourceId(125830923, 8);
70// 'sys.float.Body_L'
71const BODY_L = getNumberByResourceId(125830970, 16);
72// 'sys.float.Body_M'
73const BODY_M = getNumberByResourceId(125830971, 14);
74// 'sys.float.Body_S'
75const BODY_S = getNumberByResourceId(125830972, 12);
76// 'sys.float.Title_S'
77const TITLE_S = getNumberByResourceId(125830966, 20);
78// 'sys.float.Subtitle_S'
79const SUBTITLE_S = getNumberByResourceId(125830969, 14);
80// 'sys.float.padding_level8'
81const PADDING_LEVEL_8 = getNumberByResourceId(125830927, 16);
82// 'sys.float.dialog_divider_show'
83const DIALOG_DIVIDER_SHOW = getNumberByResourceId(125831202, 1, true);
84// 'sys.float.alert_button_style'
85const ALERT_BUTTON_STYLE = getNumberByResourceId(125831085, 2, true);
86// 'sys.float.alert_title_alignment'
87const ALERT_TITLE_ALIGNMENT = getEnumNumberByResourceId(125831126, 1);
88
89@CustomDialog
90export struct TipsDialog {
91  controller: CustomDialogController;
92  imageRes: ResourceStr | PixelMap | null = null;
93  @State imageSize?: SizeOptions = { width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE };
94  title?: ResourceStr | null = null;
95  content?: ResourceStr | null = null;
96  checkAction?: (isChecked: boolean) => void;
97  onCheckedChange?: Callback<boolean>;
98  checkTips?: ResourceStr | null = null;
99  @State isChecked?: boolean = false;
100  primaryButton?: ButtonOptions | null = null;
101  secondaryButton?: ButtonOptions | null = null;
102  buttons?: ButtonOptions[] | undefined = undefined;
103  @State textAlignment: TextAlign = TextAlign.Start;
104  marginOffset: number = 0;
105  // the controller of content area scroll
106  contentScroller: Scroller = new Scroller();
107  @State fontColorWithTheme: ResourceColor = $r('sys.color.font_primary');
108  theme?: Theme | CustomTheme = new CustomThemeImpl({});
109  themeColorMode?: ThemeColorMode = ThemeColorMode.SYSTEM;
110  @State fontSizeScale: number = 1;
111  @State minContentHeight: number = 160;
112  updateTextAlign: (maxWidth: number) => void = (maxWidth: number) => {
113    if (this.content) {
114      this.textAlignment = getTextAlign(maxWidth, this.content, `${BODY_L * this.fontSizeScale}vp`);
115    }
116  }
117  imageIndex: number = 0;
118  textIndex: number = 1;
119  checkBoxIndex: number = 2;
120
121  build() {
122    CustomDialogContentComponent({
123      controller: this.controller,
124      contentBuilder: () => {
125        this.contentBuilder();
126      },
127      buttons: this.buttons,
128      theme: this.theme,
129      themeColorMode: this.themeColorMode,
130      fontSizeScale: this.fontSizeScale,
131      minContentHeight: this.minContentHeight,
132    }).constraintSize({ maxHeight: '100%' });
133  }
134
135  @Builder
136  contentBuilder(): void {
137    TipsDialogContentLayout({
138      title: this.title,
139      content: this.content,
140      checkTips: this.checkTips,
141      minContentHeight: this.minContentHeight,
142      updateTextAlign: this.updateTextAlign
143    }) {
144      ForEach([this.imageIndex, this.textIndex, this.checkBoxIndex], (index: number) => {
145        if (index === this.imageIndex) {
146          this.imagePart();
147        } else if (index === this.textIndex) {
148          Column() {
149            this.textPart();
150          }
151          .padding({ top: $r('sys.float.padding_level8') })
152        } else {
153          WithTheme({ theme: this.theme, colorMode: this.themeColorMode }) {
154            this.checkBoxPart();
155          }
156        }
157      });
158    }
159  }
160
161  @Builder
162  checkBoxPart(): void {
163    Row() {
164      if (this.checkTips !== null) {
165        Checkbox({ name: '', group: 'checkboxGroup' }).select(this.isChecked)
166          .onChange((checked: boolean) => {
167            this.isChecked = checked;
168            if (this.checkAction) {
169              this.checkAction(checked);
170            }
171            if (this.onCheckedChange) {
172              this.onCheckedChange(checked);
173            }
174          })
175          .accessibilityLevel('yes')
176          .margin({ start: LengthMetrics.vp(0), end: LengthMetrics.vp(CHECK_BOX_MARGIN_END) })
177        Text(this.checkTips)
178          .fontSize(`${BODY_L}fp`)
179          .fontWeight(FontWeight.Regular)
180          .fontColor(this.fontColorWithTheme)
181          .maxLines(CONTENT_MAX_LINES)
182          .layoutWeight(1)
183          .focusable(false)
184          .textOverflow({ overflow: TextOverflow.Ellipsis })
185      }
186    }
187    .accessibilityGroup(true)
188    .onClick(() => {
189      this.isChecked = !this.isChecked;
190      if (this.checkAction) {
191        this.checkAction(this.isChecked);
192      }
193    })
194    .padding({ top: 8, bottom: 8 })
195    .constraintSize({ minHeight: CHECKBOX_CONTAINER_HEIGHT })
196    .width('100%')
197  }
198
199  @Builder
200  imagePart(): void {
201    Column() {
202      Image(this.imageRes)
203        .objectFit(ImageFit.Contain)
204        .borderRadius($r('sys.float.corner_radius_level6'))
205        .constraintSize({
206          maxWidth: this.imageSize?.width ?? DEFAULT_IMAGE_SIZE,
207          maxHeight: this.imageSize?.height ?? DEFAULT_IMAGE_SIZE
208        })
209    }
210    .width('100%')
211  }
212
213  @Builder
214  textPart(): void {
215    Scroll(this.contentScroller) {
216      Column() {
217        if (this.title !== null) {
218          Row() {
219            Text(this.title)
220              .fontSize(`${TITLE_S}fp`)
221              .fontWeight(FontWeight.Bold)
222              .fontColor(this.fontColorWithTheme)
223              .textAlign(TextAlign.Center)
224              .maxLines(CONTENT_MAX_LINES)
225              .textOverflow({ overflow: TextOverflow.Ellipsis })
226              .width('100%')
227          }
228          .padding({ bottom: $r('sys.float.padding_level8') })
229        }
230        if (this.content !== null) {
231          Row() {
232            Text(this.content)
233              .focusable(true)
234              .defaultFocus(!(this.primaryButton || this.secondaryButton))
235              .focusBox({
236                strokeWidth: LengthMetrics.px(0)
237              })
238              .fontSize(this.getContentFontSize())
239              .fontWeight(FontWeight.Medium)
240              .fontColor(this.fontColorWithTheme)
241              .textAlign(this.textAlignment)
242              .width('100%')
243              .onKeyEvent((event: KeyEvent) => {
244                if (event) {
245                  resolveKeyEvent(event, this.contentScroller);
246                }
247              })
248          }
249        }
250      }
251      .margin({ end: LengthMetrics.resource($r('sys.float.padding_level8')) })
252    }
253    .nestedScroll({ scrollForward: NestedScrollMode.PARALLEL, scrollBackward: NestedScrollMode.PARALLEL })
254    .margin({ end: LengthMetrics.vp(this.marginOffset) })
255  }
256
257  aboutToAppear() {
258    this.fontColorWithTheme = this.theme?.colors?.fontPrimary ?
259    this.theme.colors.fontPrimary : $r('sys.color.font_primary');
260    this.initButtons();
261    this.initMargin();
262  }
263
264  getContentFontSize(): Length {
265    return BODY_L + 'fp';
266  }
267
268  private initButtons(): void {
269    if (!this.primaryButton && !this.secondaryButton) {
270      return;
271    }
272    this.buttons = [];
273    if (this.primaryButton) {
274      this.buttons.push(this.primaryButton);
275    }
276    if (this.secondaryButton) {
277      this.buttons.push(this.secondaryButton);
278    }
279  }
280
281  private initMargin(): void {
282    this.marginOffset = 0 - PADDING_LEVEL_8;
283  }
284}
285
286@Component
287struct TipsDialogContentLayout {
288  @Builder
289  doNothingBuilder() {
290  };
291
292  title?: ResourceStr | null = null;
293  content?: ResourceStr | null = null;
294  checkTips?: ResourceStr | null = null;
295  updateTextAlign: (maxWidth: number) => void = (maxWidth: number) => {
296  };
297  @Link minContentHeight: number;
298  @BuilderParam dialogBuilder: () => void = this.doNothingBuilder;
299  imageIndex: number = 0;
300  textIndex: number = 1;
301  checkBoxIndex: number = 2;
302  childrenSize: number = 3;
303
304  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>,
305    constraint: ConstraintSizeOptions) {
306    let currentX: number = 0;
307    let currentY: number = 0;
308    for (let index = 0; index < children.length; index++) {
309      let child = children[index];
310      child.layout({ x: currentX, y: currentY });
311      currentY += child.measureResult.height;
312    }
313  }
314
315  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>,
316    constraint: ConstraintSizeOptions): SizeResult {
317    let sizeResult: SizeResult = { width: Number(constraint.maxWidth), height: 0 };
318    if (children.length < this.childrenSize) {
319      return sizeResult;
320    }
321    let height: number = 0;
322    let checkBoxHeight: number = 0;
323    if (this.checkTips !== null) {
324      let checkboxChild: Measurable = children[this.checkBoxIndex];
325      let checkboxConstraint: ConstraintSizeOptions = {
326        maxWidth: constraint.maxWidth,
327        minHeight: CHECKBOX_CONTAINER_HEIGHT,
328        maxHeight: constraint.maxHeight
329      }
330      let checkBoxMeasureResult: MeasureResult = checkboxChild.measure(checkboxConstraint);
331      checkBoxHeight = checkBoxMeasureResult.height;
332      height += checkBoxHeight;
333    }
334
335    let imageChild: Measurable = children[this.imageIndex];
336    let textMinHeight: number = 0;
337    if (this.title !== null || this.content !== null) {
338      textMinHeight = TEXT_MIN_HEIGHT + PADDING_LEVEL_8;
339    }
340    let imageMaxHeight = Number(constraint.maxHeight) - checkBoxHeight - textMinHeight;
341    let imageConstraint: ConstraintSizeOptions = {
342      maxWidth: constraint.maxWidth,
343      maxHeight: imageMaxHeight
344    }
345    let imageMeasureResult: MeasureResult = imageChild.measure(imageConstraint);
346    height += imageMeasureResult.height;
347
348    if (this.title !== null || this.content !== null) {
349      let textChild: Measurable = children[this.textIndex];
350      this.updateTextAlign(sizeResult.width);
351      let contentMaxHeight: number = Number(constraint.maxHeight) - imageMeasureResult.height - checkBoxHeight;
352      let contentConstraint: ConstraintSizeOptions =
353        {
354          maxWidth: constraint.maxWidth,
355          maxHeight: Math.max(contentMaxHeight, TEXT_MIN_HEIGHT)
356        };
357      let contentMeasureResult: MeasureResult = textChild.measure(contentConstraint);
358      height += contentMeasureResult.height;
359    }
360    sizeResult.height = height;
361    this.minContentHeight = Math.max(checkBoxHeight + imageMeasureResult.height + textMinHeight, MIN_CONTENT_HEIGHT);
362    return sizeResult;
363  }
364
365  build() {
366    this.dialogBuilder();
367  }
368}
369
370@CustomDialog
371export struct SelectDialog {
372  controller: CustomDialogController;
373  title: ResourceStr = '';
374  content?: ResourceStr = '';
375  confirm?: ButtonOptions | null = null;
376  radioContent: Array<SheetInfo> = [];
377  buttons?: ButtonOptions[] = [];
378  contentPadding ?: Padding;
379  isFocus: boolean = false;
380  currentFocusIndex?: number = -1;
381  radioHeight: number = 0;
382  itemHeight: number = 0;
383  @State selectedIndex?: number = -1;
384  @BuilderParam contentBuilder: () => void = this.buildContent;
385  @State fontColorWithTheme: ResourceColor = $r('sys.color.font_primary');
386  @State dividerColorWithTheme: ResourceColor = $r('sys.color.comp_divider');
387  theme?: Theme | CustomTheme = new CustomThemeImpl({});
388  themeColorMode?: ThemeColorMode = ThemeColorMode.SYSTEM;
389  // the controller of content list
390  contentScroller: Scroller = new Scroller();
391  @State fontSizeScale: number = 1;
392  @State minContentHeight: number = MIN_CONTENT_HEIGHT;
393
394  @Styles
395  paddingContentStyle() {
396    .padding({
397      left: $r('sys.float.padding_level12'),
398      right: $r('sys.float.padding_level12'),
399      bottom: $r('sys.float.padding_level4')
400    })
401  }
402
403  @Styles
404  paddingStyle() {
405    .padding({
406      left: $r('sys.float.padding_level6'),
407      right: $r('sys.float.padding_level6')
408    })
409  }
410
411  @Builder
412  buildContent(): void {
413    Scroll(this.contentScroller) {
414      Column() {
415        if (this.content) {
416          Row() {
417            Text(this.content)
418              .fontSize(`${BODY_M}fp`)
419              .fontWeight(FontWeight.Regular)
420              .fontColor(this.fontColorWithTheme)
421              .textOverflow({ overflow: TextOverflow.Ellipsis })
422          }.paddingContentStyle().width('100%')
423        }
424        List() {
425          ForEach(this.radioContent, (item: SheetInfo, index: number) => {
426            ListItem() {
427              Column() {
428                Button() {
429                  Row() {
430                    Text(item.title)
431                      .fontSize(`${BODY_L}fp`)
432                      .fontWeight(FontWeight.Medium)
433                      .fontColor(this.fontColorWithTheme)
434                      .layoutWeight(1)
435                    Radio({ value: 'item.title', group: 'radioGroup' })
436                      .size({ width: CHECKBOX_CONTAINER_LENGTH, height: CHECKBOX_CONTAINER_LENGTH })
437                      .checked(this.selectedIndex === index)
438                      .hitTestBehavior(HitTestMode.None)
439                      .id(String(index))
440                      .focusable(false)
441                      .accessibilityLevel('no')
442                      .onFocus(() => {
443                        this.isFocus = true;
444                        this.currentFocusIndex = index;
445                        if (index === FIRST_ITEM_INDEX) {
446                          this.contentScroller.scrollEdge(Edge.Top);
447                        } else if (index === this.radioContent.length - 1) {
448                          this.contentScroller.scrollEdge(Edge.Bottom);
449                        }
450                      })
451                      .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
452                        this.radioHeight = Number(newValue.height)
453                      })
454                  }.constraintSize({ minHeight: LIST_MIN_HEIGHT }).clip(false)
455                  .padding({ top: $r('sys.float.padding_level4'), bottom: $r('sys.float.padding_level4') })
456                }
457                .type(ButtonType.Normal)
458                .borderRadius($r('sys.float.corner_radius_level8'))
459                .buttonStyle(ButtonStyleMode.TEXTUAL)
460                .paddingStyle()
461                .focusBox({
462                  margin: { value: -2, unit: LengthUnit.VP }
463                })
464                .onClick(() => {
465                  this.selectedIndex = index;
466                  item.action && item.action();
467                  this.controller?.close();
468                })
469
470                if (index < this.radioContent.length - 1) {
471                  Divider().color(this.dividerColorWithTheme).paddingStyle();
472                }
473              }.paddingStyle()
474            }
475            .onSizeChange((oldValue: SizeOptions, newValue: SizeOptions) => {
476              this.itemHeight = Number(newValue.height)
477            })
478          })
479        }.width('100%').clip(false)
480        .onFocus(() => {
481          if (!this.contentScroller.isAtEnd()) {
482            this.contentScroller.scrollEdge(Edge.Top);
483            focusControl.requestFocus(String(FIRST_ITEM_INDEX));
484          }
485        })
486        .defaultFocus(this.buttons?.length === 0 ? true : false)
487      }
488    }.scrollBar(BarState.Auto)
489    .nestedScroll({ scrollForward: NestedScrollMode.PARALLEL, scrollBackward: NestedScrollMode.PARALLEL })
490    .onDidScroll((xOffset: number, yOffset: number) => {
491      let scrollHeight: number = (this.itemHeight - this.radioHeight) / 2
492      if (this.isFocus) {
493        if (this.currentFocusIndex === this.radioContent.length - 1) {
494          this.contentScroller.scrollEdge(Edge.Bottom);
495          this.currentFocusIndex = -1;
496        } else if (this.currentFocusIndex === FIRST_ITEM_INDEX) {
497          this.contentScroller.scrollEdge(Edge.Top);
498          this.currentFocusIndex = -1;
499        } else {
500          if (yOffset > 0) {
501            this.contentScroller.scrollBy(0, scrollHeight)
502          } else if (yOffset < 0) {
503            this.contentScroller.scrollBy(0, 0 - scrollHeight)
504          }
505        }
506        this.isFocus = false;
507      }
508    })
509  }
510
511  build() {
512    CustomDialogContentComponent({
513      controller: this.controller,
514      primaryTitle: this.title,
515      contentBuilder: () => {
516        this.contentBuilder();
517      },
518      buttons: this.buttons,
519      contentAreaPadding: this.contentPadding,
520      theme: this.theme,
521      themeColorMode: this.themeColorMode,
522      fontSizeScale: this.fontSizeScale,
523      minContentHeight: this.minContentHeight,
524    }).constraintSize({ maxHeight: '100%' });
525  }
526
527  aboutToAppear(): void {
528    this.fontColorWithTheme = this.theme?.colors?.fontPrimary ?
529    this.theme.colors.fontPrimary : $r('sys.color.font_primary');
530    this.dividerColorWithTheme = this.theme?.colors?.compDivider ?
531    this.theme.colors.compDivider : $r('sys.color.comp_divider');
532    this.initContentPadding();
533    this.initButtons();
534  }
535
536  private initContentPadding(): void {
537    this.contentPadding = {
538      left: $r('sys.float.padding_level0'),
539      right: $r('sys.float.padding_level0')
540    }
541
542    if (!this.title && !this.confirm) {
543      this.contentPadding = {
544        top: $r('sys.float.padding_level12'),
545        bottom: $r('sys.float.padding_level12')
546      }
547      return;
548    }
549
550    if (!this.title) {
551      this.contentPadding = {
552        top: $r('sys.float.padding_level12')
553      }
554    } else if (!this.confirm) {
555      this.contentPadding = {
556        bottom: $r('sys.float.padding_level12')
557      }
558    }
559  }
560
561  private initButtons(): void {
562    this.buttons = [];
563    if (this.confirm) {
564      this.buttons.push(this.confirm);
565    }
566  }
567}
568
569@Component
570struct ConfirmDialogContentLayout {
571  textIndex: number = 0;
572  checkboxIndex: number = 1;
573  @Link minContentHeight: number;
574  updateTextAlign: (maxWidth: number) => void = (maxWidth: number) => {
575  };
576
577  @Builder
578  doNothingBuilder() {
579  };
580
581  @BuilderParam dialogBuilder: () => void = this.doNothingBuilder;
582
583  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>,
584    constraint: ConstraintSizeOptions) {
585    let currentX: number = 0;
586    let currentY: number = 0;
587    for (let index = 0; index < children.length; index++) {
588      let child = children[index];
589      child.layout({ x: currentX, y: currentY });
590      currentY += child.measureResult.height;
591    }
592  }
593
594  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>,
595    constraint: ConstraintSizeOptions): SizeResult {
596    let sizeResult: SizeResult = { width: Number(constraint.maxWidth), height: 0 };
597    let childrenSize: number = 2;
598    if (children.length < childrenSize) {
599      return sizeResult;
600    }
601    this.updateTextAlign(sizeResult.width);
602    let height: number = 0;
603    let checkboxChild: Measurable = children[this.checkboxIndex];
604    let checkboxConstraint: ConstraintSizeOptions = {
605      maxWidth: constraint.maxWidth,
606      minHeight: CHECKBOX_CONTAINER_HEIGHT,
607      maxHeight: constraint.maxHeight
608    }
609    let checkBoxMeasureResult: MeasureResult = checkboxChild.measure(checkboxConstraint);
610    height += checkBoxMeasureResult.height;
611
612    let textChild: Measurable = children[this.textIndex];
613    let textConstraint: ConstraintSizeOptions = {
614      maxWidth: constraint.maxWidth,
615      maxHeight: Number(constraint.maxHeight) - height
616    }
617    let textMeasureResult: MeasureResult = textChild.measure(textConstraint);
618    height += textMeasureResult.height;
619    sizeResult.height = height;
620    this.minContentHeight = Math.max(checkBoxMeasureResult.height + TEXT_MIN_HEIGHT, MIN_CONTENT_HEIGHT);
621    return sizeResult;
622  }
623
624  build() {
625    this.dialogBuilder();
626  }
627}
628
629@CustomDialog
630export struct ConfirmDialog {
631  controller: CustomDialogController
632  title: ResourceStr = ''
633  content?: ResourceStr = ''
634  checkTips?: ResourceStr = ''
635  @State isChecked?: boolean = false
636  primaryButton?: ButtonOptions = { value: "" }
637  secondaryButton?: ButtonOptions = { value: "" }
638  @State fontColorWithTheme: ResourceColor = $r('sys.color.font_primary');
639  theme?: Theme | CustomTheme = new CustomThemeImpl({});
640  themeColorMode?: ThemeColorMode = ThemeColorMode.SYSTEM;
641  onCheckedChange?: Callback<boolean>;
642  contentScroller: Scroller = new Scroller();
643  buttons?: ButtonOptions[] | undefined = undefined;
644  @State textAlign: TextAlign = TextAlign.Start;
645  marginOffset: number = 0;
646  @State fontSizeScale: number = 1;
647  @State minContentHeight: number = MIN_CONTENT_HEIGHT;
648  textIndex: number = 0;
649  checkboxIndex: number = 1;
650  updateTextAlign: (maxWidth: number) => void = (maxWidth: number) => {
651    if (this.content) {
652      this.textAlign = getTextAlign(maxWidth, this.content, `${BODY_L * this.fontSizeScale}vp`);
653    }
654  }
655
656  @Builder
657  textBuilder(): void {
658    Column() {
659      Scroll(this.contentScroller) {
660        Column() {
661          Text(this.content)
662            .focusable(true)
663            .defaultFocus(!(this.primaryButton?.value || this.secondaryButton?.value))
664            .focusBox({
665              strokeWidth: LengthMetrics.px(0)
666            })
667            .fontSize(`${BODY_L}fp`)
668            .fontWeight(FontWeight.Medium)
669            .fontColor(this.fontColorWithTheme)
670            .textAlign(this.textAlign)
671            .onKeyEvent((event: KeyEvent) => {
672              if (event) {
673                resolveKeyEvent(event, this.contentScroller);
674              }
675            })
676            .width('100%')
677        }
678        .margin({ end: LengthMetrics.resource($r('sys.float.padding_level8')) })
679      }
680      .nestedScroll({ scrollForward: NestedScrollMode.PARALLEL, scrollBackward: NestedScrollMode.PARALLEL })
681      .margin({ end: LengthMetrics.vp(this.marginOffset) })
682    }
683  }
684
685  @Builder
686  checkBoxBuilder(): void {
687    Row() {
688      Checkbox({ name: '', group: 'checkboxGroup' }).select(this.isChecked)
689        .onChange((checked: boolean) => {
690          this.isChecked = checked;
691          if (this.onCheckedChange) {
692            this.onCheckedChange(this.isChecked);
693          }
694        })
695        .hitTestBehavior(HitTestMode.Block)
696        .accessibilityLevel('yes')
697        .margin({ start: LengthMetrics.vp(0), end: LengthMetrics.vp(CHECK_BOX_MARGIN_END) })
698
699      Text(this.checkTips)
700        .fontSize(`${BODY_M}fp`)
701        .fontWeight(FontWeight.Medium)
702        .fontColor(this.fontColorWithTheme)
703        .maxLines(CONTENT_MAX_LINES)
704        .focusable(false)
705        .layoutWeight(1)
706        .textOverflow({ overflow: TextOverflow.Ellipsis })
707    }
708    .accessibilityGroup(true)
709    .onClick(() => {
710      this.isChecked = !this.isChecked;
711    })
712    .width('100%')
713    .padding({ top: 8, bottom: 8 })
714  }
715
716  @Builder
717  buildContent(): void {
718    ConfirmDialogContentLayout({ minContentHeight: this.minContentHeight, updateTextAlign: this.updateTextAlign }) {
719      ForEach([this.textIndex, this.checkboxIndex], (index: number) => {
720        if (index === this.textIndex) {
721          this.textBuilder();
722        } else if (index === this.checkboxIndex) {
723          WithTheme({ theme: this.theme, colorMode: this.themeColorMode }) {
724            this.checkBoxBuilder();
725          }
726        }
727      });
728    }
729  }
730
731  build() {
732    CustomDialogContentComponent({
733      primaryTitle: this.title,
734      controller: this.controller,
735      contentBuilder: () => {
736        this.buildContent();
737      },
738      minContentHeight: this.minContentHeight,
739      buttons: this.buttons,
740      theme: this.theme,
741      themeColorMode: this.themeColorMode,
742      fontSizeScale: this.fontSizeScale,
743    }).constraintSize({ maxHeight: '100%' });
744  }
745
746  aboutToAppear(): void {
747    this.fontColorWithTheme = this.theme?.colors?.fontPrimary ?
748    this.theme.colors.fontPrimary : $r('sys.color.font_primary');
749    this.initButtons();
750    this.initMargin();
751  }
752
753  private initMargin(): void {
754    this.marginOffset = 0 - PADDING_LEVEL_8;
755  }
756
757  private initButtons(): void {
758    if (!this.primaryButton && !this.secondaryButton) {
759      return;
760    }
761    this.buttons = [];
762    if (this.primaryButton) {
763      this.buttons.push(this.primaryButton);
764    }
765    if (this.secondaryButton) {
766      this.buttons.push(this.secondaryButton);
767    }
768  }
769}
770
771@CustomDialog
772export struct AlertDialog {
773  controller: CustomDialogController;
774  primaryTitle?: ResourceStr | undefined = undefined;
775  secondaryTitle?: ResourceStr | undefined = undefined;
776  content: ResourceStr = '';
777  primaryButton?: ButtonOptions | null = null;
778  secondaryButton?: ButtonOptions | null = null;
779  buttons?: ButtonOptions[] | undefined = undefined;
780  @State textAlign: TextAlign = TextAlign.Start;
781  // the controller of content area
782  contentScroller: Scroller = new Scroller();
783  @State fontColorWithTheme: ResourceColor = $r('sys.color.font_primary');
784  theme?: Theme | CustomTheme = new CustomThemeImpl({});
785  themeColorMode?: ThemeColorMode = ThemeColorMode.SYSTEM;
786  @State fontSizeScale: number = 1;
787  @State minContentHeight: number = MIN_CONTENT_HEIGHT;
788
789  build() {
790    CustomDialogContentComponent({
791      primaryTitle: this.primaryTitle,
792      secondaryTitle: this.secondaryTitle,
793      controller: this.controller,
794      contentBuilder: () => {
795        this.AlertDialogContentBuilder();
796      },
797      buttons: this.buttons,
798      theme: this.theme,
799      themeColorMode: this.themeColorMode,
800      fontSizeScale: this.fontSizeScale,
801      minContentHeight: this.minContentHeight,
802    }).constraintSize({ maxHeight: '100%' });
803  }
804
805  @Builder
806  AlertDialogContentBuilder(): void {
807    Column() {
808      Scroll(this.contentScroller) {
809        Text(this.content)
810          .focusable(true)
811          .defaultFocus(!(this.primaryButton || this.secondaryButton))
812          .focusBox({
813            strokeWidth: LengthMetrics.px(0)
814          })
815          .fontSize(`${BODY_L}fp`)
816          .fontWeight(this.getFontWeight())
817          .fontColor(this.fontColorWithTheme)
818          .margin({ end: LengthMetrics.resource($r('sys.float.padding_level8')) })
819          .width(`calc(100% - ${PADDING_LEVEL_8}vp)`)
820          .textAlign(this.textAlign)
821          .onAreaChange((oldValue: Area, newValue: Area) => {
822            this.updateTextAlign(Number(newValue.width));
823          })
824          .onKeyEvent((event: KeyEvent) => {
825            if (event) {
826              resolveKeyEvent(event, this.contentScroller);
827            }
828          })
829      }
830      .nestedScroll({ scrollForward: NestedScrollMode.PARALLEL, scrollBackward: NestedScrollMode.PARALLEL })
831      .width('100%')
832    }
833    .margin({ end: LengthMetrics.vp(this.getMargin()) })
834  }
835
836  aboutToAppear(): void {
837    this.fontColorWithTheme = this.theme?.colors?.fontPrimary ?
838    this.theme.colors.fontPrimary : $r('sys.color.font_primary');
839    this.initButtons();
840  }
841
842  private updateTextAlign(maxWidth: number): void {
843    this.textAlign = getTextAlign(maxWidth, this.content, `${BODY_L * this.fontSizeScale}vp`);
844  }
845
846  private initButtons(): void {
847    if (!this.primaryButton && !this.secondaryButton) {
848      return;
849    }
850    this.buttons = [];
851    if (this.primaryButton) {
852      this.buttons.push(this.primaryButton);
853    }
854    if (this.secondaryButton) {
855      this.buttons.push(this.secondaryButton);
856    }
857  }
858
859  private getMargin(): number {
860    return 0 - PADDING_LEVEL_8;
861  }
862
863  private getFontWeight(): number {
864    if (this.primaryTitle || this.secondaryTitle) {
865      return FontWeight.Regular;
866    }
867    return FontWeight.Medium;
868  }
869}
870
871@CustomDialog
872export struct CustomContentDialog {
873  controller: CustomDialogController;
874  primaryTitle?: ResourceStr;
875  secondaryTitle?: ResourceStr;
876  @BuilderParam contentBuilder: () => void;
877  contentAreaPadding?: Padding;
878  localizedContentAreaPadding?: LocalizedPadding;
879  buttons?: ButtonOptions[];
880  theme?: Theme | CustomTheme = new CustomThemeImpl({});
881  themeColorMode?: ThemeColorMode = ThemeColorMode.SYSTEM;
882  @State fontSizeScale: number = 1;
883  @State minContentHeight: number = MIN_CONTENT_HEIGHT;
884
885  build() {
886    CustomDialogContentComponent({
887      controller: this.controller,
888      primaryTitle: this.primaryTitle,
889      secondaryTitle: this.secondaryTitle,
890      contentBuilder: () => {
891        this.contentBuilder();
892      },
893      contentAreaPadding: this.contentAreaPadding,
894      localizedContentAreaPadding: this.localizedContentAreaPadding,
895      buttons: this.buttons,
896      theme: this.theme,
897      themeColorMode: this.themeColorMode,
898      fontSizeScale: this.fontSizeScale,
899      minContentHeight: this.minContentHeight,
900      customStyle: false
901    }).constraintSize({ maxHeight: '100%' });
902  }
903}
904
905class CustomDialogControllerExtend extends CustomDialogController {
906  public arg_: CustomDialogControllerOptions;
907
908  constructor(value: CustomDialogControllerOptions) {
909    super(value);
910    this.arg_ = value;
911  }
912}
913
914@Component
915struct CustomDialogLayout {
916  @Builder
917  doNothingBuilder(): void {
918  };
919
920  @Link titleHeight: number;
921  @Link buttonHeight: number;
922  @Link titleMinHeight: Length;
923  @BuilderParam dialogBuilder: () => void = this.doNothingBuilder;
924  titleIndex: number = 0;
925  contentIndex: number = 1;
926  buttonIndex: number = 2;
927
928  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Array<Layoutable>,
929    constraint: ConstraintSizeOptions) {
930    let currentX: number = 0;
931    let currentY: number = 0;
932    for (let index = 0; index < children.length; index++) {
933      let child = children[index];
934      child.layout({ x: currentX, y: currentY });
935      currentY += child.measureResult.height;
936    }
937  }
938
939  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>,
940    constraint: ConstraintSizeOptions): SizeResult {
941    let sizeResult: SizeResult = { width: Number(constraint.maxWidth), height: 0 };
942    let childrenSize: number = 3;
943    if (children.length < childrenSize) {
944      return sizeResult;
945    }
946    let height: number = 0;
947    let titleChild: Measurable = children[this.titleIndex];
948    let titleConstraint: ConstraintSizeOptions = {
949      maxWidth: constraint.maxWidth,
950      minHeight: this.titleMinHeight,
951      maxHeight: constraint.maxHeight
952    };
953    let titleMeasureResult: MeasureResult = titleChild.measure(titleConstraint);
954    this.titleHeight = titleMeasureResult.height;
955    height += this.titleHeight;
956
957    let buttonChild: Measurable = children[this.buttonIndex];
958    let buttonMeasureResult: MeasureResult = buttonChild.measure(constraint);
959    this.buttonHeight = buttonMeasureResult.height;
960    height += this.buttonHeight;
961
962    let contentChild: Measurable = children[this.contentIndex];
963    let contentConstraint: ConstraintSizeOptions = {
964      maxWidth: constraint.maxWidth,
965      maxHeight: Number(constraint.maxHeight) - height
966    };
967
968    let contentMeasureResult: MeasureResult = contentChild.measure(contentConstraint);
969    height += contentMeasureResult.height;
970    sizeResult.height = height;
971    return sizeResult;
972  }
973
974  build() {
975    this.dialogBuilder();
976  }
977}
978
979
980@Component
981struct CustomDialogContentComponent {
982  controller?: CustomDialogController;
983  primaryTitle?: ResourceStr;
984  secondaryTitle?: ResourceStr;
985  localizedContentAreaPadding?: LocalizedPadding;
986  @BuilderParam contentBuilder: () => void = this.defaultContentBuilder;
987  buttons?: ButtonOptions[];
988  contentAreaPadding?: Padding;
989  keyIndex: number = 0;
990  theme?: Theme | CustomTheme = new CustomThemeImpl({});
991  themeColorMode?: ThemeColorMode = ThemeColorMode.SYSTEM;
992  @Link minContentHeight: number;
993
994  @Builder
995  defaultContentBuilder(): void {
996  }
997
998  @State titleHeight: number = 0;
999  @State buttonHeight: number = 0;
1000  @State contentMaxHeight: Length = '100%';
1001  @Link fontSizeScale: number;
1002  @State customStyle: boolean | undefined = undefined;
1003  @State buttonMaxFontSize: Length = `${BODY_L}fp`;
1004  @State buttonMinFontSize: Length = 9;
1005  @State primaryTitleMaxFontSize: Length = `${TITLE_S}fp`;
1006  @State primaryTitleMinFontSize: Length = `${BODY_L}fp`;
1007  @State secondaryTitleMaxFontSize: Length = `${SUBTITLE_S}fp`;
1008  @State secondaryTitleMinFontSize: Length = `${BODY_S}fp`;
1009  @State primaryTitleFontColorWithTheme: ResourceColor = $r('sys.color.font_primary');
1010  @State secondaryTitleFontColorWithTheme: ResourceColor = $r('sys.color.font_secondary');
1011  @State titleTextAlign: TextAlign = TextAlign.Center;
1012  @State isButtonVertical: boolean = false;
1013  @State titleMinHeight: Length = 0;
1014  isFollowingSystemFontScale: boolean = false;
1015  appMaxFontScale: number = 3.2;
1016  titleIndex: number = 0;
1017  contentIndex: number = 1;
1018  buttonIndex: number = 2;
1019
1020  build() {
1021    WithTheme({ theme: this.theme, colorMode: this.themeColorMode }) {
1022      Scroll() {
1023        Column() {
1024          CustomDialogLayout({
1025            buttonHeight: this.buttonHeight,
1026            titleHeight: this.titleHeight,
1027            titleMinHeight: this.titleMinHeight
1028          }) {
1029            ForEach([this.titleIndex, this.contentIndex, this.buttonIndex], (index: number) => {
1030              if (index === this.titleIndex) {
1031                WithTheme({ theme: this.theme, colorMode: this.themeColorMode }) {
1032                  this.titleBuilder();
1033                }
1034              } else if (index === this.contentIndex) {
1035                Column() {
1036                  WithTheme({ theme: this.theme, colorMode: this.themeColorMode }) {
1037                    this.contentBuilder();
1038                  }
1039                }.padding(this.getContentPadding())
1040              } else {
1041                WithTheme({ theme: this.theme, colorMode: this.themeColorMode }) {
1042                  this.ButtonBuilder();
1043                }
1044              }
1045            });
1046          }
1047        }
1048        .constraintSize({ maxHeight: this.contentMaxHeight })
1049        .backgroundBlurStyle(this.customStyle ? BlurStyle.Thick : BlurStyle.NONE)
1050        .borderRadius(this.customStyle ? $r('sys.float.ohos_id_corner_radius_dialog') : 0)
1051        .margin(this.customStyle ? {
1052          start: LengthMetrics.resource($r('sys.float.ohos_id_dialog_margin_start')),
1053          end: LengthMetrics.resource($r('sys.float.ohos_id_dialog_margin_end')),
1054          bottom: LengthMetrics.resource($r('sys.float.ohos_id_dialog_margin_bottom')),
1055        } : { left: 0, right: 0, bottom: 0 })
1056        .backgroundColor(this.customStyle ? $r('sys.color.ohos_id_color_dialog_bg') : Color.Transparent)
1057      }
1058      .backgroundColor(this.themeColorMode === ThemeColorMode.SYSTEM || undefined ?
1059      Color.Transparent : $r('sys.color.comp_background_primary'))
1060    }
1061  }
1062
1063  onMeasureSize(selfLayoutInfo: GeometryInfo, children: Array<Measurable>,
1064    constraint: ConstraintSizeOptions): SizeResult {
1065    let sizeResult: SizeResult = { width: selfLayoutInfo.width, height: selfLayoutInfo.height };
1066    let maxWidth: number = Number(constraint.maxWidth);
1067    let maxHeight: number = Number(constraint.maxHeight);
1068    this.fontSizeScale = this.updateFontScale();
1069    this.updateFontSize();
1070    this.isButtonVertical = this.isVerticalAlignButton(maxWidth - BUTTON_HORIZONTAL_MARGIN * 2);
1071    this.titleMinHeight = this.getTitleAreaMinHeight();
1072    let height: number = 0;
1073    children.forEach((child) => {
1074      this.contentMaxHeight = '100%';
1075      let measureResult: MeasureResult = child.measure(constraint);
1076      if (maxHeight - this.buttonHeight - this.titleHeight < this.minContentHeight) {
1077        this.contentMaxHeight = MAX_CONTENT_HEIGHT;
1078        measureResult = child.measure(constraint);
1079      }
1080      height += measureResult.height;
1081    });
1082    sizeResult.height = height;
1083    sizeResult.width = maxWidth;
1084    return sizeResult;
1085  }
1086
1087  aboutToAppear(): void {
1088    let uiContext: UIContext = this.getUIContext();
1089    this.isFollowingSystemFontScale = uiContext.isFollowingSystemFontScale();
1090    this.appMaxFontScale = uiContext.getMaxFontScale();
1091    this.fontSizeScale = this.updateFontScale();
1092    if (this.controller && this.customStyle === undefined) {
1093      let customController: CustomDialogControllerExtend = this.controller as CustomDialogControllerExtend;
1094      if (customController.arg_ && customController.arg_.customStyle && customController.arg_.customStyle === true) {
1095        this.customStyle = true;
1096      }
1097    }
1098    if (this.customStyle === undefined) {
1099      this.customStyle = false;
1100    }
1101    this.primaryTitleFontColorWithTheme = this.theme?.colors?.fontPrimary ?
1102    this.theme.colors.fontPrimary : $r('sys.color.font_primary');
1103    this.secondaryTitleFontColorWithTheme = this.theme?.colors?.fontSecondary ?
1104    this.theme.colors.fontSecondary : $r('sys.color.font_secondary');
1105    this.initTitleTextAlign();
1106  }
1107
1108  private updateFontSize(): void {
1109    if (this.fontSizeScale > MAX_FONT_SCALE) {
1110      this.buttonMaxFontSize = BODY_L * MAX_FONT_SCALE + 'vp';
1111      this.buttonMinFontSize = BUTTON_MIN_FONT_SIZE * MAX_FONT_SCALE + 'vp';
1112    } else {
1113      this.buttonMaxFontSize = BODY_L + 'fp';
1114      this.buttonMinFontSize = BUTTON_MIN_FONT_SIZE + 'fp';
1115    }
1116  }
1117
1118  updateFontScale(): number {
1119    try {
1120      let uiContext: UIContext = this.getUIContext();
1121      let systemFontScale = (uiContext.getHostContext() as common.UIAbilityContext)?.config.fontSizeScale ?? 1;
1122      if (!this.isFollowingSystemFontScale) {
1123        return 1;
1124      }
1125      return Math.min(systemFontScale, this.appMaxFontScale);
1126    } catch (exception) {
1127      let code: number = (exception as BusinessError).code;
1128      let message: string = (exception as BusinessError).message;
1129      hilog.error(0x3900, 'Ace', `Faild to init fontsizescale info,cause, code: ${code}, message: ${message}`);
1130      return 1;
1131    }
1132  }
1133
1134  /**
1135   * get dialog content padding
1136   *
1137   * @returns content padding
1138   */
1139  private getContentPadding(): Padding | LocalizedPadding {
1140    if (this.localizedContentAreaPadding) {
1141      return this.localizedContentAreaPadding;
1142    }
1143    if (this.contentAreaPadding) {
1144      return this.contentAreaPadding;
1145    }
1146
1147    if ((this.primaryTitle || this.secondaryTitle) && this.buttons && this.buttons.length > 0) {
1148      return {
1149        top: 0,
1150        right: $r('sys.float.alert_content_default_padding'),
1151        bottom: 0,
1152        left: $r('sys.float.alert_content_default_padding'),
1153      };
1154    } else if (this.primaryTitle || this.secondaryTitle) {
1155      return {
1156        top: 0,
1157        right: $r('sys.float.alert_content_default_padding'),
1158        bottom: $r('sys.float.alert_content_default_padding'),
1159        left: $r('sys.float.alert_content_default_padding'),
1160      };
1161    } else if (this.buttons && this.buttons.length > 0) {
1162      return {
1163        top: $r('sys.float.alert_content_default_padding'),
1164        right: $r('sys.float.alert_content_default_padding'),
1165        bottom: 0,
1166        left: $r('sys.float.alert_content_default_padding'),
1167      };
1168    } else {
1169      return {
1170        top: $r('sys.float.alert_content_default_padding'),
1171        right: $r('sys.float.alert_content_default_padding'),
1172        bottom: $r('sys.float.alert_content_default_padding'),
1173        left: $r('sys.float.alert_content_default_padding'),
1174      };
1175    }
1176  }
1177
1178  @Builder
1179  titleBuilder() {
1180    Column() {
1181      Row() {
1182        Text(this.primaryTitle)
1183          .fontWeight(FontWeight.Bold)
1184          .fontColor(this.primaryTitleFontColorWithTheme)
1185          .textAlign(this.titleTextAlign)
1186          .maxFontSize(this.primaryTitleMaxFontSize)
1187          .minFontSize(this.primaryTitleMinFontSize)
1188          .maxFontScale(Math.min(this.appMaxFontScale, MAX_FONT_SCALE))
1189          .maxLines(TITLE_MAX_LINES)
1190          .heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
1191          .textOverflow({ overflow: TextOverflow.Ellipsis })
1192          .width('100%')
1193      }
1194      .width('100%')
1195
1196      if (this.primaryTitle && this.secondaryTitle) {
1197        Row() {
1198        }.height($r('sys.float.padding_level1'))
1199      }
1200
1201      Row() {
1202        Text(this.secondaryTitle)
1203          .fontWeight(FontWeight.Regular)
1204          .fontColor(this.secondaryTitleFontColorWithTheme)
1205          .textAlign(this.titleTextAlign)
1206          .maxFontSize(this.secondaryTitleMaxFontSize)
1207          .minFontSize(this.secondaryTitleMinFontSize)
1208          .maxFontScale(Math.min(this.appMaxFontScale, MAX_FONT_SCALE))
1209          .maxLines(TITLE_MAX_LINES)
1210          .heightAdaptivePolicy(TextHeightAdaptivePolicy.MIN_FONT_SIZE_FIRST)
1211          .textOverflow({ overflow: TextOverflow.Ellipsis })
1212          .width('100%')
1213      }
1214      .width('100%')
1215    }
1216    .justifyContent(FlexAlign.Center)
1217    .width('100%')
1218    .padding(this.getTitleAreaPadding())
1219  }
1220
1221  /**
1222   * get title area padding
1223   *
1224   * @returns padding
1225   */
1226  private getTitleAreaPadding(): Padding {
1227    if (this.primaryTitle || this.secondaryTitle) {
1228      return {
1229        top: $r('sys.float.alert_title_padding_top'),
1230        right: $r('sys.float.alert_title_padding_right'),
1231        left: $r('sys.float.alert_title_padding_left'),
1232        bottom: $r('sys.float.alert_title_padding_bottom'),
1233      };
1234    }
1235
1236    return {
1237      top: 0,
1238      right: $r('sys.float.alert_title_padding_right'),
1239      left: $r('sys.float.alert_title_padding_left'),
1240      bottom: 0,
1241    };
1242  }
1243
1244  /**
1245   * get tile TextAlign
1246   * @returns TextAlign
1247   */
1248  private initTitleTextAlign(): void {
1249    let textAlign: number = ALERT_TITLE_ALIGNMENT;
1250    if (textAlign === TextAlign.Start) {
1251      this.titleTextAlign = TextAlign.Start;
1252    } else if (textAlign === TextAlign.Center) {
1253      this.titleTextAlign = TextAlign.Center;
1254    } else if (textAlign === TextAlign.End) {
1255      this.titleTextAlign = TextAlign.End;
1256    } else if (textAlign === TextAlign.JUSTIFY) {
1257      this.titleTextAlign = TextAlign.JUSTIFY;
1258    } else {
1259      this.titleTextAlign = TextAlign.Center;
1260    }
1261  }
1262
1263  /**
1264   * get title area min height
1265   *
1266   * @returns min height
1267   */
1268  private getTitleAreaMinHeight(): ResourceStr | number {
1269    if (this.secondaryTitle) {
1270      return $r('sys.float.alert_title_secondary_height');
1271    } else if (this.primaryTitle) {
1272      return $r('sys.float.alert_title_primary_height');
1273    } else {
1274      return 0;
1275    }
1276  }
1277
1278  @Builder
1279  ButtonBuilder(): void {
1280    Column() {
1281      if (this.buttons && this.buttons.length > 0) {
1282        if (this.isButtonVertical) {
1283          this.buildVerticalAlignButtons();
1284        } else {
1285          this.buildHorizontalAlignButtons();
1286        }
1287      }
1288    }
1289    .width('100%')
1290    .padding(this.getOperationAreaPadding());
1291  }
1292
1293  /**
1294   * get operation area padding
1295   *
1296   * @returns padding
1297   */
1298  private getOperationAreaPadding(): Padding {
1299    if (this.isButtonVertical) {
1300      return {
1301        top: $r('sys.float.alert_button_top_padding'),
1302        right: $r('sys.float.alert_right_padding_vertical'),
1303        left: $r('sys.float.alert_left_padding_vertical'),
1304        bottom: $r('sys.float.alert_button_bottom_padding_vertical'),
1305      };
1306    }
1307
1308    return {
1309      top: $r('sys.float.alert_button_top_padding'),
1310      right: $r('sys.float.alert_right_padding_horizontal'),
1311      left: $r('sys.float.alert_left_padding_horizontal'),
1312      bottom: $r('sys.float.alert_button_bottom_padding_horizontal'),
1313    };
1314  }
1315
1316  @Builder
1317  buildSingleButton(buttonOptions: ButtonOptions): void {
1318    if (this.isNewPropertiesHighPriority(buttonOptions)) {
1319      Button(buttonOptions.value)
1320        .setButtonProperties(buttonOptions, this.buttons, this.controller)
1321        .role(buttonOptions.role ?? ButtonRole.NORMAL)
1322        .key(`advanced_dialog_button_${this.keyIndex++}`)
1323        .labelStyle({ maxLines: 1, maxFontSize: this.buttonMaxFontSize, minFontSize: this.buttonMinFontSize })
1324    } else if (buttonOptions.background !== undefined && buttonOptions.fontColor !== undefined) {
1325      Button(buttonOptions.value)
1326        .setButtonProperties(buttonOptions, this.buttons, this.controller)
1327        .backgroundColor(buttonOptions.background)
1328        .fontColor(buttonOptions.fontColor)
1329        .key(`advanced_dialog_button_${this.keyIndex++}`)
1330        .labelStyle({ maxLines: 1, maxFontSize: this.buttonMaxFontSize, minFontSize: this.buttonMinFontSize })
1331    } else if (buttonOptions.background !== undefined) {
1332      Button(buttonOptions.value)
1333        .setButtonProperties(buttonOptions, this.buttons, this.controller)
1334        .backgroundColor(buttonOptions.background)
1335        .key(`advanced_dialog_button_${this.keyIndex++}`)
1336        .labelStyle({ maxLines: 1, maxFontSize: this.buttonMaxFontSize, minFontSize: this.buttonMinFontSize })
1337    } else {
1338      Button(buttonOptions.value)
1339        .setButtonProperties(buttonOptions, this.buttons, this.controller)
1340        .fontColor(buttonOptions.fontColor)
1341        .key(`advanced_dialog_button_${this.keyIndex++}`)
1342        .labelStyle({ maxLines: 1, maxFontSize: this.buttonMaxFontSize, minFontSize: this.buttonMinFontSize })
1343    }
1344  }
1345
1346  @Builder
1347  buildHorizontalAlignButtons(): void {
1348    if (this.buttons && this.buttons.length > 0) {
1349      Row() {
1350        this.buildSingleButton(this.buttons[0]);
1351        if (this.buttons.length === HORIZON_BUTTON_MAX_COUNT) {
1352          Divider()
1353            .width($r('sys.float.alert_divider_width'))
1354            .height($r('sys.float.alert_divider_height'))
1355            .color(this.getDividerColor())
1356            .vertical(true)
1357            .margin({
1358              left: $r('sys.float.alert_button_horizontal_space'),
1359              right: $r('sys.float.alert_button_horizontal_space'),
1360            });
1361          this.buildSingleButton(this.buttons[HORIZON_BUTTON_MAX_COUNT - 1]);
1362        }
1363      }
1364    }
1365  }
1366
1367  @Builder
1368  buildVerticalAlignButtons(): void {
1369    if (this.buttons) {
1370      Column() {
1371        ForEach(this.buttons.slice(0, VERTICAL_BUTTON_MAX_COUNT), (item: ButtonOptions, index: number) => {
1372          this.buildButtonWithDivider(this.buttons?.length === HORIZON_BUTTON_MAX_COUNT ?
1373            HORIZON_BUTTON_MAX_COUNT - index - 1 : index);
1374        }, (item: ButtonOptions) => item.value.toString());
1375      }
1376    }
1377  }
1378
1379  /**
1380   * get divider color
1381   *
1382   * @returns divider color
1383   */
1384  private getDividerColor(): ResourceColor {
1385    if (!this.buttons || this.buttons.length === 0 || !DIALOG_DIVIDER_SHOW) {
1386      return Color.Transparent;
1387    }
1388
1389    if (this.buttons[0].buttonStyle === ButtonStyleMode.TEXTUAL || this.buttons[0].buttonStyle === undefined) {
1390      if (this.buttons[HORIZON_BUTTON_MAX_COUNT - 1].buttonStyle === ButtonStyleMode.TEXTUAL ||
1391        this.buttons[HORIZON_BUTTON_MAX_COUNT - 1].buttonStyle === undefined) {
1392        return $r('sys.color.alert_divider_color');
1393      }
1394    }
1395    return Color.Transparent;
1396  }
1397
1398  /**
1399   * is button buttonStyle and role properties high priority
1400   *
1401   * @param buttonOptions button properties
1402   * @returns check result
1403   */
1404  private isNewPropertiesHighPriority(buttonOptions: ButtonOptions): boolean {
1405    if (buttonOptions.role === ButtonRole.ERROR) {
1406      return true;
1407    }
1408    if (buttonOptions.buttonStyle !== undefined &&
1409      buttonOptions.buttonStyle !== ALERT_BUTTON_STYLE) {
1410      return true;
1411    }
1412    if (buttonOptions.background === undefined && buttonOptions.fontColor === undefined) {
1413      return true;
1414    }
1415    return false;
1416  }
1417
1418  @Builder
1419  buildButtonWithDivider(index: number): void {
1420    if (this.buttons && this.buttons[index]) {
1421      Row() {
1422        this.buildSingleButton(this.buttons[index]);
1423      }
1424
1425      if ((this.buttons.length === HORIZON_BUTTON_MAX_COUNT ? HORIZON_BUTTON_MAX_COUNT - index - 1 : index) <
1426        Math.min(this.buttons.length, VERTICAL_BUTTON_MAX_COUNT) - 1) {
1427        Row() {
1428        }
1429        .height($r('sys.float.alert_button_vertical_space'))
1430      }
1431    }
1432  }
1433
1434  private isVerticalAlignButton(width: number): boolean {
1435    if (this.buttons) {
1436      if (this.buttons.length === 1) {
1437        return false;
1438      }
1439      if (this.buttons.length !== HORIZON_BUTTON_MAX_COUNT) {
1440        return true;
1441      }
1442      let isVertical: boolean = false;
1443      let maxButtonTextSize = vp2px(width / HORIZON_BUTTON_MAX_COUNT - BUTTON_HORIZONTAL_MARGIN -
1444        BUTTON_HORIZONTAL_SPACE - 2 * BUTTON_HORIZONTAL_PADDING);
1445      this.buttons.forEach((button) => {
1446        let contentSize: SizeOptions = measure.measureTextSize({
1447          textContent: button.value,
1448          fontSize: this.buttonMaxFontSize
1449        });
1450        if (Number(contentSize.width) > maxButtonTextSize) {
1451          isVertical = true;
1452        }
1453      });
1454      return isVertical;
1455    }
1456    return false;
1457  }
1458}
1459
1460@Extend(Button)
1461function setButtonProperties(buttonOptions: ButtonOptions, buttonList?: ButtonOptions[],
1462  controller?: CustomDialogController) {
1463  .onClick(() => {
1464    if (buttonOptions.action) {
1465      buttonOptions.action();
1466    }
1467    controller?.close();
1468  })
1469  .defaultFocus(buttonOptions.defaultFocus ? true : isHasDefaultFocus(buttonList) ? false : true)
1470  .buttonStyle(buttonOptions.buttonStyle ?? ALERT_BUTTON_STYLE)
1471  .layoutWeight(BUTTON_LAYOUT_WEIGHT)
1472  .type(ButtonType.Normal)
1473  .borderRadius($r('sys.float.corner_radius_level10'))
1474}
1475
1476/**
1477 * is button list has default focus
1478 *
1479 * @param buttonList button list
1480 * @returns boolean
1481 */
1482function isHasDefaultFocus(buttonList?: ButtonOptions[]): boolean {
1483  try {
1484    let isHasDefaultFocus: boolean = false;
1485    buttonList?.forEach((button) => {
1486      if (button.defaultFocus) {
1487        isHasDefaultFocus = true;
1488      }
1489    })
1490    return isHasDefaultFocus;
1491  } catch (error) {
1492    let code: number = (error as BusinessError).code;
1493    let message: string = (error as BusinessError).message;
1494    hilog.error(0x3900, 'Ace', `get defaultFocus exist error, code: ${code}, message: ${message}`);
1495    return false;
1496  }
1497}
1498
1499/**
1500 * get resource size
1501 *
1502 * @param resourceId resource id
1503 * @param defaultValue default value
1504 * @returns resource size
1505 */
1506function getNumberByResourceId(resourceId: number, defaultValue: number, allowZero?: boolean): number {
1507  try {
1508    let sourceValue: number = resourceManager.getSystemResourceManager().getNumber(resourceId);
1509    if (sourceValue > 0 || allowZero) {
1510      return sourceValue;
1511    } else {
1512      return defaultValue;
1513    }
1514  } catch (error) {
1515    let code: number = (error as BusinessError).code;
1516    let message: string = (error as BusinessError).message;
1517    hilog.error(0x3900, 'Ace', `CustomContentDialog getNumberByResourceId error, code: ${code}, message: ${message}`);
1518    return defaultValue;
1519  }
1520}
1521
1522/**
1523 * get enum number
1524 *
1525 * @param resourceId resource id
1526 * @param defaultValue default value
1527 * @returns number
1528 */
1529function getEnumNumberByResourceId(resourceId: number, defaultValue: number): number {
1530  try {
1531    let sourceValue: number = getContext().resourceManager.getNumber(resourceId);
1532    if (sourceValue > 0) {
1533      return sourceValue;
1534    } else {
1535      return defaultValue;
1536    }
1537  } catch (error) {
1538    let code: number = (error as BusinessError).code;
1539    let message: string = (error as BusinessError).message;
1540    hilog.error(0x3900, 'Ace', `getEnumNumberByResourceId error, code: ${code}, message: ${message}`);
1541    return defaultValue;
1542  }
1543}
1544
1545/**
1546 * get Text Align
1547 *
1548 * @param maxWidth maxWidth
1549 * @param content textContent
1550 * @param fontSize fontSize
1551 * @returns textAlign
1552 */
1553function getTextAlign(maxWidth: number, content: ResourceStr, fontSize: number | string | Resource): TextAlign {
1554  let contentSize: SizeOptions = measure.measureTextSize({
1555    textContent: content,
1556    fontSize: fontSize,
1557    constraintWidth: maxWidth,
1558  });
1559  let oneLineSize: SizeOptions = measure.measureTextSize({
1560    textContent: content,
1561    fontSize: fontSize,
1562  });
1563  if (getTextHeight(contentSize) <= getTextHeight(oneLineSize)) {
1564    return TextAlign.Center;
1565  }
1566  return TextAlign.Start;
1567}
1568
1569/**
1570 * get text height
1571 *
1572 * @param textSize textSize
1573 * @returns text height
1574 */
1575function getTextHeight(textSize: SizeOptions): number {
1576  if (textSize && textSize.height !== null && textSize.height !== undefined) {
1577    return Number(textSize.height);
1578  }
1579  return 0;
1580}
1581
1582/**
1583 * resolve content area keyEvent
1584 *
1585 * @param event keyEvent
1586 * @param controller the controller of content area
1587 * @returns undefined
1588 */
1589function resolveKeyEvent(event: KeyEvent, controller: Scroller) {
1590  if (event.type === IGNORE_KEY_EVENT_TYPE) {
1591    return;
1592  }
1593
1594  if (event.keyCode === KEYCODE_UP) {
1595    controller.scrollPage({ next: false });
1596    event.stopPropagation();
1597  } else if (event.keyCode === KEYCODE_DOWN) {
1598    if (controller.isAtEnd()) {
1599      return;
1600    } else {
1601      controller.scrollPage({ next: true });
1602      event.stopPropagation();
1603    }
1604  }
1605}
1606
1607@CustomDialog
1608export struct LoadingDialog {
1609  controller: CustomDialogController;
1610  content?: ResourceStr = '';
1611  @State fontColorWithTheme: ResourceColor = $r('sys.color.font_primary');
1612  @State loadingProgressIconColorWithTheme: ResourceColor = $r('sys.color.icon_secondary');
1613  theme?: Theme | CustomTheme = new CustomThemeImpl({});
1614  themeColorMode?: ThemeColorMode = ThemeColorMode.SYSTEM;
1615  @State fontSizeScale: number = 1;
1616  @State minContentHeight: number = MIN_CONTENT_HEIGHT;
1617
1618  build() {
1619    Column() {
1620      CustomDialogContentComponent({
1621        controller: this.controller,
1622        contentBuilder: () => {
1623          this.contentBuilder();
1624        },
1625        theme: this.theme,
1626        themeColorMode: this.themeColorMode,
1627        fontSizeScale: this.fontSizeScale,
1628        minContentHeight: this.minContentHeight,
1629      }).constraintSize({ maxHeight: '100%' });
1630    }
1631  }
1632
1633  @Builder
1634  contentBuilder(): void {
1635    Column() {
1636      Row() {
1637        Text(this.content)
1638          .fontSize(`${BODY_L}fp`)
1639          .fontWeight(FontWeight.Regular)
1640          .fontColor(this.fontColorWithTheme)
1641          .layoutWeight(LOADING_TEXT_LAYOUT_WEIGHT)
1642          .maxLines(this.fontSizeScale > MAX_FONT_SCALE ? LOADING_MAX_LINES_BIG_FONT : LOADING_MAX_LINES)
1643          .focusable(true)
1644          .defaultFocus(true)
1645          .focusBox({
1646            strokeWidth: LengthMetrics.px(0)
1647          })
1648          .textOverflow({ overflow: TextOverflow.Ellipsis })
1649        LoadingProgress()
1650          .color(this.loadingProgressIconColorWithTheme)
1651          .width(LOADING_PROGRESS_WIDTH)
1652          .height(LOADING_PROGRESS_HEIGHT)
1653          .margin({ start: LengthMetrics.vp(LOADING_TEXT_MARGIN_LEFT) })
1654      }
1655      .constraintSize({ minHeight: LOADING_MIN_HEIGHT })
1656    }
1657  }
1658
1659  aboutToAppear(): void {
1660    this.fontColorWithTheme = this.theme?.colors?.fontPrimary ?
1661    this.theme.colors.fontPrimary : $r('sys.color.font_primary');
1662    this.loadingProgressIconColorWithTheme = this.theme?.colors?.iconSecondary ?
1663    this.theme.colors.iconSecondary : $r('sys.color.icon_secondary');
1664  }
1665}
1666
1667@Component
1668export struct PopoverDialog {
1669  @Link visible: boolean;
1670  @Prop popover: PopoverDialogOptions;
1671  @BuilderParam targetBuilder: Callback<void>;
1672  @State dialogWidth: Dimension | undefined = this.popover.width;
1673
1674  @Builder
1675  emptyBuilder() {
1676  }
1677
1678  aboutToAppear(): void {
1679    if (this.targetBuilder === undefined || this.targetBuilder === null) {
1680      this.targetBuilder = this.emptyBuilder;
1681    }
1682  }
1683
1684  build() {
1685    Column() {
1686      this.targetBuilder();
1687    }
1688    .onClick(() => {
1689      let screenSize: display.Display = display.getDefaultDisplaySync();
1690      let screenWidth: number = px2vp(screenSize.width);
1691      if (screenWidth - BUTTON_HORIZONTAL_MARGIN - BUTTON_HORIZONTAL_MARGIN > MAX_DIALOG_WIDTH) {
1692        this.popover.width = this.popover?.width ?? MAX_DIALOG_WIDTH;
1693      } else {
1694        this.popover.width = this.dialogWidth;
1695      }
1696      this.visible = !this.visible;
1697    })
1698    .bindPopup(this.visible, {
1699      builder: this.popover?.builder,
1700      placement: this.popover?.placement ?? Placement.Bottom,
1701      popupColor: this.popover?.popupColor,
1702      enableArrow: this.popover?.enableArrow ?? true,
1703      autoCancel: this.popover?.autoCancel,
1704      onStateChange: this.popover?.onStateChange ?? ((e) => {
1705        if (!e.isVisible) {
1706          this.visible = false
1707        }
1708      }),
1709      arrowOffset: this.popover?.arrowOffset,
1710      showInSubWindow: this.popover?.showInSubWindow,
1711      mask: this.popover?.mask,
1712      targetSpace: this.popover?.targetSpace,
1713      offset: this.popover?.offset,
1714      width: this.popover?.width,
1715      arrowPointPosition: this.popover?.arrowPointPosition,
1716      arrowWidth: this.popover?.arrowWidth,
1717      arrowHeight: this.popover?.arrowHeight,
1718      radius: this.popover?.radius ?? $r('sys.float.corner_radius_level16'),
1719      shadow: this.popover?.shadow ?? ShadowStyle.OUTER_DEFAULT_MD,
1720      backgroundBlurStyle: this.popover?.backgroundBlurStyle ?? BlurStyle.COMPONENT_ULTRA_THICK,
1721      focusable: this.popover?.focusable,
1722      transition: this.popover?.transition,
1723      onWillDismiss: this.popover?.onWillDismiss
1724    })
1725  }
1726}
1727
1728export declare interface PopoverDialogOptions extends CustomPopupOptions {}