diff --git a/src/css/console.css b/src/css/console.css index 674ff42..e459495 100644 --- a/src/css/console.css +++ b/src/css/console.css @@ -59,6 +59,12 @@ border: solid #4f4f4f 0.5px; } +.settings-btn{ + box-shadow: none; + background-color: #69a; + color: #fff !important; +} + .console-button { color: #000; cursor: pointer; diff --git a/src/css/index.css b/src/css/index.css index d773f75..7be2eea 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -12,6 +12,7 @@ @import url("notification.css"); @import url("tooltip.css"); @import url("accordion.css"); +@import url("settings.css"); @import url("https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,300;0,400;0,600;0,700;1,300;1,400;1,600&display=swap"); @@ -33,7 +34,7 @@ body { } .button { - color: white; + color: #0d0c22; cursor: pointer; border-radius: 4px; display: flex; diff --git a/src/css/settings.css b/src/css/settings.css new file mode 100644 index 0000000..d4d62ea --- /dev/null +++ b/src/css/settings.css @@ -0,0 +1,111 @@ +.settingsModal { + display: block; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(102, 153, 170); + background-color: rgba(102, 153, 170, 0.4); +} + +.settingsContainer{ + display: flex; + flex-direction: column; + gap: 10px; + position: fixed; + left: 50%; + top: 25%; + transform: translate(-50%, -50%); + width: 300px; + height: 300px; + border: 1px solid black; + background-color: #fff; + padding: 10px; + font-family: Consolas, "Courier New", monospace; + border-radius: 10px; +} + +.settingsHeader{ + text-align: center; + font-size: 1.5rem; + font-weight: 500; + border-bottom: 1px solid black; +} + +.settingsFooter{ + margin-top: auto; + display: flex; + justify-content: center; + align-items: center; +} + +.exitBtn{ + padding: 8px 10px; + color: #fff; + background-color: rgb(225, 100, 116); + border-radius: 4px; + cursor: pointer; + box-shadow: rgb(0 0 0 / 20%) 0px 3px 1px -2px, rgb(0 0 0 / 14%) 0px 2px 2px 0px, rgb(0 0 0 / 12%) 0px 1px 5px 0px; +} + +.setting{ + display: flex; + justify-content: space-between; + align-items: center; +} + +.toggleBtn{ + position:relative; + display: inline-block; + width: 60px; + height: 34px; +} + +.toggleBtn input{ + opacity: 0; + width: 0; + height: 0; +} + +.toggleBtnSlider{ + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: #ccc; + -webkit-transition: .4s; + transition: .4s; + border-radius: 34px; +} + +.toggleBtnSlider:before { + position: absolute; + content: ""; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background-color: white; + -webkit-transition: .4s; + transition: .4s; + border-radius: 50%; +} + +input:checked + .toggleBtnSlider { + background-color: #69a; +} + +input:focus + .toggleBtnSlider { + box-shadow: 0 0 1px #69a; +} + +input:checked + .toggleBtnSlider:before { + -webkit-transform: translateX(26px); + -ms-transform: translateX(26px); + transform: translateX(26px); +} \ No newline at end of file diff --git a/src/css/suggestion-menu.css b/src/css/suggestion-menu.css index 311d20e..40fcf1c 100644 --- a/src/css/suggestion-menu.css +++ b/src/css/suggestion-menu.css @@ -120,3 +120,40 @@ .categoryOption { padding-left: 10px; } + +.spotlightModal{ + display: block; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0,0,0); + background-color: rgba(0,0,0,0.4); +} + +.spotlight{ + font-family: Consolas, "Courier New", monospace; + padding: 7px; + box-shadow: 0px 0px 6px 1px #ccc inset; + border: 1px solid #ccc; + + transition: box-shadow 0.15s ease-in-out; + -webkit-transition: box-shadow 0.15s ease-in-out; + -moz-transition: box-shadow 0.15s ease-in-out; + -o-transition: box-shadow 0.15s ease-in-out; + -ms-transition: box-shadow 0.15s ease-in-out; +} + +.spotlight:focus{ + box-shadow: 0px 0px 6px 1px #aaa inset; + font-weight: bold; +} + +.spotlight:focus-visible { + border: 1px solid #bbb; + outline: none; + font-weight: bold; +} \ No newline at end of file diff --git a/src/editor/action-executor.ts b/src/editor/action-executor.ts index e52fc2d..a205856 100644 --- a/src/editor/action-executor.ts +++ b/src/editor/action-executor.ts @@ -54,6 +54,7 @@ import { import { Module } from "../syntax-tree/module"; import { Reference } from "../syntax-tree/scope"; import { TypeChecker } from "../syntax-tree/type-checker"; +import { SettingsController } from "../utilities/settings"; import { getUserFriendlyType, isImportable } from "../utilities/util"; import { LogEvent, Logger, LogType } from "./../logger/analytics"; import { BinaryOperator, DataType, InsertionType } from "./../syntax-tree/consts"; @@ -272,7 +273,15 @@ export class ActionExecutor { if (flashGreen) this.flashGreen(action.data?.statement); if (statement.hasBody()) { - let scopeHighlight = new ScopeHighlight(this.module.editor, statement, statement.color); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + let scopeHighlight = new ScopeHighlight(this.module.editor, statement, statement.color); + } else { + let scopeHighlight = new ScopeHighlight( + this.module.editor, + statement, + "rgba(75, 200, 255, 0.125)" + ); + } } else { this.setTokenColor(action.data?.statement, statement.color); } @@ -1432,9 +1441,10 @@ export class ActionExecutor { this.openAutocompleteMenu( this.module.actionFilter .getProcessedInsertionsList() - .filter((item) => item.insertionResult.insertionType != InsertionType.Invalid) + .filter((item) => item.insertionResult.insertionType != InsertionType.Invalid), + true ); - this.styleAutocompleteMenu(context.position); + this.styleAutocompleteMenu(context.position, true); break; @@ -1735,6 +1745,9 @@ export class ActionExecutor { } private setTokenColor(code: CodeConstruct, color: string) { + if (!SettingsController.getInstance().config.enabledColoredBlocks) { + return; + } const aRgbHex = color.substring(1).match(/.{1,2}/g); const aRgb = [parseInt(aRgbHex[0], 16), parseInt(aRgbHex[1], 16), parseInt(aRgbHex[2], 16)]; @@ -1886,10 +1899,10 @@ export class ActionExecutor { } } - private openAutocompleteMenu(inserts: EditCodeAction[]) { + private openAutocompleteMenu(inserts: EditCodeAction[], isSpotlightSearch = false) { if (!this.module.menuController.isMenuOpen()) { inserts = inserts.filter((insert) => insert.insertionResult.insertionType !== InsertionType.Invalid); - this.module.menuController.buildSingleLevelMenu(inserts); + this.module.menuController.buildSingleLevelMenu(inserts, { left: 0, top: 0 }, isSpotlightSearch); } else this.module.menuController.removeMenus(); } @@ -2225,8 +2238,11 @@ export class ActionExecutor { ); } - private styleAutocompleteMenu(pos: Position) { + private styleAutocompleteMenu(pos: Position, isSpotlightSearch: boolean = false) { this.module.menuController.styleMenuOptions(); - this.module.menuController.updatePosition(this.module.menuController.getNewMenuPositionFromPosition(pos)); + this.module.menuController.updatePosition( + this.module.menuController.getNewMenuPositionFromPosition(pos), + isSpotlightSearch + ); } } diff --git a/src/editor/consts.ts b/src/editor/consts.ts index 2655320..b5fd349 100644 --- a/src/editor/consts.ts +++ b/src/editor/consts.ts @@ -287,6 +287,12 @@ export enum InsertActionType { InsertOperatorTkn, } +export enum settingsConfigCategories { + enabledColoredBlocks = "enabledColoredBlocks", + enabledSpotlightSearch = "enabledSpotlightSearch", + enabledTyping = "enabledTyping", +} + export const Docs: any = { AddVarDocs, AddDocs, diff --git a/src/editor/cursor.ts b/src/editor/cursor.ts index 5613094..818246e 100644 --- a/src/editor/cursor.ts +++ b/src/editor/cursor.ts @@ -1,4 +1,5 @@ import { CodeConstruct, EmptyLineStmt, TypedEmptyExpr } from "../syntax-tree/ast"; +import { SettingsController } from "../utilities/settings"; import { Editor } from "./editor"; export class Cursor { @@ -17,7 +18,11 @@ export class Cursor { const cursor = this; function loop() { - cursor.setTransform(cursor.code); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + cursor.setTransformColor(cursor.code); + } else { + cursor.setTransform(cursor.code); + } requestAnimationFrame(loop); } @@ -39,6 +44,27 @@ export class Cursor { const transform = this.editor.computeBoundingBox(selection); + this.element.style.top = `${transform.y + 5}px`; + this.element.style.left = `${transform.x - leftPadding}px`; + + this.element.style.width = `${transform.width + rightPadding}px`; + this.element.style.height = `${transform.height - 5 * 2}px`; + } + + setTransformColor(code: CodeConstruct) { + let leftPadding = 0; + let rightPadding = 0; + + const selection = code != null ? code.getSelection() : this.editor.monaco.getSelection(); + + if (code instanceof TypedEmptyExpr) this.element.style.borderRadius = "15px"; + else this.element.style.borderRadius = "0"; + + this.element.style.visibility = "visible"; + if (!code || code instanceof EmptyLineStmt) this.element.style.visibility = "hidden"; + + const transform = this.editor.computeBoundingBox(selection); + this.element.style.top = `${transform.y + 5 + 4}px`; this.element.style.left = `${transform.x - leftPadding}px`; diff --git a/src/editor/event-router.ts b/src/editor/event-router.ts index 43531ee..cf147fd 100644 --- a/src/editor/event-router.ts +++ b/src/editor/event-router.ts @@ -2,6 +2,7 @@ import { editor, IKeyboardEvent, IScrollEvent, Position } from "monaco-editor"; import * as ast from "../syntax-tree/ast"; import { Module } from "../syntax-tree/module"; +import { SettingsController } from "../utilities/settings"; import { AutoCompleteType, DataType, IdentifierRegex, InsertionType } from "./../syntax-tree/consts"; import { EditCodeAction } from "./action-filter"; import { Actions, Docs, EditActionType, InsertActionType, KeyPress } from "./consts"; @@ -271,7 +272,12 @@ export class EventRouter { case KeyPress.Space: { if (inTextEditMode) return new EditAction(EditActionType.InsertChar); - if (!inTextEditMode && e.ctrlKey && e.key.length == 1) { + if ( + !inTextEditMode && + e.ctrlKey && + e.key.length == 1 && + SettingsController.getInstance().config.enabledSpotlightSearch + ) { return new EditAction(EditActionType.OpenValidInsertMenu); } @@ -279,7 +285,7 @@ export class EventRouter { } default: { - if (e.key.length == 1) { + if (e.key.length == 1 && SettingsController.getInstance().config.enabledTyping) { if (inTextEditMode) { switch (e.key) { case KeyPress.C: diff --git a/src/editor/hole.ts b/src/editor/hole.ts index 5d56062..d4975a2 100644 --- a/src/editor/hole.ts +++ b/src/editor/hole.ts @@ -12,6 +12,7 @@ import { Callback, CallbackType } from "../syntax-tree/callback"; import { InsertionType } from "../syntax-tree/consts"; import { Module } from "../syntax-tree/module"; import { Reference } from "../syntax-tree/scope"; +import { SettingsController } from "../utilities/settings"; import { Editor } from "./editor"; import { Context } from "./focus"; import { Validator } from "./validator"; @@ -107,7 +108,11 @@ export class Hole { code.subscribe( CallbackType.delete, new Callback(() => { - hole.setTransform(null); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + hole.setTransformColor(null); + } else { + hole.setTransform(null); + } hole.remove(); }) ); @@ -115,7 +120,11 @@ export class Hole { code.subscribe( CallbackType.replace, new Callback(() => { - hole.setTransform(null); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + hole.setTransformColor(null); + } else { + hole.setTransform(null); + } hole.remove(); }) ); @@ -123,16 +132,28 @@ export class Hole { code.subscribe( CallbackType.fail, new Callback(() => { - hole.element.style.background = `rgba(255, 0, 0, 0.25)`; - - setTimeout(() => { - hole.element.style.background = `rgba(255, 255, 255, 1)`; - }, 1000); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + hole.element.style.background = `rgba(255, 0, 0, 0.25)`; + + setTimeout(() => { + hole.element.style.background = `rgba(255, 255, 255, 1)`; + }, 1000); + } else { + hole.element.style.background = `rgba(255, 0, 0, 0.06)`; + + setTimeout(() => { + hole.element.style.background = `rgba(255, 255, 255, 0)`; + }, 1000); + } }) ); function loop() { - hole.setTransform(code); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + hole.setTransformColor(code); + } else { + hole.setTransform(code); + } requestAnimationFrame(loop); } @@ -153,6 +174,27 @@ export class Hole { } } + this.element.style.top = `${transform.y + 5}px`; + this.element.style.left = `${transform.x - leftPadding}px`; + + this.element.style.width = `${transform.width + rightPadding}px`; + this.element.style.height = `${transform.height - 5 * 2}px`; + } + + setTransformColor(code: CodeConstruct) { + let leftPadding = 0; + let rightPadding = 0; + let transform = { x: 0, y: 0, width: 0, height: 0 }; + + if (code) { + transform = this.editor.computeBoundingBox(code.getSelection()); + + if (transform.width == 0) { + transform.x -= 7; + transform.width = 14; + } + } + this.element.style.top = `${transform.y + 5 + 4}px`; this.element.style.left = `${transform.x - leftPadding}px`; diff --git a/src/editor/toolbox.ts b/src/editor/toolbox.ts index 9337029..02a3a89 100644 --- a/src/editor/toolbox.ts +++ b/src/editor/toolbox.ts @@ -6,6 +6,7 @@ import { addTextToConsole, clearConsole, CONSOLE_ERR_TXT_CLASS } from "../pyodid import { CodeConstruct, Expression, Modifier, Statement, VariableReferenceExpr } from "../syntax-tree/ast"; import { DataType, InsertionType, Tooltip } from "../syntax-tree/consts"; import { Module } from "../syntax-tree/module"; +import { SettingsController } from "../utilities/settings"; import { getUserFriendlyType } from "../utilities/util"; import { LogEvent, Logger, LogType } from "./../logger/analytics"; import { Accordion, TooltipType } from "./accordion"; @@ -177,7 +178,6 @@ export class ToolboxController { const staticDummySpace = document.getElementById("static-toolbox-dummy-space"); const toolboxCategories = Actions.instance().toolboxCategories; - const hello = Actions.instance().actionsMap; for (const constructGroup of toolboxCategories) { if (constructGroup) { @@ -218,6 +218,26 @@ export class ToolboxController { toolboxDiv.clientHeight - toolboxDiv.children[toolboxDiv.children.length - 2].clientHeight - 20 }px`; } + + toggleToolboxColors() { + const toolboxCategories = Actions.instance().toolboxCategories; + const isColored = SettingsController.getInstance().config.enabledColoredBlocks; + + for (const constructGroup of toolboxCategories) { + if (constructGroup) { + for (const item of constructGroup.items) { + const button = document.getElementById(item.cssId); + if (isColored) { + button.style.backgroundColor = item.documentation.styles.backgroundColor; + button.style.color = "#fff"; + } else { + button.style.backgroundColor = "#fff"; + button.style.color = "#0d0c22"; + } + } + } + } + } } export class ToolboxButton { @@ -228,9 +248,16 @@ export class ToolboxButton { this.container.classList.add("var-button-container"); const button = document.createElement("div"); - button.style.backgroundColor = btnColor; button.classList.add("button"); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + button.style.backgroundColor = btnColor; + button.style.color = "#fff"; + } else { + button.style.backgroundColor = "#fff"; + button.style.color = "#0d0c22"; + } + if (!(code instanceof Expression) && !(code instanceof Modifier)) { button.classList.add("statement-button"); } else if (code instanceof Modifier) { diff --git a/src/index.html b/src/index.html index d0c3780..2105d1c 100644 --- a/src/index.html +++ b/src/index.html @@ -32,6 +32,7 @@

