1 /* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.inputmethod.latin.settings; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.preference.DialogPreference; 26 import android.preference.Preference; 27 import android.util.Log; 28 import android.view.View; 29 import android.view.inputmethod.InputMethodInfo; 30 import android.view.inputmethod.InputMethodSubtype; 31 import android.widget.ArrayAdapter; 32 import android.widget.Spinner; 33 import android.widget.SpinnerAdapter; 34 35 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils; 36 import com.android.inputmethod.compat.ViewCompatUtils; 37 import com.android.inputmethod.latin.R; 38 import com.android.inputmethod.latin.RichInputMethodManager; 39 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils; 40 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils; 41 42 import java.util.TreeSet; 43 44 final class CustomInputStylePreference extends DialogPreference 45 implements DialogInterface.OnCancelListener { 46 private static final boolean DEBUG_SUBTYPE_ID = false; 47 48 interface Listener { 49 public void onRemoveCustomInputStyle(CustomInputStylePreference stylePref); 50 public void onSaveCustomInputStyle(CustomInputStylePreference stylePref); 51 public void onAddCustomInputStyle(CustomInputStylePreference stylePref); 52 public SubtypeLocaleAdapter getSubtypeLocaleAdapter(); 53 public KeyboardLayoutSetAdapter getKeyboardLayoutSetAdapter(); 54 } 55 56 private static final String KEY_PREFIX = "subtype_pref_"; 57 private static final String KEY_NEW_SUBTYPE = KEY_PREFIX + "new"; 58 59 private InputMethodSubtype mSubtype; 60 private InputMethodSubtype mPreviousSubtype; 61 62 private final Listener mProxy; 63 private Spinner mSubtypeLocaleSpinner; 64 private Spinner mKeyboardLayoutSetSpinner; 65 66 public static CustomInputStylePreference newIncompleteSubtypePreference( 67 final Context context, final Listener proxy) { 68 return new CustomInputStylePreference(context, null, proxy); 69 } 70 71 public CustomInputStylePreference(final Context context, final InputMethodSubtype subtype, 72 final Listener proxy) { 73 super(context, null); 74 setDialogLayoutResource(R.layout.additional_subtype_dialog); 75 setPersistent(false); 76 mProxy = proxy; 77 setSubtype(subtype); 78 } 79 80 public void show() { 81 showDialog(null); 82 } 83 84 public final boolean isIncomplete() { 85 return mSubtype == null; 86 } 87 88 public InputMethodSubtype getSubtype() { 89 return mSubtype; 90 } 91 92 public void setSubtype(final InputMethodSubtype subtype) { 93 mPreviousSubtype = mSubtype; 94 mSubtype = subtype; 95 if (isIncomplete()) { 96 setTitle(null); 97 setDialogTitle(R.string.add_style); 98 setKey(KEY_NEW_SUBTYPE); 99 } else { 100 final String displayName = 101 SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype); 102 setTitle(displayName); 103 setDialogTitle(displayName); 104 setKey(KEY_PREFIX + subtype.getLocale() + "_" 105 + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype)); 106 } 107 } 108 109 public void revert() { 110 setSubtype(mPreviousSubtype); 111 } 112 113 public boolean hasBeenModified() { 114 return mSubtype != null && !mSubtype.equals(mPreviousSubtype); 115 } 116 117 @Override 118 protected View onCreateDialogView() { 119 final View v = super.onCreateDialogView(); 120 mSubtypeLocaleSpinner = (Spinner) v.findViewById(R.id.subtype_locale_spinner); 121 mSubtypeLocaleSpinner.setAdapter(mProxy.getSubtypeLocaleAdapter()); 122 mKeyboardLayoutSetSpinner = (Spinner) v.findViewById(R.id.keyboard_layout_set_spinner); 123 mKeyboardLayoutSetSpinner.setAdapter(mProxy.getKeyboardLayoutSetAdapter()); 124 // All keyboard layout names are in the Latin script and thus left to right. That means 125 // the view would align them to the left even if the system locale is RTL, but that 126 // would look strange. To fix this, we align them to the view's start, which will be 127 // natural for any direction. 128 ViewCompatUtils.setTextAlignment( 129 mKeyboardLayoutSetSpinner, ViewCompatUtils.TEXT_ALIGNMENT_VIEW_START); 130 return v; 131 } 132 133 @Override 134 protected void onPrepareDialogBuilder(final AlertDialog.Builder builder) { 135 builder.setCancelable(true).setOnCancelListener(this); 136 if (isIncomplete()) { 137 builder.setPositiveButton(R.string.add, this) 138 .setNegativeButton(android.R.string.cancel, this); 139 } else { 140 builder.setPositiveButton(R.string.save, this) 141 .setNeutralButton(android.R.string.cancel, this) 142 .setNegativeButton(R.string.remove, this); 143 final SubtypeLocaleItem localeItem = new SubtypeLocaleItem(mSubtype); 144 final KeyboardLayoutSetItem layoutItem = new KeyboardLayoutSetItem(mSubtype); 145 setSpinnerPosition(mSubtypeLocaleSpinner, localeItem); 146 setSpinnerPosition(mKeyboardLayoutSetSpinner, layoutItem); 147 } 148 } 149 150 private static void setSpinnerPosition(final Spinner spinner, final Object itemToSelect) { 151 final SpinnerAdapter adapter = spinner.getAdapter(); 152 final int count = adapter.getCount(); 153 for (int i = 0; i < count; i++) { 154 final Object item = spinner.getItemAtPosition(i); 155 if (item.equals(itemToSelect)) { 156 spinner.setSelection(i); 157 return; 158 } 159 } 160 } 161 162 @Override 163 public void onCancel(final DialogInterface dialog) { 164 if (isIncomplete()) { 165 mProxy.onRemoveCustomInputStyle(this); 166 } 167 } 168 169 @Override 170 public void onClick(final DialogInterface dialog, final int which) { 171 super.onClick(dialog, which); 172 switch (which) { 173 case DialogInterface.BUTTON_POSITIVE: 174 final boolean isEditing = !isIncomplete(); 175 final SubtypeLocaleItem locale = 176 (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem(); 177 final KeyboardLayoutSetItem layout = 178 (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem(); 179 final InputMethodSubtype subtype = 180 AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype( 181 locale.mLocaleString, layout.mLayoutName); 182 setSubtype(subtype); 183 notifyChanged(); 184 if (isEditing) { 185 mProxy.onSaveCustomInputStyle(this); 186 } else { 187 mProxy.onAddCustomInputStyle(this); 188 } 189 break; 190 case DialogInterface.BUTTON_NEUTRAL: 191 // Nothing to do 192 break; 193 case DialogInterface.BUTTON_NEGATIVE: 194 mProxy.onRemoveCustomInputStyle(this); 195 break; 196 } 197 } 198 199 @Override 200 protected Parcelable onSaveInstanceState() { 201 final Parcelable superState = super.onSaveInstanceState(); 202 final Dialog dialog = getDialog(); 203 if (dialog == null || !dialog.isShowing()) { 204 return superState; 205 } 206 207 final SavedState myState = new SavedState(superState); 208 myState.mSubtype = mSubtype; 209 return myState; 210 } 211 212 @Override 213 protected void onRestoreInstanceState(final Parcelable state) { 214 if (!(state instanceof SavedState)) { 215 super.onRestoreInstanceState(state); 216 return; 217 } 218 219 final SavedState myState = (SavedState) state; 220 super.onRestoreInstanceState(myState.getSuperState()); 221 setSubtype(myState.mSubtype); 222 } 223 224 static final class SavedState extends Preference.BaseSavedState { 225 InputMethodSubtype mSubtype; 226 227 public SavedState(final Parcelable superState) { 228 super(superState); 229 } 230 231 @Override 232 public void writeToParcel(final Parcel dest, final int flags) { 233 super.writeToParcel(dest, flags); 234 dest.writeParcelable(mSubtype, 0); 235 } 236 237 public SavedState(final Parcel source) { 238 super(source); 239 mSubtype = (InputMethodSubtype)source.readParcelable(null); 240 } 241 242 @SuppressWarnings("hiding") 243 public static final Parcelable.Creator<SavedState> CREATOR = 244 new Parcelable.Creator<SavedState>() { 245 @Override 246 public SavedState createFromParcel(final Parcel source) { 247 return new SavedState(source); 248 } 249 250 @Override 251 public SavedState[] newArray(final int size) { 252 return new SavedState[size]; 253 } 254 }; 255 } 256 257 static final class SubtypeLocaleItem implements Comparable<SubtypeLocaleItem> { 258 public final String mLocaleString; 259 private final String mDisplayName; 260 261 public SubtypeLocaleItem(final InputMethodSubtype subtype) { 262 mLocaleString = subtype.getLocale(); 263 mDisplayName = SubtypeLocaleUtils.getSubtypeLocaleDisplayNameInSystemLocale( 264 mLocaleString); 265 } 266 267 // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()} 268 // to get display name. 269 @Override 270 public String toString() { 271 return mDisplayName; 272 } 273 274 @Override 275 public int compareTo(final SubtypeLocaleItem o) { 276 return mLocaleString.compareTo(o.mLocaleString); 277 } 278 } 279 280 static final class SubtypeLocaleAdapter extends ArrayAdapter<SubtypeLocaleItem> { 281 private static final String TAG_SUBTYPE = SubtypeLocaleAdapter.class.getSimpleName(); 282 283 public SubtypeLocaleAdapter(final Context context) { 284 super(context, android.R.layout.simple_spinner_item); 285 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 286 287 final TreeSet<SubtypeLocaleItem> items = new TreeSet<>(); 288 final InputMethodInfo imi = RichInputMethodManager.getInstance() 289 .getInputMethodInfoOfThisIme(); 290 final int count = imi.getSubtypeCount(); 291 for (int i = 0; i < count; i++) { 292 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 293 if (DEBUG_SUBTYPE_ID) { 294 Log.d(TAG_SUBTYPE, String.format("%-6s 0x%08x %11d %s", 295 subtype.getLocale(), subtype.hashCode(), subtype.hashCode(), 296 SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype))); 297 } 298 if (InputMethodSubtypeCompatUtils.isAsciiCapable(subtype)) { 299 items.add(new SubtypeLocaleItem(subtype)); 300 } 301 } 302 // TODO: Should filter out already existing combinations of locale and layout. 303 addAll(items); 304 } 305 } 306 307 static final class KeyboardLayoutSetItem { 308 public final String mLayoutName; 309 private final String mDisplayName; 310 311 public KeyboardLayoutSetItem(final InputMethodSubtype subtype) { 312 mLayoutName = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype); 313 mDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype); 314 } 315 316 // {@link ArrayAdapter<T>} that hosts the instance of this class needs {@link #toString()} 317 // to get display name. 318 @Override 319 public String toString() { 320 return mDisplayName; 321 } 322 } 323 324 static final class KeyboardLayoutSetAdapter extends ArrayAdapter<KeyboardLayoutSetItem> { 325 public KeyboardLayoutSetAdapter(final Context context) { 326 super(context, android.R.layout.simple_spinner_item); 327 setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 328 329 final String[] predefinedKeyboardLayoutSet = context.getResources().getStringArray( 330 R.array.predefined_layouts); 331 // TODO: Should filter out already existing combinations of locale and layout. 332 for (final String layout : predefinedKeyboardLayoutSet) { 333 // This is a dummy subtype with NO_LANGUAGE, only for display. 334 final InputMethodSubtype subtype = 335 AdditionalSubtypeUtils.createDummyAdditionalSubtype( 336 SubtypeLocaleUtils.NO_LANGUAGE, layout); 337 add(new KeyboardLayoutSetItem(subtype)); 338 } 339 } 340 } 341 } 342