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 = 2.0 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 @Builder 369 IconPanel() { 370 Flex({ wrap: FlexWrap.Wrap }) { 371 if (this.editorMenuOptions) { 372 ForEach(this.editorMenuOptions, (item: EditorMenuOptions, index: number) => { 373 Button() { 374 if (item.symbolStyle !== undefined) { 375 SymbolGlyph() 376 .fontColor(this.theme.defaultSymbolTheme.fontColor) 377 .attributeModifier(item.symbolStyle) 378 .focusable(true) 379 .draggable(false) 380 .effectStrategy(SymbolEffectStrategy.NONE) 381 .symbolEffect(new SymbolEffect(), false) 382 .fontSize(this.theme.defaultSymbolTheme.fontSize) 383 } else { 384 if (Util.isSymbolResource(item.icon)) { 385 SymbolGlyph(item.icon as Resource) 386 .fontColor(this.theme.defaultSymbolTheme.fontColor) 387 .focusable(true) 388 .draggable(false) 389 .fontSize(this.theme.defaultSymbolTheme.fontSize) 390 } else { 391 Image(item.icon) 392 .width(this.theme.imageSize) 393 .height(this.theme.imageSize) 394 .fillColor(this.theme.imageFillColor) 395 .focusable(true) 396 .draggable(false) 397 } 398 399 } 400 } 401 .enabled(!(!item.action && !item.builder)) 402 .type(ButtonType.Normal) 403 .backgroundColor(this.theme.backGroundColor) 404 .onClick(() => { 405 if (item.builder) { 406 this.builder = item.builder 407 this.showCustomerIndex = index 408 this.showExpandedMenuOptions = false 409 this.customerChange = !this.customerChange 410 } else { 411 this.showCustomerIndex = WITHOUT_BUILDER 412 if (!this.controller) { 413 this.showExpandedMenuOptions = true 414 } 415 } 416 if (item.action) { 417 item.action() 418 } 419 }) 420 .borderRadius(this.theme.iconBorderRadius) 421 .width(this.measureButtonWidth()) 422 .height(this.theme.buttonSize) 423 }) 424 } 425 } 426 .onAreaChange((oldValue: Area, newValue: Area) => { 427 let newValueHeight = newValue.height as number 428 let newValueWidth = newValue.width as number 429 this.horizontalMenuHeight = newValueHeight 430 this.horizontalMenuWidth = newValueWidth 431 }) 432 .clip(true) 433 .width(this.fontScale > MAX_FONT_SCALE ? this.customMenuWidth : this.theme.defaultMenuWidth) 434 .padding({ top: this.measureFlexPadding(), bottom: this.measureFlexPadding(), 435 left: this.measureFlexPadding() - 0.1, right: this.measureFlexPadding() - 0.1 }) 436 .borderRadius(this.theme.containerBorderRadius) 437 .margin({ bottom: this.theme.menuSpacing }) 438 .backgroundColor(this.theme.backGroundColor) 439 .shadow(this.theme.iconPanelShadowStyle) 440 .border({ width: this.theme.borderWidth, color: this.theme.borderColor, 441 radius: this.theme.containerBorderRadius }) 442 .outline({ width: this.theme.outlineWidth, color: this.theme.outlineColor, 443 radius: this.theme.containerBorderRadius }) 444 } 445 446 @Builder 447 SystemMenu() { 448 Column() { 449 if (this.showCustomerIndex === -1 && 450 (this.controller || (this.expandedMenuOptions && this.expandedMenuOptions.length > 0))) { 451 Menu() { 452 if (this.controller) { 453 MenuItemGroup() { 454 MenuItem({ 455 startIcon: this.theme.cutIcon, 456 symbolStartIcon: this.theme.defaultSymbolTheme.symbolCutIcon, 457 content: '剪切', 458 labelInfo: 'Ctrl+X' 459 }) 460 .enabled(this.cutAndCopyEnable) 461 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 462 .borderRadius(this.theme.iconBorderRadius) 463 .onClick(() => { 464 if (!this.controller) { 465 return 466 } 467 let richEditorSelection = this.controller.getSelection() 468 if (this.onCut) { 469 this.onCut({ content: richEditorSelection }) 470 } else { 471 this.pushDataToPasteboard(richEditorSelection); 472 this.controller.deleteSpans({ 473 start: richEditorSelection.selection[0], 474 end: richEditorSelection.selection[1] 475 }) 476 } 477 }) 478 MenuItem({ 479 startIcon: this.theme.copyIcon, 480 symbolStartIcon: this.theme.defaultSymbolTheme.symbolCopyIcon, 481 content: '复制', 482 labelInfo: 'Ctrl+C' 483 }) 484 .enabled(this.cutAndCopyEnable) 485 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 486 .borderRadius(this.theme.iconBorderRadius) 487 .margin({ top: this.theme.menuItemPadding }) 488 .onClick(() => { 489 if (!this.controller) { 490 return 491 } 492 let richEditorSelection = this.controller.getSelection() 493 if (this.onCopy) { 494 this.onCopy({ content: richEditorSelection }) 495 } else { 496 this.pushDataToPasteboard(richEditorSelection); 497 this.controller.closeSelectionMenu() 498 } 499 }) 500 MenuItem({ 501 startIcon: this.theme.pasteIcon, 502 symbolStartIcon: this.theme.defaultSymbolTheme.symbolPasteIcon, 503 content: '粘贴', 504 labelInfo: 'Ctrl+V' 505 }) 506 .enabled(this.pasteEnable) 507 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 508 .borderRadius(this.theme.iconBorderRadius) 509 .margin({ top: this.theme.menuItemPadding }) 510 .onClick(() => { 511 if (!this.controller) { 512 return 513 } 514 let richEditorSelection = this.controller.getSelection() 515 if (this.onPaste) { 516 this.onPaste({ content: richEditorSelection }) 517 } else { 518 this.popDataFromPasteboard(richEditorSelection) 519 this.controller.closeSelectionMenu() 520 } 521 }) 522 MenuItem({ 523 startIcon: this.theme.selectAllIcon, 524 symbolStartIcon: this.theme.defaultSymbolTheme.symbolSelectAllIcon, 525 content: '全选', 526 labelInfo: 'Ctrl+A' 527 }) 528 .visibility(this.visibilityValue) 529 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 530 .borderRadius(this.theme.iconBorderRadius) 531 .margin({ top: this.theme.menuItemPadding }) 532 .onClick(() => { 533 if (!this.controller) { 534 return 535 } 536 if (this.onSelectAll) { 537 let richEditorSelection = this.controller.getSelection() 538 this.onSelectAll({ content: richEditorSelection }) 539 } else { 540 this.controller.setSelection(-1, -1) 541 this.visibilityValue = Visibility.None 542 } 543 this.controller.closeSelectionMenu() 544 }) 545 } 546 } 547 if (this.controller && !this.showExpandedMenuOptions && 548 this.expandedMenuOptions && this.expandedMenuOptions.length > 0) { 549 MenuItem({ 550 content: '更多', 551 endIcon: this.theme.arrowDownIcon, 552 symbolEndIcon: this.theme.defaultSymbolTheme.symbolArrowDownIcon 553 }) 554 .height(this.fontScale > MAX_FONT_STANDARD ? 'auto' : this.theme.buttonSize) 555 .borderRadius(this.theme.iconBorderRadius) 556 .margin({ top: this.theme.menuItemPadding }) 557 .onClick(() => { 558 this.showExpandedMenuOptions = true 559 }) 560 } else if (this.showExpandedMenuOptions && this.expandedMenuOptions && this.expandedMenuOptions.length > 0) { 561 ForEach(this.expandedMenuOptions, (expandedMenuOptionItem: ExpandedMenuOptions, index) => { 562 MenuItem({ 563 startIcon: expandedMenuOptionItem.startIcon, 564 symbolStartIcon: expandedMenuOptionItem.symbolStartIcon, 565 content: expandedMenuOptionItem.content, 566 endIcon: expandedMenuOptionItem.endIcon, 567 symbolEndIcon: expandedMenuOptionItem.symbolEndIcon, 568 labelInfo: expandedMenuOptionItem.labelInfo, 569 builder: expandedMenuOptionItem.builder 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 if (expandedMenuOptionItem.action) { 576 expandedMenuOptionItem.action() 577 } 578 }) 579 }) 580 } 581 } 582 .radius(this.theme.containerBorderRadius) 583 .clip(true) 584 .width(this.fontScale > MAX_FONT_SCALE ? 'auto' : this.theme.defaultMenuWidth) 585 .constraintSize({ 586 minWidth: this.theme.defaultMenuWidth 587 }) 588 .onAreaChange((oldValue: Area, newValue: Area) => { 589 let newValueWidth = newValue.width as number 590 this.customMenuWidth = 591 this.fontScale > MAX_FONT_SCALE && newValueWidth > this.theme.defaultMenuWidth ? newValueWidth : 592 this.theme.defaultMenuWidth 593 if (!this.controller) { 594 return 595 } 596 let richEditorSelection = this.controller.getSelection() 597 let start = richEditorSelection.selection[0] 598 let end = richEditorSelection.selection[1] 599 if (start !== end) { 600 this.cutAndCopyEnable = true 601 } 602 if (start === 0 && this.controller.getSpans({ start: end + 1, end: end + 1 }).length === 0) { 603 this.visibilityValue = Visibility.None 604 } else { 605 this.visibilityValue = Visibility.Visible 606 } 607 }) 608 } else if (this.showCustomerIndex > -1 && this.builder) { 609 Column() { 610 if (this.customerChange) { 611 this.builder() 612 } else { 613 this.builder() 614 } 615 } 616 .width(this.horizontalMenuWidth) 617 } 618 } 619 .width(this.fontScale > MAX_FONT_SCALE ? 'auto' : this.theme.defaultMenuWidth) 620 .shadow(this.theme.iconPanelShadowStyle) 621 .border({ width: this.theme.borderWidth, color: this.theme.borderColor, 622 radius: this.theme.containerBorderRadius }) 623 .constraintSize({ 624 minWidth: this.theme.defaultMenuWidth 625 }) 626 } 627} 628 629@Builder 630export function SelectionMenu(options: SelectionMenuOptions) { 631 SelectionMenuComponent({ 632 editorMenuOptions: options.editorMenuOptions, 633 expandedMenuOptions: options.expandedMenuOptions, 634 controller: options.controller, 635 onPaste: options.onPaste, 636 onCopy: options.onCopy, 637 onCut: options.onCut, 638 onSelectAll: options.onSelectAll 639 }) 640} 641 642class Util { 643 private static RESOURCE_TYPE_SYMBOL = 40000; 644 645 public static isSymbolResource(resourceStr: ResourceStr | undefined): boolean { 646 if (!Util.isResourceType(resourceStr)) { 647 return false; 648 } 649 let resource = resourceStr as Resource; 650 return resource.type === Util.RESOURCE_TYPE_SYMBOL; 651 } 652 653 public static isResourceType(resource: ResourceStr | Resource | undefined): boolean { 654 if (!resource) { 655 return false; 656 } 657 if (typeof resource === 'string' || typeof resource === 'undefined') { 658 return false; 659 } 660 return true; 661 } 662}