Repository navigation
Expand file tree
/
Copy pathbackground.js
More file actions
214 lines (199 loc) · 9.02 KB
/
Copy pathbackground.js
File metadata and controls
214 lines (199 loc) · 9.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
const {app, BrowserWindow, Menu, MenuItem, dialog, ipcMain, shell} = require('electron/main');
const path = require('path');
const fs = require('fs');
const os = require('os');
const {execFile} = require('child_process');
// Electron doesn't show a right-click context menu on its own - build one
// per-click from the params Electron already computes (isEditable/selection
// text/editFlags) rather than a fixed one, so it only offers actions that
// are actually valid for whatever the user right-clicked.
const showContextMenu = (win, params) => {
const menu = new Menu();
if (params.isEditable) {
menu.append(new MenuItem({role: 'cut', enabled: params.editFlags.canCut}));
menu.append(new MenuItem({role: 'copy', enabled: params.editFlags.canCopy}));
menu.append(new MenuItem({role: 'paste', enabled: params.editFlags.canPaste}));
menu.append(new MenuItem({role: 'selectAll'}));
} else if (params.selectionText) {
menu.append(new MenuItem({role: 'copy'}));
}
if (menu.items.length) menu.popup({window: win});
};
const createWindow = () => {
const win = new BrowserWindow({
width: 800,
height: 600,
// The window/taskbar icon on Linux (Windows and macOS take theirs from
// the packaged app).
icon: path.join(__dirname, 'build', 'icons', '512x512.png'),
webPreferences: {
preload: path.join(__dirname, 'preload.js'),
},
});
win.removeMenu();
win.setMenuBarVisibility(false);
// A link or window.open() to a web page (the About tab's links, for one)
// opens in the system's browser rather than a new Electron window.
win.webContents.setWindowOpenHandler(({url}) => {
if (/^https?:\/\//i.test(url)) shell.openExternal(url);
return {action: 'deny'};
});
win.loadFile('dist/index.html');
win.webContents.on('context-menu', (event, params) => showContextMenu(win, params));
};
// "Browse..." on the Options tab's Stella field - a plain native file picker,
// filtered to executables on Windows (Stella.exe) and left unfiltered
// elsewhere (macOS/Linux Stella builds aren't a single, predictable
// extension the way Windows .exe is - an app bundle on macOS is itself a
// directory, and a Linux build might be a plain extensionless binary or an
// AppImage). Returns null (not '') when the user cancels, so the renderer
// can tell "cancelled" apart from "cleared the field" without a separate
// flag.
ipcMain.handle('stella:pick-path', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
const filters = process.platform === 'win32' ?
[{name: 'Programs', extensions: ['exe']}, {name: 'All Files', extensions: ['*']}] :
[{name: 'All Files', extensions: ['*']}];
const result = await dialog.showOpenDialog(win, {
title: 'Locate the Stella executable',
properties: ['openFile'],
filters,
});
if (result.canceled || !result.filePaths.length) return null;
return result.filePaths[0];
});
// "Test in Stella" (App.vue) - writes the just-compiled ROM bytes to a temp
// file (Stella, like every standalone 2600 emulator, only takes a ROM as a
// file path argument, not piped/inline data), then launches Stella pointed
// at it with its own default settings (no extra CLI flags - "automatically
// open the ROM with default settings" was the explicit request, and Stella
// already remembers whatever the user last configured in its own UI across
// runs). One fixed temp filename (not a fresh one per launch) so repeated
// "Test in Stella" clicks don't leave an ever-growing pile of stale ROMs
// behind in the OS temp directory. execFile (not exec) - the ROM path is
// passed as its own argv entry, never interpolated into a shell string, so
// a path containing spaces or shell-meaningful characters can't break the
// invocation or be interpreted as shell syntax. Detached and unref'd so
// Stella keeps running (and this app doesn't wait on it) after this handler
// returns - matches how a real "launch an external app" action should
// behave, not a blocking subprocess call.
ipcMain.handle('stella:launch', async (event, {stellaPath, romBytes}) => {
if (!stellaPath) return {success: false, error: 'No Stella path configured.'};
const romPath = path.join(os.tmpdir(), 'vcs-game-maker-preview.bin');
try {
fs.writeFileSync(romPath, Buffer.from(romBytes));
} catch (err) {
return {success: false, error: `Couldn't write the ROM to a temp file: ${err.message}`};
}
return new Promise((resolve) => {
const child = execFile(stellaPath, [romPath], (err) => {
// ENOENT here specifically means the STELLA PATH itself doesn't exist/
// isn't executable (execFile's own spawn failure, not anything Stella
// did after starting) - the one failure mode worth a specific message
// for, since it's directly actionable ("check the path on the Options
// tab"). Any other error surfaces via its own message rather than a
// second hardcoded string, since there's no other single common case
// worth special-casing here.
if (!err) return;
if (err.code === 'ENOENT') {
resolve({success: false, error: `No executable found at "${stellaPath}".`});
} else {
resolve({success: false, error: err.message});
}
});
child.unref();
// Resolve success immediately once the process has actually spawned,
// rather than waiting for it to exit (Stella is a long-running GUI app -
// waiting for exit would mean the button just spins until the user
// closes Stella again).
child.once('spawn', () => resolve({success: true}));
});
});
// Project.vue's own "Save"/"Save As"/"Open Project" - the renderer's
// SUPPORTS_FILE_SYSTEM_ACCESS check (window.showSaveFilePicker/
// showOpenFilePicker) is always false in this Electron build (that API
// isn't exposed to Electron renderers the way it is in Chrome), so without
// these, "Save" silently fell all the way back to "Save As..." behavior
// (a picker on every click) - a real reported bug, not the intended
// browsers-only fallback that check's own comment describes. These give
// the desktop build its own equivalent: a real native save/open dialog
// backed by Node's fs, with the resulting absolute path handed back to the
// renderer to remember for later silent "Save" calls (see
// utils/file-handle-storage.js's own persistActiveFilePath).
ipcMain.handle('project:save-as', async (event, {content, suggestedName}) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showSaveDialog(win, {
title: 'Save VCS Game Maker Project',
defaultPath: suggestedName,
filters: [{name: 'VCS Game Maker Project', extensions: ['vcsgm']}],
});
if (result.canceled || !result.filePath) return null;
fs.writeFileSync(result.filePath, content, 'utf8');
return {path: result.filePath, name: path.basename(result.filePath)};
});
ipcMain.handle('project:save', async (event, {filePath, content}) => {
try {
fs.writeFileSync(filePath, content, 'utf8');
return true;
} catch (err) {
console.error('Error while saving project file', err);
return false;
}
});
ipcMain.handle('project:open', async (event) => {
const win = BrowserWindow.fromWebContents(event.sender);
const result = await dialog.showOpenDialog(win, {
title: 'Open VCS Game Maker Project',
properties: ['openFile'],
filters: [{name: 'VCS Game Maker Project', extensions: ['vcsgm']}],
});
if (result.canceled || !result.filePaths.length) return null;
const filePath = result.filePaths[0];
const content = fs.readFileSync(filePath, 'utf8');
return {path: filePath, name: path.basename(filePath), content};
});
// Used to double-check a path restored from a previous session (see
// utils/file-handle-storage.js's own loadPersistedFilePath) still points
// at a real file before trusting it as a silent "Save" target - the file
// may have been moved/deleted since, the Electron-side equivalent of the
// browser path's own queryPermission() === 'denied' check.
ipcMain.handle('project:path-exists', async (event, filePath) => {
try {
return fs.existsSync(filePath);
} catch (err) {
return false;
}
});
// The app has no menu bar. Windows and Linux get none at all (their text fields handle Ctrl+C, Ctrl+V and the other
// editing shortcuts without one). macOS always has a system menu bar and takes its Cmd+C, Cmd+V and Cmd+A from an Edit
// menu, so there it is kept but hidden (visible: false hides it from the menu bar while its accelerators stay
// registered, since Electron only stops firing an item's accelerator when the item is removed).
if (process.platform === 'darwin') {
Menu.setApplicationMenu(Menu.buildFromTemplate([
{
label: 'Edit',
visible: false,
submenu: [
{role: 'cut'},
{role: 'copy'},
{role: 'paste'},
{role: 'selectAll'},
],
},
]));
} else {
Menu.setApplicationMenu(null);
}
app.whenReady().then(() => {
createWindow();
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});