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());
131 Action toggleContentAction =
new AbstractAction() {
133 public void actionPerformed(java.awt.event.ActionEvent e) {
144 KeyStroke ctrlP = KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_P,
145 Toolkit.getDefaultToolkit().getMenuShortcutKeyMask());
147 codeEditor.getInputMap().put(ctrlP,
"toggleContentFormat");
148 codeEditor.getActionMap().put(
"toggleContentFormat", toggleContentAction);
151 codeEditor.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JAVA);
160 if (this.onAutoCompile !=
null) {
162 this.onAutoCompile.run();
170 Theme theme = Theme.load(getClass().getResourceAsStream(
"/org/fife/ui/rsyntaxtextarea/themes/eclipse.xml"));
172 }
catch (Exception ioe) {
178 codeEditor.getDocument().addDocumentListener(
new DocumentListener() {
184 MouseAdapter hoverAdapter =
new MouseAdapter() {
189 codeEditor.addMouseMotionListener(hoverAdapter);
193 codeEditor.getDropTarget().addDropTargetListener(
new java.awt.dnd.DropTargetAdapter() {
194 @Override public void dragExit(java.awt.dnd.DropTargetEvent dte) { clearDropHighlight(); }
195 @Override
public void drop(java.awt.dnd.DropTargetDropEvent dtde) { clearDropHighlight(); }
197 }
catch (Exception e) {}
201 paletteTabs.setPreferredSize(
new Dimension(250, 0));
205 JSplitPane splitPane =
new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,
paletteTabs,
new RTextScrollPane(
codeEditor));
206 splitPane.setDividerLocation(250);
207 add(splitPane, BorderLayout.CENTER);
228 public ParseResult
parse(RSyntaxDocument doc, String styleKey) {
229 DefaultParseResult result =
new DefaultParseResult(
this);
235 int line = error.line - 1;
236 if (line >= 0 && line < doc.getDefaultRootElement().getElementCount()) {
237 int start = doc.getDefaultRootElement().getElement(line).getStartOffset();
238 int end = doc.getDefaultRootElement().getElement(line).getEndOffset();
239 DefaultParserNotice notice =
new DefaultParserNotice(
this, error.message, line, start, end - start);
240 notice.setLevel(ParserNotice.Level.ERROR);
241 result.addNotice(notice);
243 }
catch (Exception e) {
256 if (slot.contains(offset)) {
257 if (bestMatch ==
null || slot.
length() < bestMatch.
length()) {
263 if (bestMatch !=
null) {
266 }
catch (BadLocationException ex) {}
288 }
catch (BadLocationException ex) {}
292 Map<String, JPanel> categoryPanels =
new HashMap<>();
293 try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(getClass().getResourceAsStream(
"/blocks.txt")))) {
296 StringBuilder codeBuffer =
new StringBuilder();
298 while ((line = reader.readLine()) !=
null) {
299 if (line.startsWith(
"# ")) {
300 String meta = line.substring(2);
301 String[] parts = meta.split(
" : ");
303 currentBlock.name = parts[0].trim();
304 currentBlock.category = parts[1].trim();
305 if (parts.length > 2) {
306 for (String t : parts[2].split(
",")) currentBlock.
tags.add(t.trim());
308 codeBuffer.setLength(0);
309 }
else if (line.equals(
"###")) {
310 if (currentBlock !=
null) {
311 currentBlock.code = codeBuffer.toString().trim();
312 JPanel panel = categoryPanels.computeIfAbsent(currentBlock.
category, k -> {
313 JPanel p = new JPanel();
314 p.setLayout(new BoxLayout(p, BoxLayout.Y_AXIS));
315 p.setBackground(Color.WHITE);
316 p.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
317 paletteTabs.addTab(k, new JScrollPane(p));
321 panel.add(Box.createVerticalStrut(5));
324 }
else if (currentBlock !=
null) {
325 codeBuffer.append(line).append(
"\n");
328 }
catch (Exception e) {
334 JLabel label =
new JLabel(block.
name);
335 label.setOpaque(
true);
337 label.setForeground(Color.WHITE);
338 label.setFont(
new Font(
"Arial", Font.BOLD, 12));
339 label.setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12));
340 label.setMaximumSize(
new Dimension(220, 40));
341 label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
342 label.setTransferHandler(
new TransferHandler() {
343 @Override
public int getSourceActions(JComponent c) {
return COPY; }
344 @Override
protected Transferable createTransferable(JComponent c) {
return new BlockTransferable(block); }
346 label.addMouseListener(
new MouseAdapter() {
347 @Override
public void mousePressed(MouseEvent e) {
348 ((JComponent)e.getSource()).getTransferHandler().exportAsDrag((JComponent)e.getSource(), e, TransferHandler.COPY);
356 case "Motion":
return new Color(76, 151, 255);
357 case "Looks":
return new Color(153, 102, 255);
358 case "Sound":
return new Color(207, 99, 207);
359 case "Events":
return new Color(255, 191, 0);
360 case "Control":
return new Color(255, 171, 25);
361 case "Sensing":
return new Color(92, 177, 214);
362 case "Operators":
return new Color(89, 192, 89);
363 case "Variables":
return new Color(255, 140, 26);
364 case "My Blocks":
return new Color(255, 102, 128);
365 case "Structure":
return new Color(214, 92, 214);
366 case "Helpers":
return new Color(235, 64, 52);
368 default:
return Color.GRAY;
374 SwingUtilities.invokeLater(this::refreshSlots);
382 Pattern startPattern = Pattern.compile(
"/\\* slot start \\[(.*?)\\] \\*/");
383 Matcher matcher = startPattern.matcher(text);
384 while (matcher.find()) {
385 String attrContent = matcher.group(1);
387 slot.start = matcher.start();
388 slot.startCommentEnd = matcher.end();
390 Matcher idM = Pattern.compile(
"id:\"(.*?)\"").matcher(attrContent);
391 if (idM.find()) slot.id = idM.group(1);
393 Matcher tagsM = Pattern.compile(
"tags:\"(.*?)\"").matcher(attrContent);
395 for (String t : tagsM.group(1).split(
",")) slot.
tags.add(t.trim());
398 Matcher onlyOneM = Pattern.compile(
"onlyOne:(true|false)").matcher(attrContent);
399 if (onlyOneM.find()) slot.isOnlyOne = Boolean.parseBoolean(onlyOneM.group(1));
401 if (slot.
id ==
null)
continue;
403 String endRegex =
"/\\* slot end \\[(.*?id:\"" + Pattern.quote(slot.
id) +
"\".*?)\\] \\*/";
404 Pattern endPattern = Pattern.compile(endRegex);
405 Matcher endMatcher = endPattern.matcher(text);
406 if (endMatcher.find(matcher.end())) {
407 slot.end = endMatcher.end();
408 slot.endCommentStart = endMatcher.start();
422 return (flavor.equals(DataFlavor.stringFlavor)) ? block.code :
null;
434 return new StringSelection(
codeEditor.getSelectedText());
440 if (!support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
444 if (!support.isDrop() || !support.isDataFlavorSupported(
BLOCK_FLAVOR))
return false;
447 JTextComponent.DropLocation dl = (JTextComponent.DropLocation) support.getDropLocation();
448 int offset = dl.getIndex();
453 if (slot.contains(offset)) {
454 if (activeSlot ==
null || slot.
length() < activeSlot.
length()) activeSlot = slot;
458 boolean isValid =
false;
459 if (text.trim().isEmpty() && block.
tags.contains(
"allowEmpty")) {
461 }
else if (activeSlot ==
null) {
462 isValid = block.
tags.contains(
"anywhere");
464 boolean tagMatch =
false;
465 for (String bt : block.
tags)
if (activeSlot.
hasTag(bt)) { tagMatch =
true;
break; }
470 isValid = body.isEmpty();
473 int line =
codeEditor.getLineOfOffset(offset);
474 int lineStart =
codeEditor.getLineStartOffset(line);
475 int lineEnd =
codeEditor.getLineEndOffset(line);
476 String lineText = text.substring(lineStart, lineEnd);
478 boolean isEmptyLine = lineText.trim().isEmpty();
482 if (offset >= 2 && offset <= text.length() - 2) {
483 boolean leftSafe = Character.isWhitespace(text.charAt(offset - 1)) && Character.isWhitespace(text.charAt(offset - 2));
484 boolean rightSafe = Character.isWhitespace(text.charAt(offset)) && Character.isWhitespace(text.charAt(offset + 1));
485 isValid = leftSafe && rightSafe;
488 isValid = isEmptyLine;
495 if (isValid && activeSlot !=
null) {
499 }
catch (Exception e) {
return false; }
506 if (!support.isDrop() && support.isDataFlavorSupported(DataFlavor.stringFlavor)) {
508 String text = (String) support.getTransferable().getTransferData(DataFlavor.stringFlavor);
511 }
catch (Exception e) {
return false; }
516 JTextComponent.DropLocation dl = (JTextComponent.DropLocation) support.getDropLocation();
518 String code = block.
code;
519 if (code.contains(
"${id}")) {
520 code = code.replace(
"${id}", String.valueOf(
project.blockIdCounter++));
523 codeEditor.insert(code +
"\n", dl.getIndex());
525 }
catch (Exception e) {
return false; }
531 this.currentSprite = sprite;
544 DefaultCompletionProvider provider =
new DefaultCompletionProvider() {
546 protected List<Completion> getCompletionsImpl(JTextComponent comp) {
547 List<Completion> ret =
new ArrayList<>();
548 String text = comp.getText();
549 int caret = comp.getCaretPosition();
551 String prefix = getAlreadyEnteredText(comp);
553 try { lineStart =
codeEditor.getLineStartOffset(
codeEditor.getLineOfOffset(caret)); }
catch (Exception e) {}
554 String currentLine = text.substring(lineStart, caret);
557 for (
int i = caret - 1; i >= 0 && i >= caret - 50; i--) {
558 char c = text.charAt(i);
559 if (c ==
'.') { dotIdx = i;
break; }
560 if (!Character.isJavaIdentifierPart(c))
break;
563 if (currentLine.trim().startsWith(
"import ")) {
566 }
else if (dotIdx != -1) {
571 boolean isStatic =
isClass(expr);
579 List<Completion> filtered =
new ArrayList<>();
580 for (Completion c : ret) {
581 if (c.getReplacementText().startsWith(prefix)) filtered.add(c);
583 filtered.sort((a, b) -> a.getReplacementText().compareToIgnoreCase(b.getReplacementText()));
600 autoCompletion.setTriggerKey(KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_I, java.awt.event.InputEvent.CTRL_DOWN_MASK));
604 String trimmedLine = line.trim();
605 if (trimmedLine.length() < 7)
return;
606 String path = trimmedLine.substring(7).trim();
607 if (path.endsWith(
".")) path = path.substring(0, path.length() - 1);
609 Set<String> suggestions =
new HashSet<>();
610 Package[] packages = Package.getPackages();
611 for (Package p : packages) {
612 String name = p.getName();
613 if (name.startsWith(path)) {
614 int startIdx = path.isEmpty() ? 0 : path.length();
615 if (!path.isEmpty() && name.length() > startIdx && name.charAt(startIdx) ==
'.') startIdx++;
617 if (startIdx < name.length()) {
618 String sub = name.substring(startIdx);
619 int nextDot = sub.indexOf(
'.');
620 suggestions.add(nextDot == -1 ? sub : sub.substring(0, nextDot));
629 if (path.equals(
"com.jscratch")) {
630 suggestions.add(
"Sprite"); suggestions.add(
"Stage"); suggestions.add(
"Costume");
631 }
else if (path.equals(
"java.util")) {
632 suggestions.add(
"List"); suggestions.add(
"ArrayList"); suggestions.add(
"HashMap"); suggestions.add(
"Map");
633 }
else if (path.equals(
"java.awt")) {
634 suggestions.add(
"Color"); suggestions.add(
"Graphics2D");
636 }
catch (Exception e) {}
638 for (String s : suggestions) list.add(
new BasicCompletion(provider, s));
642 int start = dotIdx - 1;
644 char c = text.charAt(start);
645 if (!Character.isJavaIdentifierPart(c) && c !=
'.')
break;
648 return text.substring(start + 1, dotIdx);
653 Class.forName(expr);
return true;
654 }
catch (Exception e) {
655 try { Class.forName(
"java.lang." + expr);
return true; }
catch (Exception e2) {}
656 try { Class.forName(
"java.util." + expr);
return true; }
catch (Exception e2) {}
658 if (loader !=
null) {
659 try { loader.loadClass(
"generated." + expr);
return true; }
catch (Exception e3) {}
666 if (expr.equals(
"this")) {
669 if (loader !=
null) {
670 try {
return loader.loadClass(
"generated." +
currentSprite.name); }
catch (Exception e) {}
674 return com.jscratch.Sprite.class;
676 if (expr.equals(
"Stage") || expr.equals(
"Stage.getInstance()"))
return com.jscratch.Stage.class;
678 try {
return Class.forName(expr); }
catch (Exception e) {}
679 try {
return Class.forName(
"java.lang." + expr); }
catch (Exception e) {}
680 try {
return Class.forName(
"java.util." + expr); }
catch (Exception e) {}
683 if (loader !=
null) {
684 try {
return loader.loadClass(
"generated." + expr); }
catch (Exception e) {}
695 Pattern p = Pattern.compile(
"\\b([A-Z][a-zA-Z0-9_]*)\\s+\\b" + Pattern.quote(varName) +
"\\b");
696 Matcher m = p.matcher(text);
697 if (m.find())
return m.group(1);
699 p = Pattern.compile(
"(?:public|private|protected)?\\s+([A-Z][a-zA-Z0-9_]*)\\s+\\b" + Pattern.quote(varName) +
"\\b\\s*[;=]");
701 if (m.find())
return m.group(1);
706 private void addTypeMembers(CompletionProvider provider, List<Completion> list, Class<?> clazz,
boolean staticOnly) {
707 Class<?> current = clazz;
708 Set<String> seenMethods =
new HashSet<>();
709 Set<String> seenFields =
new HashSet<>();
711 while (current !=
null && current != Object.class) {
713 for (Method m : current.getDeclaredMethods()) {
714 boolean isStatic = Modifier.isStatic(m.getModifiers());
715 if (staticOnly == isStatic) {
717 String sig = m.getName() + java.util.Arrays.toString(m.getParameterTypes());
718 if (seenMethods.add(sig)) {
719 FunctionCompletion fc =
new FunctionCompletion(provider, m.getName(), m.getReturnType().getSimpleName());
725 for (Field f : current.getDeclaredFields()) {
726 boolean isStatic = Modifier.isStatic(f.getModifiers());
727 if (staticOnly == isStatic) {
728 if (seenFields.add(f.getName())) {
729 VariableCompletion vc =
new VariableCompletion(provider, f.getName(), f.getType().getSimpleName());
734 }
catch (SecurityException e) {
736 if (current == clazz) {
737 for (Method m : clazz.getMethods()) {
738 boolean isStatic = Modifier.isStatic(m.getModifiers());
739 if (staticOnly == isStatic) {
740 String sig = m.getName() + java.util.Arrays.toString(m.getParameterTypes());
741 if (seenMethods.add(sig)) {
742 FunctionCompletion fc =
new FunctionCompletion(provider, m.getName(), m.getReturnType().getSimpleName());
748 for (Field f : clazz.getFields()) {
749 boolean isStatic = Modifier.isStatic(f.getModifiers());
750 if (staticOnly == isStatic) {
751 if (seenFields.add(f.getName())) {
752 VariableCompletion vc =
new VariableCompletion(provider, f.getName(), f.getType().getSimpleName());
760 current = current.getSuperclass();
768 for (
int i = line; i >= 0; i--) {
771 String lineText =
codeEditor.getText(start, end - start);
772 if (lineText.contains(
"{")) {
774 if (lineText.contains(
"static"))
return true;
776 if (lineText.contains(
"class"))
return false;
781 }
catch (Exception e) {}
786 Set<String> suggestions =
new HashSet<>();
789 String[] coreClasses = {
"String",
"Integer",
"Math",
"System",
"ArrayList",
"List",
"Color",
"KeyEvent",
"Sprite",
"Stage",
"Thread"};
790 for (String c : coreClasses) suggestions.add(c);
794 suggestions.add(sd.name);
797 for (String s : suggestions) list.add(
new BasicCompletion(provider, s));
802 if (thisType !=
null) {
812 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");
814 Set<String> seen =
new HashSet<>();
816 String name = m.group(1);
817 if (seen.add(name)) {
818 list.add(
new VariableCompletion(provider, name,
""));
822 String[] imports = {
"import java.util.*;",
"import java.awt.Color;",
"import com.jscratch.*;"};
823 for (String i : imports) list.add(
new BasicCompletion(provider, i));
827 StringBuilder desc =
new StringBuilder(
"<html><b>" + m.getName() +
"</b>(");
828 Class<?>[] params = m.getParameterTypes();
829 for (
int i = 0; i < params.length; i++) {
830 desc.append(params[i].getSimpleName());
831 if (i < params.length - 1) desc.append(
", ");
833 desc.append(
")<br>Returns: <i>" + m.getReturnType().getSimpleName() +
"</i></html>");
834 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