001: /*
002: * Gruntspud Copyright (C) 2002 Brett Smith. Written by: Brett Smith <t_magicthize@users.sourceforge.net> This program is free
003: * software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the
004: * Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in
005: * the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
006: * PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU
007: * Library General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave,
008: * Cambridge, MA 02139, USA.
009: */
010:
011: package gruntspud.ui;
012:
013: import gruntspud.Constants;
014:
015: import java.awt.BorderLayout;
016: import java.awt.Component;
017: import java.awt.Dimension;
018: import java.awt.FlowLayout;
019: import java.awt.FontMetrics;
020: import java.awt.GraphicsConfiguration;
021: import java.awt.GraphicsDevice;
022: import java.awt.GraphicsEnvironment;
023: import java.awt.Insets;
024: import java.awt.Rectangle;
025: import java.awt.Window;
026: import java.awt.event.ActionEvent;
027: import java.awt.event.ActionListener;
028: import java.awt.event.FocusEvent;
029: import java.awt.event.FocusListener;
030: import java.awt.event.KeyEvent;
031: import java.awt.event.MouseAdapter;
032: import java.awt.event.MouseEvent;
033: import java.awt.event.WindowAdapter;
034: import java.awt.event.WindowEvent;
035: import java.io.File;
036: import java.lang.reflect.Method;
037: import java.util.Collections;
038: import java.util.Iterator;
039:
040: import javax.swing.AbstractAction;
041: import javax.swing.ActionMap;
042: import javax.swing.BorderFactory;
043: import javax.swing.DefaultListModel;
044: import javax.swing.JButton;
045: import javax.swing.JFileChooser;
046: import javax.swing.JFrame;
047: import javax.swing.JLabel;
048: import javax.swing.JList;
049: import javax.swing.JPanel;
050: import javax.swing.JScrollPane;
051: import javax.swing.JWindow;
052: import javax.swing.KeyStroke;
053: import javax.swing.SwingConstants;
054: import javax.swing.SwingUtilities;
055: import javax.swing.UIManager;
056: import javax.swing.event.DocumentEvent;
057: import javax.swing.event.DocumentListener;
058: import javax.swing.text.Document;
059:
060: /**
061: * An extension of <code>XTextField</code> that provides filename completion
062: *
063: * @author Brett Smith
064: */
065: public class FileNameTextField extends XTextField {
066:
067: private JWindow fileWindow;
068: private JList fileList;
069: private DefaultListModel fileModel;
070: private boolean adjusting, focused, autoComplete, includeFiles;
071: private File cwd;
072: private Insets insets;
073: private JScrollPane scroller;
074: private JButton browse;
075:
076: /**
077: * Constructs a new FileNameTextField. A default model is created, the initial string is null, and the number of columns is set
078: * to 0.
079: */
080: public FileNameTextField() {
081: this (null, null, 0);
082: }
083:
084: /**
085: * Constructs a new FileNameTextField initialized with the specified text. A default model is created and the number of columns
086: * is 0.
087: *
088: * @param text the text to be displayed, or null
089: */
090: public FileNameTextField(String text) {
091: this (null, text, 0);
092: }
093:
094: /**
095: * Constructs a new empty FileNameTextField with the specified number of columns. A default model is created and the initial
096: * string is set to null.
097: *
098: * @param columns the number of columns to use to calculate the preferred width. If columns is set to zero, the preferred width
099: * will be whatever naturally results from the component implementation.
100: */
101: public FileNameTextField(int columns) {
102: this (null, null, columns);
103: }
104:
105: /**
106: * Constructs a new FileNameTextField initialized with the specified text and columns. A default model is created.
107: *
108: * @param text the text to be displayed, or null
109: * @param columns the number of columns to use to calculate the preferred width. If columns is set to zero, the preferred width
110: * will be whatever naturally results from the component implementation.
111: */
112: public FileNameTextField(String text, int columns) {
113: this (null, text, columns);
114: }
115:
116: /**
117: * Constructs a new FileNameTextField that uses the given text storage model and the given number of columns. This is the constructor
118: * through which the other constructors feed. If the document is null, a default model is created.
119: *
120: * @param doc the text storage to use. If this is null, a default will be provided by calling the createDefaultModel method.
121: * @param text the initial string to display, or null
122: * @param columns the number of columns to use to calculate the preferred width >= 0. If columns is set to zero, the preferred
123: * width will be whatever naturally results from the component implementation.
124: * @exception IllegalArgumentException if columns < 0
125: */
126: public FileNameTextField(Document doc, String text, int columns) {
127: this (doc, text, columns, true);
128: }
129:
130: /**
131: * Constructs a new FileNameTextField that uses the given text storage model and the given number of columns. This is the constructor
132: * through which the other constructors feed. If the document is null, a default model is created.
133: *
134: * @param doc the text storage to use. If this is null, a default will be provided by calling the createDefaultModel method.
135: * @param text the initial string to display, or null
136: * @param columns the number of columns to use to calculate the preferred width >= 0. If columns is set to zero, the preferred
137: * width will be whatever naturally results from the component implementation.
138: * @param autoComplete automatically complete filenames
139: * @exception IllegalArgumentException if columns < 0
140: */
141: public FileNameTextField(Document doc, String text, int columns,
142: boolean autoComplete) {
143: this (doc, text, columns, autoComplete, false);
144: }
145:
146: /**
147: * Constructs a new JTextField that uses the given text storage model and the given number of columns. This is the constructor
148: * through which the other constructors feed. If the document is null, a default model is created.
149: *
150: * @param doc the text storage to use. If this is null, a default will be provided by calling the createDefaultModel method.
151: * @param text the initial string to display, or null
152: * @param columns the number of columns to use to calculate the preferred width >= 0. If columns is set to zero, the preferred
153: * width will be whatever naturally results from the component implementation.
154: * @param autoComplete automatically complete filenames
155: * @param includeFiles include files in the completion
156: * @exception IllegalArgumentException if columns < 0
157: */
158: public FileNameTextField(Document doc, String text, int columns,
159: boolean autoComplete, boolean includeFiles) {
160: this (doc, text, columns, autoComplete, includeFiles, null,
161: false);
162: }
163:
164: /**
165: * Constructs a new JTextField that uses the given text storage model and the given number of columns. This is the constructor
166: * through which the other constructors feed. If the document is null, a default model is created.
167: *
168: * @param doc the text storage to use. If this is null, a default will be provided by calling the createDefaultModel method.
169: * @param text the initial string to display, or null
170: * @param columns the number of columns to use to calculate the preferred width >= 0. If columns is set to zero, the preferred
171: * width will be whatever naturally results from the component implementation.
172: * @param autoComplete automatically complete filenames
173: * @param includeFiles include files in the completion and file chooser (if selected)
174: * @param chooser include a file chooser (<code>nu
175: * @param useSave use save in chooser instead of open
176: * @exception IllegalArgumentException if columns < 0
177: */
178: public FileNameTextField(Document doc, String text, int columns,
179: boolean autoComplete, boolean includeFiles,
180: final JFileChooser chooser, final boolean useSave) {
181:
182: super (doc, text, columns);
183:
184: if (chooser != null) {
185: setLayout(new BorderLayout());
186: browse = new JButton("..") {
187: public Dimension getPreferredSize() {
188: return new Dimension(16,
189: super .getPreferredSize().height);
190: }
191:
192: public Dimension getMaximumSize() {
193: return getPreferredSize();
194: }
195:
196: public Dimension getMinimumSize() {
197: return getPreferredSize();
198: }
199: };
200: browse.setMargin(new Insets(0, 0, 0, 0));
201: // browse.setBorder(BorderFactory.createRaisedBevelBorder());
202: browse.setFocusPainted(false);
203: setFocusable(browse, false);
204: browse.setDefaultCapable(false);
205: browse.addActionListener(new ActionListener() {
206: public void actionPerformed(ActionEvent evt) {
207: chooser.setCurrentDirectory(new File(getText()));
208: int opt = 0;
209: if (useSave)
210: opt = chooser.showSaveDialog(browse);
211: else
212: opt = chooser.showOpenDialog(browse);
213: if (opt == JFileChooser.APPROVE_OPTION) {
214: FileNameTextField.this .grabFocus();
215: setFile(chooser.getSelectedFile());
216: checkVisible();
217: }
218: }
219: });
220: add(browse, BorderLayout.EAST);
221: }
222:
223: this .autoComplete = autoComplete;
224: this .includeFiles = includeFiles;
225:
226: this .addFocusListener(new FocusListener() {
227: public void focusGained(FocusEvent evt) {
228: focused = true;
229: // checkVisible();
230: }
231:
232: public void focusLost(FocusEvent evt) {
233: focused = false;
234: hide();
235: }
236:
237: });
238:
239: // what if the document changes? - no need to be bothered at the moment
240: this .getDocument().addDocumentListener(new DocumentListener() {
241: void update() {
242: checkVisible();
243: }
244:
245: public void changedUpdate(DocumentEvent evt) {
246: update();
247: }
248:
249: public void removeUpdate(DocumentEvent evt) {
250: update();
251: }
252:
253: public void insertUpdate(DocumentEvent evt) {
254: update();
255: }
256:
257: });
258: ActionMap actionMap = FileNameTextField.this .getActionMap();
259: actionMap.put("completionDown", new AbstractAction() {
260: public void actionPerformed(ActionEvent evt) {
261: if (fileList != null && isAutoComplete() && focused) {
262: int sel = fileList.getSelectedIndex();
263: if ((sel + 1) < fileModel.getSize()) {
264: sel++;
265: fileList.setSelectedIndex(sel);
266: fileList.ensureIndexIsVisible(sel);
267: }
268: }
269: }
270: });
271: actionMap.put("completionUp", new AbstractAction() {
272: public void actionPerformed(ActionEvent evt) {
273: if (fileList != null && isAutoComplete() && focused) {
274: int sel = fileList.getSelectedIndex();
275: if ((sel - 1) > -1) {
276: sel--;
277: fileList.setSelectedIndex(sel);
278: fileList.ensureIndexIsVisible(sel);
279: }
280: }
281: }
282: });
283: actionMap.put("completionEnter", new AbstractAction() {
284: public void actionPerformed(ActionEvent evt) {
285: if (fileList != null) {
286: int sel = fileList.getSelectedIndex();
287: if (sel > -1) {
288: setText(fileList.getSelectedValue().toString());
289: }
290: }
291: hide();
292: }
293: });
294: actionMap.put("completionEscape", new AbstractAction() {
295: public void actionPerformed(ActionEvent evt) {
296: hide();
297: }
298: });
299:
300: }
301:
302: protected int getColumnWidth() {
303: return super .getColumnWidth() + (browse == null ? 0 : 12);
304: }
305:
306: public void setEnabled(boolean enabled) {
307: super .setEnabled(enabled);
308: if (browse != null) {
309: browse.setEnabled(enabled);
310: }
311: }
312:
313: public void setFile(File file) {
314: setCaretPosition(0);
315: setText(file.getAbsolutePath());
316: // grabFocus();
317: setSelectionStart(getDocument().getLength());
318: setSelectionEnd(getDocument().getLength());
319: setCaretPosition(getDocument().getLength());
320: }
321:
322: public void hide() {
323: if (fileWindow != null && fileWindow.isVisible()) {
324: fileWindow.setVisible(false);
325: getInputMap(FileNameTextField.WHEN_FOCUSED).remove(
326: KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0));
327: getInputMap(FileNameTextField.WHEN_FOCUSED).remove(
328: KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0));
329: getInputMap(FileNameTextField.WHEN_FOCUSED).remove(
330: KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0));
331: getInputMap(FileNameTextField.WHEN_FOCUSED).remove(
332: KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0));
333:
334: }
335: }
336:
337: public void setAutoComplete(boolean autoComplete) {
338: this .autoComplete = autoComplete;
339: if (!autoComplete) {
340: hide();
341: }
342: }
343:
344: public boolean isAutoComplete() {
345: return autoComplete;
346: }
347:
348: public boolean isAdjusting() {
349: return adjusting;
350: }
351:
352: public synchronized void setText(String text) {
353: adjusting = true;
354: try {
355: super .setText(text);
356: } catch (Throwable t) {
357: Constants.UI_LOG.error("Failed to set the filename. ", t);
358: }
359: adjusting = false;
360: }
361:
362: public void checkVisible() {
363: // Dont popup is autocomplete is off, the component is not visible on
364: // screen, the value is adjusting or the component is not focused
365: if (!autoComplete || !isShowing() || adjusting || !focused) {
366: return;
367: }
368:
369: if (fileWindow == null) {
370: Window w = (Window) SwingUtilities.getAncestorOfClass(
371: Window.class, this );
372: fileWindow = new JWindow(w);
373: setFocusable(fileWindow, false);
374: JPanel p = new JPanel(new BorderLayout());
375: p.setBorder(UIManager.getBorder("ToolTip.border"));
376: insets = p.getBorder() == null ? null : p.getBorder()
377: .getBorderInsets(p);
378: fileModel = new DefaultListModel();
379: fileList = new JList(fileModel);
380: fileList.setBackground(UIManager
381: .getColor("ToolTip.background"));
382: fileList.setForeground(UIManager
383: .getColor("ToolTip.foreground"));
384: fileList.setSelectionBackground(fileList.getForeground());
385: fileList.setSelectionForeground(fileList.getBackground());
386: setFocusable(fileList, false);
387: fileList.addMouseListener(new MouseAdapter() {
388: public void mouseClicked(MouseEvent evt) {
389: if (fileList.getSelectedIndex() != -1) {
390: setFile((File) fileList.getSelectedValue());
391: hide();
392: }
393: }
394: });
395: scroller = new JScrollPane(fileList);
396: setFocusable(scroller, false);
397: setFocusable(scroller.getVerticalScrollBar(), false);
398: setFocusable(scroller.getHorizontalScrollBar(), false);
399: p.add(scroller);
400: setFocusable(p, false);
401: fileWindow.getContentPane().add(p);
402: }
403:
404: // Now update the file list
405: String text = getText();
406: String base = text;
407: int idx = text.lastIndexOf(File.separator);
408: if (idx != -1) {
409: base = text.substring(idx + 1);
410: }
411: base = base.toLowerCase();
412: File f = new File(text);
413: if (!f.exists()) {
414: f = f.getParentFile();
415:
416: //
417: }
418: if (f != null && f.exists()
419: && (cwd == null || (f.isDirectory() && !cwd.equals(f)))) {
420: fileModel.removeAllElements();
421: java.util.List lst = new java.util.ArrayList();
422: if (f.isDirectory() || includeFiles) {
423: lst.add(f);
424: }
425: File[] l = f.listFiles();
426: for (int i = 0; i < l.length; i++) {
427: if (l[i].isDirectory() || includeFiles) {
428: lst.add(l[i]);
429: }
430: }
431: Collections.sort(lst);
432: for (Iterator i = lst.iterator(); i.hasNext();) {
433: fileModel.addElement(i.next());
434: }
435: cwd = f;
436: }
437:
438: // Select the file that has the closes name
439: int sel = -1;
440: for (int i = 0; i < fileModel.getSize() && sel == -1; i++) {
441: File z = (File) fileModel.getElementAt(i);
442: if (z.getName().toLowerCase().startsWith(base)) {
443: sel = i;
444: }
445: }
446:
447: fileList.setSelectedIndex(sel);
448: if (sel != -1) {
449: fileList.ensureIndexIsVisible(sel);
450:
451: // Make the window visible
452: }
453: if (fileModel.getSize() > 0 && !fileWindow.isVisible()) {
454: Rectangle virtualBounds = new Rectangle();
455: GraphicsEnvironment ge = GraphicsEnvironment
456: .getLocalGraphicsEnvironment();
457: GraphicsDevice[] gs = ge.getScreenDevices();
458: for (int j = 0; j < gs.length; j++) {
459: GraphicsDevice gd = gs[j];
460: GraphicsConfiguration[] gc = gd.getConfigurations();
461: for (int i = 0; i < gc.length; i++) {
462: virtualBounds = virtualBounds.union(gc[i]
463: .getBounds());
464: }
465: }
466: FontMetrics m = fileList.getFontMetrics(fileList.getFont());
467: int width = getSize().width;
468: for (int i = 0; i < fileModel.size(); i++) {
469: int w = m
470: .stringWidth(fileModel.elementAt(i).toString());
471: if (w > width) {
472: width = w;
473: }
474: }
475: width += 24;
476: int height = 120;
477: int x = getLocationOnScreen().x;
478: int y = getLocationOnScreen().y + getSize().height;
479: if (virtualBounds != null
480: && (y + fileWindow.getSize().height) >= (virtualBounds.y
481: + virtualBounds.height - 32)) {
482: y = getLocationOnScreen().y
483: - fileWindow.getSize().height;
484: }
485: if (virtualBounds != null
486: && (x + width) >= (virtualBounds.x
487: + virtualBounds.width - 32)) {
488: x = virtualBounds.x + virtualBounds.width - width;
489: if (x < virtualBounds.x) {
490: x = virtualBounds.x;
491: width = virtualBounds.width;
492: }
493: }
494: fileWindow.setSize(width, height);
495: fileWindow.setLocation(x, y);
496:
497: getInputMap(FileNameTextField.WHEN_FOCUSED).put(
498: KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0),
499: "completionDown");
500: getInputMap(FileNameTextField.WHEN_FOCUSED).put(
501: KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0),
502: "completionUp");
503: getInputMap(FileNameTextField.WHEN_FOCUSED).put(
504: KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
505: "completionEnter");
506: getInputMap(FileNameTextField.WHEN_FOCUSED).put(
507: KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
508: "completionEscape");
509:
510: fileWindow.setVisible(true);
511: }
512: }
513:
514: public static void setFocusable(Component component,
515: boolean focusable) {
516: try {
517: Method m = component.getClass().getMethod("setFocusable",
518: new Class[] { boolean.class });
519: m
520: .invoke(component, new Object[] { new Boolean(
521: focusable) });
522: } catch (Throwable t) {
523: }
524: }
525:
526: public static void main(String[] args) throws Exception {
527: UIManager.setLookAndFeel(UIManager
528: .getSystemLookAndFeelClassName());
529: JFrame f = new JFrame("Test filename completion");
530: JFileChooser chooser = new JFileChooser(new File(System
531: .getProperty("user.dir")));
532: chooser.setDialogTitle("Test chooser");
533: chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
534: FileNameTextField t = new FileNameTextField(null, "", 20, true,
535: false, chooser, false);
536: f.getContentPane().setLayout(new FlowLayout());
537: f.getContentPane().add(new JLabel("Filename: "));
538: f.getContentPane().add(t);
539: f.pack();
540: UIUtil.positionComponent(SwingConstants.CENTER, f);
541: f.addWindowListener(new WindowAdapter() {
542: public void windowClosing(WindowEvent evt) {
543: System.exit(0);
544: }
545: });
546: f.setVisible(true);
547: }
548: }
|