From 28d010539754c74a36bae9e7c6ba99c5b1d4e1c6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 7 Dec 2025 17:37:30 -0700 Subject: [PATCH] Add a generic helper app to display settings menus similar to how Foundry does by default, but without the category selector --- module/apps/OFTSettingsMenu.mjs | 185 +++++++++++++++++++++ templates/OFTSettingsMenu/footer.hbs | 8 + templates/OFTSettingsMenu/settingsList.hbs | 7 + 3 files changed, 200 insertions(+) create mode 100644 module/apps/OFTSettingsMenu.mjs create mode 100644 templates/OFTSettingsMenu/footer.hbs create mode 100644 templates/OFTSettingsMenu/settingsList.hbs diff --git a/module/apps/OFTSettingsMenu.mjs b/module/apps/OFTSettingsMenu.mjs new file mode 100644 index 0000000..1506c25 --- /dev/null +++ b/module/apps/OFTSettingsMenu.mjs @@ -0,0 +1,185 @@ +import { filePath } from "../consts.mjs"; + +const { HandlebarsApplicationMixin: HAM, ApplicationV2 } = foundry.applications.api; + +export class OFTSettingsMenu extends HAM(ApplicationV2) { + + // #region Options + static DEFAULT_OPTIONS = { + tag: `form`, + window: { + icon: `fa-solid fa-gears`, + resizable: true, + contentClasses: [ + `standard-form`, + ], + controls: [ + { + icon: `fa-solid fa-arrow-rotate-left`, + label: `PACKAGECONFIG.Reset`, + visible: true, + action: `resetDefaults`, + }, + ], + }, + form: { + handler: this.#handleSubmit, + closeOnSubmit: true, + submitOnChange: false, + }, + position: { + width: 544, + }, + actions: { + resetDefaults: this.#resetDefaults, + }, + }; + + static PARTS = { + settingsList: { + template: filePath(`templates/OFTSettingsMenu/settingsList.hbs`), + scrollable: [``], + }, + footer: { + template: filePath(`templates/OFTSettingsMenu/footer.hbs`), + }, + }; + + static _SETTINGS = []; + // #endregion Options + + // #region Data Prep + async _prepareContext() { + return { + meta: { + idp: this.id, + }, + }; + }; + + #partContextPrep = { + settingsList: this._prepareSettingsListContext, + }; + + async _preparePartContext(partID, ctx) { + ctx = foundry.utils.deepClone(ctx); + + const prepper = this.#partContextPrep[partID]; + if (prepper && typeof prepper === `function`) { + await prepper.call(this, ctx); + }; + + return ctx; + }; + + async _prepareSettingsListContext(ctx) { + const canConfigure = game.user.can(`SETTINGS_MODIFY`); + ctx.settings = []; + + const settingIDs = this.constructor._SETTINGS; + for ( const settingID of settingIDs ) { + const setting = game.settings.settings.get(settingID); + if ( !canConfigure && setting.scope === CONST.SETTING_SCOPES.WORLD ) { continue } + const data = { + label: setting.value, + value: game.settings.get(setting.namespace, setting.key), + menu: false, + }; + + // Define a DataField for each setting not originally defined with one + const fields = foundry.data.fields; + if ( setting.type instanceof fields.DataField ) { + data.field = setting.type; + } + else if ( setting.type === Boolean ) { + data.field = new fields.BooleanField({initial: setting.default ?? false}); + } + else if ( setting.type === Number ) { + const {min, max, step} = setting.range ?? {}; + data.field = new fields.NumberField({ + required: true, + choices: setting.choices, + initial: setting.default, + min, + max, + step, + }); + } + else if ( setting.filePicker ) { + const categories = { + audio: [`AUDIO`], + folder: [], + font: [`FONT`], + graphics: [`GRAPHICS`], + image: [`IMAGE`], + imagevideo: [`IMAGE`, `VIDEO`], + text: [`TEXT`], + video: [`VIDEO`], + }[setting.filePicker] ?? Object.keys(CONST.FILE_CATEGORIES).filter(c => c !== `HTML`); + if ( categories.length ) { + data.field = new fields.FilePathField({required: true, blank: true, categories}); + } + else { + data.field = new fields.StringField({required: true}); // Folder paths cannot be FilePathFields + data.folderPicker = true; + } + } + else { + data.field = new fields.StringField({required: true, choices: setting.choices}); + } + data.field.name = `${setting.namespace}.${setting.key}`; + data.field.label ||= game.i18n.localize(setting.name ?? ``); + data.field.hint ||= game.i18n.localize(setting.hint ?? ``); + + // Categorize setting + ctx.settings.push(data); + }; + }; + // #endregion Data Prep + + // #region Actions + /** @this {OFTSettingsMenu} */ + static async #handleSubmit(_event, _form, formData) { + let requiresClientReload = false; + let requiresWorldReload = false; + for ( const [key, value] of Object.entries(formData.object) ) { + const setting = game.settings.settings.get(key); + if ( !setting ) { continue }; + const priorValue = game.settings.get(setting.namespace, setting.key, {document: true})?._source.value; + let newSetting; + try { + newSetting = await game.settings.set(setting.namespace, setting.key, value, {document: true}); + } catch(error) { + ui.notifications.error(error); + } + if ( priorValue === newSetting?._source.value ) { continue }; // Compare JSON strings + requiresClientReload ||= (setting.scope !== CONST.SETTING_SCOPES.WORLD) && setting.requiresReload; + requiresWorldReload ||= (setting.scope === CONST.SETTING_SCOPES.WORLD) && setting.requiresReload; + } + if ( requiresClientReload || requiresWorldReload ) { + await foundry.applications.settings.SettingsConfig.reloadConfirm({world: requiresWorldReload}); + } + }; + + /** @this {OFTSettingsMenu} */ + static async #resetDefaults() { + const form = this.form; + for ( const settingID of this.constructor._SETTINGS ) { + const setting = game.settings.settings.get(settingID); + console.log({ settingID, setting }); + const input = form[settingID]; + if ( !input || !setting ) { continue }; + + if ( input.type === `checkbox` ) { + input.checked = setting.default; + } + else { + input.value = setting.default; + }; + + input.dispatchEvent(new Event(`change`)); + } + ui.notifications.info(`SETTINGS.ResetInfo`, {localize: true}); + }; + // #endregion Actions +}; diff --git a/templates/OFTSettingsMenu/footer.hbs b/templates/OFTSettingsMenu/footer.hbs new file mode 100644 index 0000000..41f2375 --- /dev/null +++ b/templates/OFTSettingsMenu/footer.hbs @@ -0,0 +1,8 @@ + diff --git a/templates/OFTSettingsMenu/settingsList.hbs b/templates/OFTSettingsMenu/settingsList.hbs new file mode 100644 index 0000000..2daeb00 --- /dev/null +++ b/templates/OFTSettingsMenu/settingsList.hbs @@ -0,0 +1,7 @@ +
+ {{#each settings as |setting|}} + {{formGroup setting.field value=setting.value rootId=@root.meta.idp localize=true }} + {{else}} + {{localize "OFT.apps.no-settings-to-display"}} + {{/each}} +