Merge pull request #3976 from Snarling/ScriptEditorResponsiveness

UI: FIX #3975, #3882 Script Editor more responsive on resize, and fix dirty file indicator
This commit is contained in:
hydroflame 2022-08-23 12:12:21 -03:00 committed by GitHub
commit 46f5640dcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 79 additions and 159 deletions

@ -90,6 +90,7 @@ class OpenScript {
hostname: string; hostname: string;
lastPosition: monaco.Position; lastPosition: monaco.Position;
model: ITextModel; model: ITextModel;
isTxt: boolean;
constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) { constructor(fileName: string, code: string, hostname: string, lastPosition: monaco.Position, model: ITextModel) {
this.fileName = fileName; this.fileName = fileName;
@ -97,10 +98,11 @@ class OpenScript {
this.hostname = hostname; this.hostname = hostname;
this.lastPosition = lastPosition; this.lastPosition = lastPosition;
this.model = model; this.model = model;
this.isTxt = fileName.endsWith(".txt");
} }
} }
let openScripts: OpenScript[] = []; const openScripts: OpenScript[] = [];
let currentScript: OpenScript | null = null; let currentScript: OpenScript | null = null;
// Called every time script editor is opened // Called every time script editor is opened
@ -134,33 +136,14 @@ export function Root(props: IProps): React.ReactElement {
const [ramInfoOpen, setRamInfoOpen] = useState(false); const [ramInfoOpen, setRamInfoOpen] = useState(false);
// Prevent Crash if script is open on deleted server // Prevent Crash if script is open on deleted server
openScripts = openScripts.filter((script) => { for (let i = openScripts.length - 1; i >= 0; i--) {
return GetServer(script.hostname) !== null; GetServer(openScripts[i].hostname) === null && openScripts.splice(i, 1);
}); }
if (currentScript && GetServer(currentScript.hostname) === null) { if (currentScript && GetServer(currentScript.hostname) === null) {
currentScript = openScripts[0]; currentScript = openScripts[0];
if (currentScript === undefined) currentScript = null; if (currentScript === undefined) currentScript = null;
} }
const [dimensions, setDimensions] = useState({
height: window.innerHeight,
width: window.innerWidth,
});
useEffect(() => {
const debouncedHandleResize = debounce(function handleResize() {
setDimensions({
height: window.innerHeight,
width: window.innerWidth,
});
}, 250);
window.addEventListener("resize", debouncedHandleResize);
return () => {
window.removeEventListener("resize", debouncedHandleResize);
};
}, []);
useEffect(() => { useEffect(() => {
if (currentScript !== null) { if (currentScript !== null) {
updateRAM(currentScript.code); updateRAM(currentScript.code);
@ -253,10 +236,7 @@ export function Root(props: IProps): React.ReactElement {
// Generates a new model for the script // Generates a new model for the script
function regenerateModel(script: OpenScript): void { function regenerateModel(script: OpenScript): void {
if (monacoRef.current !== null) { if (monacoRef.current !== null) {
script.model = monacoRef.current.editor.createModel( script.model = monacoRef.current.editor.createModel(script.code, script.isTxt ? "plaintext" : "javascript");
script.code,
script.fileName.endsWith(".txt") ? "plaintext" : "javascript",
);
} }
} }
@ -271,7 +251,7 @@ export function Root(props: IProps): React.ReactElement {
); );
async function updateRAM(newCode: string): Promise<void> { async function updateRAM(newCode: string): Promise<void> {
if (currentScript != null && currentScript.fileName.endsWith(".txt")) { if (currentScript != null && currentScript.isTxt) {
debouncedSetRAM("N/A", [["N/A", ""]]); debouncedSetRAM("N/A", [["N/A", ""]]);
return; return;
} }
@ -424,7 +404,7 @@ export function Root(props: IProps): React.ReactElement {
monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"), monacoRef.current.editor.createModel(code, filename.endsWith(".txt") ? "plaintext" : "javascript"),
); );
openScripts.push(newScript); openScripts.push(newScript);
currentScript = { ...newScript }; currentScript = newScript;
editorRef.current.setModel(newScript.model); editorRef.current.setModel(newScript.model);
updateRAM(newScript.code); updateRAM(newScript.code);
} }
@ -471,18 +451,8 @@ export function Root(props: IProps): React.ReactElement {
const newPos = editorRef.current.getPosition(); const newPos = editorRef.current.getPosition();
if (newPos === null) return; if (newPos === null) return;
if (currentScript !== null) { if (currentScript !== null) {
currentScript = { ...currentScript, code: newCode, lastPosition: newPos }; currentScript.code = newCode;
const curIndex = openScripts.findIndex( currentScript.lastPosition = newPos;
(script) =>
currentScript !== null &&
script.fileName === currentScript.fileName &&
script.hostname === currentScript.hostname,
);
const newArr = [...openScripts];
const tempScript = currentScript;
tempScript.code = newCode;
newArr[curIndex] = tempScript;
openScripts = [...newArr];
} }
try { try {
infLoop(newCode); infLoop(newCode);
@ -519,7 +489,7 @@ export function Root(props: IProps): React.ReactElement {
server.scripts, server.scripts,
); );
server.scripts.push(script); server.scripts.push(script);
} else if (scriptToSave.fileName.endsWith(".txt")) { } else if (scriptToSave.isTxt) {
for (let i = 0; i < server.textFiles.length; ++i) { for (let i = 0; i < server.textFiles.length; ++i) {
if (server.textFiles[i].fn === scriptToSave.fileName) { if (server.textFiles[i].fn === scriptToSave.fileName) {
server.textFiles[i].write(scriptToSave.code); server.textFiles[i].write(scriptToSave.code);
@ -593,6 +563,7 @@ export function Root(props: IProps): React.ReactElement {
server.scripts, server.scripts,
); );
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
rerender();
return; return;
} }
} }
@ -607,11 +578,12 @@ export function Root(props: IProps): React.ReactElement {
server.scripts, server.scripts,
); );
server.scripts.push(script); server.scripts.push(script);
} else if (currentScript.fileName.endsWith(".txt")) { } else if (currentScript.isTxt) {
for (let i = 0; i < server.textFiles.length; ++i) { for (let i = 0; i < server.textFiles.length; ++i) {
if (server.textFiles[i].fn === currentScript.fileName) { if (server.textFiles[i].fn === currentScript.fileName) {
server.textFiles[i].write(currentScript.code); server.textFiles[i].write(currentScript.code);
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
rerender();
return; return;
} }
} }
@ -623,26 +595,18 @@ export function Root(props: IProps): React.ReactElement {
} }
if (Settings.SaveGameOnFileSave) saveObject.saveGame(); if (Settings.SaveGameOnFileSave) saveObject.saveGame();
rerender();
} }
function reorder(list: Array<OpenScript>, startIndex: number, endIndex: number): OpenScript[] { function reorder(list: OpenScript[], startIndex: number, endIndex: number): void {
const result = Array.from(list); const [removed] = list.splice(startIndex, 1);
const [removed] = result.splice(startIndex, 1); list.splice(endIndex, 0, removed);
result.splice(endIndex, 0, removed);
return result;
} }
function onDragEnd(result: any): void { function onDragEnd(result: any): void {
// Dropped outside of the list // Dropped outside of the list
if (!result.destination) { if (!result.destination) return;
result; reorder(openScripts, result.source.index, result.destination.index);
return;
}
const items = reorder(openScripts, result.source.index, result.destination.index);
openScripts = items;
} }
function currentTabIndex(): number | undefined { function currentTabIndex(): number | undefined {
@ -667,17 +631,17 @@ export function Root(props: IProps): React.ReactElement {
} }
} }
currentScript = { ...openScripts[index] }; currentScript = openScripts[index];
if (editorRef.current !== null && openScripts[index] !== null) { if (editorRef.current !== null && openScripts[index] !== null) {
if (openScripts[index].model === undefined || openScripts[index].model.isDisposed()) { if (currentScript.model === undefined || currentScript.model.isDisposed()) {
regenerateModel(openScripts[index]); regenerateModel(currentScript);
} }
editorRef.current.setModel(openScripts[index].model); editorRef.current.setModel(currentScript.model);
editorRef.current.setPosition(openScripts[index].lastPosition); editorRef.current.setPosition(currentScript.lastPosition);
editorRef.current.revealLineInCenter(openScripts[index].lastPosition.lineNumber); editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
updateRAM(openScripts[index].code); updateRAM(currentScript.code);
editorRef.current.focus(); editorRef.current.focus();
} }
} }
@ -685,18 +649,10 @@ export function Root(props: IProps): React.ReactElement {
function onTabClose(index: number): void { function onTabClose(index: number): void {
// See if the script on the server is up to date // See if the script on the server is up to date
const closingScript = openScripts[index]; const closingScript = openScripts[index];
const savedScriptIndex = openScripts.findIndex( const savedScriptCode = closingScript.code;
(script) => script.fileName === closingScript.fileName && script.hostname === closingScript.hostname, const wasCurrentScript = openScripts[index] === currentScript;
);
let savedScriptCode = "";
if (savedScriptIndex !== -1) {
savedScriptCode = openScripts[savedScriptIndex].code;
}
const server = GetServer(closingScript.hostname);
if (server === null) throw new Error(`Server '${closingScript.hostname}' should not be null, but it is.`);
const serverScriptIndex = server.scripts.findIndex((script) => script.filename === closingScript.fileName); if (dirty(index)) {
if (serverScriptIndex === -1 || savedScriptCode !== server.scripts[serverScriptIndex].code) {
PromptEvent.emit({ PromptEvent.emit({
txt: `Do you want to save changes to ${closingScript.fileName} on ${closingScript.hostname}?`, txt: `Do you want to save changes to ${closingScript.fileName} on ${closingScript.hostname}?`,
resolve: (result: boolean | string) => { resolve: (result: boolean | string) => {
@ -709,40 +665,29 @@ export function Root(props: IProps): React.ReactElement {
}); });
} }
if (openScripts.length > 1) { openScripts.splice(index, 1);
openScripts = openScripts.filter((value, i) => i !== index); if (openScripts.length === 0) {
let indexOffset = -1;
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 1;
if (openScripts[index + indexOffset] === undefined) {
indexOffset = 0;
}
}
// Change current script if we closed it
currentScript = openScripts[index + indexOffset];
if (editorRef.current !== null) {
if (
openScripts[index + indexOffset].model === undefined ||
openScripts[index + indexOffset].model === null ||
openScripts[index + indexOffset].model.isDisposed()
) {
regenerateModel(openScripts[index + indexOffset]);
}
editorRef.current.setModel(openScripts[index + indexOffset].model);
editorRef.current.setPosition(openScripts[index + indexOffset].lastPosition);
editorRef.current.revealLineInCenter(openScripts[index + indexOffset].lastPosition.lineNumber);
editorRef.current.focus();
}
rerender();
} else {
// No more scripts are open
openScripts = [];
currentScript = null; currentScript = null;
props.router.toTerminal(); props.router.toTerminal();
return;
} }
// Change current script if we closed it
if (wasCurrentScript) {
//Keep the same index unless we were on the last script
const indexOffset = openScripts.length === index ? -1 : 0;
currentScript = openScripts[index + indexOffset];
if (editorRef.current !== null) {
if (currentScript.model.isDisposed() || !currentScript.model) {
regenerateModel(currentScript);
}
editorRef.current.setModel(currentScript.model);
editorRef.current.setPosition(currentScript.lastPosition);
editorRef.current.revealLineInCenter(currentScript.lastPosition.lineNumber);
editorRef.current.focus();
}
}
rerender();
} }
function onTabUpdate(index: number): void { function onTabUpdate(index: number): void {
@ -782,21 +727,20 @@ export function Root(props: IProps): React.ReactElement {
function dirty(index: number): string { function dirty(index: number): string {
const openScript = openScripts[index]; const openScript = openScripts[index];
const serverScriptCode = getServerCode(index); const serverData = getServerCode(index);
if (serverScriptCode === null) return " *"; if (serverData === null) return " *";
// For scripts, server code is stored with its starting & trailing whitespace removed
// The server code is stored with its starting & trailing whitespace removed const code = openScript.isTxt ? openScript.code : Script.formatCode(openScript.code);
const openScriptFormatted = Script.formatCode(openScript.code); return serverData !== code ? " *" : "";
return serverScriptCode !== openScriptFormatted ? " *" : "";
} }
function getServerCode(index: number): string | null { function getServerCode(index: number): string | null {
const openScript = openScripts[index]; const openScript = openScripts[index];
const server = GetServer(openScript.hostname); const server = GetServer(openScript.hostname);
if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`); if (server === null) throw new Error(`Server '${openScript.hostname}' should not be null, but it is.`);
const data = openScript.isTxt
const serverScript = server.scripts.find((s) => s.filename === openScript.fileName); ? server.textFiles.find((t) => t.filename === openScript.fileName)?.text
return serverScript?.code ?? null; : server.scripts.find((s) => s.filename === openScript.fileName)?.code;
return data ?? null;
} }
function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void { function handleFilterChange(event: React.ChangeEvent<HTMLInputElement>): void {
setFilter(event.target.value); setFilter(event.target.value);
@ -809,13 +753,6 @@ export function Root(props: IProps): React.ReactElement {
(script) => script.hostname.includes(filter) || script.fileName.includes(filter), (script) => script.hostname.includes(filter) || script.fileName.includes(filter),
); );
// Toolbars are roughly 112px:
// 8px body margin top
// 38.5px filename tabs
// 5px padding for top of editor
// 44px bottom tool bar + 16px margin
// + vim bar 34px
const editorHeight = dimensions.height - (130 + (options.vim ? 34 : 0));
const tabsMaxWidth = 1640; const tabsMaxWidth = 1640;
const tabMargin = 5; const tabMargin = 5;
const tabMaxWidth = filteredOpenScripts.length ? tabsMaxWidth / filteredOpenScripts.length - tabMargin : 0; const tabMaxWidth = filteredOpenScripts.length ? tabsMaxWidth / filteredOpenScripts.length - tabMargin : 0;
@ -951,7 +888,7 @@ export function Root(props: IProps): React.ReactElement {
beforeMount={beforeMount} beforeMount={beforeMount}
onMount={onMount} onMount={onMount}
loading={<Typography>Loading script editor!</Typography>} loading={<Typography>Loading script editor!</Typography>}
height={`${editorHeight}px`} height={`calc(100vh - ${130 + (options.vim ? 34 : 0)}px)`}
defaultLanguage="javascript" defaultLanguage="javascript"
defaultValue={""} defaultValue={""}
onChange={updateCode} onChange={updateCode}

@ -76,6 +76,6 @@
</style> </style>
</head> </head>
<body> <body>
<div id="root" /> <div id="root" style="display:flex" />
</body> </body>
</html> </html>

@ -97,10 +97,10 @@ const useStyles = makeStyles((theme: Theme) =>
"scrollbar-width": "none" /* for Firefox */, "scrollbar-width": "none" /* for Firefox */,
margin: theme.spacing(0), margin: theme.spacing(0),
flexGrow: 1, flexGrow: 1,
display: "block",
padding: "8px", padding: "8px",
minHeight: "100vh", minHeight: "100vh",
boxSizing: "border-box", boxSizing: "border-box",
width: "1px",
}, },
}), }),
); );

@ -2,10 +2,6 @@ import React, { useState, useEffect } from "react";
import CircularProgress from "@mui/material/CircularProgress"; import CircularProgress from "@mui/material/CircularProgress";
import Typography from "@mui/material/Typography"; import Typography from "@mui/material/Typography";
import Grid from "@mui/material/Grid"; import Grid from "@mui/material/Grid";
import Box from "@mui/material/Box";
import { Theme } from "@mui/material/styles";
import makeStyles from "@mui/styles/makeStyles";
import createStyles from "@mui/styles/createStyles";
import { Terminal } from "../Terminal"; import { Terminal } from "../Terminal";
import { load } from "../db"; import { load } from "../db";
@ -18,16 +14,7 @@ import { ActivateRecoveryMode } from "./React/RecoveryRoot";
import { hash } from "../hash/hash"; import { hash } from "../hash/hash";
import { pushGameReady } from "../Electron"; import { pushGameReady } from "../Electron";
const useStyles = makeStyles((theme: Theme) =>
createStyles({
root: {
backgroundColor: theme.colors.backgroundprimary,
},
}),
);
export function LoadingScreen(): React.ReactElement { export function LoadingScreen(): React.ReactElement {
const classes = useStyles();
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [loaded, setLoaded] = useState(false); const [loaded, setLoaded] = useState(false);
@ -69,27 +56,23 @@ export function LoadingScreen(): React.ReactElement {
doLoad(); doLoad();
}, []); }, []);
return ( return loaded ? (
<Box className={classes.root}> <GameRoot terminal={Terminal} engine={Engine} player={Player} />
{loaded ? ( ) : (
<GameRoot terminal={Terminal} engine={Engine} player={Player} /> <Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}>
) : ( <Grid item>
<Grid container direction="column" justifyContent="center" alignItems="center" style={{ minHeight: "100vh" }}> <CircularProgress size={150} color="primary" />
<Grid item> </Grid>
<CircularProgress size={150} color="primary" /> <Grid item>
</Grid> <Typography variant="h3">Loading Bitburner {version}</Typography>
<Grid item> </Grid>
<Typography variant="h3">Loading Bitburner {version}</Typography> {show && (
</Grid> <Grid item>
{show && ( <Typography>
<Grid item> If the game fails to load, consider <a href="?noScripts">killing all scripts</a>
<Typography> </Typography>
If the game fails to load, consider <a href="?noScripts">killing all scripts</a>
</Typography>
</Grid>
)}
</Grid> </Grid>
)} )}
</Box> </Grid>
); );
} }