1package com.jscratch.ui;
3import com.jscratch.Project;
4import com.jscratch.Project.SpriteData;
5import com.jscratch.compiler.ProjectRunner;
6import com.jscratch.compiler.CompilationResult;
7import java.lang.reflect.*;
8import org.fife.ui.autocomplete.*;
9import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
10import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
11import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
12import org.fife.ui.rsyntaxtextarea.Theme;
13import org.fife.ui.rsyntaxtextarea.parser.AbstractParser;
14import org.fife.ui.rsyntaxtextarea.parser.ParseResult;
15import org.fife.ui.rsyntaxtextarea.parser.Parser;
16import org.fife.ui.rsyntaxtextarea.parser.DefaultParseResult;
17import org.fife.ui.rsyntaxtextarea.parser.ParserNotice;
18import org.fife.ui.rsyntaxtextarea.parser.DefaultParserNotice;
19import org.fife.ui.rtextarea.RTextScrollPane;
21import javax.swing.event.DocumentEvent;
22import javax.swing.event.DocumentListener;
23import javax.swing.text.BadLocationException;
24import javax.swing.text.DefaultHighlighter;
25import javax.swing.text.Highlighter;
26import javax.swing.text.JTextComponent;
28import java.awt.datatransfer.DataFlavor;
29import java.awt.datatransfer.StringSelection;
30import java.awt.datatransfer.Transferable;
31import java.awt.event.MouseAdapter;
32import java.awt.event.MouseEvent;
33import java.io.BufferedReader;
34import java.io.InputStreamReader;
35import java.io.Serializable;
36import java.util.ArrayList;
37import java.util.HashMap;
40import java.util.regex.Matcher;
41import java.util.regex.Pattern;
42import java.util.HashSet;
56 private final Highlighter.HighlightPainter
slotPainter =
new Highlighter.HighlightPainter() {
57 @Override
public void paint(Graphics g,
int p0,
int p1, Shape bounds, JTextComponent c) {
59 Rectangle r0 = c.modelToView(p0);
60 Rectangle r1 = c.modelToView(p1);
61 if (r0 ==
null || r1 ==
null)
return;
62 g.setColor(
new Color(0, 120, 215, 30));
63 g.fillRect(0, r0.y, c.getWidth(), (r1.y + r1.height) - r0.y);
64 }
catch (Exception e) {}
68 private final Highlighter.HighlightPainter
dropPainter =
new Highlighter.HighlightPainter() {
69 @Override
public void paint(Graphics g,
int p0,
int p1, Shape bounds, JTextComponent c) {
71 Rectangle r0 = c.modelToView(p0);
72 Rectangle r1 = c.modelToView(p1);
73 if (r0 ==
null || r1 ==
null)
return;
74 g.setColor(
new Color(0, 255, 0, 50));
75 g.fillRect(0, r0.y, c.getWidth(), (r1.y + r1.height) - r0.y);
76 }
catch (Exception e) {}
93 public List<String>
tags =
new ArrayList<>();
102 List<String>
tags =
new ArrayList<>();
113 setLayout(
new BorderLayout());
117 codeEditor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
126 if (this.onAutoCompile !=
null) {
128 this.onAutoCompile.run();
136 Theme theme = Theme.load(getClass().getResourceAsStream(
"/org/fife/ui/rsyntaxtextarea/themes/eclipse.xml"));
138 }
catch (Exception ioe) {
144 codeEditor.getDocument().addDocumentListener(
new DocumentListener() {
150 MouseAdapter hoverAdapter =
new MouseAdapter() {
155 codeEditor.addMouseMotionListener(hoverAdapter);
159 codeEditor.getDropTarget().addDropTargetListener(
new java.awt.dnd.DropTargetAdapter() {
160 @Override public void dragExit(java.awt.dnd.DropTargetEvent dte) { clearDropHighlight(); }
161 @Override
public void drop(java.awt.dnd.DropTargetDropEvent dtde) { clearDropHighlight(); }
163 }
catch (Exception e) {}
167 paletteTabs.setPreferredSize(
new Dimension(250, 0));
171 JSplitPane splitPane =
new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
paletteTabs,
new RTextScrollPane(
codeEditor));
172 splitPane.setDividerLocation(250);
173 add(splitPane, BorderLayout.CENTER);
194 public ParseResult
parse(RSyntaxDocument doc, String styleKey) {
195 DefaultParseResult result =
new DefaultParseResult(
this);
201 int line = error.line - 1;
202 if (line >= 0 && line < doc.getDefaultRootElement().getElementCount()) {
203 int start = doc.getDefaultRootElement().getElement(line).getStartOffset();
204 int end = doc.getDefaultRootElement().getElement(line).getEndOffset();
205 DefaultParserNotice notice =
new DefaultParserNotice(
this, error.message, line, start, end - start);
206 notice.setLevel(ParserNotice.Level.ERROR);
207 result.addNotice(notice);
209 }
catch (Exception e) {
222 if (slot.contains(offset)) {
223 if (bestMatch ==
null || slot.
length() < bestMatch.
length()) {
229 if (bestMatch !=
null) {
232 }
catch (BadLocationException ex) {}
254 }
catch (BadLocationException ex) {}
258 Map<String, JPanel> categoryPanels =
new HashMap<>();
259 try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(getClass().getResourceAsStream(
"/blocks.txt")))) {
262 StringBuilder codeBuffer =
new StringBuilder();
264 while ((line = reader.readLine()) !=
null) {
265 if (line.startsWith(
"# ")) {
266 String meta = line.substring(2);
267 String[] parts = meta.split(
" : ");
269 currentBlock.name = parts[0].trim();
270 currentBlock.category = parts[1].trim();
271 if (parts.length > 2) {
272 for (String t : parts[2].split(
",")) currentBlock.
tags.add(t.trim());
274 codeBuffer.setLength(0);
275 }
else if (line.equals(
"###")) {
276 if (currentBlock !=
null) {
277 currentBlock.code = codeBuffer.toString().trim();
278 JPanel panel = categoryPanels.computeIfAbsent(currentBlock.
category, k -> {
279 JPanel p = new JPanel();
280 p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
281 p.setBackground(Color.WHITE);
282 p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
283 paletteTabs.addTab(k, new JScrollPane(p));
287 panel.add(Box.createVerticalStrut(5));
290 }
else if (currentBlock !=
null) {
291 codeBuffer.append(line).append(
"\n");
294 }
catch (Exception e) {
300 JLabel label =
new JLabel(block.
name);
301 label.setOpaque(
true);
303 label.setForeground(Color.WHITE);
304 label.setFont(
new Font(
"Arial", Font.BOLD, 12));
305 label.setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12));
306 label.setMaximumSize(
new Dimension(220, 40));
307 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
308 label.setTransferHandler(
new TransferHandler() {
309 @Override
public int getSourceActions(JComponent c) {
return COPY; }
310 @Override
protected Transferable createTransferable(JComponent c) {
return new BlockTransferable(block); }
312 label.addMouseListener(
new MouseAdapter() {
313 @Override
public void mousePressed(MouseEvent e) {
314 ((JComponent)e.getSource()).getTransferHandler().exportAsDrag((JComponent)e.getSource(), e, TransferHandler.COPY);
322 case "Motion":
return new Color(76, 151, 255);
323 case "Looks":
return new Color(153, 102, 255);
324 case "Sound":
return new Color(207, 99, 207);
325 case "Events":
return new Color(255, 191, 0);
326 case "Control":
return new Color(255, 171, 25);
327 case "Sensing":
return new Color(92, 177, 214);
328 case "Operators":
return new Color(89, 192, 89);
329 case "Variables":
return new Color(255, 140, 26);
330 case "My Blocks":
return new Color(255, 102, 128);
331 case "Structure":
return new Color(214, 92, 214);
332 case "Helpers":
return new Color(235, 64, 52);
334 default:
return Color.GRAY;
340 SwingUtilities.invokeLater(this::refreshSlots);
348 Pattern startPattern = Pattern.compile(
"/\\* slot start \\[(.*?)\\] \\*/");
349 Matcher matcher = startPattern.matcher(text);
350 while (matcher.find()) {
351 String attrContent = matcher.group(1);
353 slot.start = matcher.start();
354 slot.startCommentEnd = matcher.end();
356 Matcher idM = Pattern.compile(
"id:\"(.*?)\"").matcher(attrContent);
357 if (idM.find()) slot.id = idM.group(1);
359 Matcher tagsM = Pattern.compile(
"tags:\"(.*?)\"").matcher(attrContent);
361 for (String t : tagsM.group(1).split(
",")) slot.
tags.add(t.trim());
364 Matcher onlyOneM = Pattern.compile(
"onlyOne:(true|false)").matcher(attrContent);
365 if (onlyOneM.find()) slot.isOnlyOne = Boolean.parseBoolean(onlyOneM.group(1));
367 if (slot.
id ==
null)
continue;
369 String endRegex =
"/\\* slot end \\[(.*?id:\"" + Pattern.quote(slot.
id) +
"\".*?)\\] \\*/";
370 Pattern endPattern = Pattern.compile(endRegex);
371 Matcher endMatcher = endPattern.matcher(text);
372 if (endMatcher.find(matcher.end())) {
373 slot.end = endMatcher.end();
374 slot.endCommentStart = endMatcher.start();
388 return (flavor.equals(DataFlavor.stringFlavor)) ? block.code :
null;
400 return new StringSelection(
codeEditor.getSelectedText());
406 if (!support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
410 if (!support.isDrop() || !support.isDataFlavorSupported(
BLOCK_FLAVOR))
return false;
413 JTextComponent.DropLocation dl = (JTextComponent.DropLocation) support.getDropLocation();
414 int offset = dl.getIndex();
419 if (slot.contains(offset)) {
420 if (activeSlot ==
null || slot.
length() < activeSlot.
length()) activeSlot = slot;
424 boolean isValid =
false;
425 if (text.trim().isEmpty() && block.
tags.contains(
"allowEmpty")) {
427 }
else if (activeSlot ==
null) {
428 isValid = block.
tags.contains(
"anywhere");
430 boolean tagMatch =
false;
431 for (String bt : block.
tags)
if (activeSlot.
hasTag(bt)) { tagMatch =
true;
break; }
436 isValid = body.isEmpty();
439 int line =
codeEditor.getLineOfOffset(offset);
440 int lineStart =
codeEditor.getLineStartOffset(line);
441 int lineEnd =
codeEditor.getLineEndOffset(line);
442 String lineText = text.substring(lineStart, lineEnd);
444 boolean isEmptyLine = lineText.trim().isEmpty();
448 if (offset >= 2 && offset <= text.length() - 2) {
449 boolean leftSafe = Character.isWhitespace(text.charAt(offset - 1)) && Character.isWhitespace(text.charAt(offset - 2));
450 boolean rightSafe = Character.isWhitespace(text.charAt(offset)) && Character.isWhitespace(text.charAt(offset + 1));
451 isValid = leftSafe && rightSafe;
454 isValid = isEmptyLine;
461 if (isValid && activeSlot !=
null) {
465 }
catch (Exception e) {
return false; }
472 if (!support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
474 String text = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
477 }
catch (Exception e) {
return false; }
482 JTextComponent.DropLocation dl = (JTextComponent.DropLocation) support.getDropLocation();
484 String code = block.
code;
485 if (code.contains(
"${id}")) {
486 code = code.replace(
"${id}", String.valueOf(
project.blockIdCounter++));
489 codeEditor.insert(code +
"\n", dl.getIndex());
491 }
catch (Exception e) {
return false; }
497 this.currentSprite = sprite;
510 DefaultCompletionProvider provider =
new DefaultCompletionProvider() {
512 protected List<Completion> getCompletionsImpl(JTextComponent comp) {
513 List<Completion> ret =
new ArrayList<>();
514 String text = comp.getText();
515 int caret = comp.getCaretPosition();
517 String prefix = getAlreadyEnteredText(comp);
519 try { lineStart =
codeEditor.getLineStartOffset(
codeEditor.getLineOfOffset(caret)); }
catch (Exception e) {}
520 String currentLine = text.substring(lineStart, caret);
523 for (
int i = caret - 1; i >= 0 && i >= caret - 50; i--) {
524 char c = text.charAt(i);
525 if (c ==
'.') { dotIdx = i;
break; }
526 if (!Character.isJavaIdentifierPart(c))
break;
529 if (currentLine.trim().startsWith(
"import ")) {
532 }
else if (dotIdx != -1) {
537 boolean isStatic =
isClass(expr);
545 List<Completion> filtered =
new ArrayList<>();
546 for (Completion c : ret) {
547 if (c.getReplacementText().startsWith(prefix)) filtered.add(c);
549 filtered.sort((a, b) -> a.getReplacementText().compareToIgnoreCase(b.getReplacementText()));
566 autoCompletion.setTriggerKey(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_I, java.awt.event.InputEvent.CTRL_DOWN_MASK));
570 String trimmedLine = line.trim();
571 if (trimmedLine.length() < 7)
return;
572 String path = trimmedLine.substring(7).trim();
573 if (path.endsWith(
".")) path = path.substring(0, path.length() - 1);
575 Set<String> suggestions =
new HashSet<>();
576 Package[] packages = Package.getPackages();
577 for (Package p : packages) {
578 String name = p.getName();
579 if (name.startsWith(path)) {
580 int startIdx = path.isEmpty() ? 0 : path.length();
581 if (!path.isEmpty() && name.length() > startIdx && name.charAt(startIdx) ==
'.') startIdx++;
583 if (startIdx < name.length()) {
584 String sub = name.substring(startIdx);
585 int nextDot = sub.indexOf(
'.');
586 suggestions.add(nextDot == -1 ? sub : sub.substring(0, nextDot));
595 if (path.equals(
"com.jscratch")) {
596 suggestions.add(
"Sprite"); suggestions.add(
"Stage"); suggestions.add(
"Costume");
597 }
else if (path.equals(
"java.util")) {
598 suggestions.add(
"List"); suggestions.add(
"ArrayList"); suggestions.add(
"HashMap"); suggestions.add(
"Map");
599 }
else if (path.equals(
"java.awt")) {
600 suggestions.add(
"Color"); suggestions.add(
"Graphics2D");
602 }
catch (Exception e) {}
604 for (String s : suggestions) list.add(
new BasicCompletion(provider, s));
608 int start = dotIdx - 1;
610 char c = text.charAt(start);
611 if (!Character.isJavaIdentifierPart(c) && c !=
'.')
break;
614 return text.substring(start + 1, dotIdx);
619 Class.forName(expr);
return true;
620 }
catch (Exception e) {
621 try { Class.forName(
"java.lang." + expr);
return true; }
catch (Exception e2) {}
622 try { Class.forName(
"java.util." + expr);
return true; }
catch (Exception e2) {}
624 if (loader !=
null) {
625 try { loader.loadClass(
"generated." + expr);
return true; }
catch (Exception e3) {}
632 if (expr.equals(
"this")) {
635 if (loader !=
null) {
636 try {
return loader.loadClass(
"generated." +
currentSprite.name); }
catch (Exception e) {}
640 return com.jscratch.Sprite.class;
642 if (expr.equals(
"Stage") || expr.equals(
"Stage.getInstance()"))
return com.jscratch.Stage.class;
644 try {
return Class.forName(expr); }
catch (Exception e) {}
645 try {
return Class.forName(
"java.lang." + expr); }
catch (Exception e) {}
646 try {
return Class.forName(
"java.util." + expr); }
catch (Exception e) {}
649 if (loader !=
null) {
650 try {
return loader.loadClass(
"generated." + expr); }
catch (Exception e) {}
661 Pattern p = Pattern.compile(
"\\b([A-Z][a-zA-Z0-9_]*)\\s+\\b" + Pattern.quote(varName) +
"\\b");
662 Matcher m = p.matcher(text);
663 if (m.find())
return m.group(1);
665 p = Pattern.compile(
"(?:public|private|protected)?\\s+([A-Z][a-zA-Z0-9_]*)\\s+\\b" + Pattern.quote(varName) +
"\\b\\s*[;=]");
667 if (m.find())
return m.group(1);
672 private void addTypeMembers(CompletionProvider provider, List<Completion> list, Class<?> clazz,
boolean staticOnly) {
673 Class<?> current = clazz;
674 Set<String> seenMethods =
new HashSet<>();
675 Set<String> seenFields =
new HashSet<>();
677 while (current !=
null && current != Object.class) {
679 for (Method m : current.getDeclaredMethods()) {
680 boolean isStatic = Modifier.isStatic(m.getModifiers());
681 if (staticOnly == isStatic) {
683 String sig = m.getName() + java.util.Arrays.toString(m.getParameterTypes());
684 if (seenMethods.add(sig)) {
685 FunctionCompletion fc =
new FunctionCompletion(provider, m.getName(), m.getReturnType().getSimpleName());
691 for (Field f : current.getDeclaredFields()) {
692 boolean isStatic = Modifier.isStatic(f.getModifiers());
693 if (staticOnly == isStatic) {
694 if (seenFields.add(f.getName())) {
695 VariableCompletion vc =
new VariableCompletion(provider, f.getName(), f.getType().getSimpleName());
700 }
catch (SecurityException e) {
702 if (current == clazz) {
703 for (Method m : clazz.getMethods()) {
704 boolean isStatic = Modifier.isStatic(m.getModifiers());
705 if (staticOnly == isStatic) {
706 String sig = m.getName() + java.util.Arrays.toString(m.getParameterTypes());
707 if (seenMethods.add(sig)) {
708 FunctionCompletion fc =
new FunctionCompletion(provider, m.getName(), m.getReturnType().getSimpleName());
714 for (Field f : clazz.getFields()) {
715 boolean isStatic = Modifier.isStatic(f.getModifiers());
716 if (staticOnly == isStatic) {
717 if (seenFields.add(f.getName())) {
718 VariableCompletion vc =
new VariableCompletion(provider, f.getName(), f.getType().getSimpleName());
726 current = current.getSuperclass();
734 for (
int i = line; i >= 0; i--) {
737 String lineText =
codeEditor.getText(start, end - start);
738 if (lineText.contains(
"{")) {
740 if (lineText.contains(
"static"))
return true;
742 if (lineText.contains(
"class"))
return false;
747 }
catch (Exception e) {}
752 Set<String> suggestions =
new HashSet<>();
755 String[] coreClasses = {
"String",
"Integer",
"Math",
"System",
"ArrayList",
"List",
"Color",
"KeyEvent",
"Sprite",
"Stage",
"Thread"};
756 for (String c : coreClasses) suggestions.add(c);
760 suggestions.add(sd.name);
763 for (String s : suggestions) list.add(
new BasicCompletion(provider, s));
768 if (thisType !=
null) {
778 Pattern p = Pattern.compile(
"\\b(?:[A-Z][a-zA-Z0-9_]*|int|double|float|long|boolean|char|byte|short|String)\\s+\\b([a-z][a-zA-Z0-9_]*)\\b");
780 Set<String> seen =
new HashSet<>();
782 String name = m.group(1);
783 if (seen.add(name)) {
784 list.add(
new VariableCompletion(provider, name,
""));
788 String[] imports = {
"import java.util.*;",
"import java.awt.Color;",
"import com.jscratch.*;"};
789 for (String i : imports) list.add(
new BasicCompletion(provider, i));
793 StringBuilder desc =
new StringBuilder(
"<html><b>" + m.getName() +
"</b>(");
794 Class<?>[] params = m.getParameterTypes();
795 for (
int i = 0; i < params.length; i++) {
796 desc.append(params[i].getSimpleName());
797 if (i < params.length - 1) desc.append(
", ");
799 desc.append(
")<br>Returns: <i>" + m.getReturnType().getSimpleName() +
"</i></html>");
800 return desc.toString();
static java.net.URLClassLoader getProjectClassLoader()
BlockTransferable(BlockSnippet block)
DataFlavor[] getTransferDataFlavors()
Object getTransferData(DataFlavor flavor)
boolean isDataFlavorSupported(DataFlavor flavor)
ParseResult parse(RSyntaxDocument doc, String styleKey)
List< CompilationResult.CompilationError > errors
void setErrors(List< CompilationResult.CompilationError > errors)
int getSourceActions(JComponent c)
boolean importData(TransferSupport support)
Transferable createTransferable(JComponent c)
boolean canImport(TransferSupport support)
boolean hasTag(String tag)
boolean contains(int offset)
String createMethodDesc(Method m)
boolean isInStaticContext()
final Highlighter.HighlightPainter dropPainter
JComponent createBlockUI(final BlockSnippet block)
String getExpressionBeforeDot(String text, int dotIdx)
CodeWorkspace(Project project, Runnable onAutoCompile)
final List< SlotRange > currentSlots
void loadSpriteCode(SpriteData sprite)
void clearDropHighlight()
AutoCompletion autoCompletion
final Highlighter.HighlightPainter slotPainter
String findVariableType(String varName)
void setCompilationErrors(List< CompilationResult.CompilationError > errors)
Class<?> resolveType(String expr)
void updateHoverHighlight(Point p)
boolean isClass(String expr)
void addGlobalCompletions(CompletionProvider provider, List< Completion > list)
void scheduleSlotRefresh()
Color getCategoryColor(String category)
static final DataFlavor BLOCK_FLAVOR
void addImportPathCompletions(CompletionProvider provider, List< Completion > list, String line, String prefix)
void addTypeMembers(CompletionProvider provider, List< Completion > list, Class<?> clazz, boolean staticOnly)
void triggerAutoCompile()
void clearHoverHighlight()
void applyDropHighlight(SlotRange slot)
Object activeHoverHighlight
Object activeDropHighlight
RSyntaxTextArea codeEditor