Defined Variables

> Run Code
Clear Console
+
Settings
diff --git a/src/messages/messages.ts b/src/messages/messages.ts index fe63a41..c6fc0f9 100644 --- a/src/messages/messages.ts +++ b/src/messages/messages.ts @@ -4,6 +4,7 @@ import { EDITOR_DOM_ID } from "../editor/toolbox"; import { nova } from "../index"; import { CodeConstruct, Expression, Statement, TypedEmptyExpr } from "../syntax-tree/ast"; import { Callback, CallbackType } from "../syntax-tree/callback"; +import { SettingsController } from "../utilities/settings"; /** * Class name of the DOM element to which messages are appended to. @@ -137,7 +138,11 @@ export class ConstructHighlight extends CodeHighlight { super.createDomElement(); this.domElement.classList.add("highlight"); - this.updateDimensions(true); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + this.updateDimensionsColor(true); + } else { + this.updateDimensions(true); + } document.querySelector(editorDomElementClass).appendChild(this.domElement); } @@ -184,6 +189,57 @@ export class ConstructHighlight extends CodeHighlight { let width = 0; let height = 0; + //no idea why these need separate handling... This was the easiest fix. + if (this.code instanceof TypedEmptyExpr) { + const transform = this.editor.computeBoundingBox(this.code.getSelection()); + const text = this.code.getRenderText(); + + top = transform.y + 5; + left = (this.code.getSelection().startColumn - 1) * this.editor.computeCharWidthInvisible(lineNumber); + + width = + text.length * this.editor.computeCharWidthInvisible(lineNumber) > 0 + ? text.length * this.editor.computeCharWidthInvisible(lineNumber) + : HIGHLIGHT_DEFAULT_WIDTH; + height = transform.height > 0 ? transform.height - 5 * 2 : HIGHLIGHT_DEFAULT_HEIGHT; + } else { + const selection = this.code.getSelection(); + const transform = this.editor.computeBoundingBox(selection); + + if (this.code instanceof Expression) { + top = (selection.startLineNumber - 1) * this.editor.computeCharHeight(); + left = transform.x; + height = Math.floor(this.editor.computeCharHeight() * 0.95); + width = + (selection.endColumn - selection.startColumn) * this.editor.computeCharWidthInvisible(lineNumber) + + 10; + } else { + top = (selection.startLineNumber - 1) * this.editor.computeCharHeight(); + left = transform.x; + height = Math.floor(this.editor.computeCharHeight() * 0.95); + width = + (selection.endColumn - selection.startColumn) * this.editor.computeCharWidthInvisible(lineNumber); + } + } + + if (firstInsertion) { + this.domElement.style.top = `${top}px`; + this.domElement.style.left = `${left}px`; + } + + this.domElement.style.width = `${width}px`; + this.domElement.style.height = `${height}px`; + } + + protected updateDimensionsColor(firstInsertion: boolean = false) { + //instanceof Token does not have lineNumber + let lineNumber = this.code.getLineNumber(); + + let top = 0; + let left = 0; + let width = 0; + let height = 0; + //no idea why these need separate handling... This was the easiest fix. if (this.code instanceof TypedEmptyExpr) { const transform = this.editor.computeBoundingBox(this.code.getSelection()); @@ -665,7 +721,11 @@ export class ScopeHighlight { const onChange = new Callback( (() => { - this.updateDimensions(); + if (SettingsController.getInstance().config.enabledColoredBlocks) { + this.updateDimensionsColor(); + } else { + this.updateDimensions(); + } }).bind(this) ); @@ -709,14 +769,19 @@ export class ScopeHighlight { this.headerElement = document.createElement("div"); this.headerElement.classList.add("scope-header-highlight"); this.headerElement.style.backgroundColor = color; - this.headerElement.style.opacity = "0.25"; this.bodyElement = document.createElement("div"); this.bodyElement.classList.add("scope-body-highlight"); this.bodyElement.style.backgroundColor = color; - this.bodyElement.style.opacity = "0.25"; - this.updateDimensions(); + //TODO: change all colors to rgba - temporary fix + if (SettingsController.getInstance().config.enabledColoredBlocks) { + this.headerElement.style.opacity = "0.25"; + this.bodyElement.style.opacity = "0.25"; + this.updateDimensionsColor(); + } else { + this.updateDimensions(); + } document.querySelector(editorDomElementClass).appendChild(this.headerElement); document.querySelector(editorDomElementClass).appendChild(this.bodyElement); @@ -748,6 +813,47 @@ export class ScopeHighlight { this.headerElement.style.top = `${headerDim.top}px`; this.headerElement.style.left = `${headerDim.left}px`; + this.headerElement.style.width = `${maxRight - headerDim.left}px`; + this.headerElement.style.height = `${headerDim.height}px`; + + let firstLineInBody = this.statement.body[0]; + let firstLineInBodyDim: LineDimension; + + if (firstLineInBody) { + firstLineInBodyDim = LineDimension.compute(firstLineInBody, this.editor); + } else { + firstLineInBodyDim = LineDimension.compute(this.statement, this.editor); + } + + this.bodyElement.style.top = `${firstLineInBodyDim.top}px`; + this.bodyElement.style.left = `${firstLineInBodyDim.left}px`; + + this.bodyElement.style.width = `${maxRight - firstLineInBodyDim.left}px`; + this.bodyElement.style.height = `${headerDim.height * (maxLineNumber - this.statement.lineNumber)}px`; + } + + protected updateDimensionsColor(): void { + const headerDim = LineDimension.compute(this.statement, this.editor); + + let maxRight = headerDim.right; + let maxLineNumber = 0; + + const stack = Array(); + stack.unshift(...this.statement.body); + + while (stack.length > 0) { + const line = stack.pop(); + + const lineDim = LineDimension.compute(line, this.editor); + if (lineDim.right > maxRight) maxRight = lineDim.right; + if (line.lineNumber > maxLineNumber) maxLineNumber = line.lineNumber; + + if (line.hasBody()) stack.unshift(...line.body); + } + + this.headerElement.style.top = `${headerDim.top}px`; + this.headerElement.style.left = `${headerDim.left}px`; + this.headerElement.style.width = `${maxRight - headerDim.left + 10}px`; this.headerElement.style.height = `${headerDim.height}px`; diff --git a/src/suggestions/suggestions-controller.ts b/src/suggestions/suggestions-controller.ts index f7305a2..43f1fad 100644 --- a/src/suggestions/suggestions-controller.ts +++ b/src/suggestions/suggestions-controller.ts @@ -1,6 +1,6 @@ import { Position } from "monaco-editor"; import { EditCodeAction } from "../editor/action-filter"; -import { Actions, Docs, InsertActionType } from "../editor/consts"; +import { Actions, Docs, EditActionType, InsertActionType } from "../editor/consts"; import { Editor } from "../editor/editor"; import { EDITOR_DOM_ID } from "../editor/toolbox"; import { Validator } from "../editor/validator"; @@ -32,18 +32,52 @@ class Menu { static menuCount = 0; static idPrefix = "suggestion-menu-"; htmlElement: HTMLDivElement; + searchBar: HTMLInputElement; + modal: HTMLDivElement; private optionsInViewPort; - constructor(options: Map) { - this.htmlElement = document.createElement("div"); - this.htmlElement.classList.add(MenuController.menuElementClass); - this.htmlElement.id = `${Menu.idPrefix}${Menu.menuCount}`; - document.getElementById(EDITOR_DOM_ID).appendChild(this.htmlElement); + constructor(options: EditCodeAction[], optionsMap: Map, isSpotlightSearch: boolean = false) { + if (isSpotlightSearch) { + this.modal = document.createElement("div"); + this.modal.classList.add(MenuController.modalClass); + document.getElementById(EDITOR_DOM_ID).appendChild(this.modal); + + this.htmlElement = document.createElement("div"); + this.htmlElement.classList.add(MenuController.menuElementClass); + this.htmlElement.id = `${Menu.idPrefix}${Menu.menuCount}`; + this.modal.appendChild(this.htmlElement); + + this.searchBar = document.createElement("input"); + this.searchBar.classList.add(MenuController.spotlightElementClass); + this.searchBar.placeholder = "Search for Blocks"; + this.htmlElement.appendChild(this.searchBar); + + const menuController = MenuController.getInstance(); + + window.onclick = (e: MouseEvent) => { + if (e.target == this.modal) { + menuController.removeMenus(); + } + }; + + this.searchBar.addEventListener("keydown", (e: KeyboardEvent) => { + menuController.spotlightSearchOnKeyDown(e, options); + }); + + this.searchBar.addEventListener("input", (e: Event) => { + menuController.spotlightSearchOnChange(e, options); + }); + } else { + this.htmlElement = document.createElement("div"); + this.htmlElement.classList.add(MenuController.menuElementClass); + this.htmlElement.id = `${Menu.idPrefix}${Menu.menuCount}`; + document.getElementById(EDITOR_DOM_ID).appendChild(this.htmlElement); + } Menu.menuCount++; - for (const [key, value] of options) { + for (const [key, value] of optionsMap) { const option = new MenuOption(key, false, null, this, null, value); option.attachToParentMenu(this); @@ -225,7 +259,13 @@ class Menu { } removeFromDOM() { - document.getElementById(EDITOR_DOM_ID).removeChild(this.htmlElement); + let node = this.htmlElement; + let parentNode = node.parentNode; + parentNode.removeChild(this.htmlElement); + + if (parentNode == this.modal) { + document.getElementById(EDITOR_DOM_ID).removeChild(this.modal); + } } getOptionByText(optionText: string) { @@ -417,6 +457,8 @@ export class MenuController { static optionElementClass: string = "suggestionOptionParent"; static draftModeOptionElementClass: string = "draftModeOptionElementClass"; static menuElementClass: string = "suggestionMenuParent"; + static modalClass: string = "spotlightModal"; + static spotlightElementClass: string = "spotlight"; static optionTextElementClass: string = "suggestionOptionText"; static selectedOptionElementClass: string = "selectedSuggestionOptionParent"; @@ -454,7 +496,11 @@ export class MenuController { * * @param pos Starting top-left corner of this menu in the editor. */ - buildSingleLevelMenu(suggestions: EditCodeAction[], pos: any = { left: 0, top: 0 }) { + buildSingleLevelMenu( + suggestions: EditCodeAction[], + pos: any = { left: 0, top: 0 }, + isSpotlightSearch: boolean = false + ) { if (this.menus.length > 0) this.removeMenus(); else if (suggestions.length >= 0) { //TODO: Very hacky way of fixing #569 @@ -464,7 +510,7 @@ export class MenuController { suggestions.push(Actions.instance().actionsList[0]); //this does not have to be this specific aciton, just need one to create the option so that the menu is created and then we immediately delete the option } - const menu = this.module.menuController.buildMenu(suggestions, pos); + const menu = this.module.menuController.buildMenu(suggestions, pos, isSpotlightSearch); //TODO: Continuation of very hacky way of fixing #569 if (suggestions.length === 0) { @@ -507,7 +553,11 @@ export class MenuController { * * @returns the constructed menu. Null if no options was empty. */ - private buildMenu(options: EditCodeAction[], pos: any = { left: 0, top: 0 }): Menu { + private buildMenu( + options: EditCodeAction[], + pos: any = { left: 0, top: 0 }, + isSpotlightSearch: boolean = false + ): Menu { if (options.length > 0) { const menuOptions = new Map(); @@ -523,7 +573,7 @@ export class MenuController { }); } - const menu = new Menu(menuOptions); + const menu = new Menu(options, menuOptions, isSpotlightSearch); //TODO: These are the same values as the ones used for mouse offset by the messages so maybe make them shared in some util file menu.htmlElement.style.left = `${pos.left + document.getElementById(EDITOR_DOM_ID).offsetLeft}px`; @@ -547,6 +597,51 @@ export class MenuController { return null; } + spotlightSearchOnKeyDown(e: KeyboardEvent, options: EditCodeAction[]) { + const context = this.module.focus.getContext(); + const action = this.module.eventRouter.getKeyAction(e, context); + + if ( + action.type == EditActionType.SelectMenuSuggestion || + action.type == EditActionType.SelectMenuSuggestionAbove || + action.type == EditActionType.SelectMenuSuggestionBelow + ) { + const preventDefaultEvent = this.module.executer.execute(action, context, e); + + if (preventDefaultEvent) { + e.preventDefault(); + e.stopPropagation(); + } + } + } + + spotlightSearchOnChange(e: Event, options: EditCodeAction[]) { + const context = this.module.focus.getContext(); + const target = e.target as HTMLInputElement; + let prevText = target.value.slice(0, -1); + let curText = target.value; + let lastkey = target.value.slice(-1); + + //check match + for (const match of options) { + if (match.terminatingChars.indexOf(lastkey) >= 0) { + if (match.trimSpacesBeforeTermChar) prevText = prevText.trim(); + + if (prevText == match.matchString || (match.matchRegex != null && match.matchRegex.test(prevText))) + match.performAction(this.module.executer, this.module.eventRouter, context, { + type: "autocomplete-menu", + precision: this.calculateAutocompleteMatchPrecision(prevText, match.matchString), + length: + match.insertActionType === InsertActionType.InsertNewVariableStmt + ? prevText.length + 1 + : match.matchString.length + 1, + }); + } + } + + this.updateMenuOptions(curText); + } + removeMenus() { this.menus.forEach((menu) => { menu.close(); @@ -942,12 +1037,20 @@ export class MenuController { } } - updatePosition(pos: { left: number; top: number }) { + updatePosition(pos: { left: number; top: number }, isSpotlightSearch: boolean = false) { const element = this.menus[this.focusedMenuIndex]?.htmlElement; if (element) { - element.style.left = `${pos.left}px`; - element.style.top = `${pos.top}px`; + if (isSpotlightSearch) { + //centers the menu on the page + element.style.left = "50%"; + element.style.top = "50%"; + element.style.marginLeft = -element.offsetWidth / 2 + "px"; + element.style.marginTop = -element.offsetHeight / 2 + "px"; + } else { + element.style.left = `${pos.left}px`; + element.style.top = `${pos.top}px`; + } } } diff --git a/src/syntax-tree/module.ts b/src/syntax-tree/module.ts index ee02f81..1d0c30a 100644 --- a/src/syntax-tree/module.ts +++ b/src/syntax-tree/module.ts @@ -1,4 +1,5 @@ import { Position, Range } from "monaco-editor"; + import { ActionExecutor } from "../editor/action-executor"; import { ActionFilter } from "../editor/action-filter"; import { CodeStatus, EditActionType } from "../editor/consts"; @@ -15,6 +16,7 @@ import { MessageController } from "../messages/message-controller"; import { ConstructHighlight } from "../messages/messages"; import { NotificationManager } from "../messages/notifications"; import { MenuController } from "../suggestions/suggestions-controller"; +import { SettingsController } from "../utilities/settings"; import { Util } from "../utilities/util"; import { AutocompleteTkn, @@ -58,6 +60,7 @@ export class Module { typeSystem: TypeChecker; notificationManager: NotificationManager; toolboxController: ToolboxController; + settingsController: SettingsController; scope: Scope; draftExpressions: DraftRecord[]; @@ -146,6 +149,9 @@ export class Module { this.menuController = MenuController.getInstance(); this.menuController.setInstance(this, this.editor); + this.settingsController = SettingsController.getInstance(); + this.settingsController.setInstance(this); + Util.getInstance(this); } diff --git a/src/utilities/settings.ts b/src/utilities/settings.ts new file mode 100644 index 0000000..d90ebc5 --- /dev/null +++ b/src/utilities/settings.ts @@ -0,0 +1,123 @@ +import { settingsConfigCategories } from "../editor/consts"; +import { Module } from "../syntax-tree/module"; + +// Singleton controlling settings for the program +export class SettingsController { + private static instance: SettingsController; + + private modal: HTMLDivElement; + private settingsContainer: HTMLDivElement; + private settingsHeader: HTMLDivElement; + private settingsFooter: HTMLDivElement; + private exitBtn: HTMLDivElement; + + public config: any; + + module: Module; + + constructor() { + this.config = { + [settingsConfigCategories.enabledColoredBlocks]: false, + [settingsConfigCategories.enabledSpotlightSearch]: false, + [settingsConfigCategories.enabledTyping]: false, + }; + + this.addEventListeners(); + } + + static getInstance() { + if (!SettingsController.instance) SettingsController.instance = new SettingsController(); + + return SettingsController.instance; + } + + setInstance(module: Module) { + this.module = module; + } + + private addEventListeners() { + const settingsBtn = document.getElementById("settingsBtn"); + + settingsBtn.addEventListener("click", () => { + this.renderSettings(); + }); + } + + private renderSettings() { + this.modal = document.createElement("div"); + this.modal.classList.add("settingsModal"); + this.modal.id = "settingsModal"; + document.getElementById("editor-container").appendChild(this.modal); + + this.settingsContainer = document.createElement("div"); + this.settingsContainer.classList.add("settingsContainer"); + this.settingsContainer.id = "settingsContainer"; + document.getElementById("settingsModal").appendChild(this.settingsContainer); + + this.settingsHeader = document.createElement("div"); + this.settingsHeader.classList.add("settingsHeader"); + this.settingsHeader.id = "settingsHeader"; + this.settingsHeader.innerHTML = "Settings"; + this.settingsContainer.appendChild(this.settingsHeader); + + Object.keys(this.config).map((key) => { + const setting = document.createElement("div"); + setting.classList.add("setting"); + this.settingsContainer.appendChild(setting); + + const settingText = document.createElement("div"); + settingText.classList.add("settingText"); + settingText.innerHTML = key; + setting.appendChild(settingText); + + const toggleBtn = document.createElement("label"); + toggleBtn.classList.add("toggleBtn"); + setting.appendChild(toggleBtn); + + const toggleBtnCheckbox = document.createElement("input"); + toggleBtnCheckbox.type = "checkbox"; + toggleBtnCheckbox.checked = this.config[key]; + toggleBtnCheckbox.id = key; + toggleBtn.appendChild(toggleBtnCheckbox); + + const toggleBtnSlider = document.createElement("span"); + toggleBtnSlider.classList.add("toggleBtnSlider"); + toggleBtn.appendChild(toggleBtnSlider); + + toggleBtnCheckbox.addEventListener("change", () => { + this.config[key] = toggleBtnCheckbox.checked; + + //TODO: update the blocks to be colorless + //re-render the toolbox + if (toggleBtnCheckbox.id == settingsConfigCategories.enabledColoredBlocks) { + this.module.toolboxController.toggleToolboxColors(); + } + }); + }); + + this.settingsFooter = document.createElement("div"); + this.settingsFooter.classList.add("settingsFooter"); + this.settingsFooter.id = "settingsFooter"; + this.settingsContainer.appendChild(this.settingsFooter); + + this.exitBtn = document.createElement("div"); + this.exitBtn.classList.add("exitBtn"); + this.exitBtn.id = "exitBtn"; + this.exitBtn.innerHTML = "Close"; + this.settingsFooter.appendChild(this.exitBtn); + + this.exitBtn.addEventListener("click", () => { + this.closeSettings(); + }); + + window.onclick = (e: MouseEvent) => { + if (e.target === this.modal) { + this.closeSettings(); + } + }; + } + + private closeSettings() { + this.modal.remove(); + } +}