package edu.mit.blocks.workspace.typeblocking;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Insets;
import java.awt.Rectangle;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.geom.GeneralPath;
import java.util.List;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import edu.mit.blocks.codeblockutil.CTracklessScrollPane;
import edu.mit.blocks.renderable.BlockUtilities;
import edu.mit.blocks.renderable.TextualFactoryBlock;
import edu.mit.blocks.workspace.Workspace;
/**
* AutCompletePanel is a Panel that displays an editable text field
* for the user to produce a desired pattern (regex). It also
* provides a JList for users to see some pre-defined choices.
* User may choose to act/react to AutoCompletePanel intuitively.
*/
public class AutoCompletePanel extends JPanel implements MouseListener, MouseMotionListener {
private static final long serialVersionUID = 328149080418L;
private static final int MARGIN = 7;
private static final Color BACKGROUND = new Color(230, 230, 150);
/**Minimum width**/
private static final int MINIMUM_WIDTH = 105;
/**Minimum height**/
private static final int MINIMUM_HEIGHT = 105;
/**Minimum width**/
private int preferredWidth = 165; //This is the default width of the keytyping window
/**Minimum height**/
private int preferredHeight = 125; //This is the default height of the keytyping window
/**font of this**/
private final Font font;
/**editable text field for user to enter in desired pattern**/
private final JTextField editor;
/**menu that displays set of possibilities from user-input patter**/
private final JList menu;
/** The workspace in use */
private final Workspace workspace;
/**Constructs AutoCompletePanel*/
@SuppressWarnings("serial")
public AutoCompletePanel(Workspace workspace) {
super(new BorderLayout());
this.workspace = workspace;
font = new Font("Ariel", Font.BOLD, 12);
//set up editor (text field)
editor = new JTextField();
editor.setFont(font);
editor.setBackground(BACKGROUND);
//Set up menu (JList)
menu = new JList();
menu.setFont(font);
menu.setBackground(BACKGROUND);
menu.setLayoutOrientation(JList.VERTICAL);
CTracklessScrollPane menuPane = new CTracklessScrollPane(menu,
7, new Color(75, 50, 0), BACKGROUND) {
@Override
public Insets getInsets() {
return new Insets(MARGIN, 0, 0, 0);
}
};
menuPane.setBackground(BACKGROUND);
menu.setCellRenderer(new QueryCellRenderer());
//Set up this
this.setOpaque(false);
this.setSize(preferredWidth, preferredHeight);
//this.setBorder(BorderFactory.createEtchedBorder(Color.white, Color.lightGray));
this.add(editor, BorderLayout.NORTH);
this.add(menuPane, BorderLayout.CENTER);
//add Listeners
this.addFocusListener(new FocusListener() {
//pass focus onto editor
@Override
public void focusGained(FocusEvent e) {
editor.requestFocus();
}
@Override
public void focusLost(FocusEvent e) {
}
});
EditorListener editorListener = new EditorListener();
//Return to phase one whenever user enters more text.
this.editor.getDocument().addDocumentListener(editorListener);
//if editor && menu loses focus, then make this TypeBlockManager disappear
this.editor.addFocusListener(editorListener);
//If the user pressed enter, then use enter phase two
this.editor.addKeyListener(editorListener);
MenuListener menuListener = new MenuListener();
//if editor && menu loses focus, then make this TypeBlockManager disappear
this.menu.addFocusListener(menuListener);
//If the user double clicks, then enter phase two
this.menu.addMouseListener(menuListener);
//If the user pressed enter, then use enter phase two
this.menu.addKeyListener(menuListener);
this.addMouseListener(this);
this.addMouseMotionListener(this);
}
@Override
public void paint(Graphics g) {
Graphics2D g2 = (Graphics2D) g;
int w = this.getWidth();
int h = this.getHeight();
g.setColor(BACKGROUND);
g.fillRoundRect(0, 0, w - 1, h - 1, MARGIN * 2, MARGIN * 2);
GeneralPath resize = new GeneralPath();
resize.moveTo(w - 2 * MARGIN, h);
resize.lineTo(w, h - 2 * MARGIN);
resize.curveTo(w - 1, h - 1, w - 1, h - 1, w - 2 * MARGIN, h);
g.setColor(Color.gray);
g2.fill(resize);
g.setColor(Color.gray);
g.drawRoundRect(0, 0, w - 1, h - 1, MARGIN * 2, MARGIN * 2);
super.paint(g);
}
@Override
public Insets getInsets() {
return new Insets(MARGIN, MARGIN, MARGIN, MARGIN);
}
private boolean resizing = false;
@Override
public void mousePressed(MouseEvent e) {
if (e.getX() > (this.getWidth() - 2 * MARGIN) && e.getY() > (this.getHeight() - 2 * MARGIN)) {
resizing = true;
}
}
@Override
public void mouseReleased(MouseEvent e) {
resizing = false;
}
@Override
public void mouseDragged(MouseEvent e) {
if (resizing) {
preferredWidth = e.getX() > MINIMUM_WIDTH ? e.getX() : MINIMUM_WIDTH;
preferredHeight = e.getY() > MINIMUM_HEIGHT ? e.getY() : MINIMUM_HEIGHT;
updateMenu();
}
}
@Override
public void mouseClicked(MouseEvent e) {
}
@Override
public void mouseEntered(MouseEvent e) {
}
@Override
public void mouseMoved(MouseEvent e) {
}
@Override
public void mouseExited(MouseEvent e) {
}
/**
* Set's user-generated pattern to be text.
* @param text
*/
public void setText(String text) {
this.editor.setText(text);
}
/**
* Updates the menu such that it can display
* all the possible blocks that match the user-generated pattern
* (the user-generated pattern is entered within AutoCompletePanel.editor)
*/
private void updateMenu() {
//resize to display entire text in editor
if (this.editor.getPreferredSize().width > preferredWidth) {
this.setSize(this.editor.getPreferredSize().width, preferredHeight);
} else {
this.setSize(preferredWidth, preferredHeight);
}
//get matching blocks
String text = editor.getText().trim();
List<TextualFactoryBlock> matchingBlocks;
//try to parse the string. if an integer, then get number block
// if not an integer: if the user wants the "+" operation, then grab the
// two "+" blocks, otherwise get the blocks matching the input text
try {
Float.valueOf(text);
matchingBlocks = BlockUtilities.getDigits(workspace, text);
} catch (NumberFormatException e) {
if (text.equals(TypeBlockManager.PLUS_OPERATION_LABEL)) {
matchingBlocks = BlockUtilities.getPlusBlocks(workspace, text);
} else {
matchingBlocks = BlockUtilities.getAllMatchingBlocks(workspace, text);
}
}
//update menu and repaint
menu.setModel(new DefaultComboBoxModel(matchingBlocks.toArray()));
this.revalidate();
this.repaint();
}
/**
* Should display whatever block was last selected in menu.
* @param workspace
*/
private void displayBlock(Workspace workspace) {
Object obj = menu.getSelectedValue();
if (obj != null && obj instanceof TextualFactoryBlock) {
//make JPanel-user-Interface invisible
this.setVisible(false);
this.revalidate();
this.repaint();
//pass created block to TpeBlockManager
try {
//if integer, then pass in the number typed by the user
Float.valueOf(obj.toString());
workspace.getTypeBlockManager().automateBlockInsertion(workspace, (TextualFactoryBlock) obj, obj.toString());
} catch (NumberFormatException e) {
// if "+" then pass the two labels in
if (obj.toString().equals(TypeBlockManager.NUMBER_PLUS_OPERATION_LABEL)
|| obj.toString().equals(TypeBlockManager.TEXT_PLUS_OPERATION_LABEL)) {
workspace.getTypeBlockManager().automateBlockInsertion(workspace, (TextualFactoryBlock) obj, obj.toString());
// if starts with quote (is a string block)
} else if (obj.toString().startsWith(TypeBlockManager.QUOTE_LABEL)) {
String[] quote = obj.toString().split(TypeBlockManager.QUOTE_LABEL);
workspace.getTypeBlockManager().automateBlockInsertion(workspace, (TextualFactoryBlock) obj, quote[1]);
// otherwise, don't pass a label in
} else {
workspace.getTypeBlockManager().automateBlockInsertion(workspace, (TextualFactoryBlock) obj);
}
}
}
}
/**
* Private helper class to provide the semantics for
* various listeners within the editor's TextField.
*/
private class EditorListener extends KeyAdapter implements DocumentListener, FocusListener {
/**Constructs this listener*/
public EditorListener() {
}
/**
* Document Listener. Whenever AutoCompletePanel.editor
* receives a new user-generated character, it must
* update AutoCompletePanel to reflect the new pattern.
*/
public void changedUpdate(DocumentEvent e) {
updateMenu();
}
/**
* Document Listener. Whenever AutoCompletePanel.editor
* receives a new user-generated character, it must
* update AutoCompletePanel to reflect the new pattern.
*/
public void insertUpdate(DocumentEvent e) {
updateMenu();
}
/**
* Document Listener. Whenever AutoCompletePanel.editor
* receives a new user-generated character, it must
* update AutoCompletePanel to reflect the new pattern.
*/
public void removeUpdate(DocumentEvent e) {
updateMenu();
}
/**Repaint AutoCompletePanel when focus gained*/
public void focusGained(FocusEvent e) {
revalidate();
repaint();
//for Macs, when the focus is gained, the text within the editor is selected automatically.
//set the text again to eliminate the selection. (doing editor.setSelectionStart(0) doesn't work).
String lcOSName = System.getProperty("os.name").toLowerCase();
boolean MAC_OS_X = lcOSName.startsWith("mac os x");
if (MAC_OS_X) {
editor.setText(editor.getText());
}
}
/**Turn Invisible if BOTH editor and menu loses focus*/
public void focusLost(FocusEvent e) {
if (e.getOppositeComponent() == null || e.getOppositeComponent().equals(menu)) {
return;
}
setVisible(false);
revalidate();
repaint();
}
/**Should respond to special key presses*/
public void keyTyped(KeyEvent e) {
}
/**Should respond to special key presses*/
public void keyPressed(KeyEvent e) {
if (e.getKeyChar() == KeyEvent.VK_ENTER) {
menu.setSelectedIndex(0);
displayBlock(workspace);
} else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
menu.setSelectedIndex(0);
menu.requestFocus();//validation and repainting done in menu.focus gained
menu.scrollRectToVisible(new Rectangle(0, 0, 0, 0));
} else if (e.getKeyChar() == KeyEvent.VK_ESCAPE) {
setVisible(false);
revalidate();
repaint();
//TODO: AutoCompletePane should not know about
//the Workspace. Need to design a better system for this.
workspace.getBlockCanvas().getCanvas().requestFocus();
}
}
}
/**
* Private helper class to provide the semantics for
* various listeners within the menu's JList.
*/
private class MenuListener extends MouseAdapter implements FocusListener, KeyListener {
/**Constructs MenuListener*/
public MenuListener() {
}
/**Repaint AutoCompletePanel upon gaining focus*/
public void focusGained(FocusEvent e) {
revalidate();
repaint();
}
/**If focus lost to BOTH menu and editor, then turn invisible*/
public void focusLost(FocusEvent e) {
if (e.getOppositeComponent() == null || e.getOppositeComponent().equals(editor)) {
return;
}
setVisible(false);
revalidate();
repaint();
}
/**Drop block if user double clicks on an item*/
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
displayBlock(workspace);
}
}
/**Drop selected block if user presses enter*/
public void keyTyped(KeyEvent e) {
if (e.getKeyChar() == KeyEvent.VK_ENTER) {
displayBlock(workspace);
} else if (e.getKeyChar() == KeyEvent.VK_ESCAPE) {
setVisible(false);
revalidate();
repaint();
//TODO: AutoCompletePane should not know about
//the Workspace. Need to design a better system for this.
workspace.getBlockCanvas().getCanvas().requestFocus();
}
}
/**Do nothing*/
public void keyPressed(KeyEvent e) {
}
/**Do nothing*/
public void keyReleased(KeyEvent e) {
}
}
/**
* CellRenderer of this.menu
*/
private class QueryCellRenderer extends DefaultListCellRenderer {
private static final long serialVersionUID = 328149080419L;
/**Color matching query red*/
public void paint(Graphics g) {
//initialize string data
String query = editor.getText().toLowerCase().trim();
String item = this.getText().toLowerCase();
FontMetrics metrics = g.getFontMetrics();
//draw cell background
if (this.getBackground() != null) {
g.setColor(this.getBackground());
g.fillRect(0, 0, this.getWidth(), this.getHeight());
}
//draw block's label
g.setColor(Color.black);
g.drawString(this.getText(), 2, this.getHeight() - metrics.getDescent());
//highlight matching portion in red
int index = item.indexOf(query);
if (index != -1) {
g.setColor(Color.red);
g.drawString(
this.getText().substring(index, index + query.length()),
(int) metrics.getStringBounds(this.getText().substring(0, index), g).getWidth() + 2,
this.getHeight() - metrics.getDescent());
}
}
}
}