<< JavaFX Too Late...But Who Cares | Home | SpringRCP IntelliJ SimpleApp >>

[Swing] Implementing Goto Class and Find resource

...and introducing GlazedLists

Last week I wrote an entry on implementing application wide hotkeys or global hotkeys. I mentioned writing a new article on how I implemented IntelliJ's Goto Class though I think calling it the same as Eclipse's Open Resource is a better name for my application since I'm not dealing with classes at all.

public class LookupDialog extends JDialog { private static final String KEY_DOWN = "KEY_DOWN"; private static final String KEY_ESC = "KEY_ESC"; private static final String KEY_ENTER = "KEY_ENTER"; private ResourceManager resource = ResourceManager.getInstance(); private JPanel panel; private JList list; private EventList<NodeModel> listNodes; private JTree tree; private JScrollPane listScroll; private JTextField searchText;

I use a JDialog for this implementation. I create some constants that are going to be used for my input and action maps. ResourceManager is a class I created to handle loading of resources like images and to handle localization. My Lookup Dialog searches a JTree which represents a bunch of information in a database. Since the JTree is in sync with the database searching it should be a little faster than searching the database for the same information. Note that I have defined an EventList<NodeModel> listNodes. This is a java.util.List implementation that will hold the search results and it used by a JList. EventList a class from the GlazedLists project. I'll show how it is used a bit later.

public LookupDialog(Frame parent, boolean modal) { super(parent, modal); setUndecorated(true); panel = new JPanel(new MigLayout("fill")); panel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(204, 204, 204), 1), BorderFactory.createTitledBorder("Enter Search Name"))); tree = Application.getInstance().getTreePanel().getTree(); listNodes = GlazedListsSwing.swingThreadProxyList(new BasicEventList<NodeModel>()); list = new JList((new EventListModel<NodeModel>(listNodes))); list.setCellRenderer(new ListRenderer()); listScroll = new JScrollPane(list); searchText = new JTextField(25); searchText.addKeyListener(new KeyAdapter() { public void keyTyped(KeyEvent e) { SwingUtilities.invokeLater(new Runnable() { public void run() { find(); } }); } }); buildKeyBindings(); panel.add(searchText, "growx, wrap"); panel.add(listScroll, "grow"); add(panel); pack(); setLocationRelativeTo(Application.getInstance().getMainFrame()); setVisible(true); }

The class constructor sets most everything up. You'll notice some simple laying out of components. I basically have a text field where I type in the search query. As I type, the JList beneath is updated to reflect the results. Note the creation of listNodes. Paraphrasing the GlazedLists docs, Swing requires all user interface access performed by the event dispatch thread. GlazedLists provides adapter classes that use a special EventList that copies your list changes to the Swing EDT. The way to create an instance of this proxy is via GlazedListsSwing.swingThreadProxyList(EventList) just as I have done. I then use SwingUtilities.invokeLater() to execute the find() method.

Technically I didn't need to do this. In fact, my first version didn't do this. But you can feel a slight delay when typing a query and seeing the results. Threading this process makes the implementation a lot quicker and smoother.

private void find() { if (searchText.getText().trim().length() > 0) { listNodes.clear(); filter((DefaultMutableTreeNode) tree.getModel().getRoot(), searchText.getText()); } else { listNodes.clear(); } } private void filter(DefaultMutableTreeNode node, String searchString) { for (int i = 0; i < node.getChildCount(); i++) { final DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); if (child.isLeaf()) { NodeModel nm = (NodeModel) child.getUserObject(); if (nm.getNodeName().toUpperCase().contains(searchString.toUpperCase())) { listNodes.add(nm); } } else { filter(child, searchString); } } }

As I type and the find() method is called I first clear the listNodes and then the filter() method is called passing in my root tree node and the query string. filter() is a recursive function that search down the tree looking for matching nodes. As it finds a match it adds the results to the listNodes. Since GlazedLists handles the boilerplate code of dealing with the different Model implementations for JList, JTable, JComboBox, etc, I don't have to worry about it. Once the results are added to listNodes they show up in the JList.

private void buildKeyBindings() { searchText.getActionMap().put(KEY_DOWN, new KeyDownAction()); searchText.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), KEY_DOWN); searchText.getActionMap().put(KEY_ESC, new EscapeKeyAction()); searchText.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), KEY_ESC); list.getActionMap().put(KEY_ESC, new EscapeKeyAction()); list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), KEY_ESC); list.getActionMap().put(KEY_ENTER, new EnterKeyAction()); list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), KEY_ENTER); } private class KeyDownAction extends AbstractAction { public void actionPerformed(ActionEvent e) { if (listNodes.size() > 0) { list.requestFocus(); list.setSelectedIndex(0); } } } private class EscapeKeyAction extends AbstractAction { public void actionPerformed(ActionEvent e) { dispose(); } } private class EnterKeyAction extends AbstractAction { public void actionPerformed(ActionEvent e) { NodeModel nm = listNodes.get(list.getSelectedIndex()); if (nm.getNode().getType().equals(NodeType.leafRule.toString())) { try { Application.getInstance().getContentPanel().addForm(new RuleForm(nm)); dispose(); } catch (Exception e1) { e1.printStackTrace(); } } } }

