/* * Copyright 2003-2011 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.ide.ui; import com.intellij.ui.ScrollPaneFactory; import com.intellij.ui.components.JBList; import javax.swing.*; import javax.swing.event.CaretEvent; import javax.swing.event.CaretListener; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.text.BadLocationException; import java.awt.BorderLayout; import java.awt.Point; import java.awt.Window; import java.awt.event.*; import java.util.List; public abstract class CompletionTextField extends JTextField { private PopupHint myHint = new PopupHint(); private Window myContainerWindow; private ComponentListener myListener = new ComponentAdapter() { @Override public void componentMoved(ComponentEvent e) { myHint.updateBounds(); } }; private MouseListener myMouseListener = new MouseAdapter() { @Override public void mousePressed(MouseEvent e) { myHint.hide(); e.consume(); } }; private UpAction myUpAction = new UpAction(); private DownAction myDownAction = new DownAction(); public CompletionTextField() { super(20); getDocument().addDocumentListener(new DocumentListener() { @Override public void insertUpdate(DocumentEvent e) { updatePopup(); } @Override public void removeUpdate(DocumentEvent e) { if (isCanShowCompletionOnRemove()) { updatePopup(); } updateActions(); } @Override public void changedUpdate(DocumentEvent e) { updatePopup(); } private void updatePopup() { if (myHint.isVisible() || canShowPopupAutomatically()) { updateCompletion(); } updateActions(); } }); addCaretListener(new CaretListener() { @Override public void caretUpdate(CaretEvent e) { if (isFocusOwner() && myHint.isVisible()) { updateCompletion(); } } }); addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER && e.getModifiers() == 0 && myHint.isVisible()) { myHint.complete(); e.consume(); } } }); registerKeyboardAction(myUpAction, KeyStroke.getKeyStroke("UP"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); registerKeyboardAction(myUpAction, KeyStroke.getKeyStroke("UP"), WHEN_FOCUSED); registerKeyboardAction(myDownAction, KeyStroke.getKeyStroke("DOWN"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); registerKeyboardAction(myDownAction, KeyStroke.getKeyStroke("DOWN"), WHEN_FOCUSED); registerKeyboardAction(new AbstractAction() { @Override public void actionPerformed(ActionEvent e) { myHint.show(); updateCompletion(); } }, KeyStroke.getKeyStroke("ctrl SPACE"), WHEN_ANCESTOR_OF_FOCUSED_COMPONENT); addKeyListener(new KeyAdapter() { @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ESCAPE && myHint.isVisible()) { myHint.hide(); e.consume(); } } }); addFocusListener(new FocusAdapter() { @Override public void focusLost(FocusEvent e) { myHint.hide(); } }); updateActions(); } protected boolean canShowPopupAutomatically() { return true; } public void setHideCompletionOnClick(boolean hide) { if (hide) { addMouseListener(myMouseListener); if(getParent() != null) getParent().addMouseListener(myMouseListener); } else { removeMouseListener(myMouseListener); if(getParent() != null) getParent().removeMouseListener(myMouseListener); } } @Override public void addNotify() { super.addNotify(); myContainerWindow = SwingUtilities.getWindowAncestor(this); myContainerWindow.addComponentListener(myListener); } @Override public void removeNotify() { if (myContainerWindow != null) { myContainerWindow.removeComponentListener(myListener); myContainerWindow = null; } super.removeNotify(); } public abstract List<String> getProposals(String text); private String getTextPrefix() { try { if (getCaretPosition() >= getText().length()) { return getText(); } return getText(0, getCaretPosition()); } catch (BadLocationException e) { throw new RuntimeException(e); } } @Override public boolean isValid() { return true; } public boolean completionIsVisible() { return myHint.isVisible(); } public void showCompletion() { updateCompletion(); } private void updateCompletion() { if (!isShowing()) { return; } List<String> proposals = getProposals(getTextPrefix()); if (proposals.isEmpty()) { myHint.hide(); return; } if (proposals.contains(getTextPrefix())) { myHint.hide(); return; } if (!myHint.isVisible()) { myHint.show(); } myHint.setProposals(proposals); } protected boolean isCanShowCompletionOnRemove() { return true; } private void updateActions() { myUpAction.setEnabled(myHint.isVisible()); myDownAction.setEnabled(myHint.isVisible() || canShowPopupAutomatically()); } private class PopupHint { private JWindow myWindow; private JScrollPane myScroller; private JList myList; private PopupHint() { } boolean isVisible() { return myWindow != null; } void show() { if (myWindow != null) { return; } Window windowAncestor = SwingUtilities.getWindowAncestor(CompletionTextField.this); myWindow = new JWindow(windowAncestor); myWindow.setFocusableWindowState(false); myWindow.setLayout(new BorderLayout()); myList = new JBList(); myScroller = ScrollPaneFactory.createScrollPane(myList); myList.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2 && myList.getSelectedValue() != null) { setText((String) myList.getSelectedValue()); } } }); myWindow.add(myScroller); updateBounds(); myWindow.setVisible(true); updateActions(); } void updateBounds() { if (myWindow == null) return; Point loc = getLocationOnScreen(); myWindow.setBounds( loc.x, loc.y + getHeight(), getWidth(), myScroller.getPreferredSize().height ); } void setProposals(List<String> proposals) { DefaultListModel model = new DefaultListModel(); for (String proposal : proposals) { model.addElement(proposal); } myList.setModel(model); } void hide() { if (myWindow != null) { myWindow.dispose(); myWindow = null; } updateActions(); } void up() { if (!isVisible()) { return; } if (myList.getSelectedIndex() == -1) { myList.setSelectedIndex(0); } else { myList.setSelectedIndex(Math.max(0, myList.getSelectedIndex() - 1)); } myList.ensureIndexIsVisible(myList.getSelectedIndex()); } void down() { if (!isVisible()) { return; } if (myList.getSelectedIndex() == -1) { myList.setSelectedIndex(0); } else { myList.setSelectedIndex(Math.min(myList.getModel().getSize() - 1, myList.getSelectedIndex() + 1)); } myList.ensureIndexIsVisible(myList.getSelectedIndex()); } void complete() { if (!isVisible()) { return; } if (myList.getSelectedIndex() == -1) { return; } else { setText(myList.getSelectedValue().toString()); setCaretPosition(getText().length()); } } } private class DownAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { if (myHint.isVisible()) { myHint.down(); } else { if (canShowPopupAutomatically()) { myHint.show(); updateCompletion(); } } } } private class UpAction extends AbstractAction { @Override public void actionPerformed(ActionEvent e) { if (myHint.isVisible()) { myHint.up(); } } } }