From f1c957c92e03d7a901542fac49b3cffbe5fe8b3f Mon Sep 17 00:00:00 2001 From: Aioros Date: Sun, 18 Aug 2024 21:49:53 -0400 Subject: [PATCH] Added a gesture calibration tool to help ignore small movements of the fingers --- src/config/GestureCalibrationMenu.js | 59 ++++++ src/config/TouchSettings.js | 54 +++-- src/logic/AppTouchPointerEventsManager.js | 1 + src/logic/CanvasTouchPointerEventsManager.js | 212 +++++++++++++++---- src/logic/TouchPointerEventsManager.js | 25 ++- src/logic/TouchVTTMouseInteractionManager.js | 5 +- style/touch-vtt.css | 22 ++ templates/gesture-calibration.hbs | 26 +++ templates/settings-override.hbs | 6 +- 9 files changed, 337 insertions(+), 73 deletions(-) create mode 100644 src/config/GestureCalibrationMenu.js create mode 100644 templates/gesture-calibration.hbs diff --git a/src/config/GestureCalibrationMenu.js b/src/config/GestureCalibrationMenu.js new file mode 100644 index 0000000..eef65d5 --- /dev/null +++ b/src/config/GestureCalibrationMenu.js @@ -0,0 +1,59 @@ +import {MODULE_NAME, MODULE_DISPLAY_NAME} from './ModuleConstants.js' +import CanvasTouchPointerEventsManager from '../logic/CanvasTouchPointerEventsManager.js' +import {getSetting, ZOOM_THRESHOLD_SETTING, PAN_THRESHOLD_SETTING} from './TouchSettings.js' + +export class GestureCalibrationMenu extends FormApplication { + constructor() { + super() + this.canvasTouchPointerEventsManager = null + } + + static get defaultOptions() { + return foundry.utils.mergeObject(super.defaultOptions, { + classes: ['form'], + popOut: true, + template: `/modules/${MODULE_NAME}/templates/gesture-calibration.hbs`, + id: `${MODULE_NAME}-gesture-calibration-form`, + title: `${MODULE_DISPLAY_NAME} - Gesture Calibration`, + resizable: true, + width: 800, + height: 600, + }) + } + + getData() { + return { + zoomThresholdSetting: getSetting(ZOOM_THRESHOLD_SETTING), + panThresholdSetting: getSetting(PAN_THRESHOLD_SETTING) + } + } + + async _updateObject(event, formData) { + const data = expandObject(formData) + for (let setting in data) { + game.settings.set(MODULE_NAME, setting, data[setting]) + } + } + + async close(...args) { + super.close(...args) + this.canvasTouchPointerEventsManager = null + game.settings.sheet.maximize() + } + + activateListeners(html) { + super.activateListeners(html) + Object.values(ui.windows).forEach(app => app.minimize()) + this.canvasTouchPointerEventsManager = CanvasTouchPointerEventsManager.init($("#touch-vtt-calibration").get(0)) + + const zoomThresholdInput = html.find(`input[name="zoomThreshold"]`) + zoomThresholdInput.change(() => { + this.canvasTouchPointerEventsManager.setForcedZoomThreshold(zoomThresholdInput.val()) + }) + + const panThresholdInput = html.find(`input[name="panThreshold"]`) + panThresholdInput.change(() => { + this.canvasTouchPointerEventsManager.setForcedPanThreshold(panThresholdInput.val()) + }) + } +} \ No newline at end of file diff --git a/src/config/TouchSettings.js b/src/config/TouchSettings.js index 2359cb2..feb12aa 100644 --- a/src/config/TouchSettings.js +++ b/src/config/TouchSettings.js @@ -1,6 +1,7 @@ import {MODULE_NAME, MODULE_DISPLAY_NAME} from './ModuleConstants.js' import {updateButtonSize} from '../tools/EnlargeButtonsTool' import {toggleUtilityControls} from '../tools/UtilityControls.js' +import {GestureCalibrationMenu} from './GestureCalibrationMenu.js' export const CORE_FUNCTIONALITY = "core" @@ -27,7 +28,8 @@ export const MEASUREMENT_HUD_OFF = "off" export const MEASUREMENT_HUD_RIGHT = "right" export const MEASUREMENT_HUD_LEFT = "left" -export const ZOOM_SENSITIVITY_SETTING = "zoomSensitivity" +export const ZOOM_THRESHOLD_SETTING = "zoomThreshold" +export const PAN_THRESHOLD_SETTING = "panThreshold" export const DEBUG_MODE_SETTING = "debugMode" @@ -48,9 +50,8 @@ export function getSetting(settingName) { } class SettingsOverrideMenu extends FormApplication { - constructor(exampleOption) { + constructor() { super() - this.exampleOption = exampleOption } static get defaultOptions() { @@ -64,20 +65,14 @@ class SettingsOverrideMenu extends FormApplication { } getData() { - var touchVttOverrideSettings = [...game.settings.settings].filter(s => s[0].startsWith(MODULE_NAME) && s[0].endsWith("_override")) - var data = { + const touchVttOverrideSettings = [...game.settings.settings].filter(s => s[0].startsWith(MODULE_NAME) && s[0].endsWith("_override")) + const data = { settings: Object.fromEntries( [...touchVttOverrideSettings] .map(s => { s[0] = s[0].split(".")[1] var settingValue = game.settings.get(MODULE_NAME, s[0]) s[1].currentValue = settingValue - for (let choice in s[1].choices) { - if (typeof s[1].choices[choice] == "string") { - s[1].choices[choice] = {label: s[1].choices[choice]} - } - s[1].choices[choice].selected = (choice == settingValue) - } return s }) ), @@ -216,7 +211,7 @@ export function registerTouchSettings() { // Client settings game.settings.register(MODULE_NAME, CORE_FUNCTIONALITY, { - name: "Core Functionality", + name: "Core Functionality" + (game.settings.get(MODULE_NAME, CORE_FUNCTIONALITY + "_override") == "override_off" ? "" : " *"), hint: "Caution: disabling this option will remove all TouchVTT functionality, and other options will be ignored", scope: "client", config: true, @@ -239,18 +234,32 @@ export function registerTouchSettings() { default: GESTURE_MODE_COMBINED, }) - game.settings.register(MODULE_NAME, ZOOM_SENSITIVITY_SETTING, { + game.settings.register(MODULE_NAME, ZOOM_THRESHOLD_SETTING, { name: "Zoom Sensitivity", hint: "Sensitivity of the zoom gesture (if enabled)", scope: "client", - config: true, + config: false, type: Number, range: { min: 0, - max: 2, - step: 0.1 + max: 100, + step: 1 }, - default: 1, + default: 100, + }) + + game.settings.register(MODULE_NAME, PAN_THRESHOLD_SETTING, { + name: "Pan Sensitivity", + hint: "Sensitivity of the pan gesture (if enabled)", + scope: "client", + config: false, + type: Number, + range: { + min: 0, + max: 100, + step: 1 + }, + default: 100, }) game.settings.register(MODULE_NAME, DIRECTIONAL_ARROWS_SETTING, { @@ -325,7 +334,6 @@ export function registerTouchSettings() { }) // Override menu - game.settings.registerMenu(MODULE_NAME, "SettingsOverrideMenu", { name: "Client Settings Overrides", label: "Configure Overrides", @@ -335,6 +343,16 @@ export function registerTouchSettings() { restricted: true }) + // Testing new calibration menu + game.settings.registerMenu(MODULE_NAME, "GestureCalibrationMenu", { + name: "Touch Gesture Calibration", + label: "Calibrate Touch Gestures", + hint: "Gesture detection can be influenced by your display size and resolution. Use this tool to calibrate if you have issues with sensitivity.", + icon: "fas fa-wrench", + type: GestureCalibrationMenu, + restricted: true + }) + // Hook to disable overridden settings Hooks.on("renderSettingsConfig", (settingsConfig, settingsElem, settingsInfo) => { var touchVttSettings = settingsInfo.categories.find(c => c.id == MODULE_NAME).settings diff --git a/src/logic/AppTouchPointerEventsManager.js b/src/logic/AppTouchPointerEventsManager.js index 3386c3c..329e597 100644 --- a/src/logic/AppTouchPointerEventsManager.js +++ b/src/logic/AppTouchPointerEventsManager.js @@ -1,4 +1,5 @@ import TouchPointerEventsManager from './TouchPointerEventsManager.js' +import {dispatchModifiedEvent} from "./FakeTouchEvent.js" class AppTouchPointerEventsManager extends TouchPointerEventsManager { constructor(selector) { diff --git a/src/logic/CanvasTouchPointerEventsManager.js b/src/logic/CanvasTouchPointerEventsManager.js index c1acd8c..2c8927b 100644 --- a/src/logic/CanvasTouchPointerEventsManager.js +++ b/src/logic/CanvasTouchPointerEventsManager.js @@ -1,8 +1,9 @@ +import {MODULE_DISPLAY_NAME} from '../config/ModuleConstants.js' import {dispatchCopy, dispatchModifiedEvent} from "./FakeTouchEvent.js" import Vectors from './Vectors.js' import FoundryCanvas from '../foundryvtt/FoundryCanvas.js' import Screen from '../browser/Screen.js' -import {getSetting, ZOOM_SENSITIVITY_SETTING, GESTURE_MODE_SETTING, GESTURE_MODE_SPLIT, GESTURE_MODE_OFF} from '../config/TouchSettings.js' +import {getSetting, DEBUG_MODE_SETTING, ZOOM_THRESHOLD_SETTING, PAN_THRESHOLD_SETTING, GESTURE_MODE_SETTING, GESTURE_MODE_SPLIT, GESTURE_MODE_OFF} from '../config/TouchSettings.js' import TouchPointerEventsManager from './TouchPointerEventsManager.js' // This class is similar in structure to the original CanvasTouchToMouseAdapter, but it doesn't capture/prevent events @@ -12,6 +13,22 @@ class CanvasTouchPointerEventsManager extends TouchPointerEventsManager { constructor(element) { super(element) + this._forcedZoomThreshold = null + this._forcedPanThreshold = null + + this.GESTURE_STATUSES = {NONE: 0, WAITING: 1, ACTIVE: 2} + this._zoomGesture = { + status: this.GESTURE_STATUSES.NONE, + initiatingCoords: null, + activationCoords: null, + activationWorldCoords: null + } + this._panGesture = { + status: this.GESTURE_STATUSES.NONE, + initiatingCoords: null, + activationCoords: null + } + // Fix for some trackpads sending pointerdown of type mouse without any previous move event document.body.addEventListener("pointerdown", evt => { if (evt.isTrusted && !evt.pressure && evt.target === element) { @@ -79,24 +96,50 @@ class CanvasTouchPointerEventsManager extends TouchPointerEventsManager { } } - onStartMultiTouch(event) { - if (this.gesturesEnabled()) { + onTouchAdded(event) { + if (this.touchIds.length > 1) { // This is to cancel any drag-style action (usually a selection rectangle) when we start having multiple touches const cancelEvent = new MouseEvent("contextmenu", {clientX: 0, clientY: 0, bubbles: true, cancelable: true, view: window, button: 2}) event.target.dispatchEvent(cancelEvent) } } + onTouchRemoved(event) { + if (this.touchIds.length > 0) { + this.disableGestures() + if (getSetting(DEBUG_MODE_SETTING)) { + console.log(MODULE_DISPLAY_NAME + ": disabled gestures") + } + } else { + this.enableGestures() + if (getSetting(DEBUG_MODE_SETTING)) { + console.log(MODULE_DISPLAY_NAME + ": enabled gestures") + } + } + + this._zoomGesture = { + status: this.GESTURE_STATUSES.NONE, + initiatingCoords: null, + activationCoords: null, + activationWorldCoords: null + } + this._panGesture = { + status: this.GESTURE_STATUSES.NONE, + initiatingCoords: null, + activationCoords: null + } + } + handleTouchMove(event) { this.updateActiveTouch(event) - + switch (this.touchIds.length) { case 2: if (this.gesturesEnabled()) { if (this.useSplitGestures()) { - this.handleTwoFingerZoom(event) + this.handleTwoFingerZoom() } else { - this.handleTwoFingerZoomAndPan(event) + this.handleTwoFingerZoomAndPan() } } break @@ -104,7 +147,7 @@ class CanvasTouchPointerEventsManager extends TouchPointerEventsManager { case 3: case 4: if (this.gesturesEnabled()) { - this.handleMultiFingerPan(event) + this.handleMultiFingerPan() } break @@ -126,7 +169,79 @@ class CanvasTouchPointerEventsManager extends TouchPointerEventsManager { const firstTouch = this.touches[touchIds[0]] const secondTouch = this.touches[touchIds[1]] - FoundryCanvas.zoom(this.calcZoom(firstTouch, secondTouch)) + if (this._zoomGesture.status == this.GESTURE_STATUSES.NONE) { + this._zoomGesture.initiatingCoords = [{...firstTouch.current}, {...secondTouch.current}] + this._zoomGesture.status = this.GESTURE_STATUSES.WAITING + } + const initiatingDistance = Vectors.distance(this._zoomGesture.initiatingCoords[0], this._zoomGesture.initiatingCoords[1]) + const currentDistance = Vectors.distance(firstTouch.current, secondTouch.current) + + if (this._zoomGesture.status < this.GESTURE_STATUSES.ACTIVE) { + if (Math.abs(currentDistance - initiatingDistance) > this.zoomThresholdFunction(this.getZoomThreshold())) { + this._zoomGesture.activationCoords = [{...firstTouch.current}, {...secondTouch.current}] + this._zoomGesture.activationWorldCoords = [FoundryCanvas.screenToWorld({...firstTouch.current}), FoundryCanvas.screenToWorld({...secondTouch.current})] + this._zoomGesture.status = this.GESTURE_STATUSES.ACTIVE + } + } + + if (this._zoomGesture.status == this.GESTURE_STATUSES.ACTIVE) { + FoundryCanvas.zoom(this.calcZoom()) + } + + } + + calcZoom() { + const touchIds = this.touchIds + const originalWorldDistance = Vectors.distance(this._zoomGesture.activationWorldCoords[0], this._zoomGesture.activationWorldCoords[1]) + const newScreenDistance = Vectors.distance(this.touches[touchIds[0]].current, this.touches[touchIds[1]].current) + const newScale = newScreenDistance / originalWorldDistance + return newScale + } + + zoomThresholdFunction(threshold) { + if (threshold == 0) return Infinity + if (threshold > 80) { + return -threshold/2 + 50 + } else if (threshold > 30) { + return -threshold + 90 + } else { + return -10 * threshold + 360 + } + } + + setForcedZoomThreshold(threshold) { + this._forcedZoomThreshold = threshold + } + + unsetForcedZoomThreshold(threshold) { + this._forcedZoomThreshold = null + } + + getZoomThreshold() { + if (this._forcedZoomThreshold !== null) { + return this._forcedZoomThreshold + } + return getSetting(ZOOM_THRESHOLD_SETTING) + } + + setForcedPanThreshold(threshold) { + this._forcedPanThreshold = threshold + } + + unsetForcedPanThreshold(threshold) { + this._forcedPanThreshold = null + } + + getPanThreshold() { + if (this._forcedPanThreshold !== null) { + return this._forcedPanThreshold + } + return getSetting(PAN_THRESHOLD_SETTING) + } + + calcPanCorrection(transform, touch) { + const touchedPointOnWorldAfter = transform.applyInverse(touch.current) + return Vectors.subtract(touchedPointOnWorldAfter, touch.world) } handleMultiFingerPan() { @@ -137,46 +252,53 @@ class CanvasTouchPointerEventsManager extends TouchPointerEventsManager { const touchIds = this.touchIds const adjustedTransform = FoundryCanvas.worldTransform - //let panCorrection - //if (touchIds.length === 2) { - // panCorrection = Vectors.centerBetween( - // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[0]]), - // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[1]]), - // ) - //} else { - // panCorrection = Vectors.centerOf( - // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[0]]), - // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[1]]), - // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[2]]), - // ) - //} - - // It seems to me that panning to the center between the touches is disorienting and creates unwanted movement - // I prefer trying out this version where we anchor to the first touch, I'll leave the existing above in case we want to revert - let panCorrection = this.calcPanCorrection(adjustedTransform, this.touches[touchIds[0]]) - - const centerBefore = FoundryCanvas.screenToWorld(Screen.center) - const worldCenter = Vectors.subtract(centerBefore, panCorrection) - - FoundryCanvas.pan({ x: worldCenter.x, y: worldCenter.y }) - } - - calcZoom(firstTouch, secondTouch) { - const originalScreenDistance = Vectors.distance(firstTouch.start, secondTouch.start) - const originalWorldDistance = Vectors.distance(firstTouch.world, secondTouch.world) - const originalScale = originalScreenDistance / originalWorldDistance - const newScreenDistance = Vectors.distance(firstTouch.current, secondTouch.current) - const newScale = newScreenDistance / originalWorldDistance - if (Math.abs(newScale - originalScale) > 0.015) { - return newScale - } else { - return originalScale + const firstTouch = this.touches[touchIds[0]] + if (this._panGesture.status == this.GESTURE_STATUSES.NONE) { + this._panGesture.initiatingCoords = {...firstTouch.current} + this._panGesture.status = this.GESTURE_STATUSES.WAITING + } + const currentDistance = Vectors.distance(firstTouch.current, this._panGesture.initiatingCoords) + if (this._panGesture.status < this.GESTURE_STATUSES.ACTIVE) { + if (currentDistance > this.panThresholdFunction(this.getPanThreshold())) { + this._panGesture.status = this.GESTURE_STATUSES.ACTIVE + } + } + + if (this._panGesture.status == this.GESTURE_STATUSES.ACTIVE) { + //let panCorrection + //if (touchIds.length === 2) { + // panCorrection = Vectors.centerBetween( + // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[0]]), + // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[1]]), + // ) + //} else { + // panCorrection = Vectors.centerOf( + // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[0]]), + // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[1]]), + // this.calcPanCorrection(adjustedTransform, this.touches[touchIds[2]]), + // ) + //} + + // It seems to me that panning to the center between the touches is disorienting and creates unwanted movement + // I prefer trying out this version where we anchor to the first touch, I'll leave the existing above in case we want to revert + let panCorrection = this.calcPanCorrection(adjustedTransform, this.touches[touchIds[0]]) + + const centerBefore = FoundryCanvas.screenToWorld(Screen.center) + const worldCenter = Vectors.subtract(centerBefore, panCorrection) + + FoundryCanvas.pan({ x: worldCenter.x, y: worldCenter.y }) } } - calcPanCorrection(transform, touch) { - const touchedPointOnWorldAfter = transform.applyInverse(touch.current) - return Vectors.subtract(touchedPointOnWorldAfter, touch.world) + panThresholdFunction(threshold) { + if (threshold == 0) return Infinity + if (threshold > 50) { + return -4/5 * threshold + 80 + } else if (threshold > 20) { + return -2 * threshold + 140 + } else { + return -10 * threshold + 300 + } } useSplitGestures() { diff --git a/src/logic/TouchPointerEventsManager.js b/src/logic/TouchPointerEventsManager.js index 23da58b..e5edb63 100644 --- a/src/logic/TouchPointerEventsManager.js +++ b/src/logic/TouchPointerEventsManager.js @@ -1,5 +1,6 @@ +import {MODULE_DISPLAY_NAME} from '../config/ModuleConstants.js' +import {getSetting, DEBUG_MODE_SETTING} from '../config/TouchSettings.js' import Touch from './Touch.js' -import {dispatchModifiedEvent} from "./FakeTouchEvent.js" class TouchPointerEventsManager { constructor(element) { @@ -25,6 +26,7 @@ class TouchPointerEventsManager { preHandleTouch(event) {} handleTouch(event) { + const preLength = this.touchIds.length this.preHandleAll(event) @@ -46,8 +48,8 @@ class TouchPointerEventsManager { break case 'pointerup': - //this.handleTouchEnd(event) - this.handleEndAll(event) + this.handleTouchEnd(event) + //this.handleEndAll(event) break case 'pointercancel': @@ -59,15 +61,27 @@ class TouchPointerEventsManager { break } } + if (preLength != this.touchIds.length && getSetting(DEBUG_MODE_SETTING)) { + console.log(MODULE_DISPLAY_NAME + ": touches changed: " + preLength + " -> " + this.touchIds.length) + } } onStartMultiTouch(event) { } + onTouchAdded(event) { + + } + + onTouchRemoved(event) { + + } + handleTouchStart(event) { + const prevTouches = this.touchIds.length this.updateActiveTouch(event) - if (this.touchIds.length > 1) { + if (prevTouches <= 1 && this.touchIds.length > 1) { this.onStartMultiTouch(event) } } @@ -91,16 +105,19 @@ class TouchPointerEventsManager { } else { if (event.type == "pointerdown" && event.buttons == 1 && event.pointerType != "pen") { this.touches[id] = new Touch(event, event) + this.onTouchAdded(event) } } } cleanUpAll() { this.touches = {} + this.onTouchRemoved(event) } cleanUpTouch(event) { delete this.touches[event.pointerId] + this.onTouchRemoved(event) } getEventListenerOptions() { diff --git a/src/logic/TouchVTTMouseInteractionManager.js b/src/logic/TouchVTTMouseInteractionManager.js index 49e971f..dd4d96c 100644 --- a/src/logic/TouchVTTMouseInteractionManager.js +++ b/src/logic/TouchVTTMouseInteractionManager.js @@ -1,3 +1,4 @@ +import {MODULE_DISPLAY_NAME} from '../config/ModuleConstants.js' import {getSetting, DEBUG_MODE_SETTING} from '../config/TouchSettings.js' export class TouchVTTMouseInteractionManager { @@ -24,7 +25,7 @@ export class TouchVTTMouseInteractionManager { return this._state; }, set(value) { - console.log(this.object.constructor.name, this._state + " -> " + value + " (" + (new Error()).stack?.split("\n")[2]?.trim().split(" ")[1] + ")") + console.log(MODULE_DISPLAY_NAME + ": " + this.object.constructor.name, this._state + " -> " + value + " (" + (new Error()).stack?.split("\n")[2]?.trim().split(" ")[1] + ")") this._state = value; } }); @@ -566,7 +567,7 @@ export class TouchVTTMouseInteractionManager { */ #handleMouseUp(event) { if (getSetting(DEBUG_MODE_SETTING)) { - console.log(this.object.constructor.name, "handleMouseUp from state " + this.state + ": ", event.target.constructor.name, event.constructor.name, event.type, event.pointerType, + console.log(MODULE_DISPLAY_NAME + ": " + this.object.constructor.name, "handleMouseUp from state " + this.state + ": ", event.target.constructor.name, event.constructor.name, event.type, event.pointerType, event.nativeEvent?.constructor.name, event.nativeEvent?.target.tagName, event.nativeEvent?.type, event.nativeEvent?.pointerType, "trust:" + event.nativeEvent?.isTrusted + "," + event.nativeEvent?.touchvttTrusted ) } diff --git a/style/touch-vtt.css b/style/touch-vtt.css index 5df629b..371cc9f 100644 --- a/style/touch-vtt.css +++ b/style/touch-vtt.css @@ -81,4 +81,26 @@ font-size: var(--font-size-10); max-width: 110px; height: 20px; +} + +#touch-vtt-gesture-calibration-form { + mix-blend-mode: hard-light; +} + +#touch-vtt-gesture-calibration-form .window-content { + background: white; +} + +#touch-vtt-gesture-calibration-form .window-content form.flexcol > *{ + flex: 0; +} + +#touch-vtt-calibration { + flex: 1 !important; + background: gray; +} + +#touch-vtt-calibration canvas { + width: 100%; + height: 100%; } \ No newline at end of file diff --git a/templates/gesture-calibration.hbs b/templates/gesture-calibration.hbs new file mode 100644 index 0000000..fe6a582 --- /dev/null +++ b/templates/gesture-calibration.hbs @@ -0,0 +1,26 @@ +
+

These setting determine how much your fingers need to move before a gesture is activated. If you have too much unwanted zoom/pan, try lowering these values and try it out in this window. Click OK to save your preferences.

+
+
+ +
+ + {{zoomThresholdSetting}} +
+
+
+ +
+ + {{panThresholdSetting}} +
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/templates/settings-override.hbs b/templates/settings-override.hbs index d96188f..353d0f0 100644 --- a/templates/settings-override.hbs +++ b/templates/settings-override.hbs @@ -4,11 +4,9 @@
-
+ {{/each}}