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 }