Appearntly you can't capture arrow key events using a KeyListener. I had to use the ActionMap and InputMap. So I just decided to deal with the escape and enter keys the same way. As you begin to search and the list is populated, pressing the down arrow key moves focus from the textfield to the list. Hitting the ESC key closes the dialog all together. Hitting the ENTER key on a list item opens that item for viewing and closes the dialog.

Here is all the code in one full swoop

public class LookupDialog extends JDialog { private static final String KEY_DOWN = "KEY_DOWN"; private static final String KEY_ESC = "KEY_ESC"; private static final String KEY_ENTER = "KEY_ENTER"; private ResourceManager resource = ResourceManager.getInstance(); private JPanel panel; private JList list; private EventList<NodeModel> listNodes; private JTree tree; private JScrollPane listScroll; private JTextField searchText; public LookupDialog(Frame parent, boolean modal) { super(parent, modal); setUndecorated(true); panel = new JPanel(new MigLayout("fill")); panel.setBorder(BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(new Color(204, 204, 204), 1), BorderFactory.createTitledBorder("Enter Search Name"))); tree = Application.getInstance().getTreePanel().getTree(); listNodes = GlazedListsSwing.swingThreadProxyList(new BasicEventList<NodeModel>()); list = new JList((new EventListModel<NodeModel>(listNodes))); list.setCellRenderer(new ListRenderer()); listScroll = new JScrollPane(list); searchText = new JTextField(25); searchText.addKeyListener(new KeyAdapter() { public void keyTyped(KeyEvent e) { SwingUtilities.invokeLater(new Runnable() { public void run() { find(); } }); } }); buildKeyBindings(); panel.add(searchText, "growx, wrap"); panel.add(listScroll, "grow"); add(panel); pack(); setLocationRelativeTo(Application.getInstance().getMainFrame()); setVisible(true); } private void find() { if (searchText.getText().trim().length() > 0) { listNodes.clear(); filter((DefaultMutableTreeNode) tree.getModel().getRoot(), searchText.getText()); } else { listNodes.clear(); } } private void filter(DefaultMutableTreeNode node, String searchString) { for (int i = 0; i < node.getChildCount(); i++) { final DefaultMutableTreeNode child = (DefaultMutableTreeNode) node.getChildAt(i); if (child.isLeaf()) { NodeModel nm = (NodeModel) child.getUserObject(); if (nm.getNodeName().toUpperCase().contains(searchString.toUpperCase())) { listNodes.add(nm); } } else { filter(child, searchString); } } } private void buildKeyBindings() { searchText.getActionMap().put(KEY_DOWN, new KeyDownAction()); searchText.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_DOWN, 0), KEY_DOWN); searchText.getActionMap().put(KEY_ESC, new EscapeKeyAction()); searchText.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), KEY_ESC); list.getActionMap().put(KEY_ESC, new EscapeKeyAction()); list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), KEY_ESC); list.getActionMap().put(KEY_ENTER, new EnterKeyAction()); list.getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), KEY_ENTER); } private class ListRenderer extends JLabel implements ListCellRenderer { public ListRenderer() { setOpaque(true); setBackground(Color.WHITE); } public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { if (isSelected) { setBackground(list.getSelectionBackground()); setForeground(list.getSelectionForeground()); } else { setBackground(Color.WHITE); setForeground(list.getForeground()); } NodeModel nm = (NodeModel) value; Map<String, Icon> iconMap = nm.getNode().getIconMap(); Icon icon = iconMap.get("default"); setIcon(resource.getImageIcon(icon.getImageClass())); setText(nm.getNodeName()); return this; } } private class KeyDownAction extends AbstractAction { public void actionPerformed(ActionEvent e) { if (listNodes.size() > 0) { list.requestFocus(); list.setSelectedIndex(0); } } } private class EscapeKeyAction extends AbstractAction { public void actionPerformed(ActionEvent e) { dispose(); } } private class EnterKeyAction extends AbstractAction { public void actionPerformed(ActionEvent e) { NodeModel nm = listNodes.get(list.getSelectedIndex()); if (nm.getNode().getType().equals(NodeType.leafRule.toString())) { try { Application.getInstance().getContentPanel().addForm(new RuleForm(nm)); dispose(); } catch (Exception e1) { e1.printStackTrace(); } } } } }

I don't have time to come up with a full working demo to webstart or download so I've created a short video of my implementation in action. Well, as much as I can show anyway. This is a commercial app so I have to be careful what I am sharing.

Get the Flash Player to see this player.
Tags :



Add a comment Send a TrackBack