001: package snow.utils.gui;
002:
003: import java.awt.EventQueue;
004: import snow.utils.storage.*;
005: import java.awt.datatransfer.DataFlavor;
006: import java.awt.datatransfer.Transferable;
007: import javax.swing.border.Border;
008: import java.awt.dnd.*;
009: import java.awt.Color;
010: import java.awt.Dimension;
011: import java.awt.GridBagConstraints;
012: import javax.swing.*;
013: import java.awt.GridBagLayout;
014: import java.awt.event.*;
015: import javax.swing.event.*;
016: import java.io.*;
017: import java.util.*;
018: import snow.utils.SysUtils;
019: import snow.files.*;
020:
021: /** A field to edit a file name, with some goodies.
022: * call setAutoColorized() to show in red invalid paths
023: * TODO tab completion is buggy... if files with same name start as folder exists
024: */
025: public class FileField extends JPanel {
026: final private JButton editPathButton = new JButton("...");
027: final private JButton prePathButton = new JButton(
028: new Icons.DownWedge(10, 10, true));
029: final private JTextField pathField = new JTextField("");
030: final private boolean saveMode;
031: final private String dialogTitle;
032: private final Vector<ActionListener> actionListeners = new Vector<ActionListener>();
033: final private int fileChooserSelectionMode;
034: private boolean autoColorized = false;
035:
036: // empty => all allowed. (extension names without dots)
037: final public Set<String> allowedExtensions = new HashSet<String>();
038: public String fileTypeDescription = "";
039:
040: /** @param fileChooserSelectionMode JFileChooser.FILES_AND_DIRECTORIES.
041: */
042: public FileField(File path, boolean saveMode, String dialogTitle,
043: int fileChooserSelectionMode) {
044: this (path != null ? path.getAbsolutePath() : null, saveMode,
045: dialogTitle, fileChooserSelectionMode);
046: }
047:
048: /** @param fileChooserSelectionMode JFileChooser.FILES_AND_DIRECTORIES.
049: */
050: public FileField(String path, boolean saveMode, String dialogTitle,
051: int fileChooserSelectionMode) {
052: super (); //new FlowLayout(FlowLayout.LEFT,0,0));
053: GridBagLayout gridbag = new GridBagLayout();
054: GridBagConstraints constr = new GridBagConstraints();
055: setLayout(gridbag);
056:
057: this .saveMode = saveMode;
058: this .dialogTitle = dialogTitle;
059: this .fileChooserSelectionMode = fileChooserSelectionMode;
060:
061: constr.weightx = 1;
062: constr.fill = GridBagConstraints.HORIZONTAL;
063: gridbag.setConstraints(pathField, constr);
064: add(pathField);
065: pathField.setText(path);
066:
067: constr.weightx = 0;
068:
069: gridbag.setConstraints(prePathButton, constr);
070: add(prePathButton);
071:
072: gridbag.setConstraints(editPathButton, constr);
073: add(editPathButton);
074: editPathButton.setToolTipText("Right click for opts...");
075:
076: Dimension dim = new Dimension((int) pathField
077: .getPreferredSize().getHeight(), (int) pathField
078: .getPreferredSize().getHeight());
079:
080: editPathButton.setPreferredSize(dim);
081: editPathButton.setMaximumSize(dim);
082: editPathButton.setMinimumSize(dim);
083: editPathButton.setFocusPainted(false);
084:
085: prePathButton.setToolTipText("Predefined paths");
086: prePathButton.setPreferredSize(dim);
087: prePathButton.setMaximumSize(dim);
088: prePathButton.setMinimumSize(dim);
089: prePathButton.setFocusPainted(false);
090: prePathButton.setVisible(false);
091:
092: editPathButton.addActionListener(new ActionListener() {
093: public void actionPerformed(ActionEvent e) {
094: browseFileAction();
095: }
096: });
097:
098: pathField.addActionListener(new ActionListener() {
099: public void actionPerformed(ActionEvent e) {
100: notifyActionListeners();
101: }
102: });
103:
104: editPathButton.addMouseListener(new MouseAdapter() {
105: @Override
106: public void mousePressed(MouseEvent me) {
107: if (me.isPopupTrigger())
108: showPopup(me);
109: }
110:
111: @Override
112: public void mouseReleased(MouseEvent me) {
113: if (me.isPopupTrigger())
114: showPopup(me);
115: }
116:
117: private void showPopup(MouseEvent me) {
118: final File dest = getPath();
119: if (dest == null)
120: return;
121:
122: JPopupMenu pop = new JPopupMenu();
123:
124: if (dest.getParentFile() != null
125: && dest.getParentFile().exists()) {
126: FileIcon dir = new FileIcon(true, 15);
127: dir.setType(FileIcon.IconColor.System);
128: JMenuItem openSFS = new JMenuItem(
129: "Open parent folder in OS file explorer",
130: dir);
131: pop.add(openSFS);
132: openSFS.addActionListener(new ActionListener() {
133: public void actionPerformed(ActionEvent ae) {
134: SysUtils.openFileExplorer(dest
135: .getParentFile());
136: }
137: });
138: }
139:
140: if (dest.exists()) {
141: if (dest.isFile()) {
142: JMenuItem openSFSE = new JMenuItem(
143: "Open file (OS Execution)",
144: Icons.sharedSmallStart);
145: pop.addSeparator();
146: pop.add(openSFSE);
147: openSFSE
148: .addActionListener(new ActionListener() {
149: public void actionPerformed(
150: ActionEvent ae) {
151: SysUtils.openFileExplorer(dest);
152: }
153: });
154: } else {
155: FileIcon dir = new FileIcon(true, 15);
156: dir.setType(FileIcon.IconColor.System);
157: JMenuItem openSFS = new JMenuItem(
158: "Open in OS file explorer", dir);
159: pop.add(openSFS);
160: openSFS.addActionListener(new ActionListener() {
161: public void actionPerformed(ActionEvent ae) {
162: SysUtils.openFileExplorer(dest);
163: }
164: });
165: }
166: }
167:
168: pop.show(editPathButton, pop.getX(), pop.getY() + 15);
169: }
170: });
171:
172: pathField.addKeyListener(new KeyAdapter() {
173: @Override
174: public void keyReleased(KeyEvent ke) {
175: boolean isCaretAtEnd = pathField.getCaretPosition() == pathField
176: .getText().length();
177:
178: if (isCaretAtEnd) {
179: if (ke.getModifiers() == KeyEvent.CTRL_MASK
180: && ke.getKeyCode() == ke.VK_T) {
181: completePathForward();
182: }
183:
184: if (ke.getKeyCode() == ke.VK_TAB) {
185: if (pathField.getCaretPosition() == pathField
186: .getText().length()) {
187: completePathForward();
188: }
189: }
190: if (ke.getKeyCode() == ke.VK_RIGHT) {
191: pathField.transferFocusBackward();
192: }
193: }
194:
195: if (ke.getKeyCode() == ke.VK_UP) {
196: pathField.transferFocusBackward();
197: }
198:
199: if (ke.getKeyCode() == ke.VK_DOWN) {
200: pathField.transferFocus();
201: }
202:
203: //System.out.println(""+ke);
204:
205: if (autoColorized) {
206: updateColor();
207: }
208: }
209: });
210:
211: pathField.addMouseListener(new MouseAdapter() {
212: @Override
213: public void mousePressed(MouseEvent me) {
214: if (me.isPopupTrigger())
215: showPredefPathsPopup();
216: }
217:
218: @Override
219: public void mouseReleased(MouseEvent me) {
220: if (me.isPopupTrigger())
221: showPredefPathsPopup();
222: }
223:
224: });
225:
226: // deactivate the original tab function ( a doNothing action does NOT work).
227: getTextField().setFocusTraversalKeysEnabled(false);
228:
229: prePathButton.addActionListener(new ActionListener() {
230: public void actionPerformed(ActionEvent ae) {
231: showPredefPathsPopup();
232: }
233: });
234:
235: // important: allow smooth resizes in GL3
236: this .setMinimumSize(this .getPreferredSize());
237:
238: } // Constructor
239:
240: private void showPredefPathsPopup() {
241: if (alternatePaths.size() == 0)
242: return;
243: JPopupMenu pop = new JPopupMenu();
244: for (final File fi : alternatePaths) {
245: JMenuItem pi = new JMenuItem(fi.getAbsolutePath());
246: pop.add(pi);
247: pi.addActionListener(new ActionListener() {
248: public void actionPerformed(ActionEvent ae) {
249: pathField.setText(fi.getAbsolutePath());
250: if (autoColorized) {
251: updateColor();
252: }
253: }
254: });
255: }
256:
257: pop.show(pathField, 0, pathField.getHeight());
258: }
259:
260: /** Will colourize the field name in red if not existing.
261: */
262: public void setAutoColorized() {
263: autoColorized = true;
264: updateColor();
265: }
266:
267: public JTextField getTextField() {
268: return pathField;
269: }
270:
271: /** Manual call to set red if not valid.
272: */
273: private void updateColor() {
274: if (new File(pathField.getText()).exists()) {
275: pathField.setForeground(UIManager
276: .getColor("TextField.foreground"));
277: } else {
278: pathField.setForeground(Color.red.darker());
279: }
280: }
281:
282: /**
283: * @return null if the field is empty. (Allow to distinguish between the File("") and NO FILE !!
284: * the path may not exist ! use
285: */
286: public File getPath() {
287: String ft = pathField.getText().trim();
288: if (ft.length() == 0)
289: return null;
290: File file = new File(ft);
291: // [Nov2006] makes correct path names ! ("E:" and "e:" not the same)
292: return FileUtils.getCanonicalFileWithCase(file);
293: }
294:
295: /** an empty string if not set
296: */
297: public String getPathName() {
298: String ft = pathField.getText().trim();
299: if (ft.length() == 0)
300: return "";
301: File file = new File(ft);
302: return FileUtils.getCanonicalFileWithCase(file)
303: .getAbsolutePath();
304: }
305:
306: /** In save mode, all paths are "valid"... otherwise, valid means "exists"
307: */
308: public boolean isPathValid() {
309: if (saveMode)
310: return true;
311:
312: File file = getPath();
313: if (file.exists())
314: return true;
315: return false;
316: }
317:
318: private void browseFileAction() {
319: JFileChooser fileChooser = new JFileChooser();
320: fileChooser.setDialogTitle(dialogTitle);
321: File base = getPath();
322: if (base != null) {
323: if (base.exists()) {
324: fileChooser.setCurrentDirectory(base);
325: } else {
326: // try with the parent
327: if (base.getParentFile() != null) {
328: fileChooser.setCurrentDirectory(base
329: .getParentFile());
330: } else {
331: File lastGood = this
332: .getFirstValidPathStartingWith();
333: if (lastGood != null) {
334: if (lastGood.isDirectory()) {
335: fileChooser.setCurrentDirectory(lastGood);
336: } else {
337: fileChooser.setCurrentDirectory(lastGood
338: .getParentFile());
339: }
340: }
341: }
342: }
343: }
344:
345: fileChooser.setFileSelectionMode(this .fileChooserSelectionMode);
346: FileViewWithAttributes view = new FileViewWithAttributes();
347: fileChooser.setFileView(view);
348:
349: if (allowedExtensions.size() > 0) {
350: fileChooser
351: .setFileFilter(new javax.swing.filechooser.FileFilter() {
352: @Override
353: public boolean accept(File f) {
354: if (f.isDirectory())
355: return true;
356: String name = f.getName().toLowerCase(
357: Locale.ENGLISH);
358: for (String ext : allowedExtensions) {
359: if (name.endsWith("." + ext))
360: return true;
361: }
362: return false;
363: }
364:
365: public String getDescription() {
366: return fileTypeDescription;
367: }
368: });
369: }
370:
371: if (saveMode) {
372: int rep = fileChooser.showSaveDialog(this );
373: if (rep == JFileChooser.APPROVE_OPTION) {
374: File file = fileChooser.getSelectedFile();
375: setPath(file.getAbsolutePath());
376: }
377: } else {
378: FileChooserFilter fsf = new FileChooserFilter(fileChooser);
379: fileChooser.setAccessory(fsf);
380:
381: int rep = fileChooser.showOpenDialog(this );
382: if (rep == JFileChooser.APPROVE_OPTION) {
383: File file = fileChooser.getSelectedFile();
384: setPath(file.getAbsolutePath());
385: }
386: }
387:
388: }
389:
390: public void setEditable(boolean editable) {
391: pathField.setEditable(editable);
392: editPathButton.setEnabled(editable);
393: }
394:
395: /** Necessary in the gridlayout3, to be preferred to FileField.setVisible()
396: */
397: public void makeVisible(boolean v) {
398: pathField.setVisible(v);
399: editPathButton.setVisible(v);
400: }
401:
402: public void setFile(File path) {
403: setPath(path);
404: }
405:
406: public void setPath(File path) {
407: setPath(path.getAbsolutePath());
408: }
409:
410: public void setPath(String path) {
411: pathField.setText(path);
412: if (autoColorized)
413: this .updateColor();
414: notifyActionListeners();
415: }
416:
417: public boolean isValidCandidate(File f) {
418: if (f == null)
419: return true; // as default (no file selected!)
420: if (fileChooserSelectionMode == JFileChooser.FILES_ONLY
421: && f.isDirectory())
422: return false;
423: if (fileChooserSelectionMode == JFileChooser.DIRECTORIES_ONLY
424: && !f.isDirectory())
425: return false;
426: return true;
427: }
428:
429: final List<File> alternatePaths = new ArrayList<File>();
430:
431: /** Will be offered as completion when pressing right mouse on the field...
432: * useful for "system wide" completions
433: */
434: public void setAlternatePaths(final List<File> paths) {
435: alternatePaths.clear();
436: alternatePaths.addAll(paths);
437:
438: prePathButton.setVisible(paths.size() > 0);
439: }
440:
441: /** Remembers the actual path.
442: */
443: public void rememberPathForGlobalCompletion(AppProperties props,
444: String key) {
445: List<File> allKnownPaths = FileUtils.getFilesFromList(props
446: .getProperty(key, ""), false);
447: List<File> rem = new ArrayList<File>();
448:
449: // remove not existing items... (an if on a memory stick ?)
450: for (File fi : allKnownPaths) {
451: if (!fi.exists()) {
452: rem.add(fi);
453: }
454: }
455: allKnownPaths.removeAll(rem);
456:
457: // remember even if not existing...
458: if (!allKnownPaths.contains(this .getPath())) {
459: allKnownPaths.add(this .getPath());
460: }
461:
462: props.setProperty(key, FileUtils.filesToList(allKnownPaths));
463: }
464:
465: /** Must have been stored previously with rememberPathForGlobalCompletion()
466: */
467: public void offerRememberedGlobalCompletion(AppProperties props,
468: String key) {
469: setAlternatePaths(FileUtils.getFilesFromList(props.getProperty(
470: key, ""), false));
471: }
472:
473: /** notified on enter and setPath...
474: */
475: public void addActionListener(ActionListener al) {
476: actionListeners.add(al);
477: }
478:
479: public void removeActionListener(ActionListener al) {
480: actionListeners.remove(al);
481: }
482:
483: private void notifyActionListeners() {
484: ActionListener[] als = actionListeners
485: .toArray(new ActionListener[actionListeners.size()]);
486: for (ActionListener al : als) {
487: al.actionPerformed(new ActionEvent(this ,
488: ActionEvent.ACTION_FIRST, "File selected"));
489: }
490: }
491:
492: public void setComponentWidth(int width) {
493: Dimension dim = pathField.getPreferredSize();
494: dim.width = width;
495: pathField.setMinimumSize(dim);
496: pathField.setPreferredSize(dim);
497: pathField.setMaximumSize(dim);
498:
499: // important: allow smooth resizes in GL3
500: this .setMinimumSize(this .getPreferredSize());
501:
502: }
503:
504: /** null if none
505: */
506: public File getFirstValidPathStartingWith() {
507: StringBuilder partialPath = new StringBuilder(this
508: .getTextField().getText());
509: while (partialPath.length() > 0) {
510: File f = new File(partialPath.toString());
511: if (f.exists())
512: return FileUtils.getCanonicalFileWithCase(f);
513: partialPath.setLength(partialPath.length() - 1);
514: }
515: return null;
516: }
517:
518: /** Called when the TAB key is pressed.
519: */
520: private void completePathForward() {
521: // TODO: buggy for windows/system3 !
522: final File firstValid = getFirstValidPathStartingWith();
523: if (firstValid == null)
524: return;
525:
526: String firstValidP = FileUtils.getCanonicalName(firstValid);
527: //System.out.println("firstValid="+firstValidP);
528:
529: String typedpath = FileUtils.getCanonicalName(getPath());
530: //System.out.println("typedpath="+typedpath);
531:
532: /*if(!typedpath.startsWith(firstValidP))
533: {
534: // remove the "/" at end, as occurng when completing "windows/system3" !
535: firstValidP = firstValidP.substring(0, firstValidP.length()-1);
536: }*/
537:
538: String invalidStartsWith = typedpath.substring(
539: firstValidP.length()).toUpperCase();
540: //System.out.println("invalidStartsWith="+invalidStartsWith);
541:
542: // look for files in firstValid startingwith invalidStartsWith
543: List<File> candidates = new ArrayList<File>();
544: File[] files = firstValid.listFiles();
545: if (files == null)
546: return;
547:
548: for (File f : files) {
549: if (this .fileChooserSelectionMode == JFileChooser.DIRECTORIES_ONLY
550: && !f.isDirectory())
551: continue;
552:
553: String can = FileUtils.getCanonicalName(f).toUpperCase();
554: String canDiff = can.substring(firstValidP.length())
555: .toUpperCase();
556: if (canDiff.startsWith(invalidStartsWith)) {
557: //System.out.println("candidate: "+can);
558:
559: if (f.isFile() && !allowedExtensions.isEmpty()) {
560: boolean ok = false;
561: String fn = f.getName().toUpperCase();
562: for (String exi : allowedExtensions) {
563: if (fn.endsWith("." + exi.toUpperCase())) {
564: ok = true;
565: break;
566: }
567: }
568:
569: if (ok)
570: candidates.add(f);
571: } else {
572: candidates.add(f);
573: }
574: }
575: }
576:
577: if (candidates.size() == 1) {
578: // autocomplete !
579: this .setPath(candidates.get(0));
580: } else if (candidates.size() > 1) {
581: // popup
582: JPopupMenu pop = new JPopupMenu();
583: int n = 0;
584: for (final File c : candidates) {
585: JMenuItem mi = new JMenuItem("" + c);
586: pop.add(mi);
587: mi.addActionListener(new ActionListener() {
588: public void actionPerformed(ActionEvent ae) {
589: setPath(c);
590: }
591: });
592: n++;
593:
594: if (n > 20) {
595: break;
596: }
597: }
598:
599: // offers parents
600: if (firstValid.getParentFile() != null) {
601: JMenuItem mi = new JMenuItem(""
602: + firstValid.getParentFile());
603: mi.setAccelerator(KeyStroke.getKeyStroke(
604: KeyEvent.VK_BACK_SPACE, 0));
605: pop.add(mi);
606: mi.addActionListener(new ActionListener() {
607: public void actionPerformed(ActionEvent ae) {
608: setPath(firstValid.getParentFile());
609: }
610: });
611:
612: }
613:
614: if (n > 20 && (candidates.size() - n) > 0) {
615: JMenuItem mib = new JMenuItem(
616: ""
617: + (candidates.size() - n)
618: + " more entries... use the file browser or be more precise");
619: pop.add(mib);
620: mib.addActionListener(new ActionListener() {
621: public void actionPerformed(ActionEvent ae) {
622: browseFileAction();
623: }
624: });
625: }
626:
627: pop.show(this .getTextField(), 0, 15);
628:
629: }
630: }
631:
632: class FileDropTarget extends DropTarget {
633: public FileDropTarget(JComponent c) {
634: super (c, DnDConstants.ACTION_COPY,
635: new FileDropTargetListener(c), true);
636: c.setDropTarget(this );
637:
638: }
639: }
640:
641: class FileDropTargetListener implements DropTargetListener {
642: JComponent comp;
643: Border originalBorder = null;
644: Border redBorder = BorderFactory.createLineBorder(Color.red, 1);
645: Border greenBorder = BorderFactory.createLineBorder(
646: Color.green, 1);
647:
648: public FileDropTargetListener(JComponent comp) {
649: this .comp = comp;
650: this .originalBorder = comp.getBorder();
651: }
652:
653: File fileToDrop = null;
654:
655: public void dragEnter(DropTargetDragEvent dte) {
656: //System.out.println("dragEnter");
657:
658: boolean accept = false;
659: fileToDrop = null;
660: for (DataFlavor df : dte.getCurrentDataFlavors()) {
661: if (df.isFlavorJavaFileListType()) {
662: Transferable tr = dte.getTransferable();
663: try {
664: Object data = tr
665: .getTransferData(df.javaFileListFlavor);
666: //System.out.println(""+data);
667: @SuppressWarnings("unchecked")
668: List<File> lf = (List<File>) data;
669: if (lf.size() == 1) {
670: if (isValidCandidate(lf.get(0))) {
671: fileToDrop = lf.get(0);
672: pathField.setToolTipText("Droping "
673: + lf.get(0));
674: dte
675: .acceptDrag(DnDConstants.ACTION_COPY);
676: accept = true;
677: break;
678: }
679: }
680: } catch (Exception e) {
681: comp.setBorder(redBorder);
682: e.printStackTrace();
683: }
684: } else if (df.isFlavorTextType()) {
685: Transferable tr = dte.getTransferable();
686: try {
687: String data = ""
688: + tr
689: .getTransferData(df.javaFileListFlavor);
690: if (data.toLowerCase().startsWith("file://"))
691: data = data.substring(6);
692:
693: File f = new File(data);
694: if (isValidCandidate(f)) {
695: fileToDrop = f;
696: pathField.setToolTipText("Droping " + f); // don't work
697: //ToolTipManager.sharedInstance().setDismissDelay(.showTipWindow();
698: dte.acceptDrag(DnDConstants.ACTION_COPY);
699: accept = true;
700: break;
701: }
702:
703: } catch (Exception e) {
704: comp.setBorder(redBorder);
705: e.printStackTrace();
706: }
707: }
708: System.out.println("" + df);
709: }
710:
711: if (accept) {
712: comp.setBorder(greenBorder);
713: } else {
714: comp.setBorder(redBorder);
715: }
716: }
717:
718: public void dragExit(DropTargetEvent dte) {
719: //System.out.println("dragExit");
720: pathField.setToolTipText(null);
721: comp.setBorder(originalBorder);
722: }
723:
724: public void dragOver(DropTargetDragEvent dte) {
725: //System.out.println("dragOver");
726: }
727:
728: public void drop(DropTargetDropEvent dte) {
729: comp.setBorder(originalBorder);
730: pathField.setToolTipText(null);
731: if (fileToDrop != null) {
732: setFile(fileToDrop);
733: }
734: //System.out.println("drop");
735: }
736:
737: public void dropActionChanged(DropTargetDragEvent dte) {
738: System.out.println("dropActionChanged");
739: }
740: }
741:
742: public void allowDropFilesFromOS() {
743: new FileDropTarget(this .pathField);
744: }
745:
746: /* test*/
747: public static void main(String[] arguments) {
748: EventQueue.invokeLater(new Runnable() {
749: public void run() {
750:
751: JFrame f = new JFrame();
752: f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
753: FileField ff = new FileField("", false, "Hello",
754: JFileChooser.FILES_ONLY);
755: ff.allowDropFilesFromOS();
756: ff.setAlternatePaths(Arrays.asList(new File("c:/temp"),
757: new File("c:/windows")));
758: ff.setComponentWidth(250);
759: ff.setAutoColorized();
760: f.setContentPane(ff);
761: f.pack();
762: f.setLocationRelativeTo(null);
763: f.setVisible(true);
764: }
765: });
766:
767: }
768:
769: }
|