/* * :tabSize=4:indentSize=4:noTabs=false: * :folding=explicit:collapseFolds=1: * * Copyright (C) 2007 KazutoshiSatoda * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.gjt.sp.jedit.gui; //{{{ Imports import java.awt.BorderLayout; import java.awt.Component; import java.awt.EventQueue; import java.awt.Point; import java.awt.Rectangle; import java.awt.Window; import java.awt.event.InputEvent; import java.awt.event.KeyAdapter; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import javax.swing.AbstractListModel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JWindow; import javax.swing.ListSelectionModel; import javax.swing.ListCellRenderer; import javax.swing.ScrollPaneConstants; import org.gjt.sp.util.GenericGUIUtilities; import org.gjt.sp.jedit.View; //}}} /** Popup window for word completion in text area. * This class provides basic UI of completion popup. * * @since jEdit 4.3pre11 */ @SuppressWarnings({"unchecked"}) // The CandidateListModel needs work public class CompletionPopup extends JWindow { //{{{ interface Candidates /** * Candidates of completion. */ public interface Candidates { /** * Returns the number of candidates. */ public int getSize(); /** * Returns whether this completion is still valid. */ public boolean isValid(); /** * Do the completion. */ public void complete(int index); /** * Returns a component to render a cell for the index * in the popup. */ public Component getCellRenderer(JList list, int index, boolean isSelected, boolean cellHasFocus); /** * Returns a description text shown when the index is * selected in the popup, or null if no description is * available. */ public String getDescription(int index); } //}}} //{{{ CompletionPopup constructor /** * Create a completion popup. * It is not shown until reset() method is called with valid * candidates. All key events for the view are intercepted by * this popup untill end of completion. * @since jEdit 4.3pre13 */ public CompletionPopup(View view) { super(view); this.view = view; this.keyHandler = new KeyHandler(); this.candidates = null; this.list = new JList(); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setCellRenderer(new CellRenderer()); list.addKeyListener(keyHandler); list.addMouseListener(new MouseHandler()); list.setFocusTraversalKeysEnabled(false); JPanel content = new JPanel(new BorderLayout()); content.setFocusTraversalKeysEnabled(false); // stupid scrollbar policy is an attempt to work around // bugs people have been seeing with IBM's JDK -- 7 Sep 2000 JScrollPane scroller = new JScrollPane(list, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER); content.add(scroller, BorderLayout.CENTER); setContentPane(content); addWindowFocusListener(new WindowFocusHandler()); } public CompletionPopup(View view, Point location) { this(view); if (location != null) { setLocation(location); } } //}}} //{{{ dispose() method /** * Quit completion. */ @Override public void dispose() { if (isDisplayable()) { if (view.getKeyEventInterceptor() == keyHandler) { view.setKeyEventInterceptor(null); } super.dispose(); // This is a workaround to ensure setting the // focus back to the textArea. Without this, the // focus gets lost after closing the popup in // some environments. It seems to be a bug in // J2SE 1.4 or 5.0. Probably it relates to the // following one. // "Frame does not receives focus after closing // of the owned window" // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4810575 EventQueue.invokeLater(new Runnable() { @Override public void run() { view.getTextArea().requestFocus(); } }); } } //}}} //{{{ reset() method /** * Start completion. * @param candidates The candidates of this completion * @param active Set focus to the popup */ public void reset(Candidates candidates, boolean active) { if(candidates == null || !candidates.isValid() || candidates.getSize() <= 0) { dispose(); return; } this.candidates = candidates; list.setModel(new CandidateListModel()); list.setVisibleRowCount(Math.min(candidates.getSize(),8)); pack(); setLocation(fitInScreen(getLocation(null),this, view.getTextArea().getPainter().getLineHeight())); if (active) { setSelectedIndex(0); GenericGUIUtilities.requestFocus(this,list); } setVisible(true); view.setKeyEventInterceptor(keyHandler); } //}}} //{{{ getCandidates() method /** * Current candidates of completion. */ public Candidates getCandidates() { return candidates; } //}}} //{{{ getSelectedIndex() method /** * Returns index of current selection. * Returns -1 if nothing is selected. */ public int getSelectedIndex() { return list.getSelectedIndex(); } //}}} //{{{ setSelectedIndex() method /** * Set selection. */ public void setSelectedIndex(int index) { if (candidates != null && 0 <= index && index < candidates.getSize()) { list.setSelectedIndex(index); list.ensureIndexIsVisible(index); String description = candidates.getDescription(index); if (description != null) { view.getStatus().setMessageAndClear(description); } } } //}}} //{{{ doSelectedCompletion() method /** * Do completion with current selection and quit. */ public boolean doSelectedCompletion() { int selected = list.getSelectedIndex(); if (candidates != null && 0 <= selected && selected < candidates.getSize()) { candidates.complete(selected); dispose(); return true; } return false; } //}}} //{{{ keyPressed() medhod /** * Handle key pressed events. * Override this method to make additional key handing. */ protected void keyPressed(KeyEvent e) { } //}}} //{{{ keyTyped() medhod /** * Handle key typed events. * Override this method to make additional key handing. */ protected void keyTyped(KeyEvent e) { } //}}} //{{{ Private members //{{{ Instance variables private final View view; private final KeyHandler keyHandler; private Candidates candidates; private final JList list; //}}} //{{{ fitInScreen() method private static Point fitInScreen(Point p, Window w, int lineHeight) { Rectangle screenSize = w.getGraphicsConfiguration().getBounds(); if(p.y + w.getHeight() >= screenSize.height) p.y = p.y - w.getHeight() - lineHeight; return p; } //}}} //{{{ moveRelative method() private void moveRelative(int n) { int selected = list.getSelectedIndex(); int newSelect = selected + n; if (newSelect < 0) { newSelect = 0; } else { int numItems = list.getModel().getSize(); if(numItems < 1) { return; } if(newSelect >= numItems) { newSelect = numItems - 1; } } if(newSelect != selected) { setSelectedIndex(newSelect); } } //}}} //{{{ moveRelativePages() method private void moveRelativePages(int n) { int pageSize = list.getVisibleRowCount() - 1; moveRelative(pageSize * n); } //}}} //{{{ passKeyEventToView() method private void passKeyEventToView(KeyEvent e) { // Remove intercepter to avoid infinite recursion. assert (view.getKeyEventInterceptor() == keyHandler); view.setKeyEventInterceptor(null); // Here depends on an implementation detail. // Use ACTION_BAR to force processing KEY_TYPED event in // the implementation of gui.InputHandler.processKeyEvent(). view.getInputHandler().processKeyEvent(e, View.ACTION_BAR, false); // Restore keyHandler only if this popup is still alive. // The key event might trigger dispose() of this popup. if (this.isDisplayable()) { view.setKeyEventInterceptor(keyHandler); } } //}}} //{{{ CandidateListModel class private class CandidateListModel extends AbstractListModel { @Override public int getSize() { return candidates.getSize(); } @Override public Object getElementAt(int index) { // This value is not used. // The list is only rendered by components // returned by getCellRenderer(). return candidates; } } //}}} //{{{ CellRenderer class private class CellRenderer implements ListCellRenderer { @Override public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { return candidates.getCellRenderer(list, index, isSelected, cellHasFocus); } } //}}} //{{{ KeyHandler class private class KeyHandler extends KeyAdapter { //{{{ keyPressed() method @Override public void keyPressed(KeyEvent e) { CompletionPopup.this.keyPressed(e); if (candidates == null || !candidates.isValid()) { dispose(); } else if (!e.isConsumed()) { switch(e.getKeyCode()) { case KeyEvent.VK_TAB: case KeyEvent.VK_ENTER: if (doSelectedCompletion()) { e.consume(); } else { dispose(); } break; case KeyEvent.VK_ESCAPE: dispose(); e.consume(); break; case KeyEvent.VK_UP: moveRelative(-1); e.consume(); break; case KeyEvent.VK_DOWN: moveRelative(1); e.consume(); break; case KeyEvent.VK_P: if (e.getModifiersEx() == InputEvent.CTRL_DOWN_MASK) { moveRelative(-1); e.consume(); } break; case KeyEvent.VK_N: if (e.getModifiersEx() == InputEvent.CTRL_DOWN_MASK) { moveRelative(1); e.consume(); } break; case KeyEvent.VK_PAGE_UP: moveRelativePages(-1); e.consume(); break; case KeyEvent.VK_PAGE_DOWN: moveRelativePages(1); e.consume(); break; default: if(e.isActionKey() || e.isAltDown() || e.isMetaDown() || e.isControlDown()) { dispose(); } break; } } if (!e.isConsumed()) { passKeyEventToView(e); } } //}}} //{{{ keyTyped() method @Override public void keyTyped(KeyEvent e) { if (e.isControlDown()) { e.consume(); } else { CompletionPopup.this.keyTyped(e); } if (candidates == null || !candidates.isValid()) { dispose(); } if (!e.isConsumed()) { passKeyEventToView(e); } } //}}} } //}}} //{{{ MouseHandler class private class MouseHandler extends MouseAdapter { @Override public void mouseClicked(MouseEvent e) { if (doSelectedCompletion()) { e.consume(); } else { dispose(); } } } //}}} //{{{ WindowFocusHandler class private class WindowFocusHandler implements WindowFocusListener { @Override public void windowGainedFocus(WindowEvent e) { } @Override public void windowLostFocus(WindowEvent e) { dispose(); } } //}}} //}}} }