Skip to content

Commit 2241b4c

Browse files
committed
#587 Can now open directory based, can't yet integreate to local files
1 parent 4e70c01 commit 2241b4c

11 files changed

Lines changed: 176 additions & 36 deletions

File tree

web-designer/tcmenu-code-generator/src/main/java/com/thecoderscorner/menu/editorui/generator/core/CoreCodeGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ private void processLocale(Path srcDir, boolean generated) throws IOException {
227227
var selFile = Paths.get(toSourceFile(srcDir, "_langSelect" + ".h", generated));
228228
Files.writeString(selFile, langSelectText.toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
229229
feedbackLogger.fileModificiation(GeneratedFile.always(selFile, langSelectText.toString()));
230-
feedbackLogger.info("Wrote out the language selector - " + selFile);
230+
feedbackLogger.info("Wrote out the language selector - " + selFile.getFileName());
231231
feedbackLogger.debug("Finished locale processing");
232232
}
233233
finally {

web-designer/tcmenu-code-generator/src/main/java/com/thecoderscorner/menu/editorui/generator/mbed/MbedSketchFileAdjuster.java

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,4 @@ protected void taskManagerIsMissing(ArrayList<String> lines) {
4747
logger.error("SKETCH ERROR: you are missing taskManager.runLoop(); in your sketch file");
4848
logger.error("SKETCH ERROR: this should be added in a loop within the main() method");
4949
}
50-
51-
protected Path getCppMainPath(Path path) {
52-
return path.resolve("tcmenu_main.cpp");
53-
}
54-
5550
}

web-designer/tcmenu-code-generator/src/test/java/com/thecoderscorner/menu/editorui/generator/mbed/MbedGeneratorTest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ public void testMbedConversion() throws IOException {
135135

136136
var cppGenerated = new String(Files.readAllBytes(generatedDir.resolve(projectDir.getFileName() + "_menu.cpp")));
137137
var hGenerated = new String(Files.readAllBytes(generatedDir.resolve(projectDir.getFileName() + "_menu.h")));
138-
var inoFile = new String(Files.readAllBytes(sourceDir.resolve("tcmenu_main.cpp")));
138+
var inoFile = new String(Files.readAllBytes(sourceDir.resolve("project_main.cpp")));
139139
var pluginGeneratedH = new String(Files.readAllBytes(generatedDir.resolve("source.h")));
140140
var pluginGeneratedCPP = new String(Files.readAllBytes(generatedDir.resolve("source.cpp")));
141141
var pluginGeneratedTransport = new String(Files.readAllBytes(generatedDir.resolve("MySpecialTransport.h")));

web-designer/tcmenu-web-generator/src/main/java/com/thecoderscorner/menu/web/controller/GenerateCodeController.java

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,9 @@ public GenerationResponse generateCode(@RequestBody String requestData, HttpServ
190190
}
191191
}
192192

193-
private @NonNull GenerationResponse doCodeGeneration(GenerateCodeRequest request, MenuTreeWithCodeOptions menuWithOptions, List<CreatorProperty> frontEndProperties, List<GeneratedFile> projectFiles, long millisStart, HttpServletRequest servletRequest) {
193+
private @NonNull GenerationResponse doCodeGeneration(GenerateCodeRequest request, MenuTreeWithCodeOptions menuWithOptions,
194+
List<CreatorProperty> frontEndProperties, List<GeneratedFile> projectFiles,
195+
long millisStart, HttpServletRequest servletRequest) {
194196
var uuid = request.getProject().getCodeOptions().getApplicationUUID();
195197
var existingOpt = codeBuildCache.asMap().values().stream()
196198
.filter(c -> c.getProjectUuid().equals(uuid))
@@ -233,7 +235,7 @@ public GenerationResponse generateCode(@RequestBody String requestData, HttpServ
233235
var plugins = allPlugins(menuWithOptions.getOptions());
234236
var combinedSetProperties = deepCopyAndPrepareProps(plugins, frontEndProperties);
235237

236-
var localeHandler = prepareLocaleHandler(projectFiles, srcPath);
238+
var localeHandler = prepareLocaleHandler(projectFiles, srcPath, logger);
237239
copyInoFileIfPresent(projectFiles, srcPath, menuWithOptions.getOptions().isUseCppMain());
238240

239241
var codeOptions = new CodeGeneratorOptionsBuilder().withExisting(menuWithOptions.getOptions())
@@ -297,7 +299,7 @@ private void getRidOfTheLot(Path tempDir) {
297299
}
298300

299301
private void copyInoFileIfPresent(List<GeneratedFile> allFiles, Path projectPath, boolean cppProject) {
300-
var inoFileOpt = allFiles.stream().filter(f -> f.getFileName().equals("PROJECT_MAIN"))
302+
var inoFileOpt = allFiles.stream().filter(f -> f.getFileName().endsWith(".ino") || f.getFileName().endsWith("_main.cpp"))
301303
.findFirst();
302304
if(inoFileOpt.isPresent()) {
303305
var inoFile = inoFileOpt.get();
@@ -317,7 +319,7 @@ private void copyInoFileIfPresent(List<GeneratedFile> allFiles, Path projectPath
317319
}
318320
}
319321

320-
private static LocaleMappingHandler prepareLocaleHandler(List<GeneratedFile> allFiles, Path rootDir) throws IOException {
322+
private static LocaleMappingHandler prepareLocaleHandler(List<GeneratedFile> allFiles, Path rootDir,ControllerFeedbackLogger logger) throws IOException {
321323
var localeHandler = LocaleMappingHandler.NOOP_IMPLEMENTATION;
322324
var localeFiles = allFiles.stream().filter(f -> f.getFileName().endsWith(".properties")).toList();
323325
if (!ObjectUtils.isEmpty(localeFiles)) {
@@ -326,16 +328,22 @@ private static LocaleMappingHandler prepareLocaleHandler(List<GeneratedFile> all
326328
Files.createDirectories(i18nDir);
327329
for (var file : localeFiles) {
328330
var fileName = file.getFileName();
331+
// select files that start with i18n/ and normalise
332+
if (!fileName.startsWith("i18n/")) continue;
333+
fileName = fileName.substring(5);
334+
335+
// ensure nothing untoward in the path. Only latin characters allowed in path at the moment
329336
if (fileName.contains("/") || fileName.contains("\\") || fileName.contains("..")) {
330337
throw new IllegalArgumentException("Invalid filename - directory traversal not allowed");
331338
}
332339
var sanitizedName = fileName.replaceAll("[^a-zA-Z0-9._-]", "_");
333340
log.info("Locale file {} copied into i18n", sanitizedName);
334341
var filePath = i18nDir.resolve(sanitizedName).normalize();
335-
if(!filePath.startsWith(i18nDir)) {
342+
if (!filePath.startsWith(i18nDir)) {
336343
throw new IllegalArgumentException("Invalid filename - resolved path outside i18n directory");
337344
}
338345
Files.writeString(filePath, file.getContent());
346+
logger.fileModificiation(GeneratedFile.always(filePath, file.getContent()));
339347
}
340348
localeHandler = new PropertiesLocaleEnabledHandler(new SafeBundleLoader(i18nDir, MENU_PROJECT_LANG_FILENAME));
341349
}

web-designer/tcmenugen/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web-designer/tcmenugen/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"react-router-dom": "^7.17.0",
2222
"react-scripts": "5.0.1",
2323
"typescript": "^4.9.5",
24-
"web-vitals": "^2.1.4"
24+
"web-vitals": "^2.1.4",
25+
"idb-keyval": "^6.2.5"
2526
},
2627
"scripts": {
2728
"start": "react-scripts start",

web-designer/tcmenugen/src/App.tsx

Lines changed: 82 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,44 @@
11
import React, {useEffect, useState} from 'react';
22
import logo from './img/logo192.png';
3+
import tcIcon from './img/logo192.png';
34
import './App.css';
4-
import { NavLink, Link, Routes, Route } from 'react-router-dom';
5+
import {Link, NavLink, Route, Routes} from 'react-router-dom';
56
import {MenuTreeWithCodeOptions, RoundTripMode} from "./domain/ProjectStruct";
67
import {getActiveProfile} from "./generator/TcCodeGeneration";
78
import {parseEmfJsonToProject, projectToPersistedJson} from "./domain/PersistedMenu";
8-
import { StartNewProject } from './StartNewProject';
9+
import {StartNewProject} from './StartNewProject';
910
import {TcMenuEditor} from "./menuedit/TcMenuEditor";
1011
import {GenerateCodeView} from "./generator/GenerateCodeView";
1112
import embedIcon from "./img/embedControlLogo300.png"
12-
import tcIcon from "./img/logo192.png"
1313
import {IoExpanderComponent} from "./generator/IoExpanderComponent";
1414
import ReleaseNotes from "./releaseNotes";
15+
import {get, set} from 'idb-keyval';
16+
import fontEdIcon from './img/font-editor-example.jpg'
1517

1618
const TC_MENU_STORAGE_KEY = "tcMenuTurboProject";
1719
const TC_MENU_POLICY_KEY = "tcMenuTurboPolicyAccepted";
1820
let currentlyOpenProject: MenuTreeWithCodeOptions|null = null;
21+
let globalDirectoryHandle: FileSystemDirectoryHandle|null = null;
1922
let projectListeners: ((proj: MenuTreeWithCodeOptions | null) => void)[] = [];
2023
let saveTimer: any = null;
2124

2225
export function getCurrentlyOpenProject(): MenuTreeWithCodeOptions|null {
2326
return currentlyOpenProject;
2427
}
2528

29+
export function getDirectoryHandle(): FileSystemDirectoryHandle|null {
30+
return globalDirectoryHandle;
31+
}
32+
33+
export const findFileWithExtension = async (directoryHandle: any, ext1: string, ext2: string = "undef"): Promise<any | null> => {
34+
for await (const entry of directoryHandle.values()) {
35+
if (entry.kind === 'file' && (entry.name.endsWith('.' + ext1) || entry.name.endsWith('.' + ext2))) {
36+
return entry;
37+
}
38+
}
39+
return null;
40+
};
41+
2642
export function saveProjectToLocalStorage(proj: MenuTreeWithCodeOptions | null) {
2743
if (saveTimer) {
2844
clearTimeout(saveTimer);
@@ -50,7 +66,7 @@ export function saveProjectToLocalStorage(proj: MenuTreeWithCodeOptions | null)
5066
}
5167
}
5268

53-
export function setCurrentlyOpenProject(proj: MenuTreeWithCodeOptions | null) {
69+
export function setCurrentlyOpenProject(proj: MenuTreeWithCodeOptions | null, directoryHandle: any = null) {
5470
currentlyOpenProject = proj;
5571
if (proj) {
5672
proj.menuTree.setTreeStructureChanged((tree, id) => {
@@ -74,12 +90,55 @@ export function setCurrentlyOpenProject(proj: MenuTreeWithCodeOptions | null) {
7490
}
7591
});
7692
}
93+
if(directoryHandle) {
94+
saveProjectToLocalStorage(currentlyOpenProject);
95+
globalDirectoryHandle = directoryHandle;
96+
set('last_project_dir', directoryHandle)
97+
.catch(() => {
98+
alert("Failed to save last project directory, project will not be able to save");
99+
});
100+
}
77101
} else {
78102
localStorage.removeItem(TC_MENU_STORAGE_KEY);
79103
}
80104
projectListeners.forEach(l => l(proj));
81105
}
82106

107+
async function rehydrateProjectDirectory(proj: MenuTreeWithCodeOptions) {
108+
const savedHandle = await get('last_project_dir');
109+
110+
if (savedHandle) {
111+
// 1. Check if we still have readwrite permissions (usually "prompt" or "denied" after tab close)
112+
let perm = await savedHandle.queryPermission({ mode: 'readwrite' });
113+
114+
console.log(`Permission status: ${perm}`);
115+
116+
if (perm !== 'granted') {
117+
console.log(`Re-request perms`);
118+
// 2. Trigger a simple browser pop-up asking to restore access to that specific folder
119+
try {
120+
perm = await savedHandle.requestPermission({ mode: 'readwrite' });
121+
} catch (e) {
122+
console.error("Failed to request permission", e);
123+
perm = 'denied';
124+
}
125+
}
126+
127+
globalDirectoryHandle = savedHandle;
128+
129+
console.log(`Permission status II: ${perm}`);
130+
131+
if (perm === 'granted') {
132+
console.log(`Successfully rehydrated folder: ${savedHandle.name}`);
133+
setCurrentlyOpenProject(proj, savedHandle);
134+
return savedHandle; // You are fully back in business without the picker!
135+
} else {
136+
console.log(`Failed to rehydrate folder: ${savedHandle.name}`);
137+
}
138+
}
139+
return null; // Fallback to recommend closing project
140+
}
141+
83142
export function useCurrentlyOpenProject() {
84143
const [project, setProject] = React.useState<MenuTreeWithCodeOptions | null>(() => {
85144
if (currentlyOpenProject) return currentlyOpenProject;
@@ -89,6 +148,12 @@ export function useCurrentlyOpenProject() {
89148
const parsed = JSON.parse(saved);
90149
const restored = parseEmfJsonToProject(parsed.json, parsed.mode as RoundTripMode);
91150
setCurrentlyOpenProject(restored);
151+
if(restored.roundTripMode === RoundTripMode.DIRECTORY_IN_BROWSER) {
152+
rehydrateProjectDirectory(restored)
153+
.catch(() => {
154+
alert("Failed to restore last project directory, project will not be able to save");
155+
});
156+
}
92157
return restored;
93158
} catch (e) {
94159
console.error("Failed to restore project from localStorage", e);
@@ -214,8 +279,18 @@ export const MainRoutes = () => {
214279
<Route path="/io-expanders" element={<IoExpanderComponent />} />
215280
<Route path="/release-notes" element={<ReleaseNotes />} />
216281
<Route path="/bitmap-generator" element={<div>
217-
<h1>Bitmap Generator</h1>
218-
<p>This will be ported from the Java version shortly, in the mean time continue to use the java version for this function.</p>
282+
<h1>Font and Bitmap Editor</h1>
283+
<p>You can edit fonts, title widgets and bitmaps using the new Font Bmp Editor application.</p>
284+
<p>The plan is to make the app available on the Windows store and MacOS app store, but even right
285+
now there are nightly builds for all platforms.</p>
286+
<img src={fontEdIcon} alt="Font and Bitmap Editor application screen"/>
287+
<h2>How to get the editor</h2>
288+
<ul>
289+
<li><a href="https://github.com/TcMenu/tcMenu/actions/workflows/build_windows.yml">Windows Nightly</a></li>
290+
<li><a href="https://github.com/TcMenu/tcMenu/actions/workflows/build_mac.yml">MacOS Nightly</a></li>
291+
<li><a href="https://github.com/TcMenu/tcMenu/actions/workflows/build_linux.yml">Linux Nightly</a></li>
292+
<li><a href="https://github.com/TcMenu/tcMenu/">From source</a></li>
293+
</ul>
219294
</div>} />
220295
</Routes>
221296
);
@@ -253,7 +328,7 @@ function App() {
253328
<li><NavLink to="/menu-edit">Menu Edit</NavLink></li>
254329
{project && <li><NavLink to="/generate-code">Generate Code</NavLink></li>}
255330
{project && <li><NavLink to="/io-expanders">Io Expanders</NavLink></li>}
256-
<li><NavLink to="/bitmap-generator">Bitmaps/Widgets/Fonts</NavLink></li>
331+
<li><NavLink to="/bitmap-generator">Font Bmp Editor</NavLink></li>
257332
</ul>
258333
</nav>
259334
</div>

web-designer/tcmenugen/src/generator/GenerateCodeView.tsx

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {useCurrentlyOpenProject} from "../App";
1+
import {findFileWithExtension, getDirectoryHandle, useCurrentlyOpenProject} from "../App";
22
import React, {useEffect, useState} from "react";
33
import {ALL_PLATFORMS} from "../domain/Platforms";
44
import {AuthenticationDefinition, EepromDefinition} from "./EepromAndAuthSupport";
@@ -13,16 +13,16 @@ import {
1313
GenerationResponse,
1414
getDefaultPlugin,
1515
getPluginsByIds,
16+
NO_REMOTE_ID,
1617
PublishableCodePluginItem,
1718
runGenerateCode,
1819
searchPlugins,
1920
ThemeMode,
2021
userNeedsChooseDisplay,
2122
userNeedsChooseInput,
22-
userNeedsChooseTheme,
23-
NO_REMOTE_ID
23+
userNeedsChooseTheme
2424
} from "./TcCodeGeneration";
25-
import {CreatorProperty, SubSystem} from "../domain/ProjectStruct";
25+
import {CreatorProperty, RoundTripMode, SubSystem} from "../domain/ProjectStruct";
2626
import {SelectPluginDialog} from "./SelectPluginDialog";
2727

2828
export function GenerateCodeView() {
@@ -321,13 +321,36 @@ export function GenerateCodeView() {
321321
setHash(h => h + 1);
322322
}
323323

324-
function requiredFilesForBuild(): GeneratedFile[] {
324+
async function requiredFilesForBuild(): Promise<GeneratedFile[]> {
325325
if(!project) return [];
326326

327-
return [];
327+
let ret = Array<GeneratedFile>();
328+
329+
if(project.roundTripMode === RoundTripMode.DIRECTORY_IN_BROWSER) {
330+
const dir = getDirectoryHandle();
331+
if(dir == null) throw new Error("No directory handle");
332+
const inoFile = await findFileWithExtension(dir, 'ino', 'cpp');
333+
if(inoFile !== null) {
334+
const inoActFile = await inoFile.getFile();
335+
ret.push({fileName: inoFile.name, content: await inoActFile.text(), alwaysOverwrite: true});
336+
}
337+
try {
338+
let i18nDir = await dir.getDirectoryHandle('i18n');
339+
for await (const entry of (i18nDir as any).values()) {
340+
if(entry.kind === 'file' && entry.name.endsWith('.properties')) {
341+
const propActFile = await entry.getFile();
342+
ret.push({fileName: "i18n/" + entry.name, content: await propActFile.text(), alwaysOverwrite: true});
343+
}
344+
}
345+
} catch(e: any) {
346+
console.log("i18n directory not found, proceeding without " + e.toString());
347+
}
348+
}
349+
350+
return ret;
328351
}
329352

330-
function onCodeGenerate() {
353+
async function onCodeGenerate() {
331354
if(!project) {
332355
console.error("Project not loaded");
333356
return;
@@ -359,7 +382,7 @@ export function GenerateCodeView() {
359382

360383
if(!project) return;
361384

362-
runGenerateCode(project, requiredFilesForBuild()).then(response => {
385+
runGenerateCode(project, await requiredFilesForBuild()).then(response => {
363386
console.log("Success:", response.successful);
364387
console.log("Generated files:", response.generatedFiles ? response.generatedFiles.length : 0);
365388
if (response.generatedFiles) {

web-designer/tcmenugen/src/generator/TcCodeGeneration.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ function jsonifyGenerationRequest(project: MenuTreeWithCodeOptions, requiredFile
147147
subsystem: SubSystem[p.subsystem],
148148
latestValue: p.latestValue
149149
})),
150-
requiredFiles: requiredFiles.map(f => ({fileName: f.fileName, alwaysOverwrite: f.alwaysOverwrite}))
150+
requiredFiles: requiredFiles.map(f => ({fileName: f.fileName, content: f.content, alwaysOverwrite: f.alwaysOverwrite}))
151151
});
152152
}
153153

33.2 KB
Loading

0 commit comments

Comments
 (0)