001: /*
002: * Copyright 2006-2007 Pentaho Corporation. All rights reserved.
003: * This software was developed by Pentaho Corporation and is provided under the terms
004: * of the Mozilla Public License, Version 1.1, or any later version. You may not use
005: * this file except in compliance with the license. If you need a copy of the license,
006: * please go to http://www.mozilla.org/MPL/MPL-1.1.txt.
007: *
008: * Software distributed under the Mozilla Public License is distributed on an "AS IS"
009: * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. Please refer to
010: * the license for the specific language governing your rights and limitations.
011: *
012: * Additional Contributor(s): Martin Schmid gridvision engineering GmbH
013: */
014: package org.pentaho.reportdesigner.lib.client.util;
015:
016: import org.jetbrains.annotations.NonNls;
017: import org.jetbrains.annotations.NotNull;
018: import org.jetbrains.annotations.Nullable;
019: import org.pentaho.reportdesigner.lib.client.commands.KeyStrokeUtil;
020:
021: import javax.swing.*;
022: import javax.swing.event.CaretEvent;
023: import javax.swing.event.CaretListener;
024: import javax.swing.text.BadLocationException;
025: import javax.swing.text.Utilities;
026: import java.awt.*;
027: import java.awt.event.ActionEvent;
028: import java.awt.event.ActionListener;
029: import java.awt.event.FocusEvent;
030: import java.awt.event.FocusListener;
031: import java.awt.event.KeyEvent;
032: import java.awt.event.MouseAdapter;
033: import java.awt.event.MouseEvent;
034: import java.util.SortedSet;
035: import java.util.TreeSet;
036: import java.util.logging.Level;
037: import java.util.logging.Logger;
038:
039: /**
040: * User: Martin
041: * Date: 11.02.2006
042: * Time: 21:00:51
043: */
044: public class TextFieldCompletionSupport {
045: @NonNls
046: @NotNull
047: private static final Logger LOG = Logger
048: .getLogger(TextFieldCompletionSupport.class.getName());
049:
050: /**
051: * Maximum number of entries shown in completion window.
052: */
053: private static final int MAX_COMPLETION_CHOICES = 10;
054:
055: //keys used to register actions
056: @NotNull
057: public static final String KEY_HIDE_COMPLETION_WINDOW = "hideCompletionWindow";
058: @NotNull
059: public static final String KEY_SELECTION_DOWN = "selectionDown";
060: @NotNull
061: public static final String KEY_SELECTION_UP = "selectionUp";
062: @NotNull
063: public static final String KEY_DELETE_SELECTED_CHOICE = "deleteSelectedChoice";
064: @NotNull
065: public static final String KEY_ACCEPT_COMPLETION_WITH_DEFAULT = "acceptCompletionWithDefault";
066: @NotNull
067: public static final String KEY_ACCEPT_COMPLETION = "acceptCompletion";
068:
069: private TextFieldCompletionSupport() {
070: }
071:
072: /**
073: * Adds auto completion support to a textfield. Note that the TreeSet will be modified.
074: *
075: * @param treeSet the completions initially available
076: * @param jTextField the textField to add the completion support
077: */
078: public static void initCompletionSupport(@NotNull
079: final TreeSet<String> treeSet, @NotNull
080: final JTextField jTextField) {
081: final CompleteWindow completeWindow = new CompleteWindow(
082: jTextField);
083:
084: jTextField.getActionMap().put(KEY_HIDE_COMPLETION_WINDOW,
085: new AbstractAction() {
086: public void actionPerformed(@NotNull
087: ActionEvent e) {
088: completeWindow.setVisible(false);
089: }
090: });
091:
092: jTextField.getInputMap().put(
093: KeyStrokeUtil.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
094: KEY_HIDE_COMPLETION_WINDOW);
095:
096: //let selection cycle through list (visually upwards)
097: jTextField.getActionMap().put(KEY_SELECTION_DOWN,
098: new AbstractAction() {
099: public void actionPerformed(@NotNull
100: ActionEvent e) {
101: if (completeWindow.getList().getSelectedIndex() < 0
102: || completeWindow.getList()
103: .getSelectedIndex() >= completeWindow
104: .getCompletionListModel()
105: .getSize() - 1) {
106: completeWindow.getList()
107: .setSelectedIndex(0);
108: return;
109: }
110: completeWindow.getList().setSelectedIndex(
111: completeWindow.getList()
112: .getSelectedIndex() + 1);
113: }
114: });
115:
116: jTextField.getInputMap().put(
117: KeyStrokeUtil.getKeyStroke(KeyEvent.VK_DOWN, 0),
118: KEY_SELECTION_DOWN);
119:
120: //let selection cycle through list (visually downwards)
121: jTextField.getActionMap().put(KEY_SELECTION_UP,
122: new AbstractAction() {
123: public void actionPerformed(@NotNull
124: ActionEvent e) {
125: if (completeWindow.getList().getSelectedIndex() <= 0) {
126: completeWindow.getList().setSelectedIndex(
127: completeWindow
128: .getCompletionListModel()
129: .getSize() - 1);
130: return;
131: }
132: completeWindow.getList().setSelectedIndex(
133: completeWindow.getList()
134: .getSelectedIndex() - 1);
135: }
136: });
137:
138: jTextField.getInputMap().put(
139: KeyStrokeUtil.getKeyStroke(KeyEvent.VK_UP, 0),
140: KEY_SELECTION_UP);
141:
142: final boolean[] ignoreInput = new boolean[] { false };//used to ignore actions when we are currently processing another action
143:
144: //action to delete the currently selected word from the list of possible completions
145: jTextField.getActionMap().put(KEY_DELETE_SELECTED_CHOICE,
146: new AbstractAction() {
147: public void actionPerformed(@NotNull
148: ActionEvent e) {
149: if (completeWindow.getList().getSelectedIndex() != -1) {
150: String value = String
151: .valueOf(completeWindow.getList()
152: .getSelectedValue());
153: treeSet.remove(value);
154: if (ignoreInput[0]) {
155: return;
156: }
157: updateWindow(treeSet, jTextField,
158: completeWindow);
159: }
160: }
161: });
162:
163: jTextField.getInputMap().put(
164: KeyStrokeUtil.getKeyStroke(KeyEvent.VK_DELETE,
165: KeyEvent.SHIFT_MASK),
166: KEY_DELETE_SELECTED_CHOICE);
167:
168: Object key = jTextField.getInputMap().get(
169: KeyStrokeUtil.getKeyStroke(KeyEvent.VK_ENTER, 0));
170: final Action enterAction = jTextField.getActionMap().get(key);
171:
172: //accepts the completion or triggers the action (if any) that was registered before
173: jTextField.getActionMap().put(
174: KEY_ACCEPT_COMPLETION_WITH_DEFAULT,
175: new AbstractAction() {
176: public void actionPerformed(@NotNull
177: ActionEvent e) {
178: int index = completeWindow.getList()
179: .getSelectedIndex();
180: if (completeWindow.isVisible() && index != -1) {
181: completeWord(jTextField, completeWindow,
182: ignoreInput);
183: } else if (enterAction != null) {
184: completeWindow.setVisible(false);
185: enterAction.actionPerformed(e);
186: }
187: }
188: });
189:
190: jTextField.getInputMap().put(
191: KeyStrokeUtil.getKeyStroke(KeyEvent.VK_ENTER, 0),
192: KEY_ACCEPT_COMPLETION_WITH_DEFAULT);
193: //jTextField.getInputMap().put(KeyStrokeUtil.getKeyStroke(KeyEvent.VK_TAB, 0), KEY_ACCEPT_COMPLETION);
194:
195: //accept the selected word in the list without doing any special processing
196: jTextField.getActionMap().put(KEY_ACCEPT_COMPLETION,
197: new AbstractAction() {
198: public void actionPerformed(@NotNull
199: ActionEvent e) {
200: int index = completeWindow.getList()
201: .getSelectedIndex();
202: if (completeWindow.isVisible() && index != -1) {
203: completeWord(jTextField, completeWindow,
204: ignoreInput);
205: }
206: }
207: });
208:
209: jTextField.getInputMap().put(
210: KeyStrokeUtil.getKeyStroke(KeyEvent.VK_SPACE,
211: KeyEvent.CTRL_MASK), KEY_ACCEPT_COMPLETION);
212:
213: jTextField.addCaretListener(new CaretListener() {
214: public void caretUpdate(@NotNull
215: CaretEvent e) {
216: if (ignoreInput[0]) {
217: return;
218: }
219: updateWindow(treeSet, jTextField, completeWindow);
220: }
221: });
222:
223: //we remember each text that was entered in the textfield when we leave the textfield or when hit enter
224: jTextField.addFocusListener(new FocusListener() {
225: public void focusGained(@NotNull
226: FocusEvent e) {
227: }
228:
229: public void focusLost(@NotNull
230: FocusEvent e) {
231: String text = jTextField.getText();
232: if (!"".equals(text)) {
233: treeSet.add(text);
234: }
235: completeWindow.setVisible(false);
236: }
237: });
238:
239: jTextField.addActionListener(new ActionListener() {
240: public void actionPerformed(@NotNull
241: ActionEvent e) {
242: String text = jTextField.getText();
243: if (!"".equals(text)) {
244: treeSet.add(text);
245: }
246: }
247: });
248:
249: //also complete the word if we click on an entry in the list
250: completeWindow.getList().addMouseListener(new MouseAdapter() {
251: public void mouseClicked(@NotNull
252: MouseEvent e) {
253: completeWord(jTextField, completeWindow, ignoreInput);
254: }
255: });
256: }
257:
258: /**
259: * Completes the word we are currently editing with the word selected in the list.
260: *
261: * @param jTextField the textfield
262: * @param completeWindow the window/popup used to display the completion choices
263: * @param ignoreInput used as out-parameter to ignore other actions while we are modifying the caret position.
264: */
265: private static void completeWord(@NotNull
266: JTextField jTextField, @NotNull
267: CompleteWindow completeWindow, @NotNull
268: boolean[] ignoreInput) {
269: String text = jTextField.getText();
270: String value = String.valueOf(completeWindow.getList()
271: .getSelectedValue());
272: ignoreInput[0] = true;
273:
274: int caretPos = jTextField.getCaretPosition();
275:
276: String word;
277: try {
278: if (caretPos == 0) {
279: completeWindow.setVisible(false);
280: return;
281: }
282:
283: int start = Utilities.getPreviousWord(jTextField, caretPos);
284: word = text.substring(start, caretPos);
285: String textToInsert = value.substring(word.length());
286: jTextField.getDocument().insertString(caretPos,
287: textToInsert, null);
288: } catch (BadLocationException ex) {
289: //exception is not useful for user in this case. Hopfully Hani does not see this...
290: }
291:
292: completeWindow.setVisible(false);
293: ignoreInput[0] = false;
294: }
295:
296: /**
297: * @param treeSet
298: * @param jTextField
299: * @param completeWindow
300: */
301: private static void updateWindow(@NotNull
302: final TreeSet<String> treeSet, @NotNull
303: final JTextField jTextField, @NotNull
304: final CompleteWindow completeWindow) {
305: if (!jTextField.isShowing()) {
306: return;
307: }
308:
309: String word = getCurrentEditingWord(jTextField, completeWindow);
310:
311: if (word == null || "".equals(word)) {
312: completeWindow.setVisible(false);
313: return;
314: }
315:
316: SortedSet<String> strings = treeSet.tailSet(word);
317: completeWindow.getCompletionListModel().clear();
318:
319: assembleCompletionChoices(strings, jTextField, word,
320: completeWindow);
321: }
322:
323: /**
324: * Find the word we are currently editing (the word at the caret).
325: *
326: * @return the editing word
327: */
328: @Nullable
329: private static String getCurrentEditingWord(@NotNull
330: JTextField jTextField, @NotNull
331: CompleteWindow completeWindow) {
332: try {
333: int caretPos = jTextField.getCaretPosition();
334: if (caretPos == 0) {
335: completeWindow.setVisible(false);
336: return null;
337: }
338: int start = Utilities.getPreviousWord(jTextField, caretPos);
339: String text = jTextField.getText();
340: return text.substring(start, caretPos);
341: } catch (BadLocationException e) {
342: if (LOG.isLoggable(Level.FINE))
343: LOG
344: .log(
345: Level.FINE,
346: "TextFieldCompletionSupport.getCurrentEditingWord ",
347: e);
348: completeWindow.setVisible(false);
349: return null;
350: }
351: }
352:
353: /**
354: * Find the words suitable as a completion choice. This are usually the completionChoices starting with the part the user is editing.
355: *
356: * @param completionChoices the set containing all possible completion choices.
357: * @param jTextField the textfield
358: * @param word the word the user is currently editing
359: * @param completeWindow the window/popup to display the choices
360: */
361: private static void assembleCompletionChoices(@NotNull
362: SortedSet<String> completionChoices, @NotNull
363: JTextField jTextField, @NotNull
364: String word, @NotNull
365: CompleteWindow completeWindow) {
366: if (!completionChoices.isEmpty()) {
367: Point p = jTextField.getLocationOnScreen();
368: int x = p.x;
369: int y = p.y + jTextField.getHeight();
370:
371: int count = 0;
372: for (String s1 : completionChoices) {
373: if (s1.startsWith(word) && !s1.equals(word)) {
374: completeWindow.getCompletionListModel().addElement(
375: s1);
376: count++;
377: }
378: if (count == MAX_COMPLETION_CHOICES) {
379: break;
380: }
381: }
382:
383: if (count > 0) {
384: completeWindow.setLocation(x, y);
385: if (!completeWindow.isVisible()) {
386: completeWindow.setVisible(true);
387: }
388: completeWindow.updateWindowSize();
389: } else {
390: completeWindow.setVisible(false);
391: }
392: } else {
393: completeWindow.setVisible(false);
394: }
395: }
396:
397: /**
398: * A Popup to display the possible completion choices.
399: */
400: private static class CompleteWindow extends JPopupMenu {
401: @NotNull
402: private JList list;
403:
404: @NotNull
405: private DefaultListModel completionListModel;
406:
407: private CompleteWindow(@NotNull
408: final JTextField jTextField) {
409: completionListModel = new DefaultListModel();
410: list = new JList(completionListModel);
411:
412: JScrollPane scrollPane = new JScrollPane(list) {
413: @NotNull
414: public Dimension getPreferredSize() {
415: Dimension preferredSize = super .getPreferredSize();
416: return new Dimension(
417: Math.max(jTextField.getWidth(),
418: preferredSize.width),
419: preferredSize.height);
420: }
421: };
422:
423: scrollPane
424: .setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
425:
426: setFocusable(false);
427: list.setFocusable(false);
428:
429: setInvoker(jTextField);
430: setLayout(new BorderLayout());
431: setBorder(null);
432:
433: add(scrollPane, BorderLayout.CENTER);
434: }
435:
436: /**
437: * Resizes the window to fit the lists content.
438: */
439: public void updateWindowSize() {
440: getList().setVisibleRowCount(completionListModel.size());
441: pack();
442: }
443:
444: @NotNull
445: public JList getList() {
446: return list;
447: }
448:
449: @NotNull
450: public DefaultListModel getCompletionListModel() {
451: return completionListModel;
452: }
453: }
454:
455: }
|