Home | History | Annotate | Download | only in event
      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