mirror of
https://github.com/processing/processing4.git
synced 2026-02-13 18:35:37 +01:00
574 lines
20 KiB
Java
574 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2012-14 Manindra Moharana <me@mkmoharana.com>
|
|
*
|
|
* 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.Dimension;
|
|
import java.awt.FontMetrics;
|
|
import java.awt.Graphics;
|
|
import java.awt.Graphics2D;
|
|
import java.awt.Point;
|
|
import java.awt.Rectangle;
|
|
import java.awt.event.MouseAdapter;
|
|
import java.awt.event.MouseEvent;
|
|
|
|
import javax.swing.DefaultListModel;
|
|
import javax.swing.JButton;
|
|
import javax.swing.JComponent;
|
|
import javax.swing.JLabel;
|
|
import javax.swing.JList;
|
|
import javax.swing.JPopupMenu;
|
|
import javax.swing.JScrollPane;
|
|
import javax.swing.ListSelectionModel;
|
|
import javax.swing.Painter;
|
|
import javax.swing.SwingUtilities;
|
|
import javax.swing.UIDefaults;
|
|
import javax.swing.UIManager;
|
|
import javax.swing.plaf.InsetsUIResource;
|
|
import javax.swing.plaf.basic.BasicScrollBarUI;
|
|
import javax.swing.text.BadLocationException;
|
|
|
|
import processing.app.syntax.JEditTextArea;
|
|
|
|
/**
|
|
* Manages the actual suggestion popup that gets displayed
|
|
* @author Manindra Moharana <me@mkmoharana.com>
|
|
*
|
|
*/
|
|
public class CompletionPanel {
|
|
|
|
/**
|
|
* The completion list generated by ASTGenerator
|
|
*/
|
|
private JList<CompletionCandidate> 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;
|
|
|
|
public static final int MOUSE_COMPLETION = 10, KEYBOARD_COMPLETION = 20;
|
|
|
|
/**
|
|
* 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<CompletionCandidate> 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();
|
|
styleScrollPane();
|
|
scrollPane.setViewportView(completionList = createSuggestionList(position, items));
|
|
popupMenu.add(scrollPane, BorderLayout.CENTER);
|
|
popupMenu.setPopupSize(calcWidth(), calcHeight(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());
|
|
}
|
|
|
|
private void styleScrollPane() {
|
|
String laf = UIManager.getLookAndFeel().getID();
|
|
if (!laf.equals("Nimbus") && !laf.equals("Windows")) return;
|
|
|
|
String thumbColor = null;
|
|
if (laf.equals("Nimbus")) {
|
|
UIDefaults defaults = new UIDefaults();
|
|
defaults.put("PopupMenu.contentMargins", new InsetsUIResource(0, 0, 0, 0));
|
|
defaults.put("ScrollPane[Enabled].borderPainter", new Painter<JComponent>() {
|
|
public void paint(Graphics2D g, JComponent t, int w, int h) {}
|
|
});
|
|
popupMenu.putClientProperty("Nimbus.Overrides", defaults);
|
|
scrollPane.putClientProperty("Nimbus.Overrides", defaults);
|
|
thumbColor = "nimbusBlueGrey";
|
|
} else if (laf.equals("Windows")) {
|
|
thumbColor = "ScrollBar.thumbShadow";
|
|
}
|
|
|
|
scrollPane.getHorizontalScrollBar().setPreferredSize(new Dimension(Integer.MAX_VALUE, 8));
|
|
scrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(8, Integer.MAX_VALUE));
|
|
scrollPane.getHorizontalScrollBar().setUI(new CompletionScrollBarUI(thumbColor));
|
|
scrollPane.getVerticalScrollBar().setUI(new CompletionScrollBarUI(thumbColor));
|
|
}
|
|
|
|
public static class CompletionScrollBarUI extends BasicScrollBarUI {
|
|
private String thumbColorName;
|
|
|
|
protected CompletionScrollBarUI(String thumbColorName) {
|
|
this.thumbColorName = thumbColorName;
|
|
}
|
|
|
|
@Override
|
|
protected void paintThumb(Graphics g, JComponent c, Rectangle trackBounds) {
|
|
g.setColor((Color) UIManager.get(thumbColorName));
|
|
g.fillRect(trackBounds.x, trackBounds.y, trackBounds.width, trackBounds.height);
|
|
}
|
|
|
|
@Override
|
|
protected JButton createDecreaseButton(int orientation) {
|
|
return createZeroButton();
|
|
}
|
|
|
|
@Override
|
|
protected JButton createIncreaseButton(int orientation) {
|
|
return createZeroButton();
|
|
}
|
|
|
|
private JButton createZeroButton() {
|
|
JButton jbutton = new JButton();
|
|
jbutton.setPreferredSize(new Dimension(0, 0));
|
|
jbutton.setMinimumSize(new Dimension(0, 0));
|
|
jbutton.setMaximumSize(new Dimension(0, 0));
|
|
return jbutton;
|
|
}
|
|
}
|
|
|
|
public boolean isVisible() {
|
|
return popupMenu.isVisible();
|
|
}
|
|
|
|
public void setVisible(boolean v){
|
|
//log("Pred popup visible.");
|
|
popupMenu.setVisible(v);
|
|
}
|
|
|
|
/**
|
|
* Dynamic height of completion panel depending on item count
|
|
* @param itemCount
|
|
* @return - height
|
|
*/
|
|
private int calcHeight(int itemCount) {
|
|
int maxHeight = 250;
|
|
FontMetrics fm = textarea.getGraphics().getFontMetrics();
|
|
float itemHeight = Math.max((fm.getHeight() + (fm.getDescent()) * 0.5f),
|
|
editor.dmode.classIcon.getIconHeight() * 1.2f);
|
|
|
|
if (horizontalScrollBarVisible)
|
|
itemCount++;
|
|
|
|
if (itemCount < 4)
|
|
itemHeight *= 1.3f; //Sorry, but it works.
|
|
|
|
float h = itemHeight * (itemCount);
|
|
|
|
if (itemCount >= 4)
|
|
h += itemHeight * 0.3; // a bit of offset
|
|
|
|
return Math.min(maxHeight, (int) h); // popup menu height
|
|
}
|
|
|
|
private boolean horizontalScrollBarVisible = false;
|
|
|
|
/**
|
|
* Dynamic width of completion panel
|
|
* @return - width
|
|
*/
|
|
private int calcWidth() {
|
|
horizontalScrollBarVisible = false;
|
|
int maxWidth = 300;
|
|
float min = 0;
|
|
FontMetrics fm = textarea.getGraphics().getFontMetrics();
|
|
for (int i = 0; i < completionList.getModel().getSize(); i++) {
|
|
float h = fm.stringWidth(((CompletionCandidate) completionList.getModel()
|
|
.getElementAt(i)).getLabel());
|
|
min = Math.max(min, h);
|
|
}
|
|
int w = Math.min((int) min, maxWidth);
|
|
if(w == maxWidth)
|
|
horizontalScrollBarVisible = true;
|
|
w += editor.dmode.classIcon.getIconWidth(); // add icon width too!
|
|
w += fm.stringWidth(" "); // a bit of offset
|
|
//log("popup width " + w);
|
|
return w; // popup menu width
|
|
}
|
|
|
|
/**
|
|
* Created the popup list to be displayed
|
|
* @param position
|
|
* @param items
|
|
* @return
|
|
*/
|
|
private JList<CompletionCandidate> createSuggestionList(final int position,
|
|
final DefaultListModel<CompletionCandidate> items) {
|
|
|
|
JList<CompletionCandidate> list = new JList<CompletionCandidate>(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(MOUSE_COMPLETION);
|
|
hide();
|
|
}
|
|
}
|
|
});
|
|
list.setCellRenderer(new CustomListRenderer());
|
|
list.setFocusable(false);
|
|
return list;
|
|
}
|
|
|
|
// possibly defunct
|
|
public boolean updateList(final DefaultListModel<CompletionCandidate> 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(calcWidth(), calcHeight(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(int completionSource) {
|
|
if (completionList.getSelectedValue() != null) {
|
|
try {
|
|
// If user types 'abc.', subword becomes '.' and null is returned
|
|
String currentSubword = fetchCurrentSubword();
|
|
int currentSubwordLen = currentSubword == null ? 0 : currentSubword
|
|
.length();
|
|
//logE(currentSubword + " <= subword,len => " + currentSubword.length());
|
|
String selectedSuggestion = ((CompletionCandidate) completionList
|
|
.getSelectedValue()).getCompletionString();
|
|
|
|
if (currentSubword != null) {
|
|
selectedSuggestion = selectedSuggestion.substring(currentSubwordLen);
|
|
} else {
|
|
currentSubword = "";
|
|
}
|
|
|
|
String completionString = ((CompletionCandidate) completionList
|
|
.getSelectedValue()).getCompletionString();
|
|
if (selectedSuggestion.endsWith(" )")) { // the case of single param methods
|
|
// selectedSuggestion = ")";
|
|
if (completionString.endsWith(" )")) {
|
|
completionString = completionString.substring(0, completionString
|
|
.length() - 2)
|
|
+ ")";
|
|
}
|
|
}
|
|
|
|
if (completionSource == MOUSE_COMPLETION) {
|
|
if (completionString.endsWith("(")) {
|
|
completionString = completionString.substring(0, completionString
|
|
.length() - 2);
|
|
}
|
|
}
|
|
|
|
logE(subWord + " <= subword, Inserting suggestion=> "
|
|
+ selectedSuggestion + " Current sub: " + currentSubword);
|
|
if (currentSubword.length() > 0) {
|
|
textarea.getDocument().remove(insertionPosition - currentSubwordLen,
|
|
currentSubwordLen);
|
|
}
|
|
|
|
textarea.getDocument()
|
|
.insertString(insertionPosition - currentSubwordLen,
|
|
completionString, null);
|
|
if (selectedSuggestion.endsWith(")") && !selectedSuggestion.endsWith("()")) {
|
|
// place the caret between '( and first ','
|
|
int x = selectedSuggestion.indexOf(',');
|
|
if(x == -1) {
|
|
if(subWord.endsWith("(")) {
|
|
// the case of single param methods with overloads shown initially, containing no ','
|
|
textarea.setCaretPosition(insertionPosition);
|
|
}
|
|
else {
|
|
// the case of single param methods with no overloads, containing no ','
|
|
textarea.setCaretPosition(textarea.getCaretPosition() - 1);
|
|
}
|
|
} else {
|
|
textarea.setCaretPosition(insertionPosition + x);
|
|
}
|
|
} else {
|
|
textarea.setCaretPosition(insertionPosition
|
|
+ selectedSuggestion.length());
|
|
}
|
|
log("Suggestion inserted: " + System.currentTimeMillis());
|
|
if (((CompletionCandidate) completionList.getSelectedValue())
|
|
.getLabel().contains("...")) {
|
|
// log("No hide");
|
|
// Why not hide it? Coz this is the case of
|
|
// overloaded methods. See #2755
|
|
} else {
|
|
hide();
|
|
}
|
|
return true;
|
|
} catch (BadLocationException e1) {
|
|
e1.printStackTrace();
|
|
} catch (Exception e) {
|
|
e.printStackTrace();
|
|
}
|
|
hide();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private String fetchCurrentSubword() {
|
|
//log("Entering 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);
|
|
//log2(s + " len " + s.length());
|
|
|
|
int x = ta.getCaretPosition() - ta.getLineStartOffset(line) - 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;
|
|
}
|
|
//log("fetchCurrentSubword 1 " + word);
|
|
if(word.equals(".")) return null; // If user types 'abc.', subword becomes '.'
|
|
// 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;
|
|
|
|
while (true) {
|
|
i++;
|
|
//TODO: currently works on single line only. "a. <new line> 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)
|
|
//log("fetchCurrentSubword 2 " + word);
|
|
if (Character.isDigit(word.charAt(0)))
|
|
return null;
|
|
word = word.trim();
|
|
if (word.endsWith("."))
|
|
word = word.substring(0, word.length() - 1);
|
|
//log("fetchCurrentSubword 3 " + word);
|
|
//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 <me@mkmoharana.com>
|
|
*
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
|
|
}
|