• 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 { KeyCode } from '@ohos.multimodalInput.keyCode';
17import window from '@ohos.window';
18import common from '@ohos.app.ability.common';
19import { BusinessError } from '@kit.BasicServicesKit';
20import hilog from '@ohos.hilog';
21
22export interface ComposeTitleBarMenuItem {
23  value: ResourceStr;
24  symbolStyle?: SymbolGlyphModifier;
25  isEnabled?: boolean;
26  action?: () => void;
27  label?: ResourceStr;
28  accessibilityText?: ResourceStr;
29  accessibilityLevel?: string;
30  accessibilityDescription?: ResourceStr;
31}
32
33const PUBLIC_MORE: Resource = $r('sys.symbol.dot_grid_2x2');
34const PUBLIC_BACK: Resource = $r('sys.symbol.arrow_left');
35const TEXT_EDITABLE_DIALOG = '18.3fp';
36const IMAGE_SIZE = '64vp';
37const MAX_DIALOG = '256vp';
38const MIN_DIALOG = '216vp';
39const RESOURCE_TYPE_SYMBOL: number = 40000;
40
41class Util {
42  public static isSymbolResource(resourceStr: ResourceStr | undefined | null): boolean {
43    if (!Util.isResourceType(resourceStr)) {
44      return false;
45    }
46    let resource: Resource = resourceStr as Resource;
47    return resource.type === RESOURCE_TYPE_SYMBOL;
48  }
49
50  public static isResourceType(resource: ResourceStr | Resource | undefined | null): boolean {
51    if (!resource) {
52      return false;
53    }
54    if (typeof resource === 'string' || typeof resource === 'undefined') {
55      return false;
56    }
57    return true;
58  }
59}
60
61class ButtonGestureModifier implements GestureModifier {
62  public static readonly longPressTime: number = 500;
63  public static readonly minFontSize: number = 1.75;
64  public fontSize: number = 1;
65  public controller: CustomDialogController | null = null;
66
67  constructor(controller: CustomDialogController | null) {
68    this.controller = controller;
69  }
70
71  applyGesture(event: UIGestureEvent): void {
72    if (this.fontSize >= ButtonGestureModifier.minFontSize) {
73      event.addGesture(
74        new LongPressGestureHandler({ repeat: false, duration: ButtonGestureModifier.longPressTime })
75          .onAction(() => {
76            if (event) {
77              this.controller?.open();
78            }
79          })
80          .onActionEnd(() => {
81            this.controller?.close();
82          })
83      )
84    } else {
85      event.clearGestures();
86    }
87  }
88}
89
90@Component
91struct ComposeTitleBar {
92  item: ComposeTitleBarMenuItem | undefined = undefined;
93  title: ResourceStr = '';
94  subtitle: ResourceStr = '';
95  menuItems?: Array<ComposeTitleBarMenuItem> = [];
96  @State titleMaxWidth: number = 0;
97  @State fontSize: number = 1;
98  private static readonly totalHeight = 56;
99  private static readonly leftPadding = 12;
100  private static readonly rightPadding = 12;
101  private static readonly portraitImageSize = 40;
102  private static readonly portraitImageLeftPadding = 4;
103  private static readonly portraitImageRightPadding = 16;
104  private static instanceCount = 0;
105  @Provide('uniqueId') uniqueId?: number = -1;
106
107  build() {
108    Flex({
109      justifyContent: FlexAlign.SpaceBetween,
110      alignItems: ItemAlign.Stretch
111    }) {
112      Row() {
113        ImageMenuItem({
114          item: {
115            value: PUBLIC_BACK,
116            isEnabled: true,
117            action: () => this.getUIContext()?.getRouter()?.back()
118          },
119          index: -1,
120          itemIndex: -1
121        })
122
123        Row() {
124          if (this.item !== undefined) {
125            Image(this.item.value)
126              .width(ComposeTitleBar.portraitImageSize)
127              .height(ComposeTitleBar.portraitImageSize)
128              .margin({
129                left: $r('sys.float.ohos_id_text_paragraph_margin_xs'),
130                right: $r('sys.float.ohos_id_text_paragraph_margin_m')
131              })
132              .focusable(false)
133              .borderRadius(ImageMenuItem.buttonBorderRadius)
134          }
135
136          Column() {
137            if (this.title !== undefined) {
138              Row() {
139                Text(this.title)
140                  .fontWeight(FontWeight.Medium)
141                  .fontSize($r('sys.float.ohos_id_text_size_headline8'))
142                  .fontColor($r('sys.color.ohos_id_color_titlebar_text'))
143                  .maxLines(this.subtitle !== undefined ? 1 : 2)
144                  .textOverflow({ overflow: TextOverflow.Ellipsis })
145                  .constraintSize({ maxWidth: this.titleMaxWidth })
146              }
147              .justifyContent(FlexAlign.Start)
148            }
149            if (this.subtitle !== undefined) {
150              Row() {
151                Text(this.subtitle)
152                  .fontSize($r('sys.float.ohos_id_text_size_over_line'))
153                  .fontColor($r('sys.color.ohos_id_color_titlebar_subtitle_text'))
154                  .maxLines(1)
155                  .textOverflow({ overflow: TextOverflow.Ellipsis })
156                  .constraintSize({ maxWidth: this.titleMaxWidth })
157              }
158              .justifyContent(FlexAlign.Start)
159            }
160          }
161          .justifyContent(FlexAlign.Start)
162          .alignItems(HorizontalAlign.Start)
163          .constraintSize({ maxWidth: this.titleMaxWidth })
164        }
165        .accessibilityGroup(true)
166        .accessibilityDescription($r('sys.string.subheader_accessibility_title'))
167      }
168      .margin({ left: $r('sys.float.ohos_id_default_padding_start') })
169
170      if (this.menuItems !== undefined && this.menuItems.length > 0) {
171        CollapsibleMenuSection({ menuItems: this.menuItems, index: 1 + ComposeTitleBar.instanceCount++ })
172      }
173    }
174    .onAppear(() => {
175      try {
176        this.uniqueId = this.getUIContext().getFrameNodeByUniqueId(this.getUniqueId())?.getFirstChild()?.getUniqueId();
177      } catch (exception) {
178        let code: number = (exception as BusinessError)?.code;
179        let message: string = (exception as BusinessError)?.message;
180        hilog.error(0x3900, 'ComposeTitleBar',
181          `Faild to getFrameNodeByUniqueId,cause, code: ${code}, message: ${message}`);
182      }
183    })
184    .width('100%')
185    .height(ComposeTitleBar.totalHeight)
186    .backgroundColor($r('sys.color.ohos_id_color_background'))
187    .onAreaChange((_oldValue: Area, newValue: Area) => {
188      let newWidth = Number(newValue.width);
189      if (this.menuItems !== undefined) {
190        let menusLength = this.menuItems.length;
191        if (menusLength >= CollapsibleMenuSection.maxCountOfVisibleItems) {
192          newWidth = newWidth - ImageMenuItem.imageHotZoneWidth * CollapsibleMenuSection.maxCountOfVisibleItems;
193        } else if (menusLength > 0) {
194          newWidth = newWidth - ImageMenuItem.imageHotZoneWidth * menusLength;
195        }
196      }
197      this.titleMaxWidth = newWidth;
198      this.titleMaxWidth -= ComposeTitleBar.leftPadding;
199      this.titleMaxWidth -= ImageMenuItem.imageHotZoneWidth;
200      if (this.item !== undefined) {
201        this.titleMaxWidth -= ComposeTitleBar.portraitImageLeftPadding +
202        ComposeTitleBar.portraitImageSize +
203        ComposeTitleBar.portraitImageRightPadding;
204      }
205      this.titleMaxWidth -= ComposeTitleBar.rightPadding;
206    })
207  }
208}
209
210@Component
211struct CollapsibleMenuSection {
212  menuItems?: Array<ComposeTitleBarMenuItem> = [];
213  item: ComposeTitleBarMenuItem = {
214    value: PUBLIC_MORE,
215    label: $r('sys.string.ohos_toolbar_more'),
216  } as ComposeTitleBarMenuItem;
217  index: number = 0;
218  minFontSize: number = 1.75;
219  isFollowingSystemFontScale: boolean = false;
220  maxFontScale: number = 1;
221  systemFontScale?: number = 1;
222  static readonly maxCountOfVisibleItems = 3;
223  private static readonly focusPadding = 4;
224  private static readonly marginsNum = 2;
225  private firstFocusableIndex = -1;
226  @State isPopupShown: boolean = false;
227  @State isMoreIconOnFocus: boolean = false;
228  @State isMoreIconOnHover: boolean = false;
229  @State isMoreIconOnClick: boolean = false;
230  @Prop @Watch('onFontSizeUpdated') fontSize: number = 1;
231  dialogController: CustomDialogController | null = new CustomDialogController({
232    builder: ComposeTitleBarDialog({
233      cancel: () => {
234      },
235      confirm: () => {
236      },
237      itemComposeTitleDialog: this.item,
238      composeTitleBarDialog: this.item.label ? this.item.label : '',
239      fontSize: this.fontSize,
240    }),
241    maskColor: Color.Transparent,
242    isModal: true,
243    customStyle: true,
244  })
245
246  @State buttonGestureModifier: ButtonGestureModifier = new ButtonGestureModifier(this.dialogController);
247
248  getMoreIconFgColor() {
249    return this.isMoreIconOnClick ?
250    $r('sys.color.ohos_id_color_titlebar_icon_pressed') :
251    $r('sys.color.ohos_id_color_titlebar_icon');
252  }
253
254  getMoreIconBgColor() {
255    if (this.isMoreIconOnClick) {
256      return $r('sys.color.ohos_id_color_click_effect');
257    } else if (this.isMoreIconOnHover) {
258      return $r('sys.color.ohos_id_color_hover');
259    } else {
260      return Color.Transparent;
261    }
262  }
263
264  aboutToAppear() {
265    try {
266      let uiContent: UIContext = this.getUIContext();
267      this.isFollowingSystemFontScale = uiContent.isFollowingSystemFontScale();
268      this.maxFontScale = uiContent.getMaxFontScale();
269    } catch (err) {
270      let code: number = (err as BusinessError)?.code;
271      let message: string = (err as BusinessError)?.message;
272      hilog.error(0x3900, 'ComposeTitleBar',
273        `Failed to init fontsizescale info, cause, code: ${code}, message: ${message}`);
274    }
275    if (this.menuItems) {
276      this.menuItems.forEach((item, index) => {
277        if (item.isEnabled && this.firstFocusableIndex === -1 &&
278          index > CollapsibleMenuSection.maxCountOfVisibleItems - 2) {
279          this.firstFocusableIndex = this.index * 1000 + index + 1;
280        }
281      })
282    }
283    this.fontSize = this.decideFontScale()
284  }
285
286  decideFontScale(): number {
287    try {
288      let uiContent: UIContext = this.getUIContext();
289      this.systemFontScale = (uiContent.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1;
290      if (!this.isFollowingSystemFontScale) {
291        return 1;
292      }
293      return Math.min(this.systemFontScale, this.maxFontScale);
294    } catch (exception) {
295      let code: number = (exception as BusinessError)?.code;
296      let message: string = (exception as BusinessError)?.message;
297      hilog.error(0x3900, 'ComposeTitleBar', `Faild to decideFontScale,cause, code: ${code}, message: ${message}`);
298      return 1;
299    }
300  }
301
302  onFontSizeUpdated(): void {
303    this.buttonGestureModifier.fontSize = this.fontSize;
304  }
305
306  build() {
307    Column() {
308      Row() {
309        if (this.menuItems) {
310          if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) {
311            ForEach(this.menuItems, (item: ComposeTitleBarMenuItem, index: number) => {
312              ImageMenuItem({ item: item, index: this.index * 1000 + index + 1, itemIndex: index })
313            })
314          } else {
315            ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1),
316              (item: ComposeTitleBarMenuItem, index: number) => {
317                ImageMenuItem({ item: item, index: this.index * 1000 + index + 1, itemIndex: index })
318              })
319
320            Button({ type: ButtonType.Normal, stateEffect: true }) {
321              SymbolGlyph(PUBLIC_MORE)
322                .fontSize(`${ImageMenuItem.imageSize}vp`)
323                .fontColor([$r('sys.color.icon_primary')])
324                .draggable(false)
325                .focusable(true)
326            }
327            .accessibilityText($r('sys.string.ohos_toolbar_more'))
328            .width(ImageMenuItem.imageHotZoneWidth)
329            .height(ImageMenuItem.imageHotZoneWidth)
330            .borderRadius(ImageMenuItem.buttonBorderRadius)
331            .foregroundColor(this.getMoreIconFgColor())
332            .backgroundColor(this.getMoreIconBgColor())
333            .stateStyles({
334              focused: {
335                .border({
336                  radius: $r('sys.float.ohos_id_corner_radius_clicked'),
337                  width: ImageMenuItem.focusBorderWidth,
338                  color: $r('sys.color.ohos_id_color_focused_outline'),
339                  style: BorderStyle.Solid
340                })
341              },
342              normal: {
343                .border({
344                  radius: $r('sys.float.ohos_id_corner_radius_clicked'),
345                  width: 0
346                })
347              }
348            })
349            .onFocus(() => this.isMoreIconOnFocus = true)
350            .onBlur(() => this.isMoreIconOnFocus = false)
351            .onHover((isOn) => this.isMoreIconOnHover = isOn)
352            .onKeyEvent((event) => {
353              if (event.keyCode !== KeyCode.KEYCODE_ENTER &&
354                event.keyCode !== KeyCode.KEYCODE_SPACE) {
355                return;
356              }
357              if (event.type === KeyType.Down) {
358                this.isMoreIconOnClick = true;
359              }
360              if (event.type === KeyType.Up) {
361                this.isMoreIconOnClick = false;
362              }
363            })
364            .onTouch((event) => {
365              if (event.type === TouchType.Down) {
366                this.isMoreIconOnClick = true;
367              }
368              if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
369                this.isMoreIconOnClick = false;
370                if (this.fontSize >= this.minFontSize) {
371                  this.dialogController?.close();
372                }
373              }
374            })
375            .onClick(() => this.isPopupShown = true)
376            .gestureModifier(this.buttonGestureModifier)
377            .bindPopup(this.isPopupShown, {
378              builder: this.popupBuilder,
379              placement: Placement.Bottom,
380              popupColor: Color.White,
381              enableArrow: false,
382              onStateChange: (e) => {
383                this.isPopupShown = e.isVisible;
384                if (!e.isVisible) {
385                  this.isMoreIconOnClick = false;
386                }
387              }
388            })
389          }
390        }
391      }
392    }
393    .height('100%')
394    .margin({ right: $r('sys.float.ohos_id_default_padding_end') })
395    .justifyContent(FlexAlign.Center)
396  }
397
398  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Layoutable[], constraint: ConstraintSizeOptions): void {
399    children.forEach((child) => {
400      child.layout({ x: 0, y: 0 });
401    })
402    this.fontSize = this.decideFontScale();
403  }
404
405  @Builder
406  popupBuilder() {
407    Column() {
408      if (this.menuItems) {
409        ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1,
410          this.menuItems.length),
411          (item: ComposeTitleBarMenuItem, index: number) => {
412            ImageMenuItem({
413              item: item, index: this.index * 1000 +
414              CollapsibleMenuSection.maxCountOfVisibleItems + index, isPopup: false
415            })
416          })
417      }
418    }
419    .width(ImageMenuItem.imageHotZoneWidth +
420      CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum)
421    .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding })
422    .onAppear(() => {
423      focusControl.requestFocus(ImageMenuItem.focusablePrefix +
424      this.firstFocusableIndex)
425    })
426  }
427}
428
429@Component
430struct ImageMenuItem {
431  item: ComposeTitleBarMenuItem = {} as ComposeTitleBarMenuItem;
432  index: number = 0;
433  itemIndex?: number = 0;
434  minFontSize: number = 1.75;
435  isFollowingSystemFontScale: boolean = false;
436  maxFontScale: number = 1;
437  systemFontScale?: number = 1;
438  isPopup: boolean = true;
439  static readonly imageSize = 24;
440  static readonly imageHotZoneWidth = 48;
441  static readonly buttonBorderRadius = 8;
442  static readonly focusBorderWidth = 2;
443  static readonly focusablePrefix = 'Id-ComposeTitleBar-ImageMenuItem-';
444  @State isOnFocus: boolean = false;
445  @State isOnHover: boolean = false;
446  @State isOnClick: boolean = false;
447  @Prop @Watch('onFontSizeUpdated') fontSize: number = 1;
448  @Consume('uniqueId') parentParentUniqueId?: number;
449  dialogController: CustomDialogController | null = new CustomDialogController({
450    builder: ComposeTitleBarDialog({
451      cancel: () => {
452      },
453      confirm: () => {
454      },
455      itemComposeTitleDialog: this.item,
456      composeTitleBarDialog: this.item.label ? this.item.label : this.textDialog(),
457      fontSize: this.fontSize,
458    }),
459    maskColor: Color.Transparent,
460    isModal: true,
461    customStyle: true,
462  })
463
464  @State buttonGestureModifier: ButtonGestureModifier = new ButtonGestureModifier(this.dialogController);
465
466  private textDialog(): ResourceStr {
467    if (this.item.value === PUBLIC_MORE) {
468      return $r('sys.string.ohos_toolbar_more');
469    } else if (this.item.value === PUBLIC_BACK) {
470      return $r('sys.string.icon_back');
471    } else {
472      return this.item.label ? this.item.label : '';
473    }
474  }
475
476  private toStringFormat(resource: ResourceStr | undefined): string | undefined {
477    if (typeof resource === 'string') {
478      return resource;
479    } else if (typeof resource === 'undefined') {
480      return '';
481    } else {
482      let resourceString: string = '';
483      try {
484        if (resource.id === -1 ) {
485          resourceString = getContext()?.resourceManager?.getStringByNameSync(resource.params?.[0].split('.').pop());
486        } else {
487          resourceString = getContext()?.resourceManager?.getStringSync(resource);
488        }
489      } catch (err) {
490        let code: number = (err as BusinessError)?.code;
491        let message: string = (err as BusinessError)?.message;
492        hilog.error(0x3900, 'Ace', `Faild to ComposeTitleBar toStringFormat,code: ${code},message:${message}`);
493      }
494      return resourceString;
495    }
496  }
497
498  private getAccessibilityReadText(): string | undefined {
499    if (this.item.value === PUBLIC_BACK) {
500      try {
501        return getContext()?.resourceManager?.getStringByNameSync('icon_back');
502      } catch (err) {
503        let code: number = (err as BusinessError)?.code;
504        let message: string = (err as BusinessError)?.message;
505        hilog.error(0x3900, 'Ace',
506          `Faild to ComposeTitleBar getStringByNameSync icon_back,code: ${code},message:${message}`);
507      }
508    } else if (this.item.value === PUBLIC_MORE) {
509      try {
510        return getContext()?.resourceManager?.getStringByNameSync('ohos_toolbar_more');
511      } catch (err) {
512        let code: number = (err as BusinessError)?.code;
513        let message: string = (err as BusinessError)?.message;
514        hilog.error(0x3900, 'Ace',
515          `Faild to ComposeTitleBar getStringByNameSync ohos_toolbar_more,code: ${code},message:${message}`);
516      }
517    } else if (this.item.accessibilityText) {
518      return this.item.accessibilityText as string;
519    } else if (this.item.label) {
520      return this.item.label as string;
521    }
522    return ' ';
523  }
524
525  onPlaceChildren(selfLayoutInfo: GeometryInfo, children: Layoutable[], constraint: ConstraintSizeOptions): void {
526    children.forEach((child) => {
527      child.layout({ x: 0, y: 0 });
528    })
529    this.fontSize = this.decideFontScale();
530  }
531
532
533  getFgColor() {
534    return this.isOnClick
535      ? $r('sys.color.ohos_id_color_titlebar_icon_pressed')
536      : $r('sys.color.ohos_id_color_titlebar_icon');
537  }
538
539  getBgColor() {
540    if (this.isOnClick) {
541      return $r('sys.color.ohos_id_color_click_effect');
542    } else if (this.isOnHover) {
543      return $r('sys.color.ohos_id_color_hover');
544    } else {
545      return Color.Transparent;
546    }
547  }
548
549  aboutToAppear() {
550    try {
551      let uiContent: UIContext = this.getUIContext();
552      this.isFollowingSystemFontScale = uiContent.isFollowingSystemFontScale();
553      this.maxFontScale = uiContent.getMaxFontScale();
554    } catch (err) {
555      let code: number = (err as BusinessError)?.code;
556      let message: string = (err as BusinessError)?.message;
557      hilog.error(0x3900, 'ComposeTitleBar',
558        `Failed to init fontsizescale info, cause, code: ${code}, message: ${message}`);
559    }
560    this.fontSize = this.decideFontScale();
561  }
562
563  onFontSizeUpdated(): void {
564    this.buttonGestureModifier.fontSize = this.fontSize;
565  }
566
567  decideFontScale(): number {
568    try {
569      let uiContent: UIContext = this.getUIContext();
570      this.systemFontScale = (uiContent.getHostContext() as common.UIAbilityContext)?.config?.fontSizeScale ?? 1;
571      if (!this.isFollowingSystemFontScale) {
572        return 1;
573      }
574      return Math.min(this.systemFontScale, this.maxFontScale);
575    } catch (exception) {
576      let code: number = (exception as BusinessError)?.code;
577      let message: string = (exception as BusinessError)?.message;
578      hilog.error(0x3900, 'ComposeTitleBar', `Faild to decideFontScale,cause, code: ${code}, message: ${message}`);
579      return 1;
580    }
581  }
582
583  build() {
584    if (this.isPopup) {
585      Button({ type: ButtonType.Normal, stateEffect: this.item.isEnabled }) {
586        if (this.item?.symbolStyle) {
587          SymbolGlyph()
588            .fontColor([$r('sys.color.ohos_id_color_text_primary')])
589            .attributeModifier(this.item?.symbolStyle)
590            .fontSize(`${ImageMenuItem.imageSize}vp`)
591            .effectStrategy(SymbolEffectStrategy.NONE)
592            .symbolEffect(new SymbolEffect(), false)
593            .draggable(false)
594            .focusable(this.item?.isEnabled)
595            .key(ImageMenuItem.focusablePrefix + this.index)
596        } else {
597          if (Util.isSymbolResource(this.item.value)) {
598            SymbolGlyph(this.item.value as Resource)
599              .fontSize(`${ImageMenuItem.imageSize}vp`)
600              .fontColor([$r('sys.color.ohos_id_color_text_primary')])
601              .draggable(false)
602              .focusable(this.item?.isEnabled)
603              .key(ImageMenuItem.focusablePrefix + this.index)
604          } else {
605            Image(this.item?.value)
606              .matchTextDirection(this.item?.value === PUBLIC_BACK ? true : false)
607              .width(ImageMenuItem.imageSize)
608              .draggable(false)
609              .height(ImageMenuItem.imageSize)
610              .focusable(this.item?.isEnabled)
611              .key(ImageMenuItem.focusablePrefix + this.index)
612              .fillColor($r('sys.color.ohos_id_color_text_primary'))
613          }
614        }
615      }
616      .id(`ComposeTitleBar_ImageMenuItem_${this.parentParentUniqueId}_${this.itemIndex}`)
617      .accessibilityText(this.getAccessibilityReadText())
618      .accessibilityLevel(this.item?.accessibilityLevel ?? 'auto')
619      .accessibilityDescription(this.item?.accessibilityDescription as string)
620      .enabled(this.item.isEnabled ? this.item.isEnabled : false)
621      .width(ImageMenuItem.imageHotZoneWidth)
622      .height(ImageMenuItem.imageHotZoneWidth)
623      .borderRadius(ImageMenuItem.buttonBorderRadius)
624      .foregroundColor(this.getFgColor())
625      .backgroundColor(this.getBgColor())
626      .stateStyles({
627        focused: {
628          .border({
629            radius: $r('sys.float.ohos_id_corner_radius_clicked'),
630            width: ImageMenuItem.focusBorderWidth,
631            color: $r('sys.color.ohos_id_color_focused_outline'),
632            style: BorderStyle.Solid
633          })
634        },
635        normal: {
636          .border({
637            radius: $r('sys.float.ohos_id_corner_radius_clicked'),
638            width: 0
639          })
640        }
641      })
642      .onFocus(() => {
643        if (!this.item?.isEnabled) {
644          return;
645        }
646        this.isOnFocus = true;
647      })
648      .onBlur(() => this.isOnFocus = false)
649      .onHover((isOn) => {
650        if (!this.item?.isEnabled) {
651          return;
652        }
653        this.isOnHover = isOn;
654      })
655      .onKeyEvent((event) => {
656        if (!this.item?.isEnabled) {
657          return;
658        }
659        if (event.keyCode !== KeyCode.KEYCODE_ENTER &&
660          event.keyCode !== KeyCode.KEYCODE_SPACE) {
661          return;
662        }
663        if (event.type === KeyType.Down) {
664          this.isOnClick = true;
665        }
666        if (event.type === KeyType.Up) {
667          this.isOnClick = false;
668        }
669      })
670      .onTouch((event) => {
671        if (!this.item?.isEnabled) {
672          return;
673        }
674        if (event.type === TouchType.Down) {
675          this.isOnClick = true;
676        }
677        if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
678          this.isOnClick = false;
679          if (this.fontSize >= this.minFontSize) {
680            this.dialogController?.close();
681          }
682        }
683      })
684      .onClick(() => {
685        if (this.item) {
686          return this.item.isEnabled && this.item.action?.();
687        }
688      })
689      .gestureModifier(this.buttonGestureModifier)
690    } else {
691      Button({ type: ButtonType.Normal, stateEffect: this.item.isEnabled }) {
692        if (this.item?.symbolStyle) {
693          SymbolGlyph()
694            .fontColor([$r('sys.color.ohos_id_color_text_primary')])
695            .attributeModifier(this.item?.symbolStyle)
696            .fontSize(`${ImageMenuItem.imageSize}vp`)
697            .effectStrategy(SymbolEffectStrategy.NONE)
698            .symbolEffect(new SymbolEffect(), false)
699            .draggable(false)
700            .focusable(this.item?.isEnabled)
701            .key(ImageMenuItem.focusablePrefix + this.index)
702        } else {
703          if (Util.isSymbolResource(this.item.value)) {
704            SymbolGlyph(this.item.value as Resource)
705              .fontSize(`${ImageMenuItem.imageSize}vp`)
706              .fontColor([$r('sys.color.ohos_id_color_text_primary')])
707              .draggable(false)
708              .focusable(this.item?.isEnabled)
709              .key(ImageMenuItem.focusablePrefix + this.index)
710          } else {
711            Image(this.item?.value)
712              .matchTextDirection(this.item?.value === PUBLIC_BACK ? true : false)
713              .width(ImageMenuItem.imageSize)
714              .draggable(false)
715              .height(ImageMenuItem.imageSize)
716              .focusable(this.item?.isEnabled)
717              .key(ImageMenuItem.focusablePrefix + this.index)
718              .fillColor($r('sys.color.ohos_id_color_text_primary'))
719          }
720        }
721      }
722      .id(`ComposeTitleBar_ImageMenuItem_${this.parentParentUniqueId}_${this.itemIndex}`)
723      .accessibilityText(this.getAccessibilityReadText())
724      .accessibilityLevel(this.item?.accessibilityLevel ?? 'auto')
725      .accessibilityDescription(this.item?.accessibilityDescription as string)
726      .enabled(this.item.isEnabled ? this.item.isEnabled : false)
727      .width(ImageMenuItem.imageHotZoneWidth)
728      .height(ImageMenuItem.imageHotZoneWidth)
729      .borderRadius(ImageMenuItem.buttonBorderRadius)
730      .foregroundColor(this.getFgColor())
731      .backgroundColor(this.getBgColor())
732      .stateStyles({
733        focused: {
734          .border({
735            radius: $r('sys.float.ohos_id_corner_radius_clicked'),
736            width: ImageMenuItem.focusBorderWidth,
737            color: $r('sys.color.ohos_id_color_focused_outline'),
738            style: BorderStyle.Solid
739          })
740        },
741        normal: {
742          .border({
743            radius: $r('sys.float.ohos_id_corner_radius_clicked'),
744            width: 0
745          })
746        }
747      })
748      .onFocus(() => {
749        if (!this.item?.isEnabled) {
750          return;
751        }
752        this.isOnFocus = true;
753      })
754      .onBlur(() => this.isOnFocus = false)
755      .onHover((isOn) => {
756        if (!this.item?.isEnabled) {
757          return;
758        }
759        this.isOnHover = isOn;
760      })
761      .onKeyEvent((event) => {
762        if (!this.item?.isEnabled) {
763          return;
764        }
765        if (event.keyCode !== KeyCode.KEYCODE_ENTER &&
766          event.keyCode !== KeyCode.KEYCODE_SPACE) {
767          return;
768        }
769        if (event.type === KeyType.Down) {
770          this.isOnClick = true;
771        }
772        if (event.type === KeyType.Up) {
773          this.isOnClick = false;
774        }
775      })
776      .onTouch((event) => {
777        if (!this.item?.isEnabled) {
778          return;
779        }
780        if (event.type === TouchType.Down) {
781          this.isOnClick = true;
782        }
783        if (event.type === TouchType.Up || event.type === TouchType.Cancel) {
784          this.isOnClick = false;
785          if (this.fontSize >= this.minFontSize) {
786            this.dialogController?.close();
787          }
788        }
789      })
790      .onClick(() => {
791        if (this.item) {
792          return this.item.isEnabled && this.item.action?.();
793        }
794      })
795    }
796
797  }
798}
799
800/**
801 *  ComposeTitleBarDialog
802 */
803@CustomDialog
804struct ComposeTitleBarDialog {
805  itemComposeTitleDialog: ComposeTitleBarMenuItem = {} as ComposeTitleBarMenuItem;
806  callbackId: number | undefined = undefined;
807  composeTitleBarDialog?: ResourceStr = '';
808  mainWindowStage: window.Window | undefined = undefined;
809  controller?: CustomDialogController;
810  minFontSize: number = 1.75;
811  maxFontSize: number = 3.2;
812  screenWidth: number = 640;
813  verticalScreenLines: number = 6;
814  horizontalsScreenLines: number = 1;
815  @StorageLink('mainWindow') mainWindow: Promise<window.Window> | undefined = undefined;
816  @State fontSize: number = 1;
817  @State maxLines: number = 1;
818  @StorageProp('windowStandardHeight') windowStandardHeight: number = 0;
819  cancel: () => void = () => {
820  }
821  confirm: () => void = () => {
822  }
823
824  build() {
825    if (this.composeTitleBarDialog) {
826      Column() {
827        if (this.itemComposeTitleDialog.symbolStyle) {
828          SymbolGlyph()
829            .fontColor([$r('sys.color.icon_primary')])
830            .attributeModifier(this.itemComposeTitleDialog.symbolStyle)
831            .fontSize(IMAGE_SIZE)
832            .effectStrategy(SymbolEffectStrategy.NONE)
833            .symbolEffect(new SymbolEffect(), false)
834            .margin({
835              top: $r('sys.float.padding_level24'),
836              bottom: $r('sys.float.padding_level8'),
837            })
838        } else {
839          if (Util.isSymbolResource(this.itemComposeTitleDialog.value)) {
840            SymbolGlyph(this.itemComposeTitleDialog.value as Resource)
841              .fontSize(IMAGE_SIZE)
842              .fontColor([$r('sys.color.icon_primary')])
843              .margin({
844                top: $r('sys.float.padding_level24'),
845                bottom: $r('sys.float.padding_level8'),
846              })
847          } else {
848            Image(this.itemComposeTitleDialog.value)
849              .width(IMAGE_SIZE)
850              .height(IMAGE_SIZE)
851              .margin({
852                top: $r('sys.float.padding_level24'),
853                bottom: $r('sys.float.padding_level8'),
854              })
855              .fillColor($r('sys.color.icon_primary'))
856          }
857        }
858        Column() {
859          Text(this.composeTitleBarDialog)
860            .fontSize(TEXT_EDITABLE_DIALOG)
861            .textOverflow({ overflow: TextOverflow.Ellipsis })
862            .maxLines(this.maxLines)
863            .width('100%')
864            .textAlign(TextAlign.Center)
865            .fontColor($r('sys.color.font_primary'))
866        }
867        .width('100%')
868        .padding({
869          left: $r('sys.float.padding_level4'),
870          right: $r('sys.float.padding_level4'),
871          bottom: $r('sys.float.padding_level12'),
872        })
873      }
874      .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG)
875      .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG })
876      .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK, undefined, { disableSystemAdaptation: true })
877      .shadow(ShadowStyle.OUTER_DEFAULT_LG)
878      .borderRadius($r('sys.float.corner_radius_level10'))
879    } else {
880      Column() {
881        if (this.itemComposeTitleDialog.symbolStyle) {
882          SymbolGlyph()
883            .fontColor([$r('sys.color.icon_primary')])
884            .attributeModifier(this.itemComposeTitleDialog.symbolStyle)
885            .fontSize(IMAGE_SIZE)
886            .effectStrategy(SymbolEffectStrategy.NONE)
887            .symbolEffect(new SymbolEffect(), false)
888        } else {
889          if (Util.isSymbolResource(this.itemComposeTitleDialog.value)) {
890            SymbolGlyph(this.itemComposeTitleDialog.value as Resource)
891              .fontSize(IMAGE_SIZE)
892              .fontColor([$r('sys.color.icon_primary')])
893          } else {
894            Image(this.itemComposeTitleDialog.value)
895              .width(IMAGE_SIZE)
896              .height(IMAGE_SIZE)
897              .fillColor($r('sys.color.icon_primary'))
898          }
899        }
900      }
901      .width(this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG)
902      .constraintSize({ minHeight: this.fontSize === this.maxFontSize ? MAX_DIALOG : MIN_DIALOG })
903      .backgroundBlurStyle(BlurStyle.COMPONENT_ULTRA_THICK, undefined, { disableSystemAdaptation: true })
904      .shadow(ShadowStyle.OUTER_DEFAULT_LG)
905      .borderRadius($r('sys.float.corner_radius_level10'))
906      .justifyContent(FlexAlign.Center)
907    }
908  }
909
910  async aboutToAppear(): Promise<void> {
911    try {
912      let context = this.getUIContext().getHostContext() as common.UIAbilityContext;
913      this.mainWindowStage = context.windowStage.getMainWindowSync();
914      let properties: window.WindowProperties = this.mainWindowStage.getWindowProperties();
915      let rect = properties.windowRect;
916      if (px2vp(rect.height) > this.screenWidth) {
917        this.maxLines = this.verticalScreenLines;
918      } else {
919        this.maxLines = this.horizontalsScreenLines;
920      }
921    } catch (exception) {
922      let code: number = (exception as BusinessError)?.code;
923      let message: string = (exception as BusinessError)?.message;
924      hilog.error(0x3900, 'ComposeTitleBar', `Faild to getMainWindowSync,cause, code: ${code}, message: ${message}`);
925    }
926  }
927}
928
929export { ComposeTitleBar }