From de91a58179c1c5bbd166e16c064d3eb8833002f1 Mon Sep 17 00:00:00 2001 From: Gordon Stein <7331488+gsteinLTU@users.noreply.github.com> Date: Wed, 14 Dec 2022 01:57:31 +0000 Subject: [PATCH] Add preferences to extension API (#1341) * Add options menu * Default to empty list * Expect test to be function * Rename to settings and add helpers * Add test for menu item and settings * Fix other failing tests --- src/extensions.js | 27 ++++++++++++++++++++++ src/gui-ext.js | 51 ++++++++++++++++++++++++++++++++++++++++- test/extensions.spec.js | 39 +++++++++++++++++++++++++++---- 3 files changed, 111 insertions(+), 6 deletions(-) diff --git a/src/extensions.js b/src/extensions.js index a599e4dc92..07f5ce7a70 100644 --- a/src/extensions.js +++ b/src/extensions.js @@ -162,6 +162,10 @@ return null; }; + Extension.prototype.getSettings = function() { + return []; + }; + Extension.prototype.getCategories = function() { return []; }; @@ -182,6 +186,29 @@ Extension.prototype.onOpenRole = function() { }; + class ExtensionSetting { + constructor(label, toggle, test, onHint = '', offHint = '', hide = false) { + this.label = label; + this.toggle = toggle, + this.test = test, + this.onHint = onHint, + this.offHint = offHint, + this.hide = hide + } + } + + ExtensionSetting.createFromLocalStorage = function(label, id, defaultValue = false, onHint = '', offHint = '', hide = false){ + return new ExtensionSetting( + label, + () => { + window.localStorage.setItem(id, !(window.localStorage.getItem(id) ?? defaultValue)); + }, + () => window.localStorage.getItem(id) ?? defaultValue, + onHint, offHint, hide); + } + + Extension.ExtensionSetting = ExtensionSetting; + class LabelPart { constructor(spec, fn) { if (spec[0] !== '%') { diff --git a/src/gui-ext.js b/src/gui-ext.js index 322ddb229b..8414e13e77 100644 --- a/src/gui-ext.js +++ b/src/gui-ext.js @@ -482,7 +482,56 @@ IDE_Morph.prototype.extensionsMenu = function() { return menu; }; - return menuFromDict(dict); + let menu = menuFromDict(dict); + + const on = new SymbolMorph( + 'checkedBox', + MorphicPreferences.menuFontSize * 0.75 + ), + off = new SymbolMorph( + 'rectangle', + MorphicPreferences.menuFontSize * 0.75 + ); + + // Add preferences + this.extensions.registry + .filter(ext => ext.getSettings()) + .forEach(ext => { + const name = ext.name || ext.constructor.name; + let thisExtMenu = menu.items.find(item => item[0] == name); + + let prefs = ext.getSettings(); + + if(thisExtMenu){ + thisExtMenu = thisExtMenu[1]; + + // Only show menu if there is a non-hidden option available + if(prefs.find(pref => !pref.hide || world.currentKey == 16) !== undefined){ + let newOptionsMenu = new MenuMorph(this); + thisExtMenu.addMenu('Options', newOptionsMenu); + + // Add each setting as a toggle + prefs.forEach(pref => { + + let test = pref.test; + + if (!pref.hide || world.currentKey == 16) { + newOptionsMenu.addItem( + [ + (test() ? on : off), + pref.label + ], + pref.toggle, + test() ? pref.onHint : pref.offHint, + pref.hide ? new Color(100, 0, 0) : null + ); + } + }); + } + } + }); + + return menu; }; IDE_Morph.prototype.requestProjectReload = async function (reason) { diff --git a/test/extensions.spec.js b/test/extensions.spec.js index 5a97e3cc53..60cc62d37d 100644 --- a/test/extensions.spec.js +++ b/test/extensions.spec.js @@ -8,9 +8,16 @@ describe('extensions', function() { TestExtension.prototype = new Extension('TestExt'); TestExtension.prototype.getMenu = () => { return { - 'hello!': function() {}, + 'TestMenuItem': function() {}, }; }; + TestExtension.prototype.getSettings = () => { + return [new Extension.ExtensionSetting( + 'Test Setting', + () => {}, + () => false + )]; + }; TestExtension.prototype.getCategories = () => [ new Extension.Category( 'TEST!', @@ -107,6 +114,27 @@ describe('extensions', function() { driver.dialog().destroy(); }); + it('should create menu item', function() { + driver.click(driver.ide().controlBar.extensionsButton); + driver.click(driver.dialog().children[1]); + const subMenuItems = driver.dialog().children[2].children.map(c => c.labelString); + assert(subMenuItems.includes('TestMenuItem')); + driver.dialog().destroy(); + }); + + it('should create settings', function() { + driver.click(driver.ide().controlBar.extensionsButton); + driver.click(driver.dialog().children[1]); + const subMenuItems = driver.dialog().children[2].children.map(c => c.labelString); + assert(subMenuItems.includes('Options')); + driver.click(driver.dialog().children[2].children[2]); + + const optionMenuItems = driver.dialog().children[2].children[3].children.map(c => c.labelString); + assert(optionMenuItems.find(item => item && item[1] == 'Test Setting')); + + driver.dialog().destroy(); + }); + it('should not load an extension twice', function() { const {NetsBloxExtensions} = driver.globals(); const extCount = NetsBloxExtensions.registry.length; @@ -143,15 +171,16 @@ describe('extensions', function() { driver.selectCategory('TEST!'); assert.equal( driver.palette().contents.children.length, - 2 + 5 ); }); it('should show new blocks on the stage', function() { driver.selectStage(); driver.selectCategory('TEST!'); - assert( - driver.palette().contents.children.length > 1 + assert.equal( + driver.palette().contents.children.length, + 4 ); }); @@ -183,7 +212,7 @@ describe('extensions', function() { driver.selectCategory('TEST!'); const block = driver.palette().contents.children.find(child => child.selector === 'spriteBlock'); const [inputSlot] = block.inputs(); - assert.equal(inputSlot.evaluate(), 'this is a second test'); + assert(Object.keys(inputSlot.choices).includes('this is a second test')); }); it('should hide sprite block on stage', function() {