diff --git a/ChangeLog b/ChangeLog index 1ade7c38cf..63403b4f68 100644 --- a/ChangeLog +++ b/ChangeLog @@ -35,6 +35,7 @@ Fix issue with Graphics.createImage if more than the first line was empty (fix #2296) Bangle.js: Now auto-reset compass if it was on while the watch was charging (fix https://github.com/espruino/BangleApps/issues/2648) Graphics: Added .floodFill method to allow flood fill + Added E.stopEventPropagation() to allow event propagation to be stopped with multiple X.on('...', ...) 2v17 : Bangle.js: When reading file info from a filename table, do it in blocks of 8 (20% faster file search) Bangle.js2: Increase flash buffer size from 16->32 bytes (5% performance increase) diff --git a/src/jsinteractive.c b/src/jsinteractive.c index 369c9b466f..2f3a6f3c02 100644 --- a/src/jsinteractive.c +++ b/src/jsinteractive.c @@ -81,7 +81,6 @@ uint16_t inputStateNumber; ///< Number from when `Esc [ 1234` is sent - for stor uint16_t jsiLineNumberOffset; ///< When we execute code, this is the 'offset' we apply to line numbers in error/debug bool hasUsedHistory = false; ///< Used to speed up - if we were cycling through history and then edit, we need to copy the string unsigned char loopsIdling = 0; ///< How many times around the loop have we been entirely idle? -bool interruptedDuringEvent; ///< Were we interrupted while executing an event? If so may want to clear timers JsErrorFlags lastJsErrorFlags = 0; ///< Compare with jsErrorFlags in order to report errors // ---------------------------------------------------------------------------- @@ -784,8 +783,6 @@ void jsiSoftKill() { void jsiSemiInit(bool autoLoad, JsfFileName *loadedFilename) { // Set up execInfo.root/etc jspInit(); - // Set state - interruptedDuringEvent = false; // Set defaults jsiStatus &= JSIS_SOFTINIT_MASK; #ifndef SAVE_ON_FLASH @@ -1179,9 +1176,9 @@ bool jsiAtEndOfInputLine() { } void jsiCheckErrors() { - if (interruptedDuringEvent) { + if (jsiStatus & JSIS_EVENTEMITTER_INTERRUPTED) { jspSetInterrupted(false); - interruptedDuringEvent = false; + jsiStatus &= ~JSIS_EVENTEMITTER_INTERRUPTED; jsiConsoleRemoveInputLine(); jsiConsolePrint("Execution Interrupted during event processing.\n"); } @@ -1683,13 +1680,6 @@ void jsiQueueObjectCallbacks(JsVar *object, const char *callbackName, JsVar **ar jsvUnLock(callback); } -void jsiExecuteObjectCallbacks(JsVar *object, const char *callbackName, JsVar **args, int argCount) { - JsVar *callback = jsvObjectGetChildIfExists(object, callbackName); - if (!callback) return; - jsiExecuteEventCallback(object, callback, (unsigned int)argCount, args); - jsvUnLock(callback); -} - void jsiExecuteEvents() { bool hasEvents = !jsvArrayIsEmpty(events); if (hasEvents) jsiSetBusy(BUSY_INTERACTIVE, true); @@ -1710,7 +1700,7 @@ void jsiExecuteEvents() { if (hasEvents) { jsiSetBusy(BUSY_INTERACTIVE, false); if (jspIsInterrupted()) - interruptedDuringEvent = true; + jsiStatus |= JSIS_EVENTEMITTER_INTERRUPTED; } } @@ -1733,35 +1723,35 @@ NO_INLINE bool jsiExecuteEventCallbackArgsArray(JsVar *thisVar, JsVar *callbackV NO_INLINE bool jsiExecuteEventCallback(JsVar *thisVar, JsVar *callbackVar, unsigned int argCount, JsVar **argPtr) { // array of functions or single function JsVar *callbackNoNames = jsvSkipName(callbackVar); + if (!callbackNoNames) return false; bool ok = true; - if (callbackNoNames) { - if (jsvIsArray(callbackNoNames)) { - JsvObjectIterator it; - jsvObjectIteratorNew(&it, callbackNoNames); - while (ok && jsvObjectIteratorHasValue(&it)) { - JsVar *child = jsvObjectIteratorGetValue(&it); - ok &= jsiExecuteEventCallback(thisVar, child, argCount, argPtr); - jsvUnLock(child); - jsvObjectIteratorNext(&it); - } - jsvObjectIteratorFree(&it); - } else if (jsvIsFunction(callbackNoNames)) { - jsvUnLock(jspExecuteFunction(callbackNoNames, thisVar, (int)argCount, argPtr)); - } else if (jsvIsString(callbackNoNames)) { - jsvUnLock(jspEvaluateVar(callbackNoNames, 0, 0)); - } else - jsError("Unknown type of callback in Event Queue"); - jsvUnLock(callbackNoNames); - } + jsiStatus |= JSIS_EVENTEMITTER_PROCESSING; + if (jsvIsArray(callbackNoNames)) { + JsvObjectIterator it; + jsvObjectIteratorNew(&it, callbackNoNames); + while (ok && jsvObjectIteratorHasValue(&it) && !(jsiStatus & JSIS_EVENTEMITTER_STOP)) { + JsVar *child = jsvObjectIteratorGetValue(&it); + ok &= jsiExecuteEventCallback(thisVar, child, argCount, argPtr); + jsvUnLock(child); + jsvObjectIteratorNext(&it); + } + jsvObjectIteratorFree(&it); + } else if (jsvIsFunction(callbackNoNames)) { + jsvUnLock(jspExecuteFunction(callbackNoNames, thisVar, (int)argCount, argPtr)); + } else if (jsvIsString(callbackNoNames)) { + jsvUnLock(jspEvaluateVar(callbackNoNames, 0, 0)); + } else + jsError("Unknown type of callback in Event Queue"); + jsvUnLock(callbackNoNames); + jsiStatus &= ~JSIS_EVENTEMITTER_PROCESSING; if (!ok || jspIsInterrupted()) { - interruptedDuringEvent = true; + jsiStatus |= JSIS_EVENTEMITTER_INTERRUPTED; return false; } return true; } - // Execute the named Event callback on object, and return true if it exists bool jsiExecuteEventCallbackName(JsVar *obj, const char *cbName, unsigned int argCount, JsVar **argPtr) { bool executed = false; @@ -1784,7 +1774,6 @@ bool jsiExecuteEventCallbackOn(const char *objectName, const char *cbName, unsig return executed; } - /// Create a timeout in JS to execute the given native function (outside of an IRQ). Returns the index JsVar *jsiSetTimeout(void (*functionPtr)(void), JsVarFloat milliseconds) { JsVar *fn = jsvNewNativeFunction((void (*)(void))functionPtr, JSWAT_VOID); @@ -1925,9 +1914,9 @@ void jsiIdle() { JsVar *usartClass = jsvSkipNameAndUnLock(jsiGetClassNameFromDevice(IOEVENTFLAGS_GETTYPE(IOEVENTFLAGS_SERIAL_STATUS_TO_SERIAL(event.flags)))); if (jsvIsObject(usartClass)) { if (event.flags & EV_SERIAL_STATUS_FRAMING_ERR) - jsiExecuteObjectCallbacks(usartClass, JS_EVENT_PREFIX"framing", 0, 0); + jsiExecuteEventCallbackName(usartClass, JS_EVENT_PREFIX"framing", 0, 0); if (event.flags & EV_SERIAL_STATUS_PARITY_ERR) - jsiExecuteObjectCallbacks(usartClass, JS_EVENT_PREFIX"parity", 0, 0); + jsiExecuteEventCallbackName(usartClass, JS_EVENT_PREFIX"parity", 0, 0); } jsvUnLock(usartClass); #endif @@ -1946,7 +1935,7 @@ void jsiIdle() { if (obj) { jsvObjectSetChildAndUnLock(obj, "addr", jsvNewFromInteger(addr&0x7F)); jsvObjectSetChildAndUnLock(obj, "length", jsvNewFromInteger(len)); - jsiExecuteObjectCallbacks(i2cClass, (addr&0x80) ? JS_EVENT_PREFIX"read" : JS_EVENT_PREFIX"write", &obj, 1); + jsiExecuteEventCallbackName(i2cClass, (addr&0x80) ? JS_EVENT_PREFIX"read" : JS_EVENT_PREFIX"write", 1, &obj); jsvUnLock(obj); } } @@ -2236,7 +2225,7 @@ void jsiIdle() { unsigned int oldJsVarsSize = jsvGetMemoryTotal(); // we must remember the old memory size - mainly for ESP32 where it can change at boot time JsiStatus s = jsiStatus; if ((s&JSIS_TODO_RESET) == JSIS_TODO_RESET) { - // shut down everything and start up again + // shut down everything and start up again jsiKill(); jsvKill(); jshReset(); @@ -2493,7 +2482,7 @@ void jsiDebuggerLoop() { // in debugger already // echo is off for line (probably uploading) if (jsiStatus & (JSIS_IN_DEBUGGER|JSIS_ECHO_OFF_FOR_LINE)) return; - + execInfo.execute &= (JsExecFlags)~( EXEC_CTRL_C_MASK | EXEC_DEBUGGER_NEXT_LINE | diff --git a/src/jsinteractive.h b/src/jsinteractive.h index de2fa1fc80..6b8d581a9b 100644 --- a/src/jsinteractive.h +++ b/src/jsinteractive.h @@ -53,8 +53,6 @@ void jsiQueueEvents(JsVar *object, JsVar *callback, JsVar **args, int argCount); bool jsiObjectHasCallbacks(JsVar *object, const char *callbackName); /// Queue up callbacks for other things (touchscreen? network?) void jsiQueueObjectCallbacks(JsVar *object, const char *callbackName, JsVar **args, int argCount); -/// Execute callbacks straight away (like jsiQueueObjectCallbacks, but without queueing) -void jsiExecuteObjectCallbacks(JsVar *object, const char *callbackName, JsVar **args, int argCount); /// Execute the given function/string/array of functions and return true on success, false on failure (break during execution) bool jsiExecuteEventCallback(JsVar *thisVar, JsVar *callbackVar, unsigned int argCount, JsVar **argPtr); /// Same as above, but with a JsVarArray (this calls jsiExecuteEventCallback, so use jsiExecuteEventCallback where possible) @@ -162,6 +160,10 @@ typedef enum { JSIS_COMPLETELY_RESET = 1<<11, ///< Has the board powered on *having not loaded anything from flash* JSIS_FIRST_BOOT = 1<<12, ///< Is this the first time we started, or has load/reset/etc been called? + JSIS_EVENTEMITTER_PROCESSING = 1<<13, ///< Are we currently executing events with jsiExecuteEvent* + JSIS_EVENTEMITTER_STOP = 1<<14, ///< Has E.stopEventPropagation() been called during event processing? + JSIS_EVENTEMITTER_INTERRUPTED = 1<<15,///< Has there been an error during jsiExecuteEvent* execution? + JSIS_ECHO_OFF_MASK = JSIS_ECHO_OFF|JSIS_ECHO_OFF_FOR_LINE, JSIS_SOFTINIT_MASK = JSIS_PASSWORD_PROTECTED|JSIS_WATCHDOG_AUTO|JSIS_TODO_MASK|JSIS_FIRST_BOOT|JSIS_COMPLETELY_RESET // stuff that DOESN'T get reset on softinit // watchdog can't be reset without a reboot so if it's set to auto we must keep it as auto diff --git a/src/jswrap_espruino.c b/src/jswrap_espruino.c index 4e8ee110e6..1d94154514 100644 --- a/src/jswrap_espruino.c +++ b/src/jswrap_espruino.c @@ -615,18 +615,18 @@ If `isAuto` is false, you must call `E.kickWatchdog()` yourself every so often or the chip will reset. ``` -E.enableWatchdog(0.5); // automatic mode +E.enableWatchdog(0.5); // automatic mode while(1); // Espruino will reboot because it has not been idle for 0.5 sec ``` ``` -E.enableWatchdog(1, false); +E.enableWatchdog(1, false); setInterval(function() { if (everything_ok) E.kickWatchdog(); }, 500); // Espruino will now reset if everything_ok is false, -// or if the interval fails to be called +// or if the interval fails to be called ``` **NOTE:** This is only implemented on STM32, nRF5x and ESP32 devices (all official @@ -2298,3 +2298,32 @@ JsVar *jswrap_espruino_decodeUTF8(JsVar *str, JsVar *lookup, JsVar *replaceFn) { return dst; } +/*JSON{ + "type" : "staticmethod", + "class" : "E", + "name" : "stopEventPropagation", + "generate" : "jswrap_espruino_stopEventPropagation" +} +When using events with `X.on('foo', function() { ... })` +and then `X.emit('foo')` you might want to stop subsequent +event handlers from being executed. + +Calling this function doing the execution of events will +ensure that no subsequent event handlers are executed. + +``` +var X = {}; // in Espruino all objects are EventEmitters +X.on('foo', function() { print("A"); }) +X.on('foo', function() { print("B"); E.stopEventPropagation(); }) +X.on('foo', function() { print("C"); }) +X.emit('foo'); +// prints A,B but not C +``` + */ +void jswrap_espruino_stopEventPropagation() { + if (jsiStatus & JSIS_EVENTEMITTER_PROCESSING) { + jsiStatus |= JSIS_EVENTEMITTER_STOP; + } else { + jsExceptionHere(JSET_ERROR, "E.stopEventPropagation() called when not handling events"); + } +} \ No newline at end of file diff --git a/src/jswrap_espruino.h b/src/jswrap_espruino.h index e9d0821ba4..9eb43cf4f9 100644 --- a/src/jswrap_espruino.h +++ b/src/jswrap_espruino.h @@ -74,5 +74,6 @@ JsVarInt jswrap_espruino_getBattery(); void jswrap_espruino_setRTCPrescaler(int prescale); int jswrap_espruino_getRTCPrescaler(bool calibrate); JsVar *jswrap_espruino_decodeUTF8(JsVar *str, JsVar *lookup, JsVar *replaceFn); +void jswrap_espruino_stopEventPropagation(); #endif // JSWRAP_ESPRUINO_H_ diff --git a/src/jswrap_object.c b/src/jswrap_object.c index 9a1ea1e663..b6c618cbf3 100644 --- a/src/jswrap_object.c +++ b/src/jswrap_object.c @@ -830,6 +830,10 @@ o.removeAllListeners('answer') o.emit('answer', 44); // nothing printed ``` + +If you have more than one handler for an event, and you'd +like that handler to stop the event being passed to other handlers +then you can call `E.stopEventPropagation()` in that handler. */ #ifndef ESPR_EMBED void jswrap_object_on(JsVar *parent, JsVar *event, JsVar *listener) { @@ -1085,7 +1089,7 @@ void jswrap_function_replaceWith(JsVar *oldFunc, JsVar *newFunc) { oldFunc->varData.native = newFunc->varData.native; // copy fn pointer } else { memset(&oldFunc->varData.native, 0, sizeof(oldFunc->varData.native)); // remove pointer info, zero it all out - oldFunc->flags = (oldFunc->flags&~JSV_VARTYPEMASK) | JSV_FUNCTION; + oldFunc->flags = (oldFunc->flags&~JSV_VARTYPEMASK) | JSV_FUNCTION; } } // If old fn started with 'return' or vice versa... diff --git a/tests/test_signal_emitter.js b/tests/test_eventemitter.js similarity index 100% rename from tests/test_signal_emitter.js rename to tests/test_eventemitter.js diff --git a/tests/test_emit_long.js b/tests/test_eventemitter_long.js similarity index 100% rename from tests/test_emit_long.js rename to tests/test_eventemitter_long.js diff --git a/tests/test_removeListener.js b/tests/test_eventemitter_removeListener.js similarity index 100% rename from tests/test_removeListener.js rename to tests/test_eventemitter_removeListener.js diff --git a/tests/test_eventemitter_stoppropagation.js b/tests/test_eventemitter_stoppropagation.js new file mode 100644 index 0000000000..f32605c467 --- /dev/null +++ b/tests/test_eventemitter_stoppropagation.js @@ -0,0 +1,18 @@ +var X = {}; // in Espruino all objects are EventEmitters +var v = ""; +X.on('foo', function() { print("A"); v+="A"; }) +X.on('foo', function() { print("B"); v+="B"; E.stopEventPropagation(); }) +X.on('foo', function() { print("C"); v+="C"; }) +X.emit('foo'); + +try { + E.stopEventPropagation(); +} catch (e) { + // expected! + v += "x"; +} + +// prints A,B but not C +setTimeout(function() { + result = v=="xAB"; +}, 1);