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