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.awt.ComponentOrientation;
23  import java.awt.Dimension;
24  import java.awt.event.ComponentAdapter;
25  import java.awt.event.ComponentEvent;
26  import java.awt.event.ComponentListener;
27  import java.awt.event.FocusAdapter;
28  import java.awt.event.FocusEvent;
29  import java.awt.event.FocusListener;
30  import java.awt.event.KeyAdapter;
31  import java.awt.event.KeyEvent;
32  import java.awt.event.KeyListener;
33  import java.awt.event.MouseAdapter;
34  import java.awt.event.MouseEvent;
35  import java.beans.PropertyChangeEvent;
36  import java.beans.PropertyChangeListener;
37  import javax.swing.JList;
38  import javax.swing.JPopupMenu;
39  import javax.swing.JScrollPane;
40  import javax.swing.JTextField;
41  import javax.swing.ListSelectionModel;
42  import javax.swing.text.JTextComponent;
43  import org.apache.commons.logging.Log;
44  import org.apache.commons.logging.LogFactory;
45  
46  /***
47   * This class presents completed objects in a popup menu below a {@link
48   * JTextComponent}.
49   * 
50   * @author Felix Berger
51   */
52  public class CompletionPopup extends JPopupMenu
53  {
54  		
55  	/***
56  	 * The completion mode handling when and how to complete.
57  	 */
58  	private Completion completion;
59  	/***
60  	 * The list presenting the completed items.
61  	 */
62  	private JList list = new JList();
63  
64  	private KeyListener keyListener = new KeyHandler();
65  
66  	private FocusListener focusListener = new FocusHandler();
67  
68  	private ComponentListener componentListener = new SizeHandler();
69  	
70  	private PropertyChangeListener propertyListener = new PropertyChangeHandler();
71  		
72  	private static Log logger = LogFactory.getLog(CompletionPopup.class);
73  	
74  	
75  	/***
76  	 * Constructs a new completion popup.
77  	 */
78  	public CompletionPopup()
79  	{
80  		list.setVisibleRowCount(4);
81  		list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
82  		JScrollPane jsp = new JScrollPane(list);
83  		add(jsp);
84  
85  		list.setFocusable(false);
86  		jsp.setFocusable(false);
87  		setFocusable(false);
88  
89  		list.addMouseListener(new MouseHandler());
90  	}
91  
92      
93  	public void enablePopup(Completion comp)
94  	{
95  		completion = comp;
96  
97  		applyComponentOrientation(completion.getTextComponent().getComponentOrientation());
98  		
99  		completion.getTextComponent().addKeyListener(keyListener);
100 		completion.getTextComponent().addFocusListener(focusListener);
101 		completion.getTextComponent().addPropertyChangeListener
102 		("componentOrientation", propertyListener);
103 
104 		// set the model
105 		list.setModel(completion.getModel());
106 		
107 		if (completion.isWholeTextCompletion()) {
108 			setPreferredSize(new Dimension(completion.getTextComponent().getWidth(),
109 										   getPreferredSize().height));
110 			completion.getTextComponent().addComponentListener(componentListener);
111 		}
112 	}
113 	
114 	public void disablePopup()
115 	{
116 		/* this breaks completion popup navigateion for the history text field,
117 		   but I guess completion + history is a bit too much anyway.  */
118 		completion.getTextComponent().removeKeyListener(keyListener);
119 		completion.getTextComponent().removeFocusListener(focusListener);
120 		completion.getTextComponent().removePropertyChangeListener
121 			("componentOrientation", propertyListener);
122 		if (completion.isWholeTextCompletion()) {
123 			completion.getTextComponent().removeComponentListener(componentListener);
124 		}
125 	}
126 
127 	private void selectNextCompletion()
128 	{
129 		if (completion.getModel().getSize() > 0) {
130 			int cur = (list.getSelectedIndex() + 1) % completion.getModel().getSize();
131 			list.setSelectedIndex(cur);
132 			list.ensureIndexIsVisible(cur);
133 		}
134 	}
135 
136 	private void selectPreviousCompletion()
137 	{
138 		if (completion.getModel().getSize() > 0) {
139 			int cur = (list.getSelectedIndex() == -1) ? 0 
140 				: list.getSelectedIndex();
141 			cur = (cur == 0) ? completion.getModel().getSize() - 1 : cur - 1;
142 			list.setSelectedIndex(cur);
143 			list.ensureIndexIsVisible(cur);
144 		}
145 	}
146 
147 	/***
148 	 * Accessor for testing.
149 	 */
150 	JList getList()
151 	{
152 		return list;
153 	}
154 
155 	/***
156 	 * Listens for size changes of the text component and updates the popup's
157 	 * width appropriately.
158 	 *
159 	 * This makes only sense for {@link JTextField}s.
160 	 */
161 	public class SizeHandler extends ComponentAdapter
162 	{
163 		public void componentResized(ComponentEvent e)
164 		{
165 			setPreferredSize
166 				(new Dimension(completion.getTextComponent().getWidth(), 
167 							   getPreferredSize().height));
168 		}
169 	}
170 
171 	/***
172 	 * Listens for key events in the text component responsible for navigation
173 	 * and confirmation.
174 	 */
175 	private class KeyHandler extends KeyAdapter
176 	{
177 		public void keyPressed(KeyEvent e)
178 		{
179 			int code = e.getKeyCode();
180 			int modifiers = e.getModifiers();
181 
182 			// catch all up an down events
183 			if (CompletionPopup.this.isVisible()) {
184 				if (code == KeyEvent.VK_DOWN) {
185 					selectNextCompletion();
186 					e.consume();
187 				}
188 				else if (code == KeyEvent.VK_UP) {
189 					selectPreviousCompletion();
190 					e.consume();
191 				}
192 				else if (code == KeyEvent.VK_ENTER) {
193 					if (list.getSelectedValue() != null) {
194 						/* object violation, maybe we should throw an event or
195                            do nothing at all.  */
196 						completion.setText(list.getSelectedValue().toString());
197 						e.consume();
198 					}
199 					CompletionPopup.this.setVisible(false);
200 				} 
201 				else if (code == KeyEvent.VK_ESCAPE) {
202 					CompletionPopup.this.setVisible(false);
203 					e.consume();
204 				}
205 			}
206 		}
207 	}
208 
209 	/***
210 	 * Listens for the loss of focus and hides the popup if necessary.
211 	 */
212 	private class FocusHandler extends FocusAdapter
213 	{
214 		public void focusLost(FocusEvent e)
215 		{
216 			if (!e.isTemporary()) {
217 				setVisible(false);
218 			}
219 		}
220 	}
221 
222 	/***
223 	 * Listens for click events in the completion list to update the text
224 	 * component's text appropriately.
225 	 */
226 	private class MouseHandler extends MouseAdapter
227 	{
228 		public void mouseClicked(MouseEvent e)
229 		{
230 			completion.setText(list.getSelectedValue().toString());
231 			CompletionPopup.this.setVisible(false);
232 		}
233 	}
234 	
235 	private class PropertyChangeHandler implements PropertyChangeListener
236 	{
237 
238 		public void propertyChange(PropertyChangeEvent evt) 
239 		{
240 			ComponentOrientation o = (ComponentOrientation)evt.getNewValue();
241 			applyComponentOrientation(o);
242 		}
243 		
244 	}
245 }