Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ Scratch for Java Studio is a desktop IDE for students moving from Scratch to
Java. It builds projects with [Scratch for Java](https://github.com/openpatch/scratch-for-java)
and keeps them as ordinary Java source files that also work in BlueJ and VS Code.

**Status: 0.1.0-alpha.11.** The IDE and its
**Status: 0.1.0-alpha.12.** The IDE and its
project format may change while we test it with students and teachers.

![Scratch for Java Studio with the code editor and the visual stage designer side by side](docs/screenshot.png)
Expand Down
2 changes: 1 addition & 1 deletion app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.openpatch.scratch4j</groupId>
<artifactId>scratch4j-ide</artifactId>
<version>0.1.0-alpha.11</version>
<version>0.1.0-alpha.12</version>
</parent>

<artifactId>app</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.openpatch.scratch4j</groupId>
<artifactId>scratch4j-ide</artifactId>
<version>0.1.0-alpha.11</version>
<version>0.1.0-alpha.12</version>
</parent>

<artifactId>core</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion export/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.openpatch.scratch4j</groupId>
<artifactId>scratch4j-ide</artifactId>
<version>0.1.0-alpha.11</version>
<version>0.1.0-alpha.12</version>
</parent>

<artifactId>export</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>org.openpatch.scratch4j</groupId>
<artifactId>scratch4j-ide</artifactId>
<version>0.1.0-alpha.11</version>
<version>0.1.0-alpha.12</version>
<packaging>pom</packaging>

<name>Scratch for Java Studio</name>
Expand Down
2 changes: 1 addition & 1 deletion runner/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.openpatch.scratch4j</groupId>
<artifactId>scratch4j-ide</artifactId>
<version>0.1.0-alpha.11</version>
<version>0.1.0-alpha.12</version>
</parent>

<artifactId>runner</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion sound/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.openpatch.scratch4j</groupId>
<artifactId>scratch4j-ide</artifactId>
<version>0.1.0-alpha.11</version>
<version>0.1.0-alpha.12</version>
</parent>

<artifactId>sound</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion ui/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<parent>
<groupId>org.openpatch.scratch4j</groupId>
<artifactId>scratch4j-ide</artifactId>
<version>0.1.0-alpha.11</version>
<version>0.1.0-alpha.12</version>
</parent>

<artifactId>ui</artifactId>
Expand Down
28 changes: 27 additions & 1 deletion ui/src/main/java/org/openpatch/scratch4j/ui/CodeEditor.java
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,9 @@ org.openpatch.scratch4j.core.compile.Completions.Result complete(Path file, Stri
});

