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 {}