package nbtool.gui; import java.awt.Color; import java.awt.Component; import java.awt.Dimension; import java.awt.FileDialog; import java.awt.KeyboardFocusManager; import java.awt.datatransfer.DataFlavor; import java.awt.dnd.DnDConstants; import java.awt.dnd.DropTarget; import java.awt.dnd.DropTargetDropEvent; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.KeyListener; import java.beans.PropertyChangeEvent; import java.beans.PropertyVetoException; import java.beans.VetoableChangeListener; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collections; import java.util.List; import javax.swing.DefaultComboBoxModel; import javax.swing.InputVerifier; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.KeyStroke; import javax.swing.event.TreeModelEvent; import javax.swing.event.TreeModelListener; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.TreeModel; import javax.swing.tree.TreePath; import javax.swing.tree.TreeSelectionModel; import nbtool.nio.FileIO; import nbtool.util.Debug; import nbtool.util.Debug.DebugSettings; import nbtool.util.ToolSettings; import nbtool.util.UserSettings; import nbtool.util.Utility; public class PathChooser { public static String suggestion(String robot) { Calendar cal = Calendar.getInstance(); int day = cal.get(Calendar.DAY_OF_MONTH); int month = cal.get(Calendar.MONTH) + 1; return ToolSettings.NBITES_DIR + "data/logs/" + String.format("%s/%d_%d/%s/", UserSettings.venue, month, day, robot); } /* nbites/data/logs/edwards16/01/batman/new_ball_logs */ public static void main(String[] args) { Path path = chooseDirPath(null, null); Debug.print("end: %s", path); } /* * input verifier, * tab complete * select via tree * BACK BUTTON * * */ public static Path chooseLogPath(Component centered, String suggest) { return internal(centered, true, suggest); } public static Path chooseDirPath(Component centered, String suggest) { return internal(centered, false, suggest); } // public static Path chooseDirPath(Component centered, String ) private static Path internal(Component centered, boolean file, String suggest) { final JDialog dialog = new JDialog(); dialog.setModal(true); dialog.setTitle("choose path"); dialog.setLocationRelativeTo(centered); handler.setMode(file, dialog, suggest); dialog.setContentPane(handler.display); dialog.setSize(handler.display.getPreferredSize()); dialog.setMinimumSize(handler.display.getPreferredSize()); ActionListener escListener = new ActionListener() { @Override public void actionPerformed(ActionEvent e) { dialog.dispose(); } }; dialog.getRootPane().registerKeyboardAction(escListener, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); dialog.setVisible(true); dialog.dispose(); return handler.get(); } private static final DebugSettings debug = Debug.createSettings(true, true, true, Debug.INFO, null); private static final PathChooserHandler handler = new PathChooserHandler(); private static final Color DARK_GREEN = new Color(0,100,0); private static final int DISPLAYED_CONTENTS_LIMIT = 500; private static class PathChooserHandler implements KeyListener, ActionListener, TreeModel, TreeSelectionListener { protected final PathChooseDisplay display = new PathChooseDisplay(); private JDialog dialog; private boolean forceFile; private Path chosen; protected PathChooserHandler() { display.chooserTextField.setDropTarget(new DropTarget() { public synchronized void drop(DropTargetDropEvent evt) { try { evt.acceptDrop(DnDConstants.ACTION_COPY); @SuppressWarnings("unchecked") List<File> droppedFiles = ((List<File>) evt .getTransferable().getTransferData( DataFlavor.javaFileListFlavor)); if (!droppedFiles.isEmpty()) { File dropped = droppedFiles.get(droppedFiles.size() - 1); Debug.warn("using dragged file: %s", dropped.getAbsolutePath()); display.chooserTextField.setText(dropped.getAbsolutePath()); } } catch (Exception ex) { ex.printStackTrace(); } } }); display.chooserTextField.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.EMPTY_SET); display.chooserTextField.addKeyListener(this); display.loadPathComboBox.addActionListener(new ActionListener(){ @Override public void actionPerformed(ActionEvent e) { display.chooserTextField.setText( display.loadPathComboBox.getSelectedItem().toString() ); validate(); } }); display.choosePathButton.addActionListener(this); display.workingDirTree.setModel(this); display.workingDirTree.addTreeSelectionListener(this); display.workingDirTree.setEditable(false); display.workingDirTree.setRootVisible(false); display.workingDirTree.getSelectionModel(). setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); display.createPathBox.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { validate(); } }); } final String tip = " (prefix with '~' or '/'," + " otherwise path is relative to NBITES_DIR)"; final String fileText = "choose file path " + tip; final String dirText = "choose dir path " + tip; protected void setMode(boolean file, JDialog dialog, String suggest) { this.forceFile = file; this.dialog = dialog; this.chosen = null; display.chooserInfoAndTips.setText(file ? fileText:dirText); display.loadPathComboBox.setModel( new DefaultComboBoxModel<Path>(UserSettings.loadPathes.vector())); if (suggest != null) { display.chooserTextField.setText(suggest); } else if (UserSettings.loadPathes.vector().isEmpty()) { display.chooserTextField.setText(ToolSettings.NBITES_DIR); } else { display.loadPathComboBox.setSelectedIndex(0); display.chooserTextField.setText(display.loadPathComboBox.getSelectedItem().toString()); } display.createPathBox.setSelected(true); validate(); } protected Path get() { debug.event("get() returned: %s", chosen); return chosen; } private Path tabHelper() { Path current = this.expand(); Path enclosing = this.enclosing(); if (enclosing == null) { Debug.warn("cannot get enclosing: %s", current); return null; } Path[] parts = FileIO.getContentsOf(enclosing, DISPLAYED_CONTENTS_LIMIT); assert(parts != null); if (parts.length == 1) { return parts[0]; } else { if (!Files.isDirectory(current)) { Path last = current.getFileName(); Path best = null; for (Path sub : parts) { Path subName = sub.getFileName(); if (subName.toString().startsWith(last.toString())) { if (best == null || subName.toString().length() < best.toString().length()) best = subName; } } if (best != null) { Path resolved = enclosing.resolve(best); return resolved; } else return null; } else { return current; } } } @Override public void keyTyped(KeyEvent e) { if (e.getKeyChar() == '\t') { e.consume(); Path tabCompleted = tabHelper(); if (tabCompleted == null) return; if (Files.isDirectory(tabCompleted)) { display.chooserTextField.setText(tabCompleted.toString() + "/"); } else { display.chooserTextField.setText(tabCompleted.toString()); } } } public void keyPressed(KeyEvent e) {} public void keyReleased(KeyEvent e) { validate(); } private boolean isValid() { String text = display.chooserTextField.getText().trim(); debug.event("isValid() on: %s", text); Path current = expand(); debug.event("isValid() path: %s", current); if (forceFile) { if (text.endsWith("/")) return false; if (Files.exists(current)) { return !Files.isDirectory(current); } return display.createPathBox.isSelected(); } else { if (Files.exists(current)) { return Files.isDirectory(current); } return display.createPathBox.isSelected(); } } private boolean validate() { Path enclosing = enclosing(); Debug.event("validate() enclosing = %s", enclosing); if (display.isVisible() && enclosing != null) { restructure(); } boolean valid = isValid(); display.choosePathButton.setEnabled(valid); display.choosePathButton.setForeground( valid ? DARK_GREEN : Color.BLACK); return valid; } private Path expand() { String text = display.chooserTextField.getText().trim(); Path expanded = null; if (text.isEmpty()) { expanded = ToolSettings.NBITES_DIR_PATH; } else { char start = text.charAt(0); switch(start) { case '~': expanded = Paths.get(System.getProperty("user.home"), text.substring(1)); break; case '/': expanded = Paths.get(text); break; default: expanded = ToolSettings.NBITES_DIR_PATH.resolve(text); break; } } assert(expanded.isAbsolute()); return expanded; } private Path enclosing() { Path path = expand(); if (Files.exists(path) && Files.isDirectory(path)) { return path; } else { Path enclosing = path.getParent(); // Debug.info("enclosing() internal enclosing = %s exists %B isAbsolute %B", enclosing, // Files.exists(enclosing), enclosing.isAbsolute()); if (Files.exists(enclosing)) { assert(Files.isDirectory(enclosing)); return enclosing; } else { return null; } } } private Path[] workingContents() { Path dir = enclosing(); if (dir == null) return new Path[0]; Path[] found = FileIO.getContentsOf(dir, DISPLAYED_CONTENTS_LIMIT); return (found == null) ? new Path[0] : found; } @Override public void valueChanged(TreeSelectionEvent e) { if (e.isAddedPath()) { TreePath path = e.getPath(); Path clicked = (Path) path.getLastPathComponent(); display.chooserTextField.setText(clicked.toString()); validate(); } } @Override public Object getRoot() { return this; } @Override public Object getChild(Object parent, int index) { if (parent == this) { return workingContents()[index]; } else { throw new Error("no children at index " + index); } } @Override public int getChildCount(Object parent) { if (parent == this) return workingContents().length; else return 0; } @Override public boolean isLeaf(Object node) { if (node == this) return false; else { Path path = (Path) node; return !Files.isDirectory(path); } } @Override public void valueForPathChanged(TreePath path, Object newValue) { } @Override public int getIndexOfChild(Object parent, Object child) { assert(parent == this && child instanceof Path); return Arrays.asList(workingContents()).indexOf(child); } private final ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>(); @Override public void addTreeModelListener(TreeModelListener l) { listeners.add(l); } @Override public void removeTreeModelListener(TreeModelListener l) { listeners.remove(l); } private void restructure() { TreeModelEvent changed = new TreeModelEvent(this, new Object[]{this}); for (TreeModelListener listener : listeners) listener.treeStructureChanged(changed); } private void chooseAction() { if (isValid()) { chosen = expand(); if (display.createPathBox.isSelected() && !Files.exists(chosen)) { if (forceFile) { //last part is file name Path needsCreate = chosen.getParent(); if (!Files.exists(needsCreate)) { try { Files.createDirectories(needsCreate); ToolMessage.displayInfo("created path: %s", needsCreate); } catch (IOException e1) { e1.printStackTrace(); ToolMessage.displayError("error creating path to: %s", chosen); chosen = null; } } } else { Path needsCreate = chosen; try { Files.createDirectories(needsCreate); ToolMessage.displayInfo("created path: %s", needsCreate); } catch (IOException e1) { e1.printStackTrace(); ToolMessage.displayError("error creating path: %s", chosen); chosen = null; } } } dialog.dispose(); } } @Override public void actionPerformed(ActionEvent e) { assert(e.getSource() == display.choosePathButton); chooseAction(); } } private static class PathChooseDisplay extends JPanel { private static final long serialVersionUID = 1L; protected PathChooseDisplay() { super(); initComponents(); } /** * This method is called from within the constructor to initialize the form. * WARNING: Do NOT modify this code. The content of this method is always * regenerated by the Form Editor. */ // <editor-fold defaultstate="collapsed" desc="Generated Code"> private void initComponents() { jScrollPane1 = new javax.swing.JScrollPane(); chooserTextField = new javax.swing.JTextField(); chooserInfoAndTips = new javax.swing.JLabel(); loadPathComboBox = new javax.swing.JComboBox<>(); jScrollPane2 = new javax.swing.JScrollPane(); workingDirTree = new javax.swing.JTree(); choosePathButton = new javax.swing.JButton(); createPathBox = new javax.swing.JCheckBox(); setMaximumSize(new java.awt.Dimension(600, 32767)); setMinimumSize(new java.awt.Dimension(500, 200)); chooserTextField.setText("jTextField1"); jScrollPane1.setViewportView(chooserTextField); chooserInfoAndTips.setText("jLabel1"); jScrollPane2.setViewportView(workingDirTree); choosePathButton.setText("choose this path"); createPathBox.setText("create path if necessary"); javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); this.setLayout(layout); layout.setHorizontalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(jScrollPane2) .addGroup(layout.createSequentialGroup() .addContainerGap() .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addComponent(loadPathComboBox, javax.swing.GroupLayout.Alignment.TRAILING, 0, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addComponent(jScrollPane1) .addComponent(chooserInfoAndTips, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() .addComponent(createPathBox, javax.swing.GroupLayout.PREFERRED_SIZE, 289, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(choosePathButton, javax.swing.GroupLayout.DEFAULT_SIZE, 242, Short.MAX_VALUE))))) .addContainerGap()) ); layout.setVerticalGroup( layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) .addGroup(layout.createSequentialGroup() .addGap(7, 7, 7) .addComponent(chooserInfoAndTips) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 49, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(loadPathComboBox, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addComponent(jScrollPane2, javax.swing.GroupLayout.DEFAULT_SIZE, 171, Short.MAX_VALUE) .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) .addComponent(choosePathButton) .addComponent(createPathBox))) ); }// </editor-fold> // Variables declaration - do not modify protected javax.swing.JButton choosePathButton; protected javax.swing.JLabel chooserInfoAndTips; protected javax.swing.JTextField chooserTextField; protected javax.swing.JCheckBox createPathBox; protected javax.swing.JScrollPane jScrollPane1; protected javax.swing.JScrollPane jScrollPane2; protected javax.swing.JComboBox<Path> loadPathComboBox; protected javax.swing.JTree workingDirTree; // End of variables declaration } }