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