• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}