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