JScratch
Loading...
Searching...
No Matches
ScratchEditor.java
Go to the documentation of this file.
1package com.jscratch;
2
3import com.google.gson.Gson;
4import com.google.gson.GsonBuilder;
5import com.jscratch.ui.*;
6import com.jscratch.Project.SpriteData;
7import com.jscratch.compiler.CompilerService;
8import com.jscratch.compiler.ProjectRunner;
9import com.jscratch.compiler.CompilationResult;
10import javax.swing.*;
11import java.awt.*;
12import java.io.*;
13import java.nio.file.*;
14import java.util.ArrayList;
15import java.util.List;
16import java.util.zip.*;
17
18public class ScratchEditor extends JFrame {
19 private JTabbedPane tabbedPane;
22
28
29 private DefaultListModel<String> spriteListModel = new DefaultListModel<>();
30 private JList<String> spriteList = new JList<>(spriteListModel);
31
32 private JTextArea consoleArea;
33 private File currentProjectFile;
34
35 public ScratchEditor() {
37 setTitle("JScratch - Java IDE");
38 setSize(1400, 900);
39 setDefaultCloseOperation(EXIT_ON_CLOSE);
40 setLayout(new BorderLayout());
41
42 getContentPane().setBackground(new Color(77, 151, 255));
43
44 project = new Project();
45
46 stagePreview = new Stage();
47 stagePreview.setBorder(BorderFactory.createLineBorder(new Color(220, 220, 220), 2));
48
49 setupTabs();
51
52 JPanel rightPanel = new JPanel(new BorderLayout());
53 rightPanel.setOpaque(false);
54 rightPanel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
55
56 JPanel stageContainer = new JPanel(new BorderLayout());
57 stageContainer.add(stagePreview, BorderLayout.CENTER);
58 stageContainer.setOpaque(false);
59 rightPanel.add(stageContainer, BorderLayout.NORTH);
60
61 JPanel spritePanel = new JPanel(new BorderLayout());
62 spritePanel.setBackground(Color.WHITE);
63 spritePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
64
65 spriteList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
66 spritePanel.add(new JScrollPane(spriteList), BorderLayout.CENTER);
67
68 JPopupMenu spriteMenu = new JPopupMenu();
69 JMenuItem renameItem = new JMenuItem("Rename");
70 renameItem.addActionListener(e -> renameSprite());
71 spriteMenu.add(renameItem);
72
73 spriteList.addMouseListener(new java.awt.event.MouseAdapter() {
74 public void mousePressed(java.awt.event.MouseEvent e) {
75 if (e.isPopupTrigger()) showMenu(e);
76 }
77 public void mouseReleased(java.awt.event.MouseEvent e) {
78 if (e.isPopupTrigger()) showMenu(e);
79 }
80 private void showMenu(java.awt.event.MouseEvent e) {
81 int index = spriteList.locationToIndex(e.getPoint());
82 if (index != -1) {
83 spriteList.setSelectedIndex(index);
84 spriteMenu.show(e.getComponent(), e.getX(), e.getY());
85 }
86 }
87 });
88
89 JButton addSpriteBtn = new JButton("Add Sprite");
90 addSpriteBtn.addActionListener(e -> addSprite());
91 spritePanel.add(addSpriteBtn, BorderLayout.SOUTH);
92
93 rightPanel.add(spritePanel, BorderLayout.CENTER);
94
95 JSplitPane leftSplit = new JSplitPane(JSplitPane.VERTICAL_SPLIT, tabbedPane, new JScrollPane(consoleArea));
96 leftSplit.setDividerLocation(600);
97 leftSplit.setResizeWeight(0.7);
98
99 JSplitPane mainSplit = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftSplit, rightPanel);
100 mainSplit.setDividerLocation(1000);
101 add(mainSplit, BorderLayout.CENTER);
102
103 setJMenuBar(createMenuBar());
104
105 spriteList.addListSelectionListener(e -> {
106 int idx = spriteList.getSelectedIndex();
107 if (idx != -1) {
108 codeWorkspace.saveCurrentCode();
109 SpriteData sd = project.sprites.get(idx);
110 codeWorkspace.loadSpriteCode(sd);
111 soundPanel.refreshList(sd.sounds);
112 }
113 });
114
115 syncWithSourceFiles();
116 ensureMainSprite();
117 refreshSpriteList();
118
119 // Initial compile to populate autocomplete and highlights
120 autoCompile();
121
122 setLocationRelativeTo(null);
123 }
124
125 private void setupConsole() {
126 consoleArea = new JTextArea();
127 consoleArea.setEditable(false);
128 consoleArea.setBackground(new Color(30, 30, 30));
129 consoleArea.setForeground(new Color(200, 200, 200));
130 consoleArea.setFont(new Font("Consolas", Font.PLAIN, 12));
131 consoleArea.setMargin(new Insets(5, 5, 5, 5));
132
133 PrintStream consoleStream = new PrintStream(new OutputStream() {
134 @Override
135 public void write(int b) throws IOException {
136 appendToConsole(String.valueOf((char) b));
137 }
138 @Override
139 public void write(byte[] b, int off, int len) throws IOException {
140 appendToConsole(new String(b, off, len));
141 }
142 });
143 System.setOut(consoleStream);
144 System.setErr(consoleStream);
145 }
146
147 private void appendToConsole(String text) {
148 SwingUtilities.invokeLater(() -> {
149 consoleArea.append(text);
150 consoleArea.setCaretPosition(consoleArea.getDocument().getLength());
151 });
152 }
153
154 private void logToConsole(String message) {
155 appendToConsole(message + "\n");
156 }
157
158 private void clearConsole() {
159 SwingUtilities.invokeLater(() -> consoleArea.setText(""));
160 }
161
162 private void ensureMainSprite() {
163 boolean found = false;
164 for (SpriteData sd : project.sprites) {
165 if (sd.name.equals("Main")) {
166 found = true;
167 break;
168 }
169 }
170 if (!found) {
171 SpriteData main = new SpriteData("Main");
172 main.sourceCode = generateDefaultMainCode();
173 project.sprites.add(0, main);
174 }
175 }
176
177 private void syncWithSourceFiles() {
178 File srcDir = WorkspaceManager.getSrcDir();
179 if (!srcDir.exists() || !srcDir.isDirectory()) return;
180
181 List<File> allJavaFiles = new ArrayList<>();
182 addJavaFilesRecursive(srcDir, allJavaFiles);
183
184 for (File f : allJavaFiles) {
185 String className = f.getName().replace(".java", "");
186 try {
187 String content = new String(Files.readAllBytes(f.toPath()));
188 boolean found = false;
189 for (SpriteData sd : project.sprites) {
190 if (sd.name.equals(className)) {
191 sd.sourceCode = content;
192 found = true;
193 break;
194 }
195 }
196 if (!found) {
197 SpriteData sd = new SpriteData(className);
198 sd.sourceCode = content;
199 project.sprites.add(sd);
200 }
201 } catch (IOException e) {
202 e.printStackTrace();
203 }
204 }
205 }
206
207 private void addJavaFilesRecursive(File dir, List<File> list) {
208 File[] children = dir.listFiles();
209 if (children == null) return;
210 for (File child : children) {
211 if (child.isDirectory()) {
212 addJavaFilesRecursive(child, list);
213 } else if (child.getName().endsWith(".java")) {
214 list.add(child);
215 }
216 }
217 }
218
219 private void refreshSpriteList() {
220 spriteListModel.clear();
221 for (SpriteData sd : project.sprites) {
222 spriteListModel.addElement(sd.name);
223 }
224 if (!project.sprites.isEmpty()) {
225 spriteList.setSelectedIndex(0);
226 SpriteData sd = project.sprites.get(0);
227 codeWorkspace.loadSpriteCode(sd);
228 soundPanel.refreshList(sd.sounds);
229 }
230 }
231
232 private String generateDefaultMainCode() {
233 return "package generated;\n" +
234 "import com.jscratch.Stage;\n" +
235 "import com.jscratch.Sprite;\n\n" +
236 "public class Main {\n" +
237 " public static void main(String[] args) {\n" +
238 " Stage stage = Stage.getInstance();\n" +
239 " // Auto-generated: add all sprites\n" +
240 " /* slot start [id:\"init\" tags:\"init\"] */\n" +
241 " \n" +
242 " /* slot end [id:\"init\"] */\n" +
243 " }\n" +
244 "}";
245 }
246
247 private void addSprite() {
248 String name = JOptionPane.showInputDialog(this, "Enter Sprite Name:", "Sprite" + (project.sprites.size() + 1));
249 if (name == null || name.trim().isEmpty()) return;
250 name = name.trim();
251
252 SpriteData sd = new SpriteData(name);
253 String template = "package generated;\n\n" +
254 "import com.jscratch.*;\n" +
255 "import java.awt.Color;\n" +
256 "import java.awt.event.KeyEvent;\n\n" +
257 "public class " + name + " extends Sprite {\n" +
258 " /* slot start [id:\"" + name + "_members\" tags:\"inClassNotMethod\"] */\n" +
259 " public " + name + "() {\n" +
260 " super(\"" + name + "\");\n" +
261 " \n" +
262 " // Example: addCostume(new Costume(\"costume1\", \"path/to/img.png\"));\n\n" +
263 " /* slot start [id:\"" + name + "_init\" tags:\"inInstanceMethod\"] */\n" +
264 " /* slot end [id:\"" + name + "_init\"] */\n" +
265 " }\n" +
266 " /* slot end [id:\"" + name + "_members\"] */\n" +
267 "}";
268 sd.sourceCode = template;
269 project.sprites.add(sd);
270 spriteListModel.addElement(name);
271 spriteList.setSelectedIndex(project.sprites.size() - 1);
272 }
273
274 private void renameSprite() {
275 int idx = spriteList.getSelectedIndex();
276 if (idx == -1) return;
277 SpriteData sd = project.sprites.get(idx);
278 if (sd.name.equals("Main")) {
279 JOptionPane.showMessageDialog(this, "Cannot rename Main sprite.");
280 return;
281 }
282
283 String newName = JOptionPane.showInputDialog(this, "Enter New Sprite Name:", sd.name);
284 if (newName == null || newName.trim().isEmpty() || newName.equals(sd.name)) return;
285 newName = newName.trim();
286
287 // Check for duplicates
288 for (SpriteData other : project.sprites) {
289 if (other.name.equals(newName)) {
290 JOptionPane.showMessageDialog(this, "A sprite with this name already exists.");
291 return;
292 }
293 }
294
295 String oldName = sd.name;
296 sd.name = newName;
297
298 // Update source code
299 sd.sourceCode = sd.sourceCode.replace(oldName, newName);
300
301 spriteListModel.set(idx, newName);
302 codeWorkspace.loadSpriteCode(sd);
303 autoCompile();
304 }
305
306 private void setupTabs() {
307 tabbedPane = new JTabbedPane();
309 costumePanel = new CostumeEditorPanel(new ArrayList<>());
310 soundPanel = new AssetPanel("Sound", new ArrayList<>());
311 mapMakerPanel = new MapMakerPanel(project.mapMaker);
313
314 tabbedPane.addTab("Code", codeWorkspace);
315 tabbedPane.addTab("Costumes", costumePanel);
316 tabbedPane.addTab("Sound", soundPanel);
317 tabbedPane.addTab("Map Maker", mapMakerPanel);
318 tabbedPane.addTab("Files", fileManagerPanel);
319 }
320
321 private void autoCompile() {
322 new Thread(() -> {
323 File srcBackup = createSrcBackup();
325 SwingUtilities.invokeLater(() -> {
326 codeWorkspace.setCompilationErrors(res.errors);
327 if (res.success) {
329 deleteRecursive(srcBackup);
330 } else {
331 restoreSrcBackup(srcBackup);
332 }
333 });
334 }).start();
335 }
336
337 private JMenuBar createMenuBar() {
338 JMenuBar mb = new JMenuBar();
339
340 JMenu fileMenu = new JMenu("File");
341 JMenuItem newItem = new JMenuItem("New Project");
342 newItem.addActionListener(e -> newProject());
343 JMenuItem openItem = new JMenuItem("Open Project...");
344 openItem.addActionListener(e -> openProject());
345 JMenuItem saveItem = new JMenuItem("Save Project");
346 saveItem.addActionListener(e -> saveProject());
347 JMenuItem saveAsItem = new JMenuItem("Save Project As...");
348 saveAsItem.addActionListener(e -> saveProjectAs());
349
350 fileMenu.add(newItem);
351 fileMenu.addSeparator();
352 fileMenu.add(openItem);
353 fileMenu.add(saveItem);
354 fileMenu.add(saveAsItem);
355
356 JMenu runMenu = new JMenu("Run");
357 JMenuItem runItem = new JMenuItem("Run Project");
358 runItem.addActionListener(e -> runAndRunProject());
359 runMenu.add(runItem);
360
361 mb.add(fileMenu);
362 mb.add(runMenu);
363 return mb;
364 }
365
366 private void newProject() {
367 project = new Project();
370 currentProjectFile = null;
371 clearConsole();
372 logToConsole("New Project Created.");
373 }
374
375 private void openProject() {
376 JFileChooser chooser = new JFileChooser(WorkspaceManager.getRoot());
377 if (chooser.showOpenDialog(this) == JFileChooser.APPROVE_OPTION) {
378 File file = chooser.getSelectedFile();
379 try {
380 unzipProject(file);
381 currentProjectFile = file;
384 mapMakerPanel.setMapData(project.mapMaker);
385 clearConsole();
386 logToConsole("Project Loaded: " + file.getName());
387 } catch (Exception e) {
388 e.printStackTrace();
389 JOptionPane.showMessageDialog(this, "Error loading project: " + e.getMessage());
390 }
391 }
392 }
393
394 private void unzipProject(File zipFile) throws IOException {
395 File assetsDir = WorkspaceManager.getAssetsDir();
396 if (assetsDir.exists()) {
397 File[] files = assetsDir.listFiles();
398 if (files != null) for (File f : files) deleteRecursive(f);
399 } else {
400 assetsDir.mkdirs();
401 }
402
403 try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {
404 ZipEntry entry;
405 while ((entry = zis.getNextEntry()) != null) {
406 if (entry.getName().equals("project.json")) {
407 InputStreamReader reader = new InputStreamReader(zis);
408 project = new Gson().fromJson(reader, Project.class);
409 } else if (entry.getName().startsWith("assets/")) {
410 String filename = entry.getName().substring(7);
411 if (filename.isEmpty()) {
412 zis.closeEntry();
413 continue;
414 }
415 File outFile = new File(assetsDir, filename);
416 Files.copy(zis, outFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
417 }
418 zis.closeEntry();
419 }
420 }
421 }
422
423 private void saveProject() {
424 if (currentProjectFile == null) {
426 } else {
428 }
429 }
430
431 private void saveProjectAs() {
432 JFileChooser chooser = new JFileChooser(WorkspaceManager.getRoot());
433 if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) {
434 File file = chooser.getSelectedFile();
435 if (!file.getName().endsWith(".jsz")) {
436 file = new File(file.getAbsolutePath() + ".jsz");
437 }
438 saveProjectToFile(file);
439 currentProjectFile = file;
440 }
441 }
442
443 private void saveProjectToFile(File file) {
444 codeWorkspace.saveCurrentCode();
445 try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(file))) {
446 zos.putNextEntry(new ZipEntry("project.json"));
447 String json = new GsonBuilder().setPrettyPrinting().create().toJson(project);
448 zos.write(json.getBytes());
449 zos.closeEntry();
450
451 File assetsDir = WorkspaceManager.getAssetsDir();
452 if (assetsDir.exists()) {
453 File[] files = assetsDir.listFiles();
454 if (files != null) {
455 for (File f : files) {
456 zos.putNextEntry(new ZipEntry("assets/" + f.getName()));
457 Files.copy(f.toPath(), zos);
458 zos.closeEntry();
459 }
460 }
461 }
462 logToConsole("Project Saved: " + file.getName());
463 } catch (Exception e) {
464 e.printStackTrace();
465 JOptionPane.showMessageDialog(this, "Error saving project: " + e.getMessage());
466 }
467 }
468
469 private void runAndRunProject() {
470 new Thread(() -> {
471 File srcBackup = createSrcBackup();
473 SwingUtilities.invokeLater(() -> {
474 codeWorkspace.setCompilationErrors(res.errors);
475 if (res.success) {
476 logToConsole("--- Build Successful ---");
479 deleteRecursive(srcBackup);
480 } else {
481 logToConsole("--- Build Failed ---");
482 logToConsole(res.output);
483 restoreSrcBackup(srcBackup);
484 }
485 });
486 }).start();
487 }
488
490 codeWorkspace.saveCurrentCode();
491 File binDir = WorkspaceManager.getBinDir();
492 File srcDir = WorkspaceManager.getSrcDir();
493 File generatedSrcDir = new File(srcDir, "generated");
494
495 // 1. Clear ONLY bin
496 if (binDir.exists()) {
497 File[] files = binDir.listFiles();
498 if (files != null) for (File f : files) deleteRecursive(f);
499 }
500 binDir.mkdirs();
501
502 // 2. Targeted cleanup of 'generated' package source (orphaned files)
503 // This removes .java files that are no longer in our project sprite list
504 if (generatedSrcDir.exists()) {
505 File[] files = generatedSrcDir.listFiles((dir, name) -> name.endsWith(".java"));
506 if (files != null) {
507 for (File f : files) {
508 String className = f.getName().replace(".java", "");
509 boolean existsInProject = false;
510 for (SpriteData sd : project.sprites) {
511 if (sd.name.equals(className)) { existsInProject = true; break; }
512 }
513 if (!existsInProject) f.delete();
514 }
515 }
516 } else {
517 generatedSrcDir.mkdirs();
518 }
519
520 int spriteCount = project.sprites.size();
521 String[] sourceCode = new String[spriteCount];
522 String[] classNames = new String[spriteCount];
523 for (int i = 0; i < spriteCount; i++) {
524 SpriteData sprite = project.sprites.get(i);
525 sourceCode[i] = sprite.sourceCode;
526 classNames[i] = "generated." + sprite.name;
527 }
528
529 return CompilerService.compile(sourceCode, classNames, srcDir, binDir);
530 }
531
532 private File createSrcBackup() {
533 File srcDir = WorkspaceManager.getSrcDir();
534 File backupDir = new File(srcDir.getParentFile(), "src_backup_" + System.currentTimeMillis());
535 try {
536 copyRecursive(srcDir, backupDir);
537 } catch (IOException e) { e.printStackTrace(); }
538 return backupDir;
539 }
540
541 private void restoreSrcBackup(File backupDir) {
542 File srcDir = WorkspaceManager.getSrcDir();
543 deleteRecursive(srcDir);
544 try {
545 copyRecursive(backupDir, srcDir);
546 } catch (IOException e) { e.printStackTrace(); }
547 deleteRecursive(backupDir);
548 }
549
550 private void copyRecursive(File source, File dest) throws IOException {
551 if (source.isDirectory()) {
552 if (!dest.exists()) dest.mkdirs();
553 String[] children = source.list();
554 if (children != null) {
555 for (String child : children) {
556 copyRecursive(new File(source, child), new File(dest, child));
557 }
558 }
559 } else {
560 Files.copy(source.toPath(), dest.toPath(), StandardCopyOption.REPLACE_EXISTING);
561 }
562 }
563
564 private void deleteRecursive(File f) {
565 if (f == null || !f.exists()) return;
566 if (f.isDirectory()) {
567 File[] files = f.listFiles();
568 if (files != null) for (File child : files) deleteRecursive(child);
569 }
570 f.delete();
571 }
572
573 public static void main(String[] args) {
574 try {
575 // CHANGE THIS LINE: Use CrossPlatform instead of System
576 UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
577 } catch (Exception e) {
578 // Fallback if needed
579 }
580 SwingUtilities.invokeLater(() -> new ScratchEditor().setVisible(true));
581 }
582}
void unzipProject(File zipFile)
void copyRecursive(File source, File dest)
void restoreSrcBackup(File backupDir)
CompilationResult runCompile()
CostumeEditorPanel costumePanel
DefaultListModel< String > spriteListModel
void addJavaFilesRecursive(File dir, List< File > list)
void saveProjectToFile(File file)
void logToConsole(String message)
static void main(String[] args)
FileManagerPanel fileManagerPanel
void appendToConsole(String text)
static CompilationResult compile(String[] sourceCode, String className[], File srcdir, File bindir)
static void mount(File buildDir)
static void run(File buildDir, String mainClassName)