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