• 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 = 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}