private static final String INDENT = " ";
/** A method snippet from the block palette (an event block): its name. */
private static final java.util.regex.Pattern EVENT_METHOD = java.util.regex.Pattern.compile(
"^(?:public |protected |private )?void (\\w+)\\([^)]*\\) \\{\n");
private static final Pattern IDENTIFIER = Pattern.compile("[A-Za-z_][A-Za-z0-9_]*$");
private static final Pattern WORD = Pattern.compile("\\b[A-Za-z_][A-Za-z0-9_]{2,}\\b");
private static final Pattern IMAGE_STRING = Pattern.compile(
Expand Down Expand Up @@ -536,6 +539,27 @@ void insertBlock(String snippet) {
}
String body = snippet.substring(0, snippet.length() - 1);
boolean multiLine = body.contains("\n");
var method = EVENT_METHOD.matcher(body);
if (method.find()) {
// an event block is a method: it goes into the class body, not into a method
int current = area.getCurrentParagraph();
int lineEnd = area.getAbsolutePosition(current, area.getParagraph(current).length());
var place = MemberPlacement.place(area.getText(), lineEnd, method.group(1));
if (place != null && place.existing()) {
// the class already reacts to this event: go there instead of a second copy
area.moveTo(place.offset());
int open = area.getCurrentParagraph();
if (open + 1 < area.getParagraphs().size()
&& !area.getParagraph(open + 1).getText().strip().startsWith("}")) {
open++; // the method's first line
}
area.moveTo(open, area.getParagraph(open).length());
area.requestFollowCaret();
area.requestFocus();
return;
}
if (place != null) area.moveTo(place.offset());
}
int paragraph = area.getCurrentParagraph();
String line = area.getParagraph(paragraph).getText();
boolean blank = line.isBlank();
Expand All @@ -545,8 +569,10 @@ void insertBlock(String snippet) {
boolean prevIsBlank = blank
? paragraph == 0 || area.getParagraph(paragraph - 1).getText().isBlank()
: false;
// a closing brace below needs no blank line before it either
boolean nextIsBlank = paragraph + 1 >= area.getParagraphs().size()
|| area.getParagraph(paragraph + 1).getText().isBlank();
|| area.getParagraph(paragraph + 1).getText().isBlank()
|| area.getParagraph(paragraph + 1).getText().strip().startsWith("}");
int lineStart = area.getAbsolutePosition(paragraph, 0);
// multi-line snippets (method overrides) keep a blank line on both sides
String before = blank ? (multiLine && !prevIsBlank ? "\n" : "")
Expand Down
12 changes: 12 additions & 0 deletions ui/src/main/java/org/openpatch/scratch4j/ui/ImageEditorView.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,18 @@ private record Snapshot(List<WritableImage> layers, List<Boolean> visible, int a
HBox bottom = new HBox(8, status, spacer, credits);
bottom.getStyleClass().add("editor-status");
setBottom(bottom);
// Ctrl+Z / Ctrl+Y (Cmd on macOS) anywhere in the editor; text fields keep their own
addEventHandler(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> {
if (!e.isShortcutDown()
|| e.getTarget() instanceof javafx.scene.control.TextInputControl) return;
if (e.getCode() == javafx.scene.input.KeyCode.Z) {
if (e.isShiftDown()) redo(); else undo();
e.consume();
} else if (e.getCode() == javafx.scene.input.KeyCode.Y) {
redo();
e.consume();
}
});
updateApplyButton();
updateStatus();
redrawOverlay();
Expand Down
147 changes: 147 additions & 0 deletions ui/src/main/java/org/openpatch/scratch4j/ui/MemberPlacement.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package org.openpatch.scratch4j.ui;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
* Where a method from the block palette (an event block, {@code when ...})
* belongs: in a class body, never inside another method. Braces are counted
* past comments, strings and char literals; a brace opens a class when the
* code before it names one ({@code class}, {@code interface}, {@code enum},
* {@code record}); anonymous classes and lambdas count as code.
*/
final class MemberPlacement {

private MemberPlacement() { }

/**
* Where to put the method: after the line of {@code offset}, or, when
* {@code existing}, the caret inside the method of that name the class
* already has.
*/
record Place(int offset, boolean existing) { }

private static final Pattern TYPE = Pattern.compile(
"(^|[^\\w@])(class|interface|enum|record)\\s+\\w+");

private static final class Brace {
final int open;
final boolean type;
final String header;
final Brace parent;
int close = -1;

Brace(int open, boolean type, String header, Brace parent) {
this.open = open;
this.type = type;
this.header = header;
this.parent = parent;
}
}

/**
* The place for method {@code name} when the block would be inserted after
* the line ending at {@code lineEnd}; null when that line already is in a
* class body (or the file has no class to put it in).
*/
static Place place(String text, int lineEnd, String name) {
List<Brace> braces = new ArrayList<>();
List<Brace> atLine = null;
List<Brace> stack = new ArrayList<>();
StringBuilder header = new StringBuilder();
int i = 0;
while (i <= text.length()) {
if (i >= lineEnd && atLine == null) {
atLine = List.copyOf(stack);
}
if (i == text.length()) break;
char c = text.charAt(i);
char next = i + 1 < text.length() ? text.charAt(i + 1) : 0;
if (c == '/' && next == '/') {
int end = text.indexOf('\n', i);
i = end < 0 ? text.length() : end;
continue;
}
if (c == '/' && next == '*') {
int end = text.indexOf("*/", i + 2);
i = end < 0 ? text.length() : end + 2;
header.append(' ');
continue;
}
if (c == '"' || c == '\'') {
i = skipLiteral(text, i);
header.append(" \"\" ");
continue;
}
if (c == '{') {
Brace parent = stack.isEmpty() ? null : stack.get(stack.size() - 1);
String head = header.toString();
Brace brace = new Brace(i, TYPE.matcher(head).find(), head, parent);
braces.add(brace);
stack.add(brace);
header.setLength(0);
} else if (c == '}') {
if (!stack.isEmpty()) stack.remove(stack.size() - 1).close = i;
header.setLength(0);
} else if (c == ';') {
header.setLength(0);
} else {
header.append(c);
}
i++;
}
if (atLine == null) atLine = List.copyOf(stack);

// the innermost class around the line, else the file's first class
int typeIndex = -1;
for (int k = atLine.size() - 1; k >= 0; k--) {
if (atLine.get(k).type) {
typeIndex = k;
break;
}
}
Brace type = typeIndex >= 0 ? atLine.get(typeIndex)
: braces.stream().filter(b -> b.type && b.parent == null).findFirst().orElse(null);
if (type == null) return null;

Pattern same = Pattern.compile("\\bvoid\\s+" + Pattern.quote(name) + "\\s*\\(");
for (Brace member : braces) {
if (member.parent == type && !member.type && same.matcher(member.header).find()) {
return new Place(member.open + 1, true);
}
}
if (typeIndex >= 0 && typeIndex == atLine.size() - 1) {
return null; // already in the class body
}
if (typeIndex >= 0) {
// inside a method (or a block of the class): after the member's closing brace
Brace member = atLine.get(typeIndex + 1);
return member.close < 0 ? null : new Place(member.close, false);
}
// outside the class: before its closing brace
if (type.close < 0) return null;
int lineBefore = text.lastIndexOf('\n', type.close - 1);
return lineBefore <= type.open ? null : new Place(lineBefore, false);
}

/** The index after the string, text block or char literal starting at {@code start}. */
private static int skipLiteral(String text, int start) {
char quote = text.charAt(start);
if (quote == '"' && text.startsWith("\"\"\"", start)) {
int end = text.indexOf("\"\"\"", start + 3);
return end < 0 ? text.length() : end + 3;
}
int i = start + 1;
while (i < text.length()) {
char c = text.charAt(i);
if (c == '\\') {
i += 2;
continue;
}
if (c == quote || c == '\n') return i + 1;
i++;
}
return text.length();
}
}
15 changes: 15 additions & 0 deletions ui/src/main/java/org/openpatch/scratch4j/ui/SoundEditorView.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,22 @@ final class SoundEditorView extends BorderPane {
this.clip = SoundIO.decode(file);
getStyleClass().add("sound-editor");

// Ctrl+Z / Ctrl+Y (Cmd on macOS) anywhere in the editor; text fields keep their own
addEventHandler(javafx.scene.input.KeyEvent.KEY_PRESSED, e -> {
if (!e.isShortcutDown()
|| e.getTarget() instanceof javafx.scene.control.TextInputControl) return;
if (e.getCode() == javafx.scene.input.KeyCode.Z) {
if (e.isShiftDown()) redo(); else undo();
e.consume();
} else if (e.getCode() == javafx.scene.input.KeyCode.Y) {
redo();
e.consume();
}
});

wave.setFocusTraversable(true);
wave.setOnMousePressed(e -> {
wave.requestFocus(); // the shortcuts need the focus in the editor
dragStartX = e.getX();
selection = null;
redraw();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ imageeditor.nineslice.help=Die vier blauen Linien verschieben, um die dehnbare M
imageeditor.nineslice.saved=Neun-Scheiben-R\u00e4nder in {0} geschrieben
imageeditor.nineslice.no.uisprite=Vor dem Speichern eine UISprite-Klasse erstellen.
imageeditor.save=Speichern
imageeditor.undo=R\u00fcckg\u00e4ngig
imageeditor.redo=Wiederholen
imageeditor.undo=R\u00fcckg\u00e4ngig (Strg+Z)
imageeditor.redo=Wiederholen (Strg+Y)
imageeditor.zoom=Zoom
imageeditor.grid=Raster
imageeditor.grid.tooltip=Pixelgrenzen ab 600 % Vergr\u00f6\u00dferung anzeigen
Expand Down Expand Up @@ -166,8 +166,8 @@ soundeditor.normalize=Normalisieren
soundeditor.reverse=Umkehren
soundeditor.faster=Schneller
soundeditor.slower=Langsamer
soundeditor.undo=R\u00fcckg\u00e4ngig
soundeditor.redo=Wiederholen
soundeditor.undo=R\u00fcckg\u00e4ngig (Strg+Z)
soundeditor.redo=Wiederholen (Strg+Y)
soundeditor.effect.all=Keine Auswahl - Effekt wirkt auf den ganzen Klang
soundeditor.mp3.saved=Als WAV gespeichert (die Bibliothek kann kein MP3)
menu.export=Exportieren
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ imageeditor.nineslice.help=Drag the four blue guides to set the stretchable cent
imageeditor.nineslice.saved=Nine-slice margins written to {0}
imageeditor.nineslice.no.uisprite=Create a UISprite class before saving nine-slice guides.
imageeditor.save=Save
imageeditor.undo=Undo
imageeditor.redo=Redo
imageeditor.undo=Undo (Ctrl+Z)
imageeditor.redo=Redo (Ctrl+Y)
imageeditor.zoom=Zoom
imageeditor.grid=Grid
imageeditor.grid.tooltip=Show pixel boundaries at 600% zoom or higher
Expand Down Expand Up @@ -166,8 +166,8 @@ soundeditor.normalize=Normalize
soundeditor.reverse=Reverse
soundeditor.faster=Faster
soundeditor.slower=Slower
soundeditor.undo=Undo
soundeditor.redo=Redo
soundeditor.undo=Undo (Ctrl+Z)
soundeditor.redo=Redo (Ctrl+Y)
soundeditor.effect.all=No selection - effect applies to the whole sound
soundeditor.mp3.saved=Saved as WAV (the library cannot play MP3)
menu.export=Export
Expand Down
15 changes: 15 additions & 0 deletions ui/src/test/java/org/openpatch/scratch4j/ui/EditorLinesIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,21 @@ void lineOperationsAndRefactoringMenu() throws Exception {
.forEach(i -> i.fire());
assertThat(renamed).containsExactly(4);
assertThat(usages).containsExactly(4);

// an event block dropped into a method lands after it, in the class body
area.replaceText("public class Cat extends Sprite {\n public void run() {\n"
+ " this.move(1);\n }\n}\n");
area.moveTo(2, 4);
code.insertBlock("public void whenClicked() {\n \n}\n");
assertThat(area.getText()).isEqualTo("public class Cat extends Sprite {\n"
+ " public void run() {\n this.move(1);\n }\n\n"
+ " public void whenClicked() {\n \n }\n}\n");
// a second one takes the caret into the method that is already there
area.moveTo(2, 4);
String before = area.getText();
code.insertBlock("public void whenClicked() {\n \n}\n");
assertThat(area.getText()).isEqualTo(before);
assertThat(area.getCurrentParagraph()).isEqualTo(6);
});
}
}
Loading
Loading