001: /*
002: * Copyright (C) 2003 Gerd Wagner
003: *
004: * This program is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU General Public License
006: * as published by the Free Software Foundation; either version 2
007: * of the License, or any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
017: */
018: package net.sourceforge.squirrel_sql.fw.completion;
019:
020: import javax.swing.*;
021: import javax.swing.text.BadLocationException;
022: import javax.swing.text.JTextComponent;
023: import javax.swing.text.Keymap;
024: import java.awt.*;
025: import java.awt.event.*;
026: import java.util.Vector;
027:
028: public class Completor {
029: private Vector<CompletorListener> _listeners = new Vector<CompletorListener>();
030: private ICompletorModel _model;
031: private JPanel _completionPanel;
032: private JList _completionList;
033:
034: private Rectangle _curCompletionPanelSize;
035: private PopupManager _popupMan;
036: private TextComponentProvider _txtComp;
037: private CompletionFocusHandler _completionFocusHandler;
038: private FocusListener _completionFocusListener;
039:
040: private MouseAdapter _listMouseAdapter;
041: private KeyListener _filterKeyListener;
042: private static final int MAX_ITEMS_IN_COMPLETION_LIST = 10;
043: private JScrollPane _completionListScrollPane;
044:
045: private CompletionCandidates _currCandidates;
046:
047: private KeyStroke[] _keysToDisableWhenPopUpOpen = new KeyStroke[] {
048: KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0, false),
049: KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0, false),
050: KeyStroke.getKeyStroke(KeyEvent.VK_LEFT, 0, false),
051: KeyStroke.getKeyStroke(KeyEvent.VK_RIGHT, 0, false),
052: KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0, false),
053: KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0, false),
054: KeyStroke.getKeyStroke(KeyEvent.VK_TAB, 0, false),
055: KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_UP, 0, false),
056: KeyStroke.getKeyStroke(KeyEvent.VK_PAGE_DOWN, 0, false),
057: KeyStroke.getKeyStroke(KeyEvent.VK_SPACE,
058: KeyEvent.CTRL_MASK, false) };
059:
060: private Action[] _originalActions = null;
061:
062: public Completor(JTextComponent txtComp, ICompletorModel model) {
063: this (txtComp, model, new Color(255, 255, 204), false); // light yellow
064: }
065:
066: public Completor(JTextComponent txtComp, ICompletorModel model,
067: Color popUpBackGround, boolean useOwnFilterTextField) {
068: _txtComp = new TextComponentProvider(txtComp,
069: useOwnFilterTextField);
070:
071: _model = model;
072:
073: _completionPanel = new JPanel(new BorderLayout()) {
074: private static final long serialVersionUID = 1L;
075:
076: public void setSize(int width, int height) {
077: // without this the completion panel's size will be weird
078: super .setSize(_curCompletionPanelSize.width,
079: _curCompletionPanelSize.height);
080: }
081: };
082:
083: _completionList = new JList(new DefaultListModel());
084: _completionList
085: .setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
086: _completionList.setBackground(popUpBackGround);
087:
088: _completionFocusHandler = new CompletionFocusHandler(_txtComp,
089: _completionList);
090: _completionFocusListener = new FocusAdapter() {
091: public void focusLost(FocusEvent e) {
092: closePopup();
093: }
094: };
095:
096: _listMouseAdapter = new MouseAdapter() {
097: public void mouseClicked(MouseEvent e) {
098: onMousClicked(e);
099: }
100: };
101:
102: _filterKeyListener = new KeyAdapter() {
103: public void keyPressed(KeyEvent e) {
104: onKeyPressedOnList(e);
105: }
106: };
107:
108: _completionListScrollPane = new JScrollPane(_completionList);
109:
110: if (_txtComp.editorEqualsFilter()) {
111: _completionPanel.add(_completionListScrollPane,
112: BorderLayout.CENTER);
113: } else {
114: _completionPanel.add(_txtComp.getFilter(),
115: BorderLayout.NORTH);
116: _completionPanel.add(_completionListScrollPane,
117: BorderLayout.CENTER);
118: }
119:
120: _completionPanel.setVisible(false);
121:
122: _popupMan = new PopupManager(txtComp);
123: }
124:
125: private void onKeyPressedOnList(KeyEvent e) {
126: if (e.getKeyCode() == KeyEvent.VK_ENTER
127: || e.getKeyCode() == KeyEvent.VK_SPACE
128: || e.getKeyCode() == KeyEvent.VK_TAB) {
129: completionSelected(e.getKeyCode(), e.getModifiers());
130: } else if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
131: closePopup();
132: } else if (e.getKeyCode() == KeyEvent.VK_BACK_SPACE) {
133: if (_txtComp.editorEqualsFilter()
134: && 1 >= _currCandidates.getStringToReplace()
135: .length()) {
136: closePopup();
137: } else {
138: reInitList();
139: }
140: } else if (e.getKeyCode() == KeyEvent.VK_LEFT
141: || e.getKeyCode() == KeyEvent.VK_RIGHT
142: || e.getKeyCode() == KeyEvent.VK_TAB) {
143: // do nothing
144: } else if (e.getKeyCode() == KeyEvent.VK_UP) {
145: if (0 < _completionList.getSelectedIndex()) {
146: int newSelIx = _completionList.getSelectedIndex() - 1;
147: _completionList
148: .setSelectionInterval(newSelIx, newSelIx);
149: _completionList.ensureIndexIsVisible(newSelIx);
150: } else {
151: int lastIx = _completionList.getModel().getSize() - 1;
152: _completionList.setSelectionInterval(lastIx, lastIx);
153: _completionList.ensureIndexIsVisible(lastIx);
154: }
155: } else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
156: if (_completionList.getSelectedIndex() + 1 < _completionList
157: .getModel().getSize()) {
158: int newSelIx = _completionList.getSelectedIndex() + 1;
159: _completionList
160: .setSelectionInterval(newSelIx, newSelIx);
161: _completionList.ensureIndexIsVisible(newSelIx);
162: } else {
163: _completionList.setSelectionInterval(0, 0);
164: _completionList.ensureIndexIsVisible(0);
165: }
166: } else if (e.getKeyCode() == KeyEvent.VK_PAGE_UP) {
167: if (0 < _completionList.getSelectedIndex()
168: - MAX_ITEMS_IN_COMPLETION_LIST) {
169: int newSelIx = _completionList.getSelectedIndex()
170: - MAX_ITEMS_IN_COMPLETION_LIST;
171: _completionList
172: .setSelectionInterval(newSelIx, newSelIx);
173: _completionList.ensureIndexIsVisible(newSelIx);
174: } else {
175: _completionList.setSelectionInterval(0, 0);
176: _completionList.ensureIndexIsVisible(0);
177: }
178: } else if (e.getKeyCode() == KeyEvent.VK_PAGE_DOWN) {
179: if (_completionList.getSelectedIndex()
180: + MAX_ITEMS_IN_COMPLETION_LIST < _completionList
181: .getModel().getSize()) {
182: int newSelIx = _completionList.getSelectedIndex()
183: + MAX_ITEMS_IN_COMPLETION_LIST;
184: _completionList
185: .setSelectionInterval(newSelIx, newSelIx);
186: _completionList.ensureIndexIsVisible(newSelIx);
187: } else {
188: int lastIndex = _completionList.getModel().getSize() - 1;
189: _completionList.setSelectionInterval(lastIndex,
190: lastIndex);
191: _completionList.ensureIndexIsVisible(lastIndex);
192: }
193:
194: } else {
195: reInitList();
196:
197: DefaultListModel listModel = (DefaultListModel) _completionList
198: .getModel();
199: if (1 == listModel.size()) {
200: CompletionInfo info = (CompletionInfo) listModel
201: .getElementAt(0);
202: if (_txtComp.editorEqualsFilter()
203: && _currCandidates.getStringToReplace()
204: .toUpperCase().startsWith(
205: info.getCompareString()
206: .toUpperCase())) {
207: closePopup();
208: }
209: }
210: }
211: }
212:
213: private void reInitList() {
214: SwingUtilities.invokeLater(new Runnable() {
215: public void run() {
216: // Needs to be done later because when reInitList is called,
217: // the text componetes model is not yet up to date.
218: // E.g. the last character is missing.
219: reInitListLater();
220: }
221: });
222: }
223:
224: private void reInitListLater() {
225: _currCandidates = _model
226: .getCompletionCandidates(getTextTillCarret());
227:
228: if (0 == _currCandidates.getCandidates().length
229: && _txtComp.editorEqualsFilter()) {
230: closePopup();
231: } else {
232: fillAndShowCompletionList(_currCandidates.getCandidates());
233: }
234: }
235:
236: /**
237: *
238: * @return If there is an extra filter text field the complete text in this text field is returned
239: * @throws BadLocationException
240: */
241: public String getTextTillCarret() {
242: try {
243: if (_txtComp.editorEqualsFilter()) {
244: return _txtComp.getEditor().getText(0,
245: _txtComp.getFilter().getCaretPosition());
246: } else {
247: return _txtComp.getFilter().getText();
248: }
249: } catch (BadLocationException e) {
250: throw new RuntimeException(e);
251: }
252: }
253:
254: private void onMousClicked(MouseEvent e) {
255: if (2 == e.getClickCount()) {
256: completionSelected(KeyEvent.VK_ENTER, 0);
257: }
258: }
259:
260: private void completionSelected(int keyCode, int modifiers) {
261: Object selected = null;
262: if (0 < _completionList.getModel().getSize()) {
263: selected = _completionList.getSelectedValue();
264: }
265: closePopup();
266: if (null != selected && selected instanceof CompletionInfo) {
267: fireEvent((CompletionInfo) selected, keyCode, modifiers);
268: }
269: }
270:
271: private void closePopup() {
272: _completionList.removeMouseListener(_listMouseAdapter);
273: _txtComp.getFilter().removeKeyListener(_filterKeyListener);
274: _completionFocusHandler.setFocusListener(null);
275:
276: _completionPanel.setVisible(false);
277:
278: if (_txtComp.editorEqualsFilter()) {
279: SwingUtilities.invokeLater(new Runnable() {
280: public void run() {
281: Keymap km = _txtComp.getEditor().getKeymap();
282: for (int i = 0; i < _keysToDisableWhenPopUpOpen.length; i++) {
283: km
284: .removeKeyStrokeBinding(_keysToDisableWhenPopUpOpen[i]);
285:
286: if (null != _originalActions[i]) {
287: km.addActionForKeyStroke(
288: _keysToDisableWhenPopUpOpen[i],
289: _originalActions[i]);
290: }
291: }
292: }
293: });
294: } else {
295: _txtComp.getFilter().setText("");
296: _txtComp.getEditor().requestFocusInWindow();
297: }
298: }
299:
300: public void show() {
301: try {
302: _currCandidates = _model
303: .getCompletionCandidates(getTextTillCarret());
304:
305: if (0 == _currCandidates.getCandidates().length) {
306: return;
307: }
308: if (1 == _currCandidates.getCandidates().length) {
309: fireEvent(_currCandidates.getCandidates()[0],
310: KeyEvent.VK_ENTER, 0);
311: return;
312: }
313:
314: _txtComp.getEditor().modelToView(
315: _currCandidates.getReplacementStart());
316:
317: _completionList.setFont(_txtComp.getEditor().getFont());
318: fillAndShowCompletionList(_currCandidates.getCandidates());
319: } catch (BadLocationException e) {
320: throw new RuntimeException(e);
321: }
322: }
323:
324: private void fillAndShowCompletionList(CompletionInfo[] candidates) {
325: try {
326: // needed to resize completion panle appropriately
327: // see initializationof _curCompletionPanelSize
328: _curCompletionPanelSize = getCurCompletionPanelSize(candidates);
329:
330: DefaultListModel model = (DefaultListModel) _completionList
331: .getModel();
332: model.removeAllElements();
333:
334: for (int i = 0; i < candidates.length; i++) {
335: model.addElement(candidates[i]);
336: }
337:
338: Rectangle caretBounds;
339: if (_txtComp.editorEqualsFilter()) {
340: caretBounds = _txtComp.getEditor().modelToView(
341: _currCandidates.getReplacementStart());
342: } else {
343: caretBounds = _txtComp.getEditor().modelToView(
344: _txtComp.getEditor().getCaretPosition());
345: }
346:
347: _popupMan.install(_completionPanel, caretBounds,
348: PopupManager.BelowPreferred);
349:
350: _completionList.setSelectedIndex(0);
351: _completionList.ensureIndexIsVisible(0);
352: _completionPanel.setVisible(true);
353:
354: _completionList.removeMouseListener(_listMouseAdapter);
355: _completionList.addMouseListener(_listMouseAdapter);
356: _txtComp.getFilter().removeKeyListener(_filterKeyListener);
357: _txtComp.getFilter().addKeyListener(_filterKeyListener);
358:
359: _completionFocusHandler
360: .setFocusListener(_completionFocusListener);
361:
362: if (_txtComp.editorEqualsFilter()) {
363: Action doNothingAction = new AbstractAction(
364: "doNothingAction") {
365: private static final long serialVersionUID = 1L;
366:
367: public void actionPerformed(ActionEvent e) {
368: }
369: };
370:
371: Keymap km = _txtComp.getEditor().getKeymap();
372:
373: if (null == _originalActions) {
374: _originalActions = new Action[_keysToDisableWhenPopUpOpen.length];
375:
376: for (int i = 0; i < _keysToDisableWhenPopUpOpen.length; i++) {
377: _originalActions[i] = km
378: .getAction(_keysToDisableWhenPopUpOpen[i]);
379: }
380: }
381:
382: for (int i = 0; i < _keysToDisableWhenPopUpOpen.length; i++) {
383: km.addActionForKeyStroke(
384: _keysToDisableWhenPopUpOpen[i],
385: doNothingAction);
386: }
387: } else {
388: _txtComp.getFilter().requestFocusInWindow();
389: }
390: } catch (BadLocationException e) {
391: throw new RuntimeException(e);
392: }
393: }
394:
395: private Rectangle getCurCompletionPanelSize(
396: CompletionInfo[] candidates) {
397: FontMetrics fm = _txtComp.getEditor().getGraphics()
398: .getFontMetrics(_txtComp.getEditor().getFont());
399: int width = getCurCompletionPanelWidth(candidates, fm) + 30;
400: int height = (int) (Math.min(candidates.length,
401: MAX_ITEMS_IN_COMPLETION_LIST)
402: * (fm.getHeight() + 2.3) + 3);
403:
404: if (false == _txtComp.editorEqualsFilter()) {
405: height += _txtComp.getFilter().getPreferredSize()
406: .getHeight();
407: }
408:
409: return new Rectangle(width, height);
410: }
411:
412: private int getCurCompletionPanelWidth(CompletionInfo[] infos,
413: FontMetrics fontMetrics) {
414: int maxSize = 0;
415: if (false == _txtComp.editorEqualsFilter()
416: && null != _txtComp.getFilter().getText()) {
417: maxSize = Math.max(fontMetrics.stringWidth(_txtComp
418: .getFilter().getText()
419: + " "), maxSize);
420: }
421:
422: for (int i = 0; i < infos.length; i++) {
423: maxSize = Math.max(fontMetrics.stringWidth(infos[i]
424: .toString()), maxSize);
425: }
426: return maxSize;
427:
428: }
429:
430: private void fireEvent(CompletionInfo completion, int keyCode,
431: int modifiers) {
432: Vector<CompletorListener> clone = new Vector<CompletorListener>(
433: _listeners);
434:
435: for (int i = 0; i < clone.size(); i++) {
436: CompletorListener completorListener = clone.elementAt(i);
437: if (_txtComp.editorEqualsFilter()) {
438: completorListener.completionSelected(completion,
439: _currCandidates.getReplacementStart(), keyCode,
440: modifiers);
441: } else {
442: completorListener.completionSelected(completion, -1,
443: keyCode, modifiers);
444: }
445: }
446: }
447:
448: public void addCodeCompletorListener(CompletorListener l) {
449: _listeners.add(l);
450: }
451:
452: public void removeCodeCompletorListener(CompletorListener l) {
453: _listeners.remove(l);
454: }
455:
456: }
|