1/* 2 * Copyright (c) 2023-2023 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 pasteboard from '@ohos.pasteboard' 17import { BusinessError } from '@ohos.base'; 18import hilog from '@ohos.hilog'; 19import common from '@ohos.app.ability.common'; 20import { SymbolGlyphModifier } from '@kit.ArkUI'; 21 22const WITHOUT_BUILDER = -2 23const MAX_FONT_STANDARD = 1.0 24const MAX_FONT_SCALE = 1.75 25const SYMBOL_SIZE: number = 24; 26 27export interface EditorMenuOptions { 28 icon: ResourceStr 29 symbolStyle?: SymbolGlyphModifier 30 action?: () => void 31 builder?: () => void 32} 33 34export interface ExpandedMenuOptions extends MenuItemOptions { 35 action?: () => void; 36} 37 38export interface EditorEventInfo { 39 content?: RichEditorSelection; 40} 41 42export interface SelectionMenuOptions { 43 editorMenuOptions?: Array<EditorMenuOptions> 44 expandedMenuOptions?: Array<ExpandedMenuOptions> 45 controller?: RichEditorController 46 onPaste?: (event?: EditorEventInfo) => void 47 onCopy?: (event?: EditorEventInfo) => void 48 onCut?: (event?: EditorEventInfo) => void; 49 onSelectAll?: (event?: EditorEventInfo) => void; 50} 51 52interface SelectionMenuSymbolTheme { 53 fontSize: string; 54 fontColor: Array<ResourceColor>; 55 symbolCutIcon: SymbolGlyphModifier; 56 symbolCopyIcon: SymbolGlyphModifier; 57 symbolPasteIcon: SymbolGlyphModifier; 58 symbolSelectAllIcon: SymbolGlyphModifier; 59 symbolShareIcon: SymbolGlyphModifier; 60 symbolTranslateIcon: SymbolGlyphModifier; 61 symbolSearchIcon: SymbolGlyphModifier; 62 symbolArrowDownIcon: SymbolGlyphModifier; 63} 64 65interface SelectionMenuTheme { 66 imageSize: number; 67 buttonSize: number; 68 menuSpacing: number; 69 expandedOptionPadding: number; 70 defaultMenuWidth: number; 71 menuItemPadding: Resource; 72 imageFillColor: Resource; 73 backGroundColor: Resource; 74 iconBorderRadius: Resource; 75 containerBorderRadius: Resource; 76 borderWidth: Resource; 77 borderColor: Resource; 78 outlineWidth: Resource; 79 outlineColor: Resource; 80 cutIcon: Resource; 81 copyIcon: Resource; 82 pasteIcon: Resource; 83 selectAllIcon: Resource; 84 shareIcon: Resource; 85 translateIcon: Resource; 86 searchIcon: Resource; 87 arrowDownIcon: Resource; 88 iconPanelShadowStyle: ShadowStyle; 89 defaultSymbolTheme: SelectionMenuSymbolTheme; 90} 91 92const defaultTheme: SelectionMenuTheme = { 93 imageSize: 24, 94 buttonSize: 40, 95 menuSpacing: 8, 96 expandedOptionPadding: 4, 97 defaultMenuWidth: 224, 98 menuItemPadding: $r('sys.float.padding_level1'), 99 imageFillColor: $r('sys.color.ohos_id_color_primary'), 100 backGroundColor: $r('sys.color.ohos_id_color_dialog_bg'), 101 iconBorderRadius: $r('sys.float.corner_radius_level2'), 102 containerBorderRadius: $r('sys.float.corner_radius_level4'), 103 borderWidth: $r('sys.float.ohos_id_menu_inner_border_width'), 104 borderColor: $r('sys.color.ohos_id_menu_inner_border_color'), 105 outlineWidth: $r('sys.float.ohos_id_menu_outer_border_width'), 106 outlineColor: $r('sys.color.ohos_id_menu_outer_border_color'), 107 cutIcon: $r('sys.media.ohos_ic_public_cut'), 108 copyIcon: $r('sys.media.ohos_ic_public_copy'), 109 pasteIcon: $r('sys.media.ohos_ic_public_paste'), 110 selectAllIcon: $r('sys.media.ohos_ic_public_select_all'), 111 shareIcon: $r('sys.media.ohos_ic_public_share'), 112 translateIcon: $r('sys.media.ohos_ic_public_translate_c2e'), 113 searchIcon: $r('sys.media.ohos_ic_public_search_filled'), 114 arrowDownIcon: $r('sys.media.ohos_ic_public_arrow_down'), 115 iconPanelShadowStyle: ShadowStyle.OUTER_DEFAULT_SM, 116 defaultSymbolTheme: { 117 fontSize: `${SYMBOL_SIZE}vp`, 118 fontColor: [$r('sys.color.ohos_id_color_primary')], 119 symbolCutIcon: new SymbolGlyphModifier($r('sys.symbol.cut')), 120 symbolCopyIcon: new SymbolGlyphModifier($r('sys.symbol.plus_square_on_square')), 121 symbolPasteIcon: new SymbolGlyphModifier($r('sys.symbol.plus_square_dashed_on_square')), 122 symbolSelectAllIcon: new SymbolGlyphModifier($r('sys.symbol.checkmark_square_on_square')), 123 symbolShareIcon: new SymbolGlyphModifier($r('sys.symbol.share')), 124 symbolTranslateIcon: new SymbolGlyphModifier($r('sys.symbol.translate_c2e')), 125 symbolSearchIcon: new SymbolGlyphModifier($r('sys.symbol.magnifyingglass')), 126 symbolArrowDownIcon: new SymbolGlyphModifier($r('sys.symbol.chevron_down')), 127 }, 128} 129 130@Component 131struct SelectionMenuComponent { 132 editorMenuOptions?: Array<EditorMenuOptions> 133 expandedMenuOptions?: Array<ExpandedMenuOptions> 134 controller?: RichEditorController 135 onPaste?: (event?: EditorEventInfo) => void 136 onCopy?: (event?: EditorEventInfo) => void 137 onCut?: (event?: EditorEventInfo) => void; 138 onSelectAll?: (event?: EditorEventInfo) => void; 139 private theme: SelectionMenuTheme = defaultTheme; 140 141 @Builder 142 CloserFun() { 143 } 144 145 @BuilderParam builder: CustomBuilder = this.CloserFun 146 @State showExpandedMenuOptions: boolean = false 147 @State showCustomerIndex: number = -1 148 @State customerChange: boolean = false 149 @State cutAndCopyEnable: boolean = false 150 @State pasteEnable: boolean = false 151 @State visibilityValue: Visibility = Visibility.Visible 152 @State fontScale: number = 1 153 @State customMenuWidth: number = this.theme.defaultMenuWidth 154 @State horizontalMenuHeight: number = 0 155 @State horizontalMenuWidth: number = this.theme.defaultMenuWidth 156 private fontWeightTable: string[] = 157 ['100', '200', '300', '400', '500', '600', '700', '800', '900', 'bold', 'normal', 'bolder', 'lighter', 'medium', 158 'regular'] 159 private isFollowingSystemFontScale: boolean = false 160 private appMaxFontScale: number = 3.2 161 162 aboutToAppear() { 163 if (this.controller) { 164 let richEditorSelection = this.controller.getSelection() 165 let start = richEditorSelection.selection[0] 166 let end = richEditorSelection.selection[1] 167 if (start !== end) { 168 this.cutAndCopyEnable = true 169 } 170 if (start === 0 && this.controller.getSpans({ start: end + 1, end: end + 1 }).length === 0) { 171 this.visibilityValue = Visibility.None 172 } else { 173 this.visibilityValue = Visibility.Visible 174 } 175 } else if (this.expandedMenuOptions && this.expandedMenuOptions.length > 0) { 176 this.showExpandedMenuOptions = true 177 } 178 let sysBoard = pasteboard.getSystemPasteboard() 179 if (sysBoard && sysBoard.hasDataSync()) { 180 this.pasteEnable = true 181 } 182 let uiContext: UIContext = this.getUIContext() 183 if (uiContext) { 184 this.isFollowingSystemFontScale = uiContext.isFollowingSystemFontScale() 185 this.appMaxFontScale = uiContext.getMaxFontScale() 186 } 187 this.fontScale = this.getFontScale() 188 } 189 190 hasSystemMenu(): boolean { 191 let showMenuOption = this.showCustomerIndex === -1 && 192 (this.controller || (this.expandedMenuOptions && this.expandedMenuOptions.length > 0)) 193 let showBuilder = this.showCustomerIndex > -1 && this.builder 194 return Boolean(showMenuOption || showBuilder) 195 } 196 197 build() { 198 Column() { 199 if (this.editorMenuOptions && this.editorMenuOptions.length > 0) { 200 this.IconPanel() 201 } 202 Scroll() { 203 this.SystemMenu() 204 } 205 .backgroundColor(this.theme.backGroundColor) 206 .shadow(this.theme.iconPanelShadowStyle) 207 .borderRadius(this.theme.containerBorderRadius) 208 .outline(this.hasSystemMenu() ? { width: this.theme.outlineWidth, color: this.theme.outlineColor, 209 radius: this.theme.containerBorderRadius } : undefined) 210 .constraintSize({ 211 maxHeight: `calc(100% - ${this.horizontalMenuHeight > 0 ? this.horizontalMenuHeight + this.theme.menuSpacing : 0}vp)`, 212 minWidth: this.theme.defaultMenuWidth 213 }) 214 } 215 .useShadowBatching(true) 216 .constraintSize({ 217 maxHeight: '100%', 218 minWidth: this.theme.defaultMenuWidth 219 }) 220 } 221 222 pushDataToPasteboard(richEditorSelection: RichEditorSelection) { 223 let sysBoard = pasteboard.getSystemPasteboard() 224 let pasteData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, '') 225 if (richEditorSelection.spans && richEditorSelection.spans.length > 0) { 226 let count = richEditorSelection.spans.length 227 for (let i = count - 1; i >= 0; i--) { 228 let item = richEditorSelection.spans[i] 229 if ((item as RichEditorTextSpanResult)?.textStyle) { 230 let span = item as RichEditorTextSpanResult 231 let style = span.textStyle 232 let data = pasteboard.createRecord(pasteboard.MIMETYPE_TEXT_PLAIN, 233 span.value.substring(span.offsetInSpan[0], span.offsetInSpan[1])) 234 let prop = pasteData.getProperty() 235 let temp: Record<string, Object> = { 236 'color': style.fontColor, 237 'size': style.fontSize, 238 'style': style.fontStyle, 239 'weight': this.fontWeightTable[style.fontWeight], 240 'fontFamily': style.fontFamily, 241 'decorationType': style.decoration.type, 242 'decorationColor': style.decoration.color 243 } 244 prop.additions[i] = temp; 245 pasteData.addRecord(data) 246 pasteData.setProperty(prop) 247 } 248 } 249 } 250 sysBoard.clearData() 251 sysBoard.setData(pasteData).then(() => { 252 hilog.info(0x3900, 'Ace', 'SelectionMenu copy option, Succeeded in setting PasteData.'); 253 }).catch((err: BusinessError) => { 254 hilog.info(0x3900, 'Ace', 'SelectionMenu copy option, Failed to set PasteData. Cause:' + err.message); 255 }) 256 } 257 258 popDataFromPasteboard(richEditorSelection: RichEditorSelection) { 259 let start = richEditorSelection.selection[0] 260 let end = richEditorSelection.selection[1] 261 if (start === end && this.controller) { 262 start = this.controller.getCaretOffset() 263 end = this.controller.getCaretOffset() 264 } 265 let moveOffset = 0 266 let sysBoard = pasteboard.getSystemPasteboard() 267 sysBoard.getData((err, data) => { 268 if (err) { 269 return 270 } 271 let count = data.getRecordCount() 272 for (let i = 0; i < count; i++) { 273 const element = data.getRecord(i); 274 let tex: RichEditorTextStyle = { 275 fontSize: 16, 276 fontColor: Color.Black, 277 fontWeight: FontWeight.Normal, 278 fontFamily: "HarmonyOS Sans", 279 fontStyle: FontStyle.Normal, 280 decoration: { type: TextDecorationType.None, color: '#FF000000' } 281 } 282 if (data.getProperty() && data.getProperty().additions[i]) { 283 const tmp = data.getProperty().additions[i] as Record<string, Object | undefined>; 284 if (tmp.color) { 285 tex.fontColor = tmp.color as ResourceColor; 286 } 287 if (tmp.size) { 288 tex.fontSize = tmp.size as Length | number; 289 } 290 if (tmp.style) { 291 tex.fontStyle = tmp.style as FontStyle; 292 } 293 if (tmp.weight) { 294 tex.fontWeight = tmp.weight as number | FontWeight | string; 295 } 296 if (tmp.fontFamily) { 297 tex.fontFamily = tmp.fontFamily as ResourceStr; 298 } 299 if (tmp.decorationType && tex.decoration) { 300 tex.decoration.type = tmp.decorationType as TextDecorationType; 301 } 302 if (tmp.decorationColor && tex.decoration) { 303 tex.decoration.color = tmp.decorationColor as ResourceColor; 304 } 305 if (tex.decoration) { 306 tex.decoration = { type: tex.decoration.type, color: tex.decoration.color } 307 } 308 } 309 if (element && element.plainText && element.mimeType === pasteboard.MIMETYPE_TEXT_PLAIN && this.controller) { 310 this.controller.addTextSpan(element.plainText, 311 { 312 style: tex, 313 offset: start + moveOffset 314 } 315 ) 316 moveOffset += element.plainText.length 317 } 318 } 319 if (this.controller) { 320 this.controller.setCaretOffset(start + moveOffset) 321 } 322 if (start !== end && this.controller) { 323 this.controller.deleteSpans({ start: start + moveOffset, end: end + moveOffset }) 324 } 325 }) 326 } 327 328 measureButtonWidth(): number { 329 let numOfBtnPerRow = 5 330 let width = this.fontScale > MAX_FONT_SCALE ? this.customMenuWidth : this.theme.defaultMenuWidth; 331 if (this.editorMenuOptions && this.editorMenuOptions.length <= numOfBtnPerRow) { 332 return (width - this.theme.expandedOptionPadding * 2) / this.editorMenuOptions.length 333 } 334 return (width - this.theme.expandedOptionPadding * 2) / numOfBtnPerRow 335 } 336 337 measureFlexPadding(): number { 338 return Math.floor((this.theme.expandedOptionPadding - px2vp(2.0)) * 10) / 10 339 } 340 341 getFontScale(): number { 342 try { 343 let uiContext: UIContext = this.getUIContext(); 344 let systemFontScale = (uiContext.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1; 345 if (!this.isFollowingSystemFontScale) { 346 return 1; 347 } 348 return Math.min(systemFontScale, this.appMaxFontScale); 349 } catch (exception) { 350 let code: number = (exception as BusinessError).code; 351 let message: string = (exception as BusinessError).message; 352 hilog.error(0x3900, 'Ace', `Faild to init fontsizescale info,cause, code: ${code}, message: ${message}`); 353 return 1; 354 } 355 } 356 357 onMeasureSize(selfLayoutInfo: GeometryInfo, children: Measurable[], constraint: ConstraintSizeOptions): SizeResult { 358 this.fontScale = this.getFontScale(); 359 let sizeResult: SizeResult = { height: 0, width: 0 }; 360 children.forEach((child) => { 361 let childMeasureResult: MeasureResult = child.measure(constraint); 362 sizeResult.width = childMeasureResult.width; 363 sizeResult.height = childMeasureResult.height; 364 }); 365 return sizeResult; 366 } 367 368 updateMenuItemVisibility() { 369 if (!this.controller) { 370 return 371 } 372 let richEditorSelection = this.controller.getSelection() 373 let start = richEditorSelection.selection[0] 374 let end = richEditorSelection.selection[1] 375 if (start !== end) { 376 this.cutAndCopyEnable = true 377 } 378 if (start === 0 && this.controller.getSpans({ start: end + 1, end: end + 1 }).length === 0) { 379 this.visibilityValue = Visibility.None 380 } else { 381 this.visibilityValue = Visibility.Visible 382 } 383 } 384 385 @Builder 386 IconPanel() { 387 Flex({ wrap: FlexWrap.Wrap }) { 388 if (this.editorMenuOptions) { 389 ForEach(this.editorMenuOptions, (item: EditorMenuOptions, index: number) => { 390 Button() { 391 if (item.symbolStyle !== undefined) { 392 SymbolGlyph() 393 .fontColor(this.theme.defaultSymbolTheme.fontColor) 394 .attributeModifier(item.symbolStyle) 395 .focusable(true) 396 .draggable(false) 397 .effectStrategy(SymbolEffectStrategy.NONE) 398 .symbolEffect(new SymbolEffect(), false) 399 .fontSize(this.theme.defaultSymbolTheme.fontSize) 400 } else { 401 if (Util.isSymbolResource(item.icon)) { 402 SymbolGlyph(item.icon as Resource) 403 .fontColor(this.theme.defaultSymbolTheme.fontColor) 404 .focusable(true) 405 .draggable(false) 406 .fontSize(this.theme.defaultSymbolTheme.fontSize) 407 } else { 408 Image(item.icon) 409 .width(this.theme.imageSize) 410 .height(this.theme.imageSize) 411 .fillColor(this.theme.imageFillColor) 412 .focusable(true) 413 .draggable(false) 414 } 415 416 } 417 } 418 .enabled(!(!item.action && !item.builder)) 419 .type(ButtonType.Normal) 420 .backgroundColor(this.theme.backGroundColor) 421 .onClick(() => { 422 if (item.builder) { 423 this.builder = item.builder 424 this.showCustomerIndex = index 425 this.showExpandedMenuOptions = false 426 this.customerChange = !this.customerChange 427 } else { 428 this.showCustomerIndex = WITHOUT_BUILDER 429 if (!this.controller) { 430 this.showExpandedMenuOptions = true 431 } 432 } 433 if (item.action) { 434 item.action() 435 } 436 }) 437 .borderRadius(this.theme.iconBorderRadius) 438 .width(this.measureButtonWidth()) 439 .height(this.theme.buttonSize) 440 }) 441 } 442 } 443 .onAreaChange((oldValue: Area, newValue: Area) => { 444 let newValueHeight = newValue.height as number 445 let newValueWidth = newValue.width as number 446 this.horizontalMenuHeight = newValueHeight 447 this.horizontalMenuWidth = newValueWidth 448 }) 449 .clip(true) 450 .width(this.fontScale > MAX_FONT_SCALE ? this.customMenuWidth : this.theme.defaultMenuWidth) 451 .padding({ top: this.measureFlexPadding(), bottom: this.measureFlexPadding(), 452 left: this.measureFlexPadding() - 0.1, right: this.measureFlexPadding() - 0.1 }) 453 .borderRadius(this.theme.containerBorderRadius) 454 .margin({ bottom: this.theme.menuSpacing }) 455 .backgroundColor(this.theme.backGroundColor) 456 .shadow(this.theme.iconPanelShadowStyle) 457 .border({ width: this.theme.borderWidth, color: this.theme.borderColor, 458 radius: this.theme.containerBorderRadius }) 459 .outline({ width: this.theme.outlineWidth, color: this.theme.outlineColor, 460 radius: this.theme.containerBorderRadius }) 461 } 462 463 @Builder 464 SystemMenu() { 465 Column() { 466 if (this.showCustomerIndex === -1 && 467 (this.controller || (this.expandedMenuOptions && this.expandedMenuOptions.length > 0))) { 468 Menu() { 469 if (this.controller) { 470 MenuItemGroup() { 471 MenuItem({ 472 startIcon: this.theme.cutIcon, 473 symbolStartIcon: this.theme.defaultSymbolTheme.symbolCutIcon, 474 content: '剪切', 475 labelInfo: 'Ctrl+X' 476 }) 477 .enabled(this.cutAndCopyEnable) 478 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 479 .borderRadius(this.theme.iconBorderRadius) 480 .onClick(() => { 481 if (!this.controller) { 482 return 483 } 484 let richEditorSelection = this.controller.getSelection() 485 if (this.onCut) { 486 this.onCut({ content: richEditorSelection }) 487 } else { 488 this.pushDataToPasteboard(richEditorSelection); 489 this.controller.deleteSpans({ 490 start: richEditorSelection.selection[0], 491 end: richEditorSelection.selection[1] 492 }) 493 } 494 }) 495 MenuItem({ 496 startIcon: this.theme.copyIcon, 497 symbolStartIcon: this.theme.defaultSymbolTheme.symbolCopyIcon, 498 content: '复制', 499 labelInfo: 'Ctrl+C' 500 }) 501 .enabled(this.cutAndCopyEnable) 502 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 503 .borderRadius(this.theme.iconBorderRadius) 504 .margin({ top: this.theme.menuItemPadding }) 505 .onClick(() => { 506 if (!this.controller) { 507 return 508 } 509 let richEditorSelection = this.controller.getSelection() 510 if (this.onCopy) { 511 this.onCopy({ content: richEditorSelection }) 512 } else { 513 this.pushDataToPasteboard(richEditorSelection); 514 this.controller.closeSelectionMenu() 515 } 516 }) 517 MenuItem({ 518 startIcon: this.theme.pasteIcon, 519 symbolStartIcon: this.theme.defaultSymbolTheme.symbolPasteIcon, 520 content: '粘贴', 521 labelInfo: 'Ctrl+V' 522 }) 523 .enabled(this.pasteEnable) 524 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 525 .borderRadius(this.theme.iconBorderRadius) 526 .margin({ top: this.theme.menuItemPadding }) 527 .onClick(() => { 528 if (!this.controller) { 529 return 530 } 531 let richEditorSelection = this.controller.getSelection() 532 if (this.onPaste) { 533 this.onPaste({ content: richEditorSelection }) 534 } else { 535 this.popDataFromPasteboard(richEditorSelection) 536 this.controller.closeSelectionMenu() 537 } 538 }) 539 MenuItem({ 540 startIcon: this.theme.selectAllIcon, 541 symbolStartIcon: this.theme.defaultSymbolTheme.symbolSelectAllIcon, 542 content: '全选', 543 labelInfo: 'Ctrl+A' 544 }) 545 .visibility(this.visibilityValue) 546 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 547 .borderRadius(this.theme.iconBorderRadius) 548 .margin({ top: this.theme.menuItemPadding }) 549 .onClick(() => { 550 if (!this.controller) { 551 return 552 } 553 if (this.onSelectAll) { 554 let richEditorSelection = this.controller.getSelection() 555 this.onSelectAll({ content: richEditorSelection }) 556 } else { 557 this.controller.setSelection(-1, -1) 558 this.visibilityValue = Visibility.None 559 } 560 this.controller.closeSelectionMenu() 561 }) 562 } 563 } 564 if (this.controller && !this.showExpandedMenuOptions && 565 this.expandedMenuOptions && this.expandedMenuOptions.length > 0) { 566 MenuItem({ 567 content: '更多', 568 endIcon: this.theme.arrowDownIcon, 569 symbolEndIcon: this.theme.defaultSymbolTheme.symbolArrowDownIcon 570 }) 571 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 572 .borderRadius(this.theme.iconBorderRadius) 573 .margin({ top: this.theme.menuItemPadding }) 574 .onClick(() => { 575 this.showExpandedMenuOptions = true 576 }) 577 } else if (this.showExpandedMenuOptions && this.expandedMenuOptions && this.expandedMenuOptions.length > 0) { 578 ForEach(this.expandedMenuOptions, (expandedMenuOptionItem: ExpandedMenuOptions, index) => { 579 MenuItem({ 580 startIcon: expandedMenuOptionItem.startIcon, 581 symbolStartIcon: expandedMenuOptionItem.symbolStartIcon, 582 content: expandedMenuOptionItem.content, 583 endIcon: expandedMenuOptionItem.endIcon, 584 symbolEndIcon: expandedMenuOptionItem.symbolEndIcon, 585 labelInfo: expandedMenuOptionItem.labelInfo, 586 builder: expandedMenuOptionItem.builder 587 }) 588 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 589 .borderRadius(this.theme.iconBorderRadius) 590 .margin({ top: this.theme.menuItemPadding }) 591 .onClick(() => { 592 if (expandedMenuOptionItem.action) { 593 expandedMenuOptionItem.action() 594 } 595 }) 596 }) 597 } 598 } 599 .radius(this.theme.containerBorderRadius) 600 .clip(true) 601 .width(this.fontScale > MAX_FONT_SCALE ? 'auto' : this.theme.defaultMenuWidth) 602 .constraintSize({ 603 minWidth: this.theme.defaultMenuWidth 604 }) 605 .onVisibleAreaChange([0.0, 1.0], () => { 606 this.updateMenuItemVisibility() 607 }) 608 .onAreaChange((oldValue: Area, newValue: Area) => { 609 let newValueWidth = newValue.width as number 610 this.customMenuWidth = 611 this.fontScale > MAX_FONT_SCALE && newValueWidth > this.theme.defaultMenuWidth ? newValueWidth : 612 this.theme.defaultMenuWidth 613 this.updateMenuItemVisibility() 614 }) 615 } else if (this.showCustomerIndex > -1 && this.builder) { 616 Column() { 617 if (this.customerChange) { 618 this.builder() 619 } else { 620 this.builder() 621 } 622 } 623 .width(this.horizontalMenuWidth) 624 } 625 } 626 .width(this.fontScale > MAX_FONT_SCALE ? 'auto' : this.theme.defaultMenuWidth) 627 .shadow(this.theme.iconPanelShadowStyle) 628 .border({ width: this.theme.borderWidth, color: this.theme.borderColor, 629 radius: this.theme.containerBorderRadius }) 630 .constraintSize({ 631 minWidth: this.theme.defaultMenuWidth 632 }) 633 } 634} 635 636@Builder 637export function SelectionMenu(options: SelectionMenuOptions) { 638 SelectionMenuComponent({ 639 editorMenuOptions: options.editorMenuOptions, 640 expandedMenuOptions: options.expandedMenuOptions, 641 controller: options.controller, 642 onPaste: options.onPaste, 643 onCopy: options.onCopy, 644 onCut: options.onCut, 645 onSelectAll: options.onSelectAll 646 }) 647} 648 649class Util { 650 private static RESOURCE_TYPE_SYMBOL = 40000; 651 652 public static isSymbolResource(resourceStr: ResourceStr | undefined): boolean { 653 if (!Util.isResourceType(resourceStr)) { 654 return false; 655 } 656 let resource = resourceStr as Resource; 657 return resource.type === Util.RESOURCE_TYPE_SYMBOL; 658 } 659 660 public static isResourceType(resource: ResourceStr | Resource | undefined): boolean { 661 if (!resource) { 662 return false; 663 } 664 if (typeof resource === 'string' || typeof resource === 'undefined') { 665 return false; 666 } 667 return true; 668 } 669}