1/*
2 * Copyright (c) 2023-2024 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 InputMethodSubtype from '@ohos.InputMethodSubtype';
17import inputMethod from '@ohos.inputMethod';
18import settings from '@ohos.settings';
19import common from '@ohos.app.ability.common';
20import deviceInfo from '@ohos.deviceInfo';
21import bundleManager from '@ohos.bundle.bundleManager';
22import inputMethodEngine from '@ohos.inputMethodEngine';
23
24export interface PatternOptions {
25  defaultSelected: number;
26  patterns: Array<Pattern>
27  action: (index: number) => void;
28}
29
30export interface Pattern {
31  icon: Resource;
32  selectedIcon: Resource;
33}
34
35interface SubType {
36  name: string;
37  id: string;
38  isCurrent: boolean;
39}
40
41@Extend(Divider)
42function divider() {
43  .height('1px')
44  .color('#10000000')
45  .margin({ left: 12, right: 12 })
46}
47
48@Extend(Text)
49function textStyle() {
50  .width('100%')
51  .fontWeight(400)
52  .maxLines(1)
53}
54
55const TAG: string = 'InputMethodListDialog';
56const BIG_IMAGE_SIZE: number = 30;
57const NORMAL_IMAGE_SIZE: number = 24;
58const BIG_DIALOG_WIDTH: number = 196;
59const NORMAL_DIALOG_WIDTH: number = 156;
60const BIG_FONT_SIZE: number = 20;
61const NORMAL_FONT_SIZE: number = 16;
62const BIG_ITEM_HEIGHT: number = 60;
63const NORMAL_ITEM_HEIGHT: number = 48;
64const NORMAL_IMAGE_BUTTON_WIDTH: number = 40;
65const NORMAL_IMAGE_BUTTON_HEIGHT: number = 32;
66const BIG_IMAGE_BUTTON_WIDTH: number = 50;
67const BIG_IMAGE_BUTTON_HEIGHT: number = 40;
68const NORMAL_COLUMN_PADDING: number = 4;
69const BIG_COLUMN_PADDING: number = 5;
70const NORMAL_IMAGE_RADIUS: number = 8;
71const BIG_IMAGE_RADIUS: number = 10;
72const NORMAL_FONT_PADDING: number = 12;
73const BIG_FONT_PADDING: number = 20;
74const NORMAL_ITEM_RADIUS: number = 16;
75const BIG_ITEM_RADIUS: number = 12;
76
77
78@CustomDialog
79export struct InputMethodListDialog {
80  private listBgColor: ResourceColor = '#ffffff';
81  private pressedColor: ResourceColor = '#1A000000'
82  private selectedColor: ResourceColor = '#220A59F7';
83  private fontColor: ResourceColor = '#E6000000';
84  private selectedFontColor: ResourceColor = '#0A59F7';
85  @State listItemHeight: number = NORMAL_ITEM_HEIGHT;
86  @State listItemRadius: number = NORMAL_IMAGE_RADIUS;
87  @State inputMethods: Array<inputMethod.InputMethodProperty> = [];
88  @State fontSize: number = NORMAL_FONT_SIZE;
89  @State fontPadding: number = NORMAL_FONT_PADDING;
90  @State dialogWidth: number = NORMAL_DIALOG_WIDTH;
91  @State imageSize: number = NORMAL_IMAGE_SIZE;
92  @State imageBtnWidth: number = NORMAL_IMAGE_BUTTON_WIDTH;
93  @State imageBtnHeight: number = NORMAL_IMAGE_BUTTON_HEIGHT;
94  @State columnPadding: number = NORMAL_COLUMN_PADDING;
95  @State imageRadius: number = NORMAL_IMAGE_RADIUS;
96  @State subTypes: Array<InputMethodSubtype> = [];
97  @State showHand: boolean = false;
98  @State inputMethodConfig: bundleManager.ElementName | undefined = undefined;
99  @State defaultInputMethod: inputMethod.InputMethodProperty | undefined = undefined;
100  @State currentInputMethod: inputMethod.InputMethodProperty | undefined = undefined;
101  @State currentSub: InputMethodSubtype | undefined = undefined;
102  @StorageLink('patternMode') patternMode: number | undefined = 0;
103  @StorageLink('maxListNum') maxListNum: number = 0;
104  private activeSubtypes: Array<SubType> = [];
105  controller: CustomDialogController = new CustomDialogController({ builder: undefined });
106  patternOptions?: PatternOptions;
107
108  async getDefaultInputMethodSubType(): Promise<void> {
109    console.info(`${TAG} getDefaultInputMethodSubType`);
110    this.inputMethodConfig = inputMethod.getSystemInputMethodConfigAbility();
111    if (this.inputMethodConfig) {
112      console.info(`${TAG} inputMethodConfig:  ${JSON.stringify(this.inputMethodConfig)}`);
113    }
114    this.inputMethods = await inputMethod.getSetting().getInputMethods(true);
115    this.defaultInputMethod = inputMethod.getDefaultInputMethod();
116    this.currentInputMethod = inputMethod.getCurrentInputMethod();
117    let index = 0;
118    for (let k = 0; k < this.inputMethods.length; k++) {
119      if (this.inputMethods[k].name === this.defaultInputMethod.name) {
120        index = k;
121        break;
122      }
123    }
124    this.inputMethods.splice(index, 1);
125    this.inputMethods.unshift(this.defaultInputMethod);
126    this.currentSub = inputMethod.getCurrentInputMethodSubtype();
127    console.info(`${TAG} defaultInput: ${JSON.stringify(this.defaultInputMethod)}`);
128    if (this.defaultInputMethod.name === this.currentInputMethod.name) {
129      if (this.patternOptions) {
130        if (AppStorage.get<number>('patternMode') === undefined) {
131          if (this.patternOptions.defaultSelected) {
132            this.patternMode = this.patternOptions.defaultSelected;
133          } else {
134            this.patternMode = 0;
135          }
136          AppStorage.setOrCreate('patternMode', this.patternMode);
137        } else {
138          this.patternMode = AppStorage.get<number>('patternMode');
139        }
140        this.showHand = true;
141      }
142    }
143    let context = getContext(this) as common.UIAbilityContext;
144    try {
145      let activeSubTypeStr = await settings.getValue(context, settings.input.ACTIVATED_INPUT_METHOD_SUB_MODE);
146      let activeSubType: SubType[] = JSON.parse(activeSubTypeStr);
147      if (activeSubType) {
148        console.info(`${TAG} activeSubType: ${JSON.stringify(activeSubType)}`);
149        for (let i = 0; i < this.inputMethods.length; i++) {
150          if (this.inputMethods[i].name === this.defaultInputMethod.name) {
151            this.defaultInputMethod = this.inputMethods[i];
152            let defaultSubTypes = await inputMethod.getSetting().listInputMethodSubtype(this.inputMethods[i]);
153            console.info(`${TAG} defaultSubTypes: ${JSON.stringify(defaultSubTypes)}`)
154            for (let k = 0; k < defaultSubTypes.length; k++) {
155              for (let j = 0; j < activeSubType.length; j++) {
156                if (activeSubType[j].id === defaultSubTypes[k].id) {
157                  this.subTypes.push(defaultSubTypes[k]);
158                  this.activeSubtypes.push(activeSubType[j]);
159                }
160              }
161            }
162          }
163        }
164        console.info(`${TAG} this.subTypes: ${JSON.stringify(this.subTypes)}`)
165        console.info(`${TAG} this.activeSubtypes: ${JSON.stringify(this.activeSubtypes)}`)
166      }
167    } catch (err) {
168      this.subTypes = [];
169      console.info(`${TAG} subTypes is empty, err = ${JSON.stringify(err)}`);
170    }
171  }
172
173  aboutToAppear(): void {
174    console.info(`${TAG} aboutToAppear`);
175    this.dialogWidth = NORMAL_DIALOG_WIDTH;
176    this.fontSize = NORMAL_FONT_SIZE;
177    this.imageSize = NORMAL_IMAGE_SIZE;
178    this.listItemHeight = NORMAL_ITEM_HEIGHT;
179    this.imageBtnWidth = NORMAL_IMAGE_BUTTON_WIDTH;
180    this.imageBtnHeight = NORMAL_IMAGE_BUTTON_HEIGHT;
181    this.columnPadding = NORMAL_COLUMN_PADDING;
182    this.fontPadding = NORMAL_FONT_PADDING;
183    this.listItemRadius = NORMAL_ITEM_RADIUS;
184    this.imageRadius = BIG_IMAGE_RADIUS;
185    this.getDefaultInputMethodSubType();
186    let inputMethodAbility = inputMethodEngine.getInputMethodAbility();
187    inputMethodAbility.on('keyboardHide', () => {
188      this.controller.close();
189    });
190  }
191
192  isDefaultInputMethodCurrentSubType(subTypeId: string): boolean {
193    return this.defaultInputMethod?.name === this.currentInputMethod?.name && this.currentSub?.id === subTypeId;
194  }
195
196  @Styles
197  listItemStyle() {
198    .padding({ left: this.fontPadding, right: this.fontPadding })
199    .height(this.listItemHeight)
200    .borderRadius(this.listItemRadius)
201  }
202
203  @Builder
204  InputMethodItem(name: string | undefined, fontColor: ResourceColor, normalColor: ResourceColor,
205                  pressedColor: ResourceColor, isDivider: boolean, handleClick: Function) {
206    Column() {
207      Row() {
208      Text(name)
209        .fontSize(this.fontSize)
210        .textStyle()
211        .listItemStyle()
212        .fontColor(fontColor)
213        .stateStyles({
214          pressed: {
215            .backgroundColor(pressedColor)
216          },
217          normal: {
218            .backgroundColor(normalColor)
219          }
220        })
221        Blank()
222        if (this.isDefaultInputMethodCurrentSubType(id, isSubType)) {
223          Image($r('app.media.ohos_ic_public_ok'))
224            .size({width: this.dialogStyle.listImageSize, height: this.dialogStyle.listImageSize})
225            .fillColor(this.dialogStyle.listFontColor)
226        }
227      }
228      if (isDivider) {
229        Divider()
230          .divider()
231      }
232    }
233    .width('100%')
234    .onClick(() => {
235      handleClick();
236    })
237  }
238
239  build() {
240    Column() {
241      if (this.inputMethodConfig && this.inputMethodConfig.bundleName.length > 0) {
242        Text($r('sys.string.ohos_id_input_method_settings'))
243          .textStyle()
244          .listItemStyle()
245          .fontSize(this.fontSize)
246          .fontColor(this.fontColor)
247          .stateStyles({
248            pressed: {
249              .backgroundColor(this.pressedColor)
250            },
251            normal: {
252              .backgroundColor(this.listBgColor)
253            }
254          })
255          .onClick(() => {
256            if (this.inputMethodConfig) {
257              let context = getContext(this) as common.UIAbilityContext;
258              context.startAbility({
259                bundleName: this.inputMethodConfig.bundleName,
260                moduleName: this.inputMethodConfig.moduleName,
261                abilityName: this.inputMethodConfig.abilityName,
262                uri: 'set_input'
263              });
264            }
265          })
266        Divider()
267          .divider()
268      }
269      Scroll() {
270        Column() {
271          ForEach(this.subTypes, (item: InputMethodSubtype, index: number) => {
272            this.InputMethodItem(this.activeSubtypes[index].name,
273              this.isDefaultInputMethodCurrentSubType(item.id) ? this.selectedFontColor : this.fontColor,
274              this.isDefaultInputMethodCurrentSubType(item.id) ? this.selectedColor : this.listBgColor,
275              this.pressedColor, this.inputMethods.length > 1 || index < this.subTypes.length,
276              () => {
277                this.switchMethodSub(item);
278              })
279          }, (item: inputMethod.InputMethodProperty) => JSON.stringify(item));
280
281          ForEach(this.inputMethods, (item: inputMethod.InputMethodProperty, index: number) => {
282            if (this.subTypes.length === 0 || (this.defaultInputMethod && item.name !== this.defaultInputMethod.name)) {
283              this.InputMethodItem(this.inputMethods[index].label,
284                this.currentInputMethod?.name === item.name ? this.selectedFontColor : this.fontColor,
285                this.currentInputMethod?.name === item.name ? this.selectedColor : this.listBgColor,
286                this.pressedColor,
287                index < this.inputMethods.length - 1,
288                () => {
289                  this.switchMethod(item);
290                })
291            }
292          }, (item: inputMethod.InputMethodProperty) => JSON.stringify(item));
293        }
294        .width('100%')
295      }
296      .width('100%')
297      .constraintSize({ maxHeight: this.maxListNum > 0 ? this.maxListNum * this.listItemHeight : '100%' })
298      .scrollBar(BarState.Off)
299
300      if (this.patternOptions && this.showHand) {
301        Divider()
302          .divider()
303        Row() {
304          ForEach(this.patternOptions.patterns, (item: Pattern, index: number) => {
305            Row() {
306              Image(index === this.patternMode ? item.selectedIcon : item.icon)
307                .size({ width: this.imageSize, height: this.imageSize })
308                .objectFit(ImageFit.Contain)
309            }
310            .justifyContent(FlexAlign.Center)
311            .size({ width: this.imageBtnWidth, height: this.imageBtnHeight })
312            .borderRadius(this.imageRadius)
313            .stateStyles({
314              pressed: {
315                .backgroundColor(this.pressedColor)
316              },
317              normal: {
318                .backgroundColor(this.listBgColor)
319              }
320            })
321            .onClick(() => {
322              this.switchPositionPattern(index);
323            })
324          }, (item: Resource) => JSON.stringify(item));
325        }
326        .width('100%')
327        .height(this.listItemHeight)
328        .justifyContent(FlexAlign.SpaceEvenly)
329      }
330    }
331    .width(this.dialogWidth)
332    .margin({ top: this.columnPadding })
333    .borderRadius('16vp')
334    .backgroundColor(this.listBgColor)
335    .padding(this.columnPadding)
336    .shadow(ShadowStyle.OUTER_DEFAULT_SM)
337  }
338
339  switchPositionPattern(mode: number): void {
340    if (this.patternOptions) {
341      this.patternMode = mode;
342      AppStorage.set('patternMode', this.patternMode);
343      console.info(`${TAG} this.handMode = ${this.patternMode}`);
344      this.patternOptions.action(this.patternMode);
345      this.controller.close();
346    }
347  }
348
349  async switchMethod(inputProperty: inputMethod.InputMethodProperty): Promise<void> {
350    if (this.currentInputMethod && this.currentInputMethod.name !== inputProperty.name) {
351      let subTypes = await inputMethod.getSetting().listInputMethodSubtype(inputProperty);
352      inputMethod.switchCurrentInputMethodAndSubtype(inputProperty, subTypes[0], (err: Error, result: boolean) => {
353        if (result) {
354          this.currentInputMethod = inputProperty;
355        }
356        this.controller.close();
357      })
358    }
359  }
360
361  switchMethodSub(inputSub: InputMethodSubtype): void {
362    if (this.currentInputMethod && this.defaultInputMethod) {
363      if (this.currentInputMethod.name !== this.defaultInputMethod.name) {
364        inputMethod.switchCurrentInputMethodAndSubtype(this.defaultInputMethod, inputSub, () => {
365          this.currentInputMethod = this.defaultInputMethod;
366          this.currentSub = inputSub;
367          this.controller.close();
368        })
369      } else {
370        inputMethod.switchCurrentInputMethodSubtype(inputSub, () => {
371          this.currentSub = inputSub;
372          this.controller.close();
373        })
374      }
375    }
376  }
377}