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.event; 18 19 import android.text.SpannableStringBuilder; 20 import android.text.TextUtils; 21 22 import com.android.inputmethod.latin.Constants; 23 24 import java.util.ArrayList; 25 import java.util.HashMap; 26 27 import javax.annotation.Nonnull; 28 29 /** 30 * This class implements the logic chain between receiving events and generating code points. 31 * 32 * Event sources are multiple. It may be a hardware keyboard, a D-PAD, a software keyboard, 33 * or any exotic input source. 34 * This class will orchestrate the composing chain that starts with an event as its input. Each 35 * composer will be given turns one after the other. 36 * The output is composed of two sequences of code points: the first, representing the already 37 * finished combining part, will be shown normally as the composing string, while the second is 38 * feedback on the composing state and will typically be shown with different styling such as 39 * a colored background. 40 */ 41 public class CombinerChain { 42 // The already combined text, as described above 43 private StringBuilder mCombinedText; 44 // The feedback on the composing state, as described above 45 private SpannableStringBuilder mStateFeedback; 46 private final ArrayList<Combiner> mCombiners; 47 48 private static final HashMap<String, Class<? extends Combiner>> IMPLEMENTED_COMBINERS = 49 new HashMap<>(); 50 static { 51 IMPLEMENTED_COMBINERS.put("MyanmarReordering", MyanmarReordering.class); 52 } 53 private static final String COMBINER_SPEC_SEPARATOR = ";"; 54 55 /** 56 * Create an combiner chain. 57 * 58 * The combiner chain takes events as inputs and outputs code points and combining state. 59 * For example, if the input language is Japanese, the combining chain will typically perform 60 * kana conversion. This takes a string for initial text, taken to be present before the 61 * cursor: we'll start after this. 62 * 63 * @param initialText The text that has already been combined so far. 64 * @param combinerList A list of combiners to be applied in order. 65 */ 66 public CombinerChain(final String initialText, final Combiner... combinerList) { 67 mCombiners = new ArrayList<>(); 68 // The dead key combiner is always active, and always first 69 mCombiners.add(new DeadKeyCombiner()); 70 for (final Combiner combiner : combinerList) { 71 mCombiners.add(combiner); 72 } 73 mCombinedText = new StringBuilder(initialText); 74 mStateFeedback = new SpannableStringBuilder(); 75 } 76 77 public void reset() { 78 mCombinedText.setLength(0); 79 mStateFeedback.clear(); 80 for (final Combiner c : mCombiners) { 81 c.reset(); 82 } 83 } 84 85 private void updateStateFeedback() { 86 mStateFeedback.clear(); 87 for (int i = mCombiners.size() - 1; i >= 0; --i) { 88 mStateFeedback.append(mCombiners.get(i).getCombiningStateFeedback()); 89 } 90 } 91 92 /** 93 * Process an event through the combining chain, and return a processed event to apply. 94 * @param previousEvents the list of previous events in this composition 95 * @param newEvent the new event to process 96 * @return the processed event. It may be the same event, or a consumed event, or a completely 97 * new event. However it may never be null. 98 */ 99 @Nonnull 100 public Event processEvent(final ArrayList<Event> previousEvents, final Event newEvent) { 101 final ArrayList<Event> modifiablePreviousEvents = new ArrayList<>(previousEvents); 102 Event event = newEvent; 103 for (final Combiner combiner : mCombiners) { 104 // A combiner can never return more than one event; it can return several 105 // code points, but they should be encapsulated within one event. 106 event = combiner.processEvent(modifiablePreviousEvents, event); 107 if (event.isConsumed()) { 108 // If the event is consumed, then we don't pass it to subsequent combiners: 109 // they should not see it at all. 110 break; 111 } 112 } 113 updateStateFeedback(); 114 return event; 115 } 116 117 /** 118 * Apply a processed event. 119 * @param event the event to be applied 120 */ 121 public void applyProcessedEvent(final Event event) { 122 if (null != event) { 123 // TODO: figure out the generic way of doing this 124 if (Constants.CODE_DELETE == event.mKeyCode) { 125 final int length = mCombinedText.length(); 126 if (length > 0) { 127 final int lastCodePoint = mCombinedText.codePointBefore(length); 128 mCombinedText.delete(length - Character.charCount(lastCodePoint), length); 129 } 130 } else { 131 final CharSequence textToCommit = event.getTextToCommit(); 132 if (!TextUtils.isEmpty(textToCommit)) { 133 mCombinedText.append(textToCommit); 134 } 135 } 136 } 137 updateStateFeedback(); 138 } 139 140 /** 141 * Get the char sequence that should be displayed as the composing word. It may include 142 * styling spans. 143 */ 144 public CharSequence getComposingWordWithCombiningFeedback() { 145 final SpannableStringBuilder s = new SpannableStringBuilder(mCombinedText); 146 return s.append(mStateFeedback); 147 } 148 149 public static Combiner[] createCombiners(final String spec) { 150 if (TextUtils.isEmpty(spec)) { 151 return new Combiner[0]; 152 } 153 final String[] combinerDescriptors = spec.split(COMBINER_SPEC_SEPARATOR); 154 final Combiner[] combiners = new Combiner[combinerDescriptors.length]; 155 int i = 0; 156 for (final String combinerDescriptor : combinerDescriptors) { 157 final Class<? extends Combiner> combinerClass = 158 IMPLEMENTED_COMBINERS.get(combinerDescriptor); 159 if (null == combinerClass) { 160 throw new RuntimeException("Unknown combiner descriptor: " + combinerDescriptor); 161 } 162 try { 163 combiners[i++] = combinerClass.newInstance(); 164 } catch (InstantiationException e) { 165 throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, 166 e); 167 } catch (IllegalAccessException e) { 168 throw new RuntimeException("Unable to instantiate combiner: " + combinerDescriptor, 169 e); 170 } 171 } 172 return combiners; 173 } 174 } 175