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