View Javadoc

1   /*
2    *  XNap Commons
3    *
4    *  Copyright (C) 2005  Felix Berger
5    *
6    *  This library is free software; you can redistribute it and/or
7    *  modify it under the terms of the GNU Lesser General Public
8    *  License as published by the Free Software Foundation; either
9    *  version 2.1 of the License, or (at your option) any later version.
10   *
11   *  This library is distributed in the hope that it will be useful,
12   *  but WITHOUT ANY WARRANTY; without even the implied warranty of
13   *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14   *  Lesser General Public License for more details.
15   *
16   *  You should have received a copy of the GNU Lesser General Public
17   *  License along with this library; if not, write to the Free Software
18   *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
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 		// TODO test this part and maybe update it to the getPreviousWord() code
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 			// special case, because wordIterator would tell us it's a boundary at any rate
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 }