• 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'
17
18export declare type SelectTitleBarMenuItem = {
19  value: ResourceStr
20  isEnabled: boolean
21  action?: () => void
22}
23
24const PUBLIC_MORE = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAY' +
25  'AAABS3GwHAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAEZ0FNQQAAsY58+1GTAAA' +
26  'AAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAOxAAADsQBlSsOGwAABEZJREFUeNrt3D1rFFEUBuA' +
27  'xhmAhFlYpUohYiYWFRcAmKAhWK2pjo1iKf8BCMIKFf8BarCyMhVj4VZhGSKEg2FqJyCKWIhYWnstMINgYsh+cmfs88BI' +
28  'Cydxw7jmzu2HvNg0AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADBN+3r6dx+LXIqsRpa7FF8j48hm5Fn3Peo9mAEYRdY' +
29  'jJ3f582Vj7nZfUe/eDsCRyMPI2h5/fyNyI/JDT6v3Tvt7sBllE15ETkxwjeORi5G3ke/6W737MgBnI68jh6ZwrcORq5H' +
30  'nhkC9+zAA5YXXy8jBKV5zKXIu8jjyS7+rd+YBeNVtyrSVO9PRyBM9r94LSTfjWuTUDK9/eYIXeENUbb0zDsBi5PYc1rm' +
31  'j79U74wCszuih+F/ljrSi/+uud8YBGA10rayqrnfGAVgb6FpZVV3vjAOwPNC1sqq63hkHYGWga2VVdb0XKt/8Rf1fd70' +
32  'zDsB4jmt5u3Tl9a59AMb6v+56ZxyArYGulVXV9c44ABtzXOup/q+73hkH4N2cHio/Rj7r/7rrnXEAfkfuz2Gddb2v3ln' +
33  '/DfpgxneLzaY9xE3l9c46AH8iVyI/Z3Dt8nB/Xc+rd5H5QMy3yJemPVs6zY0edc9HUe/0Z4I/dQ/N5Vjd0oTXKp9QcKF' +
34  'pD2qj3r0YgO1NeRM507TH6/bifeR85IMeV++d+vTBWOV9JDcjt5rdv6uw3M3uRR7pa/Xu+wBsOxA53bTnTP/3UX1b3fN' +
35  'Q1BsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKqyr6d/97HIpchqZLlL8TUyjmxGnnX' +
36  'fo96DGYBRZD1ycpc/XzbmbvcV9e7tAByJPIys7fH3NyI3Ij/0tHrvtL8Hm1E24UXkxATXOB65GHkb+a6/1bsvA3A28jp' +
37  'yaArXOhy5GnluCNS7DwNQXni9jByc4jWXIucijyO/9Lt6Zx6AV92mTFu5Mx2NPNHz6r2QdDOuRU7N8PqXJ3iBN0TV1jv' +
38  'jACxGbs9hnTv6Xr0zDsDqjB6K/1XuSCv6v+56ZxyA0UDXyqrqemccgLWBrpVV1fXOOADLA10rq6rrnXEAVga6VlZV13u' +
39  'h8s1f1P911zvjAIznuJa3S1de79oHYKz/6653xgHYGuhaWVVd74wDsDHHtZ7q/7rrnXEA3s3pofJj5LP+r7veGQfgd+T' +
40  '+HNZZ1/vqnfXfoA9mfLfYbNpD3FRe76wD8CdyJfJzBtcuD/fX9bx6F5kPxHyLfGnas6XT3OhR93wU9U5/JvhT99BcjtU' +
41  'tTXit8gkFF5r2oDbq3YsB2N6UN5EzTXu8bi/eR85HPuhx9d6pTx+MVd5HcjNyq9n9uwrL3exe5JG+Vu++D8C2A5HTTXv' +
42  'O9H8f1bfVPQ9FvQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgCn7C9HjBtwWfXpKAAAAAElFTkSuQmCC'
43
44const PUBLIC_BACK = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAY' +
45  'AAABS3GwHAAAAIGNIUk0AAHomAACAhAAA+gAAAIDoAAB1MAAA6mAAADqYAAAXcJy6UTwAAAAEZ0FNQQAAsY58+1GTAAAAA' +
46  'XNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAAOxAAADsQBlSsOGwAAA8VJREFUeNrt3LFLlHEYwPFXz0G' +
47  'iIZpEoikkwsFRIiK3gqCigxIC/4Kmhv6OoChouaGoqKCgCKducGh0cDAIamhwiCaHCIeelztpUszee/vl8/nAM3Vd8nufr' +
48  '+fddVYVAAAAAAAAAAAAAAAAAAAAAABQijFH0KhrMd2Y2ZitmNWYRzHLjkYAB9lUzMOYizv8eS/mZsymoypLxxE0svzvY07' +
49  'vcpu5mOmY145LAAdx+U/u4bZzwx+JPjq2cow7glaWf1vXsQkg6/JvPwoggJTLjwDSL/8nRyiAzN/5nzpGAWRd/n7MM0cpg' +
50  'IzLvx6z6CjL453gdpZ/IWbDcQrA8iMAy48ALD8CsPwIwPIjAMuPACw/ArD8CMDyIwDLjwAsPwKw/AjA8iMAy48ALD8CsPw' +
51  'IwPIjAMuPACw/ArD85A3A8pM2AMtP2gAsP2kDsPykDcDykzYAy0/aACw/aQOw/KQNwPKTNgDLT9oALD9pA7D8pA3A8pM2A' +
52  'MtP2gAsP2kDsPykDcDykzYAy0/aACw/aQOw/KQNwPKTNgDLT9oALD9pA7D8pA3A8pM2AMtP2gAsP2kDsPykDcDykzYAy0/' +
53  'aACw/aQOw/KQNwPLz3xlv6H4mYp5YfrI+AizF9BwnI/AlZi3mbsxy03feaeh+HsQcc60YgSMxMzE3YmZj3sX8LOlHoPoLn' +
54  'HedaEE35n5pzwF856dN9SPBpZICmHRNaNnlkgL46nrQsvmSAqhftlx1TWjR4ZICqPVcE1q0XloA96rBa7XQhl5pAWzFXKm' +
55  '8i8vo9WMeN3VnnQa/sO8xL2POxEy7Toxo+RdjNpu6w1F9HuBqNXi99lw1eKMM9utHzIeYV8MftbccCQAAAAAAsBdt/XLc+s' +
56  'Py9W+MmPqL+1iJuVA1+C4gdFr6d77FvK0GH2nb739lPR5zNuZ51eBnQhFAJQIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIE' +
57  'IAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAI' +
58  'EIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIEIAIE8M8jmBlGgABSRnAqZiXms+MUQNYIDnkUKMu4I/gj6z' +
59  'ELMRv7/PsnHKEAMkcw6fgEkDmCNUcngMwRvHFsngRnfWJcL/9tRyaAgxrB+ZijO9ymH7MUs+m4yjLmCBozEXMr5nr1+9We1' +
60  'ZgXMXccDwAAAAAAAAAAAAAAAAAAAAAAwO5+AfVgtqHKRnawAAAAAElFTkSuQmCC'
61
62@Component
63export struct SelectTitleBar {
64  @State selected: number = 0
65
66  options: Array<SelectOption>
67  menuItems: Array<SelectTitleBarMenuItem>
68
69  subtitle: ResourceStr
70  badgeValue: number
71  hidesBackButton: boolean
72
73  onSelected: ((index: number) => void)
74
75  private static readonly badgeSize = 16
76  private static readonly totalHeight = 56
77  private static readonly leftPadding = 24
78  private static readonly leftPaddingWithBack = 12
79  private static readonly rightPadding = 24
80  private static readonly badgePadding = 16
81  private static readonly subtitleLeftPadding = 4
82  private static instanceCount = 0
83
84  @State selectMaxWidth: number = 0
85  @State backActive: boolean = false
86
87  build() {
88    Flex({
89      justifyContent: FlexAlign.SpaceBetween,
90      alignItems: ItemAlign.Stretch
91    }) {
92      Row() {
93        if (!this.hidesBackButton) {
94          Navigator()
95            .active(this.backActive)
96
97          ImageMenuItem({ item: {
98            value: PUBLIC_BACK,
99            isEnabled: true,
100            action: () => this.backActive = true
101          }, index: -1 })
102        }
103
104        Column() {
105          if (this.badgeValue !== undefined) {
106            Badge({
107              count: this.badgeValue,
108              position: BadgePosition.Right,
109              style: {
110                badgeSize: SelectTitleBar.badgeSize,
111                badgeColor: $r('sys.color.ohos_id_color_emphasize'),
112                borderColor: $r('sys.color.ohos_id_color_emphasize'),
113                borderWidth: 0
114              }
115            }) {
116              Row() {
117                Select(this.options)
118                  .selected(this.selected)
119                  .value(this.selected < this.options.length ? this.options[this.selected].value.toString() : "")
120                  .font({ size: this.hidesBackButton && this.subtitle === undefined
121                    ? $r('sys.float.ohos_id_text_size_headline7')
122                    : $r('sys.float.ohos_id_text_size_headline8') })
123                  .fontColor($r('sys.color.ohos_id_color_titlebar_text'))
124                  .backgroundColor(Color.Transparent)
125                  .onSelect(this.onSelected)
126                  .constraintSize({ maxWidth: this.selectMaxWidth })
127                  .offset({ x: -4 })
128              }
129              .justifyContent(FlexAlign.Start)
130              .margin({ right: $r('sys.float.ohos_id_elements_margin_horizontal_l') })
131            }
132          } else {
133            Row() {
134              Select(this.options)
135                .selected(this.selected)
136                .value(this.selected < this.options.length ? this.options[this.selected].value.toString() : "")
137                .font({ size: this.hidesBackButton && this.subtitle === undefined
138                  ? $r('sys.float.ohos_id_text_size_headline7')
139                  : $r('sys.float.ohos_id_text_size_headline8') })
140                .fontColor($r('sys.color.ohos_id_color_titlebar_text'))
141                .backgroundColor(Color.Transparent)
142                .onSelect(this.onSelected)
143                .constraintSize({ maxWidth: this.selectMaxWidth })
144                .offset({ x: -4 })
145            }
146            .justifyContent(FlexAlign.Start)
147          }
148          if (this.subtitle !== undefined) {
149            Row() {
150              Text(this.subtitle)
151                .fontSize($r('sys.float.ohos_id_text_size_over_line'))
152                .fontColor($r('sys.color.ohos_id_color_titlebar_subtitle_text'))
153                .maxLines(1)
154                .textOverflow({ overflow: TextOverflow.Ellipsis })
155                .constraintSize({ maxWidth: this.selectMaxWidth })
156                .offset({ y: -4 })
157            }
158            .justifyContent(FlexAlign.Start)
159            .margin({ left: SelectTitleBar.subtitleLeftPadding })
160          }
161        }
162        .justifyContent(FlexAlign.Start)
163        .alignItems(HorizontalAlign.Start)
164        .constraintSize({ maxWidth: this.selectMaxWidth })
165      }
166      .margin({ left: this.hidesBackButton ? $r('sys.float.ohos_id_max_padding_start') : $r('sys.float.ohos_id_default_padding_start') })
167
168      if (this.menuItems !== undefined && this.menuItems.length > 0) {
169        CollapsibleMenuSection({ menuItems: this.menuItems, index: 1 + SelectTitleBar.instanceCount++ })
170      }
171    }
172    .width('100%')
173    .height(SelectTitleBar.totalHeight)
174    .backgroundColor($r('sys.color.ohos_id_color_background'))
175    .onAreaChange((_oldValue: Area, newValue: Area) => {
176      let newWidth = Number(newValue.width)
177      if (!this.hidesBackButton) {
178        newWidth -= ImageMenuItem.imageHotZoneWidth
179        newWidth += SelectTitleBar.leftPadding
180        newWidth -= SelectTitleBar.leftPaddingWithBack
181      }
182      if (this.menuItems !== undefined) {
183        let menusLength = this.menuItems.length
184        if (menusLength >= CollapsibleMenuSection.maxCountOfVisibleItems) {
185          newWidth -= ImageMenuItem.imageHotZoneWidth * CollapsibleMenuSection.maxCountOfVisibleItems
186        } else if (menusLength > 0) {
187          newWidth -= ImageMenuItem.imageHotZoneWidth * menusLength
188        }
189      }
190      if (this.badgeValue !== undefined) {
191        this.selectMaxWidth = newWidth - SelectTitleBar.badgeSize - SelectTitleBar.leftPadding - SelectTitleBar.rightPadding - SelectTitleBar.badgePadding
192      } else {
193        this.selectMaxWidth = newWidth - SelectTitleBar.leftPadding - SelectTitleBar.rightPadding
194      }
195    })
196  }
197}
198
199@Component
200struct CollapsibleMenuSection {
201  menuItems: Array<SelectTitleBarMenuItem>
202  index: number
203
204  static readonly maxCountOfVisibleItems = 3
205  private static readonly focusPadding = 4
206  private static readonly marginsNum = 2
207  private firstFocusableIndex = -1
208
209  @State isPopupShown: boolean = false
210
211  @State isMoreIconOnFocus: boolean = false
212  @State isMoreIconOnHover: boolean = false
213  @State isMoreIconOnClick: boolean = false
214
215  getMoreIconFgColor() {
216    return this.isMoreIconOnClick
217      ? $r('sys.color.ohos_id_color_titlebar_icon_pressed')
218      : $r('sys.color.ohos_id_color_titlebar_icon')
219  }
220
221  getMoreIconBgColor() {
222    if (this.isMoreIconOnClick) {
223      return $r('sys.color.ohos_id_color_click_effect')
224    } else if (this.isMoreIconOnHover) {
225      return $r('sys.color.ohos_id_color_hover')
226    } else {
227      return Color.Transparent
228    }
229  }
230
231  aboutToAppear() {
232    this.menuItems.forEach((item, index) => {
233      if (item.isEnabled && this.firstFocusableIndex == -1 && index > CollapsibleMenuSection.maxCountOfVisibleItems - 2) {
234        this.firstFocusableIndex = this.index * 1000 + index + 1
235      }
236    })
237  }
238
239  build() {
240    Column() {
241      Row() {
242        if (this.menuItems.length <= CollapsibleMenuSection.maxCountOfVisibleItems) {
243          ForEach(this.menuItems, (item, index) => {
244            ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 })
245          })
246        } else {
247          ForEach(this.menuItems.slice(0, CollapsibleMenuSection.maxCountOfVisibleItems - 1), (item, index) => {
248            ImageMenuItem({ item: item, index: this.index * 1000 + index + 1 })
249          })
250
251          Row() {
252            Image(PUBLIC_MORE)
253              .width(ImageMenuItem.imageSize)
254              .height(ImageMenuItem.imageSize)
255              .focusable(true)
256          }
257          .width(ImageMenuItem.imageHotZoneWidth)
258          .height(ImageMenuItem.imageHotZoneWidth)
259          .borderRadius(ImageMenuItem.buttonBorderRadius)
260          .foregroundColor(this.getMoreIconFgColor())
261          .backgroundColor(this.getMoreIconBgColor())
262          .justifyContent(FlexAlign.Center)
263          .stateStyles({
264            focused: {
265              .border({
266                radius: $r('sys.float.ohos_id_corner_radius_clicked'),
267                width: ImageMenuItem.focusBorderWidth,
268                color: $r('sys.color.ohos_id_color_focused_outline'),
269                style: BorderStyle.Solid
270              })
271            },
272            normal: {
273              .border({
274                radius: $r('sys.float.ohos_id_corner_radius_clicked'),
275                width: 0
276              })
277            }
278          })
279          .onFocus(() => this.isMoreIconOnFocus = true)
280          .onBlur(() => this.isMoreIconOnFocus = false)
281          .onHover((isOn) => this.isMoreIconOnHover = isOn)
282          .onKeyEvent((event) => {
283            if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) {
284              return
285            }
286            if (event.type === KeyType.Down) {
287              this.isMoreIconOnClick = true
288            }
289            if (event.type === KeyType.Up) {
290              this.isMoreIconOnClick = false
291            }
292          })
293          .onTouch((event) => {
294            if (event.type === TouchType.Down) {
295              this.isMoreIconOnClick = true
296            }
297            if (event.type === TouchType.Up) {
298              this.isMoreIconOnClick = false
299            }
300          })
301          .onClick(() => this.isPopupShown = true)
302          .bindPopup(this.isPopupShown, {
303            builder: this.popupBuilder,
304            placement: Placement.Bottom,
305            popupColor: Color.White,
306            enableArrow: false,
307            onStateChange: (e) => {
308              this.isPopupShown = e.isVisible
309              if (!e.isVisible) {
310                this.isMoreIconOnClick = false
311              }
312            }
313          })
314        }
315      }
316    }
317    .height('100%')
318    .margin({ right: $r('sys.float.ohos_id_default_padding_end') })
319    .justifyContent(FlexAlign.Center)
320  }
321
322  @Builder
323  popupBuilder() {
324    Column() {
325      ForEach(this.menuItems.slice(CollapsibleMenuSection.maxCountOfVisibleItems - 1, this.menuItems.length), (item, index) => {
326        ImageMenuItem({ item: item, index: this.index * 1000 + CollapsibleMenuSection.maxCountOfVisibleItems + index })
327      })
328    }
329    .width(ImageMenuItem.imageHotZoneWidth + CollapsibleMenuSection.focusPadding * CollapsibleMenuSection.marginsNum)
330    .margin({ top: CollapsibleMenuSection.focusPadding, bottom: CollapsibleMenuSection.focusPadding })
331    .onAppear(() => {
332      focusControl.requestFocus(ImageMenuItem.focusablePrefix + this.firstFocusableIndex)
333    })
334  }
335}
336
337@Component
338struct ImageMenuItem {
339  item: SelectTitleBarMenuItem
340  index: number
341
342  static readonly imageSize = 24
343  static readonly imageHotZoneWidth = 48
344  static readonly buttonBorderRadius = 8
345  static readonly focusBorderWidth = 2
346  static readonly disabledImageOpacity = 0.4
347  static readonly focusablePrefix = "Id-SelectTitleBar-ImageMenuItem-"
348
349  @State isOnFocus: boolean = false
350  @State isOnHover: boolean = false
351  @State isOnClick: boolean = false
352
353  getFgColor() {
354    return this.isOnClick
355      ? $r('sys.color.ohos_id_color_titlebar_icon_pressed')
356      : $r('sys.color.ohos_id_color_titlebar_icon')
357  }
358
359  getBgColor() {
360    if (this.isOnClick) {
361      return $r('sys.color.ohos_id_color_click_effect')
362    } else if (this.isOnHover) {
363      return $r('sys.color.ohos_id_color_hover')
364    } else {
365      return Color.Transparent
366    }
367  }
368
369  build() {
370    Row() {
371      Image(this.item.value)
372        .width(ImageMenuItem.imageSize)
373        .height(ImageMenuItem.imageSize)
374        .focusable(this.item.isEnabled)
375        .key(ImageMenuItem.focusablePrefix + this.index)
376    }
377    .width(ImageMenuItem.imageHotZoneWidth)
378    .height(ImageMenuItem.imageHotZoneWidth)
379    .borderRadius(ImageMenuItem.buttonBorderRadius)
380    .foregroundColor(this.getFgColor())
381    .backgroundColor(this.getBgColor())
382    .justifyContent(FlexAlign.Center)
383    .opacity(this.item.isEnabled ? 1 : ImageMenuItem.disabledImageOpacity)
384    .stateStyles({
385      focused: {
386        .border({
387          radius: $r('sys.float.ohos_id_corner_radius_clicked'),
388          width: ImageMenuItem.focusBorderWidth,
389          color: $r('sys.color.ohos_id_color_focused_outline'),
390          style: BorderStyle.Solid
391        })
392      },
393      normal: {
394        .border({
395          radius: $r('sys.float.ohos_id_corner_radius_clicked'),
396          width: 0
397        })
398      }
399    })
400    .onFocus(() => {
401      if (!this.item.isEnabled) {
402        return
403      }
404      this.isOnFocus = true
405    })
406    .onBlur(() => this.isOnFocus = false)
407    .onHover((isOn) => {
408      if (!this.item.isEnabled) {
409        return
410      }
411      this.isOnHover = isOn
412    })
413    .onKeyEvent((event) => {
414      if (!this.item.isEnabled) {
415        return
416      }
417      if (event.keyCode !== KeyCode.KEYCODE_ENTER && event.keyCode !== KeyCode.KEYCODE_SPACE) {
418        return
419      }
420      if (event.type === KeyType.Down) {
421        this.isOnClick = true
422      }
423      if (event.type === KeyType.Up) {
424        this.isOnClick = false
425      }
426    })
427    .onTouch((event) => {
428      if (!this.item.isEnabled) {
429        return
430      }
431      if (event.type === TouchType.Down) {
432        this.isOnClick = true
433      }
434      if (event.type === TouchType.Up) {
435        this.isOnClick = false
436      }
437    })
438    .onClick(() => this.item.isEnabled && this.item.action && this.item.action())
439  }
440}
441
442export default { SelectTitleBar }