1 /*
2  * Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 package java.awt.event;
27 
28 import java.awt.AWTEvent;
29 import java.awt.Component;
30 import java.awt.EventQueue;
31 import java.awt.font.TextHitInfo;
32 import java.io.IOException;
33 import java.io.ObjectInputStream;
34 import java.io.Serial;
35 import java.lang.annotation.Native;
36 import java.text.AttributedCharacterIterator;
37 import java.text.CharacterIterator;
38 
39 import sun.awt.AWTAccessor;
40 import sun.awt.AppContext;
41 import sun.awt.SunToolkit;
42 
43 /**
44  * Input method events contain information about text that is being
45  * composed using an input method. Whenever the text changes, the
46  * input method sends an event. If the text component that's currently
47  * using the input method is an active client, the event is dispatched
48  * to that component. Otherwise, it is dispatched to a separate
49  * composition window.
50  *
51  * <p>
52  * The text included with the input method event consists of two parts:
53  * committed text and composed text. Either part may be empty. The two
54  * parts together replace any uncommitted composed text sent in previous events,
55  * or the currently selected committed text.
56  * Committed text should be integrated into the text component's persistent
57  * data, it will not be sent again. Composed text may be sent repeatedly,
58  * with changes to reflect the user's editing operations. Committed text
59  * always precedes composed text.
60  *
61  * @author JavaSoft Asia/Pacific
62  * @since 1.2
63  */
64 public class InputMethodEvent extends AWTEvent {
65 
66     /**
67      * Use serialVersionUID from JDK 1.4 for interoperability.
68      */
69     @Serial
70     private static final long serialVersionUID = 4727190874778922661L;
71 
72     /**
73      * Marks the first integer id for the range of input method event ids.
74      */
75     @Native public static final int INPUT_METHOD_FIRST = 1100;
76 
77     /**
78      * The event type indicating changed input method text. This event is
79      * generated by input methods while processing input.
80      */
81     @Native public static final int INPUT_METHOD_TEXT_CHANGED = INPUT_METHOD_FIRST;
82 
83     /**
84      * The event type indicating a changed insertion point in input method text.
85      * This event is
86      * generated by input methods while processing input if only the caret changed.
87      */
88     @Native public static final int CARET_POSITION_CHANGED = INPUT_METHOD_FIRST + 1;
89 
90     /**
91      * Marks the last integer id for the range of input method event ids.
92      */
93     @Native public static final int INPUT_METHOD_LAST = INPUT_METHOD_FIRST + 1;
94 
95     /**
96      * The time stamp that indicates when the event was created.
97      *
98      * @serial
99      * @see #getWhen
100      * @since 1.4
101      */
102     long when;
103 
104     // Text object
105     private transient AttributedCharacterIterator text;
106     private transient int committedCharacterCount;
107     private transient TextHitInfo caret;
108     private transient TextHitInfo visiblePosition;
109 
110     /**
111      * Constructs an {@code InputMethodEvent} with the specified
112      * source component, type, time, text, caret, and visiblePosition.
113      * <p>
114      * The offsets of caret and visiblePosition are relative to the current
115      * composed text; that is, the composed text within {@code text}
116      * if this is an {@code INPUT_METHOD_TEXT_CHANGED} event,
117      * the composed text within the {@code text} of the
118      * preceding {@code INPUT_METHOD_TEXT_CHANGED} event otherwise.
119      * <p>Note that passing in an invalid {@code id} results in
120      * unspecified behavior. This method throws an
121      * {@code IllegalArgumentException} if {@code source}
122      * is {@code null}.
123      *
124      * @param source the object where the event originated
125      * @param id the event type
126      * @param when a long integer that specifies the time the event occurred
127      * @param text the combined committed and composed text,
128      *      committed text first; must be {@code null}
129      *      when the event type is {@code CARET_POSITION_CHANGED};
130      *      may be {@code null} for
131      *      {@code INPUT_METHOD_TEXT_CHANGED} if there's no
132      *      committed or composed text
133      * @param committedCharacterCount the number of committed
134      *      characters in the text
135      * @param caret the caret (a.k.a. insertion point);
136      *      {@code null} if there's no caret within current
137      *      composed text
138      * @param visiblePosition the position that's most important
139      *      to be visible; {@code null} if there's no
140      *      recommendation for a visible position within current
141      *      composed text
142      * @throws IllegalArgumentException if {@code id} is not
143      *      in the range
144      *      {@code INPUT_METHOD_FIRST}..{@code INPUT_METHOD_LAST};
145      *      or if id is {@code CARET_POSITION_CHANGED} and
146      *      {@code text} is not {@code null};
147      *      or if {@code committedCharacterCount} is not in the range
148      *      {@code 0}..{@code (text.getEndIndex() - text.getBeginIndex())}
149      * @throws IllegalArgumentException if {@code source} is null
150      *
151      * @since 1.4
152      */
InputMethodEvent(Component source, int id, long when, AttributedCharacterIterator text, int committedCharacterCount, TextHitInfo caret, TextHitInfo visiblePosition)153     public InputMethodEvent(Component source, int id, long when,
154             AttributedCharacterIterator text, int committedCharacterCount,
155             TextHitInfo caret, TextHitInfo visiblePosition) {
156         super(source, id);
157         if (id < INPUT_METHOD_FIRST || id > INPUT_METHOD_LAST) {
158             throw new IllegalArgumentException("id outside of valid range");
159         }
160 
161         if (id == CARET_POSITION_CHANGED && text != null) {
162             throw new IllegalArgumentException("text must be null for CARET_POSITION_CHANGED");
163         }
164 
165         this.when = when;
166         this.text = text;
167         int textLength = 0;
168         if (text != null) {
169             textLength = text.getEndIndex() - text.getBeginIndex();
170         }
171 
172         if (committedCharacterCount < 0 || committedCharacterCount > textLength) {
173             throw new IllegalArgumentException("committedCharacterCount outside of valid range");
174         }
175         this.committedCharacterCount = committedCharacterCount;
176 
177         this.caret = caret;
178         this.visiblePosition = visiblePosition;
179    }
180 
181     /**
182      * Constructs an {@code InputMethodEvent} with the specified
183      * source component, type, text, caret, and visiblePosition.
184      * <p>
185      * The offsets of caret and visiblePosition are relative to the current
186      * composed text; that is, the composed text within {@code text}
187      * if this is an {@code INPUT_METHOD_TEXT_CHANGED} event,
188      * the composed text within the {@code text} of the
189      * preceding {@code INPUT_METHOD_TEXT_CHANGED} event otherwise.
190      * The time stamp for this event is initialized by invoking
191      * {@link java.awt.EventQueue#getMostRecentEventTime()}.
192      * <p>Note that passing in an invalid {@code id} results in
193      * unspecified behavior. This method throws an
194      * {@code IllegalArgumentException} if {@code source}
195      * is {@code null}.
196      *
197      * @param source the object where the event originated
198      * @param id the event type
199      * @param text the combined committed and composed text,
200      *      committed text first; must be {@code null}
201      *      when the event type is {@code CARET_POSITION_CHANGED};
202      *      may be {@code null} for
203      *      {@code INPUT_METHOD_TEXT_CHANGED} if there's no
204      *      committed or composed text
205      * @param committedCharacterCount the number of committed
206      *      characters in the text
207      * @param caret the caret (a.k.a. insertion point);
208      *      {@code null} if there's no caret within current
209      *      composed text
210      * @param visiblePosition the position that's most important
211      *      to be visible; {@code null} if there's no
212      *      recommendation for a visible position within current
213      *      composed text
214      * @throws IllegalArgumentException if {@code id} is not
215      *      in the range
216      *      {@code INPUT_METHOD_FIRST}..{@code INPUT_METHOD_LAST};
217      *      or if id is {@code CARET_POSITION_CHANGED} and
218      *      {@code text} is not {@code null};
219      *      or if {@code committedCharacterCount} is not in the range
220      *      {@code 0}..{@code (text.getEndIndex() - text.getBeginIndex())}
221      * @throws IllegalArgumentException if {@code source} is null
222      */
InputMethodEvent(Component source, int id, AttributedCharacterIterator text, int committedCharacterCount, TextHitInfo caret, TextHitInfo visiblePosition)223     public InputMethodEvent(Component source, int id,
224             AttributedCharacterIterator text, int committedCharacterCount,
225             TextHitInfo caret, TextHitInfo visiblePosition) {
226         this(source, id,
227                 getMostRecentEventTimeForSource(source),
228                 text, committedCharacterCount,
229                 caret, visiblePosition);
230     }
231 
232     /**
233      * Constructs an {@code InputMethodEvent} with the
234      * specified source component, type, caret, and visiblePosition.
235      * The text is set to {@code null},
236      * {@code committedCharacterCount} to 0.
237      * <p>
238      * The offsets of {@code caret} and {@code visiblePosition}
239      * are relative to the current composed text; that is,
240      * the composed text within the {@code text} of the
241      * preceding {@code INPUT_METHOD_TEXT_CHANGED} event if the
242      * event being constructed as a {@code CARET_POSITION_CHANGED} event.
243      * For an {@code INPUT_METHOD_TEXT_CHANGED} event without text,
244      * {@code caret} and {@code visiblePosition} must be
245      * {@code null}.
246      * The time stamp for this event is initialized by invoking
247      * {@link java.awt.EventQueue#getMostRecentEventTime()}.
248      * <p>Note that passing in an invalid {@code id} results in
249      * unspecified behavior. This method throws an
250      * {@code IllegalArgumentException} if {@code source}
251      * is {@code null}.
252      *
253      * @param source the object where the event originated
254      * @param id the event type
255      * @param caret the caret (a.k.a. insertion point);
256      *      {@code null} if there's no caret within current
257      *      composed text
258      * @param visiblePosition the position that's most important
259      *      to be visible; {@code null} if there's no
260      *      recommendation for a visible position within current
261      *      composed text
262      * @throws IllegalArgumentException if {@code id} is not
263      *      in the range
264      *      {@code INPUT_METHOD_FIRST}..{@code INPUT_METHOD_LAST}
265      * @throws IllegalArgumentException if {@code source} is null
266      */
InputMethodEvent(Component source, int id, TextHitInfo caret, TextHitInfo visiblePosition)267     public InputMethodEvent(Component source, int id, TextHitInfo caret,
268             TextHitInfo visiblePosition) {
269         this(source, id,
270                 getMostRecentEventTimeForSource(source),
271                 null, 0, caret, visiblePosition);
272     }
273 
274     /**
275      * Gets the combined committed and composed text.
276      * Characters from index 0 to index {@code getCommittedCharacterCount() - 1} are committed
277      * text, the remaining characters are composed text.
278      *
279      * @return the text.
280      * Always null for CARET_POSITION_CHANGED;
281      * may be null for INPUT_METHOD_TEXT_CHANGED if there's no composed or committed text.
282      */
getText()283     public AttributedCharacterIterator getText() {
284         return text;
285     }
286 
287     /**
288      * Gets the number of committed characters in the text.
289      * @return the number of committed characters in the text
290      */
getCommittedCharacterCount()291     public int getCommittedCharacterCount() {
292         return committedCharacterCount;
293     }
294 
295     /**
296      * Gets the caret.
297      * <p>
298      * The offset of the caret is relative to the current
299      * composed text; that is, the composed text within getText()
300      * if this is an {@code INPUT_METHOD_TEXT_CHANGED} event,
301      * the composed text within getText() of the
302      * preceding {@code INPUT_METHOD_TEXT_CHANGED} event otherwise.
303      *
304      * @return the caret (a.k.a. insertion point).
305      * Null if there's no caret within current composed text.
306      */
getCaret()307     public TextHitInfo getCaret() {
308         return caret;
309     }
310 
311     /**
312      * Gets the position that's most important to be visible.
313      * <p>
314      * The offset of the visible position is relative to the current
315      * composed text; that is, the composed text within getText()
316      * if this is an {@code INPUT_METHOD_TEXT_CHANGED} event,
317      * the composed text within getText() of the
318      * preceding {@code INPUT_METHOD_TEXT_CHANGED} event otherwise.
319      *
320      * @return the position that's most important to be visible.
321      * Null if there's no recommendation for a visible position within current composed text.
322      */
getVisiblePosition()323     public TextHitInfo getVisiblePosition() {
324         return visiblePosition;
325     }
326 
327     /**
328      * Consumes this event so that it will not be processed
329      * in the default manner by the source which originated it.
330      */
consume()331     public void consume() {
332         consumed = true;
333     }
334 
335     /**
336      * Returns whether or not this event has been consumed.
337      * @see #consume
338      */
isConsumed()339     public boolean isConsumed() {
340         return consumed;
341     }
342 
343     /**
344      * Returns the time stamp of when this event occurred.
345      *
346      * @return this event's timestamp
347      * @since 1.4
348      */
getWhen()349     public long getWhen() {
350       return when;
351     }
352 
353     /**
354      * Returns a parameter string identifying this event.
355      * This method is useful for event-logging and for debugging.
356      * It contains the event ID in text form, the characters of the
357      * committed and composed text
358      * separated by "+", the number of committed characters,
359      * the caret, and the visible position.
360      *
361      * @return a string identifying the event and its attributes
362      */
paramString()363     public String paramString() {
364         String typeStr;
365         switch(id) {
366           case INPUT_METHOD_TEXT_CHANGED:
367               typeStr = "INPUT_METHOD_TEXT_CHANGED";
368               break;
369           case CARET_POSITION_CHANGED:
370               typeStr = "CARET_POSITION_CHANGED";
371               break;
372           default:
373               typeStr = "unknown type";
374         }
375 
376         String textString;
377         if (text == null) {
378             textString = "no text";
379         } else {
380             StringBuilder textBuffer = new StringBuilder("\"");
381             int committedCharacterCount = this.committedCharacterCount;
382             char c = text.first();
383             while (committedCharacterCount-- > 0) {
384                 textBuffer.append(c);
385                 c = text.next();
386             }
387             textBuffer.append("\" + \"");
388             while (c != CharacterIterator.DONE) {
389                 textBuffer.append(c);
390                 c = text.next();
391             }
392             textBuffer.append("\"");
393             textString = textBuffer.toString();
394         }
395 
396         String countString = committedCharacterCount + " characters committed";
397 
398         String caretString;
399         if (caret == null) {
400             caretString = "no caret";
401         } else {
402             caretString = "caret: " + caret.toString();
403         }
404 
405         String visiblePositionString;
406         if (visiblePosition == null) {
407             visiblePositionString = "no visible position";
408         } else {
409             visiblePositionString = "visible position: " + visiblePosition.toString();
410         }
411 
412         return typeStr + ", " + textString + ", " + countString + ", " + caretString + ", " + visiblePositionString;
413     }
414 
415     /**
416      * Initializes the {@code when} field if it is not present in the
417      * object input stream. In that case, the field will be initialized by
418      * invoking {@link java.awt.EventQueue#getMostRecentEventTime()}.
419      *
420      * @param  s the {@code ObjectInputStream} to read
421      * @throws ClassNotFoundException if the class of a serialized object could
422      *         not be found
423      * @throws IOException if an I/O error occurs
424      */
425     @Serial
readObject(ObjectInputStream s)426     private void readObject(ObjectInputStream s) throws ClassNotFoundException, IOException {
427         s.defaultReadObject();
428         if (when == 0) {
429             // Can't use getMostRecentEventTimeForSource because source is always null during deserialization
430             when = EventQueue.getMostRecentEventTime();
431         }
432     }
433 
434     /**
435      * Get the most recent event time in the {@code EventQueue} which the {@code source}
436      * belongs to.
437      *
438      * @param source the source of the event
439      * @exception  IllegalArgumentException  if source is null.
440      * @return most recent event time in the {@code EventQueue}
441      */
getMostRecentEventTimeForSource(Object source)442     private static long getMostRecentEventTimeForSource(Object source) {
443         if (source == null) {
444             // throw the IllegalArgumentException to conform to EventObject spec
445             throw new IllegalArgumentException("null source");
446         }
447         AppContext appContext = SunToolkit.targetToAppContext(source);
448         EventQueue eventQueue = SunToolkit.getSystemEventQueueImplPP(appContext);
449         return AWTAccessor.getEventQueueAccessor().getMostRecentEventTime(eventQueue);
450     }
451 }
452