diff --git a/CHANGELOG.md b/CHANGELOG.md index b379209..f742d0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,18 @@ ## In development -### Other -- Drag Ruler is now aware of good default values for the lancer game system (thanks to Grygon) +### New features +- The color used to indicate speed ranges is now configurable +- The settings dialog has been reworked + +### System compatibility +- Drag Ruler's Generic SpeedProvider is now aware of good default values for the lancer game system (thanks to Grygon) + +### API changes +This release introduces a new API that is incompatible with the old API. The new API offers more flexibility for users and Speed Providers alike and allows to add new features in the future without breaking compatibility again. The old API will continue to function, but to profit from any of the features below Speed Providers need to switch to the new API. For more details check out the API documentation. + +The following things have changed with the new API: +- Colors used by speed providers can now be changed by the user via configuration +- Speed Providers can now offer settings to the user that will be integrated into Drag Ruler's settings menu + ## 1.2.2 ### Translation diff --git a/lang/en.json b/lang/en.json index 4b837be..af05514 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,31 +1,58 @@ { "drag-ruler": { + "genericSpeedProvider": { + "settings": { + "dashMultiplier": { + "name": "Dash Multiplier", + "hint": "This can be used to give tokens a secondary speed during coloring of the measured path. Set it to 0 to disable the secondary speed." + }, + "speedAttribute": { + "name": "Speed Attribute", + "hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path." + } + }, + "speeds": { + "walk": "walk", + "dash": "dash" + } + }, "settings": { "alwaysShowSpeedForPCs": { "name": "Show PC speed to everyone", "hint": "If enabled the coloring based on actor speed for player characters will shown to everyone, even if they don't have observer permission for the character sheet." }, - "dashMultiplier": { - "name": "Dash Multiplier", - "hint": "This can be used to give tokens a secondary speed during coloring of the measured path. Set it to 0 to disable the secondary speed." - }, - "speedAttribute": { - "name": "Speed Attribute", - "hint": "The attribute that defines a token's walking speed. This is used during coloring of the measured path." - }, - "speedProvider": { - "name": "Speed Settings Provider", - "hint": "Select who provides speed information for tokens duing coloring. Using a game system or module may provide more flexible coloring than sticking to the options provided by Drag Ruler.", - "choices": { - "module": "Module", - "native": "Drag Ruler", - "system": "System" + "speedProviderSettings": { + "name": "Speed Provider Settings", + "hint": "The Speed Provider Settings contain all the game system specific settings.", + "button": "Speed Provider Settings", + "windowTitle": "Speed Provider Settings", + "headers": { + "speedProvider": "Speed Provider", + "speedProviderSettings": "Speed Provider specific settings" + }, + "activeProvider": { + "name": "Currently active Speed Provider", + "hint": "The settings show below depend on the active speed provider. If the GM selects a different speed provider the available settings may change." + }, + "noSettings": "This speed provider doesn't offer any configuration options.", + "color": { + "name": "Color for {colorName}", + "hint": "The color that will be used to color square that are within {colorName} range", + "unreachable": { + "name": "unreachable", + "hint": "The color for spaces that aren't reachable by the dragged token" + } + }, + "speedProvider": { + "name": "Speed Settings Provider", + "hint": "Select who provides speed information for tokens duing coloring. Drag Ruler offers a generic speed provider that provides basic functionality and should work for all game systems if configured correctly. More speed providers can be made available via game systems and installed modules. Selecting a different speed provider than the generic speed provider may offer a better integration into the rules of the game system you're using. The options below are dependent upon the speed provider selected here.", + "choices": { + "module": "Module {name}", + "native": "Generic", + "system": "System {name}" + } } }, - "staticFirstColor": { - "name": "Static First Color", - "hint": "Use a static color for the first movement range instead of the player color" - }, "swapSpacebarRightClick": { "name": "Swap spacebar and right click", "hint": "Swaps the functions of spacebar and right click during dragging. If enabled right click will place waypoints and spacebar will delete them" diff --git a/module.json b/module.json index 2505557..1dec6e0 100644 --- a/module.json +++ b/module.json @@ -15,6 +15,9 @@ "esmodules": [ "src/main.js" ], + "templates": [ + "speed_provider_settings.html" + ], "languages": [ { "lang": "en", diff --git a/src/api.js b/src/api.js index 0d1a8d1..2e65855 100644 --- a/src/api.js +++ b/src/api.js @@ -1,37 +1,60 @@ +import {GenericSpeedProvider, SpeedProvider} from "./speed_provider.js" +import {settingsKey} from "./settings.js" + export const availableSpeedProviders = {} export let currentSpeedProvider = undefined function register(module, type, speedProvider) { - const providerSetting = game.settings.settings.get("drag-ruler.speedProvider") + const id = `${type}.${module.id}` + let providerInstance + if (speedProvider.prototype instanceof SpeedProvider) { + providerInstance = new speedProvider(id) + } + else { + speedProvider.id = id + providerInstance = speedProvider + } + setupProvider(providerInstance) +} - // Add the registered module to the settings entry - providerSetting.config = true - const moduleName = module.data.title - const typeTitle = game.i18n.localize(`drag-ruler.settings.speedProvider.choices.${type}`) - providerSetting.choices[`${type}.${module.id}`] = `${typeTitle} ${moduleName}` - availableSpeedProviders[`${type}.${module.id}`] = speedProvider - providerSetting.default = getDefaultSpeedProvider() +function setupProvider(speedProvider) { + if (speedProvider instanceof SpeedProvider) { + const unreachableColor = {id: "unreachable", default: speedProvider.defaultUnreachableColor, name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"} + for (const color of speedProvider.colors.concat([unreachableColor])) { + game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.color.${color.id}`, { + config: false, + scope: "client", + type: Number, + default: color.default, + }) + } + for (const setting of speedProvider.settings) { + setting.config = false + game.settings.register(settingsKey, `speedProviders.${speedProvider.id}.setting.${setting.id}`, setting) + } + } + availableSpeedProviders[speedProvider.id] = speedProvider + game.settings.settings.get("drag-ruler.speedProvider").default = getDefaultSpeedProvider() updateSpeedProvider() } -function getDefaultSpeedProvider() { - const providerSetting = game.settings.settings.get("drag-ruler.speedProvider") - const settingKeys = Object.keys(providerSetting.choices) +export function getDefaultSpeedProvider() { + const providerIds = Object.keys(availableSpeedProviders) // Game systems take the highest precedence for the being the default - const gameSystem = settingKeys.find(key => key.startsWith("system.")) + const gameSystem = providerIds.find(key => key.startsWith("system.")) if (gameSystem) return gameSystem // If no game system is registered modules are next up. // For lack of a method to select the best module we're just falling back to taking the next best module // settingKeys should always be sorted the same way so this should achive a stable default - const module = settingKeys.find(key => key.startsWith("module.")) + const module = providerIds.find(key => key.startsWith("module.")) if (module) return module // If neither a game system or a module is found fall back to the native implementation - return settingKeys[0] + return providerIds[0] } export function updateSpeedProvider() { @@ -40,8 +63,37 @@ export function updateSpeedProvider() { currentSpeedProvider = availableSpeedProviders[configuredProvider] ?? availableSpeedProviders[game.settings.settings.get("drag-ruler.speedProvider").default] } -export function setCurrentSpeedProvider(newSpeedProvider) { - currentSpeedProvider = newSpeedProvider +export function initApi() { + const genericSpeedProviderInstance = new GenericSpeedProvider("native") + setupProvider(genericSpeedProviderInstance) +} + +export function getRangesFromSpeedProvider(token) { + try { + if (currentSpeedProvider instanceof Function) + return currentSpeedProvider(token, 0x00FF00) + const ranges = currentSpeedProvider.getRanges(token) + for (const range of ranges) { + range.color = game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.${range.color}`) + } + return ranges + } + catch (e) { + console.error(e) + return [] + } +} + +export function getUnreachableColorFromSpeedProvider() { + if (currentSpeedProvider instanceof Function) + return 0xFF0000 + try { + return game.settings.get(settingsKey, `speedProviders.${currentSpeedProvider.id}.color.unreachable`) + } + catch (e) { + console.error(e) + return 0xFF0000 + } } export function registerModule(moduleId, speedProvider) { diff --git a/src/main.js b/src/main.js index c8f9545..364a61a 100644 --- a/src/main.js +++ b/src/main.js @@ -1,30 +1,28 @@ "use strict" -import {availableSpeedProviders, currentSpeedProvider, registerModule, registerSystem, setCurrentSpeedProvider} from "./api.js" +import {getRangesFromSpeedProvider, getUnreachableColorFromSpeedProvider, initApi, registerModule, registerSystem} from "./api.js" import {getHexSizeSupportTokenGridCenter} from "./compatibility.js" import {measure, moveTokens, onMouseMove} from "./foundry_imports.js" import {registerSettings, settingsKey} from "./settings.js" +import {SpeedProvider} from "./speed_provider.js" Hooks.once("init", () => { registerSettings() + initApi() hookTokenDragHandlers() hookRulerFunctions() hookKeyboardManagerFunctions() patchRulerHighlightMeasurement() - availableSpeedProviders["native"] = nativeSpeedProvider - setCurrentSpeedProvider(nativeSpeedProvider) - window.dragRuler = { getColorForDistance, registerModule, - registerSystem + registerSystem, } - }) Hooks.once("ready", () => { - Hooks.callAll("dragRuler.ready") + Hooks.callAll("dragRuler.ready", SpeedProvider) }) Hooks.on("canvasReady", () => { @@ -241,21 +239,6 @@ function strInsertAfter(haystack, needle, strToInsert) { return haystack.slice(0, pos) + strToInsert + haystack.slice(pos) } -function nativeSpeedProvider(token, playercolor) { - const speedAttribute = game.settings.get(settingsKey, "speedAttribute") - if (!speedAttribute) - return [] - const tokenSpeed = getProperty(token, speedAttribute) - if (tokenSpeed === undefined) { - console.warn(`Drag Ruler | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`) - return [] - } - const dashMultiplier = game.settings.get(settingsKey, "dashMultiplier") - if (!dashMultiplier) - return [{range: tokenSpeed, color: playercolor}] - return [{range: tokenSpeed, color: playercolor}, {range: tokenSpeed * dashMultiplier, color: 0xFFFF00}] -} - export function getColorForDistance(startDistance, subDistance=0) { if (!this.isDragRuler) return this.color @@ -266,15 +249,14 @@ export function getColorForDistance(startDistance, subDistance=0) { return this.color } const distance = startDistance + subDistance - const firstColor = game.settings.get(settingsKey, "staticFirstColor") ? 0x00FF00 : this.color - const ranges = currentSpeedProvider(this.draggedToken, firstColor) + const ranges = getRangesFromSpeedProvider(this.draggedToken) if (ranges.length === 0) return this.color const currentRange = ranges.reduce((minRange, currentRange) => { if (distance <= currentRange.range && currentRange.range < minRange.range) return currentRange return minRange - }, {range: Infinity, color: 0xFF0000}) + }, {range: Infinity, color: getUnreachableColorFromSpeedProvider()}) return currentRange.color } diff --git a/src/settings.js b/src/settings.js index 8dcc4bb..5e3f0a3 100644 --- a/src/settings.js +++ b/src/settings.js @@ -1,5 +1,5 @@ -import {updateSpeedProvider} from "./api.js"; -import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js" +import {availableSpeedProviders, getDefaultSpeedProvider, updateSpeedProvider} from "./api.js"; +import {SpeedProvider} from "./speed_provider.js" export const settingsKey = "drag-ruler"; @@ -24,42 +24,189 @@ export function registerSettings() { // This setting will be modified by the api if modules register to it game.settings.register(settingsKey, "speedProvider", { - name: "drag-ruler.settings.speedProvider.name", - hint: "drag-ruler.settings.speedProvider.hint", scope: "world", config: false, - type: Object, - choices: { - "native": game.i18n.localize("drag-ruler.settings.speedProvider.choices.native") - }, - default: "native", + type: String, + default: getDefaultSpeedProvider(), onChange: updateSpeedProvider, }) - game.settings.register(settingsKey, "speedAttribute", { - name: "drag-ruler.settings.speedAttribute.name", - hint: "drag-ruler.settings.speedAttribute.hint", - scope: "world", - config: true, - type: String, - default: getDefaultSpeedAttribute(), - }) - - game.settings.register(settingsKey, "dashMultiplier", { - name: "drag-ruler.settings.dashMultiplier.name", - hint: "drag-ruler.settings.dashMultiplier.hint", - scope: "world", - config: true, - type: Number, - default: getDefaultDashMultiplier(), - }) - - game.settings.register(settingsKey, "staticFirstColor", { - name: "drag-ruler.settings.staticFirstColor.name", - hint: "drag-ruler.settings.staticFirstColor.hint", - scope: "world", - config: true, - type: Boolean, - default: false, + game.settings.registerMenu(settingsKey, "speedProviderSettings", { + name: "drag-ruler.settings.speedProviderSettings.name", + hint: "drag-ruler.settings.speedProviderSettings.hint", + label: "drag-ruler.settings.speedProviderSettings.button", + icon: "fas fa-tachometer-alt", + type: SpeedProviderSettings, + restricted: false, }) } + +class SpeedProviderSettings extends FormApplication { + static get defaultOptions() { + return mergeObject(super.defaultOptions, { + id: "drag-ruler-speed-provider-settings", + title: game.i18n.localize("drag-ruler.settings.speedProviderSettings.windowTitle"), + template: "modules/drag-ruler/templates/speed_provider_settings.html", + width: 600, + }) + } + + getData(options={}) { + const data = {} + data.isGM = game.user.isGM + const selectedProvider = game.settings.get(settingsKey, "speedProvider") + + // Insert all speed providers into the template data + data.providers = Object.values(availableSpeedProviders).map(speedProvider => { + const provider = {} + provider.id = speedProvider.id + provider.hasSettings = speedProvider instanceof SpeedProvider + if (provider.hasSettings) + provider.settings = enumerateProviderSettings(speedProvider) + let dotPosition = provider.id.indexOf(".") + if (dotPosition === -1) + dotPosition = provider.id.length + const type = provider.id.substring(0, dotPosition) + const id = provider.id.substring(dotPosition + 1) + if (type === "native") { + provider.selectTitle = game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.choices.native") + } + else { + let name + if (type === "module") { + name = game.modules.get(id).data.title + } + else { + name = game.system.data.title + } + provider.selectTitle = game.i18n.format(`drag-ruler.settings.speedProviderSettings.speedProvider.choices.${type}`, {name}) + } + provider.isSelected = provider.id === selectedProvider + return provider + }) + data.selectedProviderName = data.providers.find(provider => provider.isSelected).selectTitle + + data.providerSelection = { + id: "speedProvider", + name: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.name"), + hint: game.i18n.localize("drag-ruler.settings.speedProviderSettings.speedProvider.hint"), + type: String, + choices: data.providers.reduce((choices, provider) => { + choices[provider.id] = provider.selectTitle + return choices + }, {}), + value: selectedProvider, + isCheckbox: false, + isSelect: true, + isRange: false, + } + return data + } + + async _updateObject(event, formData) { + for (let [key, value] of Object.entries(formData)) { + // Check if this is color, convert the value to an integer + const splitKey = key.split(".", 3) + if (splitKey[0] !== "native") + splitKey.shift() + if (splitKey.length >= 2 && splitKey[1] == "color") { + value = parseInt(value.substring(1), 16) + } + + // Don't change settings for speed providers that aren't currently active + if (key !== "speedProvider" && !key.startsWith(formData.speedProvider)) + continue + + // Get the key for the current setting + let setting + if (key === "speedProvider") + setting = "speedProvider" + else + setting = `speedProviders.${key}` + + // Get the old setting value + const oldValue = game.settings.get(settingsKey, setting) + + // Only update the setting if it has been changed (this leaves the default in place if it hasn't been touched) + if (value !== oldValue) + game.settings.set(settingsKey, setting, value) + } + + // Activate the configured speed provider + updateSpeedProvider() + } + + activateListeners(html) { + super.activateListeners(html) + html.find("select[name=speedProvider]").change(this.onSpeedProviderChange.bind(this)) + } + + onSpeedProviderChange(event) { + // Hide all module settings + document.querySelectorAll(".drag-ruler-provider-settings").forEach(element => element.style.display = "none") + // Show the settings block for the currently selected module + document.getElementById(`drag-ruler.provider.${event.currentTarget.value}`).style.display = "" + + // Recalculate window height + this.element[0].style.height = null + this.position.height = undefined + } +} + +function toDomHex(value) { + const hex = value.toString(16) + return "#" + "0".repeat(Math.max(0, 6 - hex.length)) + hex +} + +function enumerateProviderSettings(provider) { + const colorSettings = [] + const unreachableColor = {id: "unreachable", name: "drag-ruler.settings.speedProviderSettings.color.unreachable.name"} + + // Resolve settings for the colors + for (const color of provider.colors.concat([unreachableColor])) { + // Localize the name, if avaliable. If no name is available use the id as name + const colorName = color.name ? game.i18n.localize(color.name) : color.id + let hint + if (color === unreachableColor) + hint = game.i18n.localize("drag-ruler.settings.speedProviderSettings.color.unreachable.hint") + else + hint = game.i18n.format("drag-ruler.settings.speedProviderSettings.color.hint", {colorName}) + colorSettings.push({ + id: `${provider.id}.color.${color.id}`, + name: game.i18n.format("drag-ruler.settings.speedProviderSettings.color.name", {colorName}), + hint: hint, + type: Number, + value: toDomHex(game.settings.get(settingsKey, `speedProviders.${provider.id}.color.${color.id}`)), + isCheckbox: false, + isSelect: false, + isRange: false, + isColor: true, + }) + } + + // Prepare regular settings + const settings = [] + for (const setting of provider.settings) { + try { + if (setting.scope === "world" && !game.user.isGM) + continue + const s = duplicate(setting) + s.id = `${provider.id}.setting.${s.id}` + s.name = game.i18n.localize(s.name) + s.hint = game.i18n.localize(s.hint) + s.value = provider.getSetting(setting.id) + s.type = setting.type instanceof Function ? setting.type.name : "String" + s.isCheckbox = setting.type === Boolean + s.isSelect = s.choices !== undefined + s.isRange = (setting.type === Number) && s.range + s.isColor = false + settings.push(s) + } + catch (e) { + console.warn(`Drag Ruler | The following error occured while rendering setting "${setting.id}" of module/system "${this.id}. It won't be displayed.`) + console.error(e) + } + } + + return settings.concat(colorSettings) +} diff --git a/src/speed_provider.js b/src/speed_provider.js new file mode 100644 index 0000000..09ee50b --- /dev/null +++ b/src/speed_provider.js @@ -0,0 +1,132 @@ +import {settingsKey} from "./settings.js" +import {getDefaultDashMultiplier, getDefaultSpeedAttribute} from "./systems.js" + +/** + * Base class for all speed providers. + * If you want to offer a speed provider in your system/module you must derive this class. + * Each speed provider must at least implement + */ +export class SpeedProvider { + /** + * Returns an array of colors used by this speed provider. Each color corresponds to one speed that a token may have. + * Each color must be an object with the following properties: + * - id: A value that identfies the color. Must be unique for each color returned. + * - default: The color that is used to highlight that speed by default. + * - name: A user readable name for the speed represented by the color. This name is used in the color configuration dialog. Drag Ruler will attempt to localize this string using `game.i18n` + * + * Of these properties, id and defaultColor are required. name is optional, but it's recommended to set it + * + * Implementing this method is required for all speed providers + */ + get colors() { + throw new Error("A SpeedProvider must implement the colors function") + } + + /** + * Returns an array of speeds that the token passed in the arguments this token can reach. + * Each range is an object that with the following properties: + * - range: A number indicating the distance that the token can travel with this speed + * - color: The id (as defined in the `colors` getter) of the color that should be used to represent this range + * + * Implementing this method is required for all speed providers + */ + getRanges(token) { + throw new Error("A SpeedProvider must implement the getRanges function") + } + + /** + * Returns an array of configuration options for this module. The settings will be shown in the Speed Provider Settings of Drag Ruler. + * Each configuration option is an object that has the same attributes as a native foundry setting passed to `game.settings.register`, + * except for these exceptions: + * - id: A string that identifies the setting. Must be unique for each setting returned. This id will be used to fetch the setting. + * - config: This property is not supported by Drag Ruler module settings. Use foundries native settings instead if you need settings that don't show up in the configuration dialog. + * + * Implementing this method is optional and only needs to be done if you want to provide custom provider settings + */ + get settings() { + return [] + } + + /** + * Returns the default color for ranges that a token cannot reach. + * + * Implementing this method is optional and only needs to be done if you want to provide a custom default for that color. + */ + get defaultUnreachableColor() { + return 0xFF0000 + } + + /** + * Returns the value that is currently set for the setting registered with the provided settingId. + * + * This function shouldn't be overridden by speed provider implementations. It can be called to fetch speed provider specific settings. + */ + getSetting(settingId) { + try { + return game.settings.get(settingsKey, `speedProviders.${this.id}.setting.${settingId}`) + } + catch (e) { + if (this.settings.some(setting => setting.id === settingId)) { + throw e + } + throw new Error(`Drag Ruler | "${settingId}" is not a registered setting for "${this.id}". If you're the module/system developer, please add it to the return values of your Speed Providers "get settings()" function.`) + } + } + + /** + * Constructs a new instance of he speed provider + * + * This function should neither be called or overridden by speed provider implementations + */ + constructor(id) { + this.id = id + } +} + + +export class GenericSpeedProvider extends SpeedProvider { + get colors() { + return [ + {id: "walk", default: 0x00FF00, name: "drag-ruler.genericSpeedProvider.speeds.walk"}, + {id: "dash", default: 0xFFFF00, name: "drag-ruler.genericSpeedProvider.speeds.dash"} + ] + } + + getRanges(token) { + const speedAttribute = this.getSetting("speedAttribute") + if (!speedAttribute) + return [] + const tokenSpeed = getProperty(token, speedAttribute) + if (tokenSpeed === undefined) { + console.warn(`Drag Ruler (Generic Speed Provider) | The configured token speed attribute "${speedAttribute}" didn't return a speed value. To use colors based on drag distance set the setting to the correct value (or clear the box to disable this feature).`) + return [] + } + const dashMultiplier = this.getSetting("dashMultiplier") + if (!dashMultiplier) + return [{range: tokenSpeed, color: playercolor}] + return [{range: tokenSpeed, color: "walk"}, {range: tokenSpeed * dashMultiplier, color: "dash"}] + } + + get settings() { + return [ + { + id: "speedAttribute", + name: "drag-ruler.genericSpeedProvider.settings.speedAttribute.name", + hint: "drag-ruler.genericSpeedProvider.settings.speedAttribute.hint", + scope: "world", + config: true, + type: String, + default: getDefaultSpeedAttribute(), + }, + { + id: "dashMultiplier", + name: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.name", + hint: "drag-ruler.genericSpeedProvider.settings.dashMultiplier.hint", + scope: "world", + config: true, + type: Number, + default: getDefaultDashMultiplier(), + } + ] + } +} diff --git a/templates/speed_provider_settings.html b/templates/speed_provider_settings.html new file mode 100644 index 0000000..c2655db --- /dev/null +++ b/templates/speed_provider_settings.html @@ -0,0 +1,63 @@ +{{! This partial is based on the foundry settings partial}} +{{#*inline "settingPartial"}} +
+ +
+ {{#if this.isCheckbox}} + + {{else if this.isSelect}} + + {{else if this.isRange}} + + {{this.value}} + {{else if this.isColor}} + + {{else}} + + {{/if}} +
+

{{this.hint}}

+
+{{/inline}} + + + +
+
+
+

{{localize "drag-ruler.settings.speedProviderSettings.headers.speedProvider"}}

+ {{#if this.isGM}} + {{#with this.providerSelection}} + {{> settingPartial}} + {{/with}} + {{else}} +
{{this.selectedProviderName}}
+

{{localize "drag-ruler.settings.speedProviderSettings.activeProvider.hint"}}

+ {{/if}} +

{{localize "drag-ruler.settings.speedProviderSettings.headers.speedProviderSettings"}}

+ {{#each this.providers}} +
+ {{#if this.hasSettings}} + {{#each settings}} + {{> settingPartial}} + {{/each}} + {{else}} +

{{localize "drag-ruler.settings.speedProviderSettings.noSettings"}}

+ {{/if}} +
+ {{/each}} +
+ + +