1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20 package org.xnap.commons.gui.completion;
21
22 import java.text.BreakIterator;
23 import java.util.ArrayList;
24 import javax.swing.text.BadLocationException;
25 import javax.swing.text.JTextComponent;
26 import javax.swing.text.Utilities;
27 import org.apache.commons.logging.Log;
28 import org.apache.commons.logging.LogFactory;
29
30 /***
31 * The Completion class follows the mediator pattern.
32 * <p>
33 * It manages the text component and the completion model and decouples
34 * them from the {@link CompletionMode} which decides when and how to
35 * offer completion to the user.
36 * <p>
37 * If you want to provide your own completion modes, install them in the
38 * {@link org.xnap.commons.gui.completion.CompletionModeFactory}.
39 * <p>
40 * During its lifetime the completion object is coupled to only one
41 * JTextComponent, it's a one-to-one mapping. To disable it, use
42 * {@link #setEnabled(boolean) setEnabled(false)}.
43 * <p>
44 * A completion object can be used as follows:
45 *
46 * <pre>
47 * JTextField jtf = new JTextField();
48 * Completion comp = new Completion(jtf, new FileCompletionModel());
49 * comp.setMode(new AutomaticDropDownCompletionMode());
50 * </pre>
51 *
52 * When typing into the text field above the text is matched against existing
53 * files on the filesystem and possible completions are presented according
54 * to the {@link org.xnap.commons.gui.completion.AutomaticDropDownCompletionMode}.
55 *
56 * @author Felix Berger
57 */
58 public class Completion
59 {
60 private static Log logger = LogFactory.getLog(Completion.class);
61
62 private CompletionModel model;
63 private CompletionMode mode;
64 private JTextComponent jtc;
65 private boolean wholeText;
66 private boolean enabled = true;
67 private BreakIterator wordIterator = null;
68
69 /***
70 * The list of CompletionModeListeners for this Completion instance.
71 */
72 private ArrayList<CompletionModeListener> listeners =
73 new ArrayList<CompletionModeListener>();
74
75 /***
76 * Constructs a new Completion object.
77 *
78 * {@link GlobalDefaultCompletionMode} is set as the default completion mode.
79 *
80 * @param textComponent the text component completion will be done for
81 * @param model the completion model that provides the possible completions
82 * @param wholeText if true the whole text of the text component is used
83 * for completion otherwise only the last word before the cursor is
84 * completed.
85 *
86 * @throws NullPointerException if <code>textComponent</code> is null.
87 */
88 public Completion(JTextComponent textComponent, CompletionModel model,
89 boolean wholeText)
90 {
91 if (textComponent == null) {
92 throw new NullPointerException("textComponent must not be null");
93 }
94 this.jtc = textComponent;
95 this.model = model;
96 this.wholeText = wholeText;
97 this.mode = new GlobalDefaultCompletionMode();
98 mode.enable(this);
99 }
100
101 /***
102 * Convenience wrapper for {@link #Completion(JTextComponent,
103 * CompletionModel, boolean)}.
104 *
105 * The missing completion model defaults to the {@link
106 * DefaultCompletionModel}.
107 */
108 public Completion(JTextComponent textComponent, boolean wholeText)
109 {
110 this(textComponent, new DefaultCompletionModel(), wholeText);
111 }
112
113 /***
114 * Convenience wrapper for {@link #Completion(JTextComponent,
115 * CompletionModel, boolean)}.
116 *
117 * The missing boolean defaults to <code>true</code>.
118 */
119 public Completion(JTextComponent textComponent, CompletionModel model)
120 {
121 this(textComponent, model, true);
122 }
123
124 /***
125 * Convenience wrapper for {@link #Completion(JTextComponent, boolean)}.
126 *
127 * The missing boolean defaults to <code>true</code>.
128 */
129 public Completion(JTextComponent textComponent)
130 {
131 this(textComponent, true);
132 }
133
134 /***
135 * Sets a new completion mode for the managed text component and enables it
136 * if the completion is currently enabled.
137 *
138 * The old mode is disabled and {@link CompletionModeListener
139 * CompletionModeListeners} are notified of the mode change.
140 *
141 * @param newMode the new completion mode
142 * @throws NullPointerException if <code>newMode</code> is
143 * <code>null</code>
144 */
145 public void setMode(CompletionMode newMode)
146 {
147 Class oldMode = mode.getClass();
148 if (enabled) {
149 mode.disable();
150 }
151 mode = newMode;
152 if (enabled) {
153 mode.enable(this);
154 }
155 fireModeChanged(oldMode, mode.getClass());
156 }
157
158 /***
159 * Sets the completion model.
160 *
161 * The current completion mode is disabled and enabled in order to be
162 * notified of this change.
163 *
164 * @param model the new completion model which will be used henceforth
165 */
166 public void setModel(CompletionModel model)
167 {
168 mode.disable();
169 this.model = model;
170 if (enabled) {
171 mode.enable(this);
172 }
173 }
174
175 /***
176 * Enables or disables the currently set completion mode.
177 */
178 public void setEnabled(boolean enabled)
179 {
180 if (this.enabled == enabled) {
181 return;
182 }
183 this.enabled = enabled;
184 if (enabled) {
185 mode.enable(this);
186 }
187 else {
188 mode.disable();
189 }
190 }
191
192 /***
193 * Returns whether the currently set completion mode is enabled.
194 */
195 public boolean isEnabled()
196 {
197 return enabled;
198 }
199
200 /***
201 * Returns the currently used model.
202 */
203 public CompletionModel getModel()
204 {
205 return model;
206 }
207
208 /***
209 * Returns the currently active completion mode.
210 */
211 public CompletionMode getMode()
212 {
213 return mode;
214 }
215
216 /***
217 * Returns the text component this completion mode is responsible for
218 */
219 public JTextComponent getTextComponent()
220 {
221 return jtc;
222 }
223
224 /***
225 * Returns whether this completion mode is supposed to complete the whole
226 * text of its text component made availabe through {@link
227 * JTextComponent#getText()} or just the last word before the cursor.
228 */
229 public final boolean isWholeTextCompletion()
230 {
231 return wholeText;
232 }
233
234 /***
235 * Convenience wrapper for {@link #setText(String, int, int)}.
236 *
237 * Sets the text without any selection and setts the cursor to the end of
238 * the set text.
239 *
240 * @param text
241 * the text to set
242 */
243 public void setText(String text)
244 {
245 setText(text, text.length(), text.length());
246 }
247
248 /***
249 * Sets the given text honoring the whole text mode.
250 *
251 * @param text
252 * the text to set
253 * @param selectionStart
254 * the offset of the selection start relative to the beginning of
255 * the set text. Can be greater than <code>selectionEnd</code>.
256 * @param selectionEnd
257 * the offset of the selection end relative to the beginning of
258 * the set text. This is where the cursor is afterwards.
259 *
260 * TODO why is this not public?
261 */
262 protected void setText(String text, int selectionStart, int selectionEnd)
263 {
264 if (isWholeTextCompletion()) {
265 jtc.setText(text);
266 jtc.setCaretPosition(selectionStart);
267 jtc.moveCaretPosition(selectionEnd);
268 }
269
270 else {
271 int offs = -1;
272 try {
273 offs = Utilities.getPreviousWord(jtc, jtc.getCaretPosition());
274 }
275 catch (BadLocationException e) {
276 logger.debug(e);
277 }
278 jtc.moveCaretPosition(offs);
279 jtc.replaceSelection(text);
280 jtc.setCaretPosition(offs + selectionStart);
281 jtc.moveCaretPosition(offs + selectionEnd);
282 }
283 }
284
285 private String getPreviousWord()
286 {
287 if (wordIterator == null) {
288 wordIterator = BreakIterator.getWordInstance();
289 }
290 int start = jtc.getCaretPosition();
291 try {
292 start = Utilities.getPreviousWord(jtc, start);
293 int length = jtc.getCaretPosition() - start;
294 if (length > 1) {
295 String ret = jtc.getText(start, length);
296 wordIterator.setText(ret);
297 if (!wordIterator.isBoundary(length - 1)) {
298 return ret;
299 }
300 }
301
302 else if (length == 1) {
303 String ret = jtc.getText(start, length);
304 if (Character.isLetterOrDigit(ret.charAt(0))) {
305 return ret;
306 }
307 }
308 }
309 catch (BadLocationException ble) {
310 }
311 return "";
312 }
313
314 /***
315 * Returns the text which should be completed.
316 *
317 * @return Returns {@link JTextComponent#getText()}if wholeText is true
318 * otherwise the word before the cursor.
319 */
320 public String getText()
321 {
322 if (wholeText) {
323 return jtc.getText();
324 }
325 else {
326 return getPreviousWord();
327 }
328 }
329
330 /***
331 * Adds a CompletionModeListener to the listener list.
332 * @param l the CompletionModeListener to be added
333 */
334 public void addCompletionModeListener(CompletionModeListener l)
335 {
336 synchronized (listeners) {
337 listeners.add(l);
338 }
339 }
340
341 /***
342 * Removes a CompletionModeListener from the listener list.
343 * @param l the CompletionModeListener to be removed
344 */
345 public void removeCompletionModeListener(CompletionModeListener l)
346 {
347 synchronized (listeners) {
348 listeners.remove(l);
349 }
350 }
351
352 /***
353 * Can be used by subclasses to fire a completion mode change in case they
354 * provide their own implementation of {@link #setMode(CompletionMode)}.
355 * @param oldMode the class of the old completion mode
356 * @param newMode the class of the new completion mode
357 */
358 protected void fireModeChanged(Class oldMode, Class newMode)
359 {
360 CompletionModeListener[] array;
361 synchronized (listeners) {
362 array = listeners.toArray(new CompletionModeListener[0]);
363 }
364 for (int i = array.length - 1; i >= 0; i--) {
365 array[i].modeChanged(oldMode, newMode);
366 }
367 }
368 }