/* * Copyright (C) 2012-14 Manindra Moharana * * 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 (at your option) 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 processing.mode.experimental; import static processing.mode.experimental.ExperimentalMode.log; import static processing.mode.experimental.ExperimentalMode.log2; import static processing.mode.experimental.ExperimentalMode.logE; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.FontMetrics; import java.awt.Point; import java.awt.event.KeyEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.util.Iterator; import javax.swing.BorderFactory; import javax.swing.DefaultListModel; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.ListSelectionModel; import javax.swing.SwingUtilities; import javax.swing.text.BadLocationException; import processing.app.syntax.JEditTextArea; /** * Manages the actual suggestion popup that gets displayed * @author Manindra Moharana * */ public class CompletionPanel { /** * The completion list generated by ASTGenerator */ private JList completionList; /** * The popup menu in which the suggestion list is shown */ private JPopupMenu popupMenu; /** * Partial word which triggered the code completion and which needs to be completed */ private String subWord; /** * Postion where the completion has to be inserted */ private int insertionPosition; private TextArea textarea; /** * Scroll pane in which the completion list is displayed */ private JScrollPane scrollPane; protected DebugEditor editor; /** * Triggers the completion popup * @param textarea * @param position - insertion position(caret pos) * @param subWord - Partial word which triggered the code completion and which needs to be completed * @param items - completion candidates * @param location - Point location where popup list is to be displayed * @param dedit */ public CompletionPanel(final JEditTextArea textarea, int position, String subWord, DefaultListModel items, final Point location, DebugEditor dedit) { this.textarea = (TextArea) textarea; editor = dedit; this.insertionPosition = position; if (subWord.indexOf('.') != -1) this.subWord = subWord.substring(subWord.lastIndexOf('.') + 1); else this.subWord = subWord; popupMenu = new JPopupMenu(); popupMenu.removeAll(); popupMenu.setOpaque(false); popupMenu.setBorder(null); scrollPane = new JScrollPane(); scrollPane.setViewportView(completionList = createSuggestionList(position, items)); popupMenu.add(scrollPane, BorderLayout.CENTER); popupMenu.setPopupSize(280, setHeight(items.getSize())); //TODO: Eradicate this evil this.textarea.errorCheckerService.getASTGenerator() .updateJavaDoc((CompletionCandidate) completionList.getSelectedValue()); textarea.requestFocusInWindow(); popupMenu.show(textarea, location.x, textarea.getBaseline(0, 0) + location.y); log("Suggestion shown: " + System.currentTimeMillis()); } public boolean isVisible() { return popupMenu.isVisible(); } public void setVisible(boolean v){ log("Pred popup visible."); popupMenu.setVisible(v); } private int setHeight(int itemCount){ FontMetrics fm = textarea.getFontMetrics(textarea.getFont()); float h = (fm.getHeight() + (fm.getDescent()) * 0.5f) * (itemCount); if (scrollPane.getHorizontalScrollBar().isVisible()) h += scrollPane.getHorizontalScrollBar().getHeight() + fm.getHeight() + (fm.getDescent() + fm.getAscent()) * 0.8f; // 0.5f and 0.8f scaling give respectable results. //log("popup height " + Math.min(250,h) //+ scrollPane.getHorizontalScrollBar().isVisible()); return Math.min(250, (int) h); // popup menu height } /*TODO: Make width dynamic protected int setWidth(){ if(scrollPane.getVerticalScrollBar().isVisible()) return 280; float min = 280; FontMetrics fm = textarea.getFontMetrics(textarea.getFont()); for (int i = 0; i < completionList.getModel().getSize(); i++) { float h = fm.stringWidth(completionList.getModel().getElementAt(i).toString()); min = Math.min(min, h); } min += fm.stringWidth(" "); log("popup width " + Math.min(280,min)); return Math.min(280,(int)min); // popup menu height }*/ /** * Created the popup list to be displayed * @param position * @param items * @return */ private JList createSuggestionList(final int position, final DefaultListModel items) { JList list = new JList(items); //list.setBorder(BorderFactory.createLineBorder(Color.DARK_GRAY, 1)); list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); list.setSelectedIndex(0); list.addMouseListener(new MouseAdapter() { @Override public void mouseClicked(MouseEvent e) { if (e.getClickCount() == 2) { insertSelection(); hide(); } } }); list.setCellRenderer(new CustomListRenderer()); list.setFocusable(false); return list; } // possibly defunct public boolean updateList(final DefaultListModel items, String newSubword, final Point location, int position) { this.subWord = new String(newSubword); if (subWord.indexOf('.') != -1) this.subWord = subWord.substring(subWord.lastIndexOf('.') + 1); insertionPosition = position; SwingUtilities.invokeLater(new Runnable() { @Override public void run() { scrollPane.getViewport().removeAll(); completionList.setModel(items); completionList.setSelectedIndex(0); scrollPane.setViewportView(completionList); popupMenu.setPopupSize(popupMenu.getSize().width, setHeight(items.getSize())); //log("Suggestion updated" + System.nanoTime()); textarea.requestFocusInWindow(); popupMenu.show(textarea, location.x, textarea.getBaseline(0, 0) + location.y); completionList.validate(); scrollPane.validate(); popupMenu.validate(); } }); return true; } /** * Inserts the CompletionCandidate chosen from the suggestion list * * @return */ public boolean insertSelection() { if (completionList.getSelectedValue() != null) { try { String currentSubword = fetchCurrentSubword(); String selectedSuggestion = ((CompletionCandidate) completionList .getSelectedValue()).getCompletionString().substring(currentSubword .length()); logE(subWord + " <= subword,Inserting suggestion=> " + selectedSuggestion + " Current sub: " + currentSubword); textarea.getDocument().remove(insertionPosition - currentSubword.length(), currentSubword.length()); textarea.getDocument() .insertString(insertionPosition - currentSubword.length(), ((CompletionCandidate) completionList .getSelectedValue()).getCompletionString(), null); if (selectedSuggestion.endsWith(")")) { if (!selectedSuggestion.endsWith("()")) { int x = selectedSuggestion.indexOf('('); if (x != -1) { //log("X................... " + x); textarea.setCaretPosition(insertionPosition + (x + 1)); } } } else { textarea.setCaretPosition(insertionPosition + selectedSuggestion.length()); } log("Suggestion inserted: " + System.currentTimeMillis()); return true; } catch (BadLocationException e1) { e1.printStackTrace(); } hide(); } return false; } private String fetchCurrentSubword() { TextArea ta = editor.ta; int off = ta.getCaretPosition(); //log2("off " + off); if (off < 0) return null; int line = ta.getCaretLine(); if (line < 0) return null; String s = ta.getLineText(line); //log2("lin " + line); /* * if (s == null) return null; else if (s.length() == 0) return null; */ // else { //log2(s + " len " + s.length()); int x = ta.getCaretPosition() - ta.getLineStartOffset(line) - 1, x2 = x + 1, x1 = x - 1; if(x >= s.length() || x < 0) return null; //TODO: Does this check cause problems? Verify. //log2(" x char: " + s.charAt(x)); //int xLS = off - getLineStartNonWhiteSpaceOffset(line); String word = (x < s.length() ? s.charAt(x) : "") + ""; if (s.trim().length() == 1) { // word = "" // + (keyChar == KeyEvent.CHAR_UNDEFINED ? s.charAt(x - 1) : keyChar); //word = (x < s.length()?s.charAt(x):"") + ""; word = word.trim(); if (word.endsWith(".")) word = word.substring(0, word.length() - 1); return word; } // if (keyChar == KeyEvent.VK_BACK_SPACE || keyChar == KeyEvent.VK_DELETE) // ; // accepted these keys // else if (!(Character.isLetterOrDigit(keyChar) || keyChar == '_' || keyChar == '$')) // return null; int i = 0; int closeB = 0; while (true) { i++; //TODO: currently works on single line only. "a. b()" won't be detected if (x1 >= 0) { // if (s.charAt(x1) != ';' && s.charAt(x1) != ',' && s.charAt(x1) != '(') if (Character.isLetterOrDigit(s.charAt(x1)) || s.charAt(x1) == '_') { word = s.charAt(x1--) + word; } else { break; } } else { break; } if (i > 200) { // time out! break; } } // if (keyChar != KeyEvent.CHAR_UNDEFINED) if (Character.isDigit(word.charAt(0))) return null; word = word.trim(); if (word.endsWith(".")) word = word.substring(0, word.length() - 1); //showSuggestionLater(); return word; //} } /** * Hide the suggestion list */ public void hide() { popupMenu.setVisible(false); //log("Suggestion hidden" + System.nanoTime()); //textarea.errorCheckerService.getASTGenerator().jdocWindowVisible(false); } /** * When up arrow key is pressed, moves the highlighted selection up in the list */ public void moveUp() { if (completionList.getSelectedIndex() == 0) { scrollPane.getVerticalScrollBar().setValue(scrollPane.getVerticalScrollBar().getMaximum()); selectIndex(completionList.getModel().getSize() - 1); return; } else { int index = Math.max(completionList.getSelectedIndex() - 1, 0); selectIndex(index); } int step = scrollPane.getVerticalScrollBar().getMaximum() / completionList.getModel().getSize(); scrollPane.getVerticalScrollBar().setValue(scrollPane .getVerticalScrollBar() .getValue() - step); textarea.errorCheckerService.getASTGenerator() .updateJavaDoc((CompletionCandidate) completionList.getSelectedValue()); } /** * When down arrow key is pressed, moves the highlighted selection down in the list */ public void moveDown() { if (completionList.getSelectedIndex() == completionList.getModel().getSize() - 1) { scrollPane.getVerticalScrollBar().setValue(0); selectIndex(0); return; } else { int index = Math.min(completionList.getSelectedIndex() + 1, completionList.getModel() .getSize() - 1); selectIndex(index); } textarea.errorCheckerService.getASTGenerator() .updateJavaDoc((CompletionCandidate) completionList.getSelectedValue()); int step = scrollPane.getVerticalScrollBar().getMaximum() / completionList.getModel().getSize(); scrollPane.getVerticalScrollBar().setValue(scrollPane .getVerticalScrollBar() .getValue() + step); } private void selectIndex(int index) { completionList.setSelectedIndex(index); // final int position = textarea.getCaretPosition(); // SwingUtilities.invokeLater(new Runnable() { // @Override // public void run() { // textarea.setCaretPosition(position); // }; // }); } /** * Custom cell renderer to display icons along with the completion candidates * @author Manindra Moharana * */ private class CustomListRenderer extends javax.swing.DefaultListCellRenderer { //protected final ImageIcon classIcon, fieldIcon, methodIcon; public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { JLabel label = (JLabel) super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); if (value instanceof CompletionCandidate) { CompletionCandidate cc = (CompletionCandidate) value; switch (cc.getType()) { case CompletionCandidate.LOCAL_VAR: label.setIcon(editor.dmode.localVarIcon); break; case CompletionCandidate.LOCAL_FIELD: case CompletionCandidate.PREDEF_FIELD: label.setIcon(editor.dmode.fieldIcon); break; case CompletionCandidate.LOCAL_METHOD: case CompletionCandidate.PREDEF_METHOD: label.setIcon(editor.dmode.methodIcon); break; case CompletionCandidate.LOCAL_CLASS: case CompletionCandidate.PREDEF_CLASS: label.setIcon(editor.dmode.classIcon); break; default: log("(CustomListRenderer)Unknown CompletionCandidate type " + cc.getType()); break; } } else log("(CustomListRenderer)Unknown CompletionCandidate object " + value); return label; } } }