001: /*
002: * CompletionPopup.java
003: *
004: * This file is part of SQL Workbench/J, http://www.sql-workbench.net
005: *
006: * Copyright 2002-2008, Thomas Kellerer
007: * No part of this code maybe reused without the permission of the author
008: *
009: * To contact the author please send an email to: support@sql-workbench.net
010: *
011: */
012: package workbench.gui.completion;
013:
014: import java.awt.BorderLayout;
015: import java.awt.Dimension;
016: import java.awt.EventQueue;
017: import java.awt.Frame;
018: import java.awt.Point;
019: import java.awt.event.FocusEvent;
020: import java.awt.event.FocusListener;
021: import java.awt.event.KeyEvent;
022: import java.awt.event.KeyListener;
023: import java.awt.event.MouseEvent;
024: import java.awt.event.MouseListener;
025: import java.awt.event.WindowEvent;
026: import java.awt.event.WindowListener;
027: import java.util.ArrayList;
028: import java.util.List;
029: import javax.swing.JComponent;
030: import javax.swing.JList;
031: import javax.swing.JPanel;
032: import javax.swing.JScrollPane;
033: import javax.swing.JWindow;
034: import javax.swing.ListModel;
035: import javax.swing.ListSelectionModel;
036: import javax.swing.SwingUtilities;
037: import javax.swing.border.Border;
038: import javax.swing.border.CompoundBorder;
039: import javax.swing.border.EmptyBorder;
040: import javax.swing.border.EtchedBorder;
041: import workbench.db.ColumnIdentifier;
042: import workbench.gui.WbSwingUtilities;
043: import workbench.gui.editor.JEditTextArea;
044: import workbench.log.LogMgr;
045: import workbench.resource.ColumnSortType;
046: import workbench.resource.Settings;
047: import workbench.util.StringUtil;
048: import workbench.util.TableAlias;
049: import workbench.util.WbThread;
050:
051: /**
052: * @author support@sql-workbench.net
053: */
054: public class CompletionPopup implements FocusListener, MouseListener,
055: KeyListener, WindowListener {
056: protected JEditTextArea editor;
057: private JScrollPane scroll;
058: private JWindow window;
059: private JPanel content;
060: protected JList elementList;
061: private ListModel data;
062: private JComponent headerComponent;
063:
064: private StatementContext context;
065: private boolean selectCurrentWordInEditor;
066: protected CompletionSearchField searchField;
067: private boolean dbStoresMixedCase = false;
068:
069: public CompletionPopup(JEditTextArea ed, JComponent header,
070: ListModel listData) {
071: this .data = listData;
072: this .editor = ed;
073: this .headerComponent = header;
074:
075: this .elementList = new JList();
076: this .elementList.setModel(this .data);
077: this .elementList
078: .setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
079: Border b = new CompoundBorder(elementList.getBorder(),
080: new EmptyBorder(0, 2, 0, 2));
081: this .elementList.setBorder(b);
082:
083: elementList.addFocusListener(this );
084: elementList.addMouseListener(this );
085:
086: content = new DummyPanel();
087:
088: content.setLayout(new BorderLayout());
089: scroll = new JScrollPane(this .elementList);
090: scroll.setBorder(new EtchedBorder(EtchedBorder.LOWERED));
091: elementList.setVisibleRowCount(10);
092: content.add(scroll);
093: elementList.addKeyListener(this );
094: }
095:
096: public void setContext(StatementContext c) {
097: this .context = c;
098: }
099:
100: public void showPopup(String valueToSelect) {
101: //if (window != null) closePopup(false);
102: try {
103: scroll.setColumnHeaderView(headerComponent);
104: headerComponent.doLayout();
105: Dimension d = headerComponent.getPreferredSize();
106: d.height += 25;
107: elementList.setMinimumSize(d);
108: scroll.setMinimumSize(d);
109:
110: Point p = editor.getCursorLocation();
111: SwingUtilities.convertPointToScreen(p, editor);
112:
113: if (selectCurrentWordInEditor) {
114: Thread t = new WbThread("select") {
115: public void run() {
116: // Make sure this is executed on the EDT
117: WbSwingUtilities.invoke(new Runnable() {
118: public void run() {
119: editor
120: .selectWordAtCursor(BaseAnalyzer.SELECT_WORD_DELIM);
121: }
122: });
123: }
124: };
125: t.start();
126: }
127: int count = data.getSize();
128: elementList.setVisibleRowCount(count < 12 ? count + 1 : 12);
129:
130: int index = 0;
131: String s = editor.getSelectedText();
132: if (s != null) {
133: index = findEntry(s);
134: } else if (valueToSelect != null) {
135: index = findEntry(valueToSelect);
136: }
137: if (index == -1)
138: index = 0;
139:
140: if (window == null) {
141: window = new JWindow((Frame) SwingUtilities
142: .getWindowAncestor(editor));
143: }
144:
145: editor.setKeyEventInterceptor(this );
146:
147: elementList.doLayout();
148: scroll.invalidate();
149: scroll.doLayout();
150:
151: window.setLocation(p);
152: window.setContentPane(content);
153: window.addKeyListener(this );
154: window.pack();
155: window.addWindowListener(this );
156: if (window.getWidth() < d.width + 5) {
157: window.setSize(d.width + 5, window.getHeight());
158: }
159:
160: elementList.setSelectedIndex(index);
161: elementList.ensureIndexIsVisible(index);
162:
163: WbSwingUtilities.requestFocus(window, elementList);
164: window.setVisible(true);
165:
166: } catch (Exception e) {
167: LogMgr.logWarning("CompletionPopup.showPopup()",
168: "Error displaying popup window", e);
169: }
170: }
171:
172: private void cleanup() {
173: this .searchField = null;
174: if (editor != null)
175: editor.removeKeyEventInterceptor();
176: if (this .window != null) {
177: this .window.removeWindowListener(this );
178: this .window.setVisible(false);
179: this .window.dispose();
180: }
181: this .scroll.setColumnHeaderView(this .headerComponent);
182: this .headerComponent.doLayout();
183: }
184:
185: public void closeQuickSearch() {
186: this .searchField = null;
187: this .scroll.setColumnHeaderView(this .headerComponent);
188: this .headerComponent.doLayout();
189:
190: if (Settings.getInstance().getCloseAutoCompletionWithSearch()) {
191: this .closePopup(false);
192: } else {
193: EventQueue.invokeLater(new Runnable() {
194: public void run() {
195: elementList.requestFocusInWindow();
196: }
197: });
198: }
199: }
200:
201: public void setDbStoresMixedCase(boolean flag) {
202: this .dbStoresMixedCase = flag;
203: }
204:
205: /**
206: * Callback from the SearchField when enter has been pressed in the search field
207: */
208: public void quickSearchValueSelected() {
209: this .closePopup(true);
210: }
211:
212: private String getPasteValue(String value) {
213: if (value == null)
214: return value;
215: String result;
216: String pasteCase = Settings.getInstance()
217: .getAutoCompletionPasteCase();
218: if (value.trim().charAt(0) == '"'
219: || StringUtil.isMixedCase(value) || dbStoresMixedCase) {
220: result = value;
221: } else if ("lower".equalsIgnoreCase(pasteCase)) {
222: result = value.toLowerCase();
223: } else if ("upper".equalsIgnoreCase(pasteCase)) {
224: result = value.toUpperCase();
225: } else {
226: result = value;
227: }
228: if (this .context.getAnalyzer().appendDotToSelection())
229: result += ".";
230: if (this .context.getAnalyzer().isKeywordList())
231: result += " ";
232: if (this .context.getAnalyzer().isWbParam()) {
233: result = "-" + result + "=";
234: }
235: char c = this .context.getAnalyzer().quoteCharForValue(result);
236: if (c != 0) {
237: result = c + result + c;
238: }
239: return result;
240: }
241:
242: public void cancelPopup() {
243: if (this .window == null)
244: return;
245: window.setVisible(false);
246: window.dispose();
247: }
248:
249: private void selectEditor() {
250: EventQueue.invokeLater(new Runnable() {
251: public void run() {
252: editor.requestFocus();
253: editor.requestFocusInWindow();
254: }
255: });
256: }
257:
258: private List<ColumnIdentifier> getColumnsFromData() {
259: int count = data.getSize();
260: List<ColumnIdentifier> result = new ArrayList<ColumnIdentifier>(
261: count);
262:
263: // The first element is the SelectAllMarker, so we do not
264: // need to include it
265: for (int i = 1; i < count; i++) {
266: Object c = this .data.getElementAt(i);
267: if (c instanceof ColumnIdentifier) {
268: result.add((ColumnIdentifier) c);
269: } else if (c instanceof String) {
270: result.add(new ColumnIdentifier((String) c));
271: }
272: }
273:
274: if (Settings.getInstance().getAutoCompletionColumnSortType() == ColumnSortType.position) {
275: ColumnIdentifier.sortByPosition(result);
276: }
277: return result;
278: }
279:
280: private void closePopup(boolean pasteEntry) {
281: editor.removeKeyEventInterceptor();
282: scroll.setColumnHeaderView(this .headerComponent);
283:
284: if (this .window == null) {
285: return;
286: }
287:
288: try {
289: if (pasteEntry) {
290: doPaste();
291: }
292: } finally {
293: this .window.removeWindowListener(this );
294: this .window.setVisible(false);
295: this .window.dispose();
296: this .window = null;
297: this .searchField = null;
298: selectEditor();
299: }
300: }
301:
302: private void doPaste() {
303: Object[] selected = this .elementList.getSelectedValues();
304: if (selected == null) {
305: return;
306: }
307: String value = "";
308:
309: for (Object o : selected) {
310: if (o instanceof TableAlias) {
311: TableAlias a = (TableAlias) o;
312: String table = getPasteValue(a.getNameToUse());
313: if (value.length() > 0) {
314: value += ", ";
315: }
316: value += table;
317: } else if (o instanceof SelectAllMarker) {
318: // The SelectAllMarker is only used when columns are beeing displayed
319: List<ColumnIdentifier> columns = getColumnsFromData();
320:
321: int count = columns.size();
322: StringBuilder cols = new StringBuilder(count * 10);
323:
324: for (int i = 0; i < count; i++) {
325: ColumnIdentifier c = columns.get(i);
326: String v = c.getColumnName();
327: if (i > 0) {
328: cols.append(", ");
329: }
330: if (context.getAnalyzer().getColumnPrefix() != null
331: && i > 0) {
332: cols.append(context.getAnalyzer()
333: .getColumnPrefix());
334: cols.append(".");
335: }
336: cols.append(v);
337: }
338: value = cols.toString();
339: break;
340: } else {
341: if (value.length() > 0) {
342: value += ", ";
343: }
344: value += getPasteValue(o.toString());
345: }
346: }
347:
348: if (!StringUtil.isEmptyString(value)) {
349: editor.setSelectedText(value);
350: if (value.startsWith("<") && value.endsWith(">")) {
351: editor.selectWordAtCursor(" =-\t\n");
352: }
353: }
354: }
355:
356: public void selectCurrentWordInEditor(boolean flag) {
357: this .selectCurrentWordInEditor = flag;
358: }
359:
360: public void selectMatchingEntry(String s) {
361: int index = this .findEntry(s);
362: if (index >= 0) {
363: elementList.setSelectedIndex(index);
364: elementList.ensureIndexIsVisible(index);
365: } else {
366: elementList.clearSelection();
367: }
368: }
369:
370: private int findEntry(String s) {
371: if (s == null)
372: return -1;
373: int count = this .data.getSize();
374: String search = s.toLowerCase();
375: for (int i = 0; i < count; i++) {
376: String entry = StringUtil.trimQuotes(this .data
377: .getElementAt(i).toString());
378: if (entry.toLowerCase().startsWith(search))
379: return i;
380: }
381: return -1;
382: }
383:
384: protected int findEntry(char c) {
385: int count = this .data.getSize();
386: char sc = Character.toLowerCase(c);
387: for (int i = 0; i < count; i++) {
388: String entry = this .data.getElementAt(i).toString();
389: if (entry.length() == 0)
390: continue;
391: char ec = Character.toLowerCase(entry.charAt(0));
392:
393: if (ec == sc)
394: return i;
395: }
396: return -1;
397: }
398:
399: /**
400: * Implementation of the FocusListener interface
401: */
402: public void focusGained(FocusEvent focusEvent) {
403: }
404:
405: /**
406: * Implementation of the FocusListener interface
407: */
408: public void focusLost(FocusEvent focusEvent) {
409: if (this .searchField == null)
410: closePopup(false);
411: }
412:
413: /**
414: * Implementation of the MouseListener interface
415: */
416: public void mouseClicked(java.awt.event.MouseEvent mouseEvent) {
417: int clicks = mouseEvent.getClickCount();
418: if (clicks == 2) {
419: closePopup(true);
420: } else if (clicks == 1 && this .searchField != null) {
421: closeQuickSearch();
422: }
423: }
424:
425: public void mouseEntered(MouseEvent mouseEvent) {
426: }
427:
428: public void mouseExited(MouseEvent mouseEvent) {
429: }
430:
431: public void mousePressed(MouseEvent mouseEvent) {
432: }
433:
434: public void mouseReleased(MouseEvent mouseEvent) {
435: }
436:
437: public void keyPressed(KeyEvent evt) {
438: int index = -1;
439: boolean syncEntry = false;
440:
441: switch (evt.getKeyCode()) {
442: case KeyEvent.VK_TAB:
443: evt.consume();
444: break;
445: case KeyEvent.VK_ENTER:
446: closePopup(true);
447: evt.consume();
448: break;
449: case KeyEvent.VK_ESCAPE:
450: closePopup(false);
451: evt.consume();
452: break;
453:
454: case KeyEvent.VK_UP:
455: // When the searchfield is displayed the list
456: // does not have the focus, und therefor the up and down
457: // keys only scroll the list, but do not move the selection
458: if (this .searchField != null) {
459: index = elementList.getSelectedIndex();
460: if (index > 0) {
461: elementList.setSelectedIndex(index - 1);
462: elementList.ensureIndexIsVisible(index - 1);
463: syncEntry = true;
464: }
465: evt.consume();
466: }
467: break;
468: case KeyEvent.VK_DOWN:
469: if (this .searchField != null) {
470: index = elementList.getSelectedIndex();
471: if (index < data.getSize() - 1) {
472: elementList.setSelectedIndex(index + 1);
473: elementList.ensureIndexIsVisible(index + 1);
474: syncEntry = true;
475: }
476: evt.consume();
477: }
478: break;
479: }
480: if (syncEntry) {
481: Object o = elementList.getSelectedValue();
482: if (o != null) {
483: this .searchField.setText(o.toString());
484: this .searchField.selectAll();
485: }
486: }
487: }
488:
489: public void keyTyped(KeyEvent evt) {
490: if (this .searchField == null) {
491: String text = String.valueOf(evt.getKeyChar());
492: this .searchField = new CompletionSearchField(this , text);
493: this .scroll.setColumnHeaderView(this .searchField);
494: this .scroll.doLayout();
495: }
496: WbSwingUtilities.invoke(new Runnable() {
497: public void run() {
498: if (searchField != null) {
499: searchField.requestFocusInWindow();
500: }
501: }
502: });
503: // The JGoodies look and feel automatically selects
504: // the content of the text field when a focusGained event
505: // occurs. The moving of the caret has to come later
506: // than the focusGained that's why the requestFocus()
507: // and the moving of the caret are done in two steps
508: EventQueue.invokeLater(new Runnable() {
509: public void run() {
510: if (searchField != null) {
511: int len = searchField.getText().length();
512: searchField.setCaretPosition(len);
513: searchField.select(len, len);
514: }
515: }
516: });
517:
518: }
519:
520: public void keyReleased(KeyEvent keyEvent) {
521: }
522:
523: public void windowOpened(WindowEvent e) {
524: }
525:
526: public void windowClosing(WindowEvent e) {
527: }
528:
529: public void windowClosed(WindowEvent e) {
530: this .cleanup();
531: }
532:
533: public void windowIconified(WindowEvent e) {
534: }
535:
536: public void windowDeiconified(WindowEvent e) {
537: }
538:
539: public void windowActivated(WindowEvent e) {
540: }
541:
542: public void windowDeactivated(WindowEvent e) {
543: }
544:
545: class DummyPanel extends JPanel {
546: @SuppressWarnings("deprecation")
547: public boolean isManagingFocus() {
548: return false;
549: }
550:
551: public boolean getFocusTraversalKeysEnabled() {
552: return false;
553: }
554: }
555:
556: }
|