Files

365 lines
15 KiB
JavaScript
Raw Permalink Normal View History

2025-01-04 00:34:03 +01:00
/**
* Document Sheet Configuration Application
*/
class DocumentSheetConfig extends FormApplication {
/** @inheritdoc */
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
classes: ["form", "sheet-config"],
template: "templates/sheets/sheet-config.html",
width: 400
});
}
/**
* An array of pending sheet assignments which are submitted before other elements of the framework are ready.
* @type {object[]}
* @private
*/
static #pending = [];
/* -------------------------------------------- */
/** @inheritdoc */
get title() {
const name = this.object.name ?? game.i18n.localize(this.object.constructor.metadata.label);
return `${name}: Sheet Configuration`;
}
/* -------------------------------------------- */
/** @inheritDoc */
getData(options={}) {
const {sheetClasses, defaultClasses, defaultClass} = this.constructor.getSheetClassesForSubType(
this.object.documentName,
this.object.type || CONST.BASE_DOCUMENT_TYPE
);
// Return data
return {
isGM: game.user.isGM,
object: this.object.toObject(),
options: this.options,
sheetClass: this.object.getFlag("core", "sheetClass") ?? "",
blankLabel: game.i18n.localize("SHEETS.DefaultSheet"),
sheetClasses, defaultClass, defaultClasses
};
}
/* -------------------------------------------- */
/** @inheritdoc */
async _updateObject(event, formData) {
event.preventDefault();
const original = this.getData({});
const defaultSheetChanged = formData.defaultClass !== original.defaultClass;
const documentSheetChanged = formData.sheetClass !== original.sheetClass;
// Update world settings
if ( game.user.isGM && defaultSheetChanged ) {
const setting = game.settings.get("core", "sheetClasses") || {};
const type = this.object.type || CONST.BASE_DOCUMENT_TYPE;
foundry.utils.mergeObject(setting, {[`${this.object.documentName}.${type}`]: formData.defaultClass});
await game.settings.set("core", "sheetClasses", setting);
// Trigger a sheet change manually if it wouldn't be triggered by the normal ClientDocument#_onUpdate workflow.
if ( !documentSheetChanged ) return this.object._onSheetChange({ sheetOpen: true });
}
// Update the document-specific override
if ( documentSheetChanged ) return this.object.setFlag("core", "sheetClass", formData.sheetClass);
}
/* -------------------------------------------- */
/**
* Marshal information on the available sheet classes for a given document type and sub-type, and format it for
* display.
* @param {string} documentName The Document type.
* @param {string} subType The Document sub-type.
* @returns {{sheetClasses: object, defaultClasses: object, defaultClass: string}}
*/
static getSheetClassesForSubType(documentName, subType) {
const config = CONFIG[documentName];
const defaultClasses = {};
let defaultClass = null;
const sheetClasses = Object.values(config.sheetClasses[subType]).reduce((obj, cfg) => {
if ( cfg.canConfigure ) obj[cfg.id] = cfg.label;
if ( cfg.default && !defaultClass ) defaultClass = cfg.id;
if ( cfg.canConfigure && cfg.canBeDefault ) defaultClasses[cfg.id] = cfg.label;
return obj;
}, {});
return {sheetClasses, defaultClasses, defaultClass};
}
/* -------------------------------------------- */
/* Configuration Methods
/* -------------------------------------------- */
/**
* Initialize the configured Sheet preferences for Documents which support dynamic Sheet assignment
* Create the configuration structure for supported documents
* Process any pending sheet registrations
* Update the default values from settings data
*/
static initializeSheets() {
for ( let cls of Object.values(foundry.documents) ) {
const types = this._getDocumentTypes(cls);
CONFIG[cls.documentName].sheetClasses = types.reduce((obj, type) => {
obj[type] = {};
return obj;
}, {});
}
// Register any pending sheets
this.#pending.forEach(p => {
if ( p.action === "register" ) this.#registerSheet(p);
else if ( p.action === "unregister" ) this.#unregisterSheet(p);
});
this.#pending = [];
// Update default sheet preferences
const defaults = game.settings.get("core", "sheetClasses");
this.updateDefaultSheets(defaults);
}
/* -------------------------------------------- */
static _getDocumentTypes(cls, types=[]) {
if ( types.length ) return types;
return game.documentTypes[cls.documentName];
}
/* -------------------------------------------- */
/**
* Register a sheet class as a candidate which can be used to display documents of a given type
* @param {typeof ClientDocument} documentClass The Document class for which to register a new Sheet option
* @param {string} scope Provide a unique namespace scope for this sheet
* @param {typeof DocumentSheet} sheetClass A defined Application class used to render the sheet
* @param {object} [config] Additional options used for sheet registration
* @param {string|Function} [config.label] A human-readable label for the sheet name, which will be localized
* @param {string[]} [config.types] An array of document types for which this sheet should be used
* @param {boolean} [config.makeDefault=false] Whether to make this sheet the default for provided types
* @param {boolean} [config.canBeDefault=true] Whether this sheet is available to be selected as a default sheet for
* all Documents of that type.
* @param {boolean} [config.canConfigure=true] Whether this sheet appears in the sheet configuration UI for users.
*/
static registerSheet(documentClass, scope, sheetClass, {
label, types, makeDefault=false, canBeDefault=true, canConfigure=true
}={}) {
const id = `${scope}.${sheetClass.name}`;
const config = {documentClass, id, label, sheetClass, types, makeDefault, canBeDefault, canConfigure};
if ( game.ready ) this.#registerSheet(config);
else {
config.action = "register";
this.#pending.push(config);
}
}
/**
* Perform the sheet registration.
* @param {object} config Configuration for how the sheet should be registered
* @param {typeof ClientDocument} config.documentClass The Document class being registered
* @param {string} config.id The sheet ID being registered
* @param {string} config.label The human-readable sheet label
* @param {typeof DocumentSheet} config.sheetClass The sheet class definition being registered
* @param {object[]} config.types An array of types for which this sheet is added
* @param {boolean} config.makeDefault Make this sheet the default for provided types?
* @param {boolean} config.canBeDefault Whether this sheet is available to be selected as a default
* sheet for all Documents of that type.
* @param {boolean} config.canConfigure Whether the sheet appears in the sheet configuration UI for
* users.
*/
static #registerSheet({documentClass, id, label, sheetClass, types, makeDefault, canBeDefault, canConfigure}={}) {
types = this._getDocumentTypes(documentClass, types);
const classes = CONFIG[documentClass.documentName]?.sheetClasses;
const defaults = game.ready ? game.settings.get("core", "sheetClasses") : {};
if ( typeof classes !== "object" ) return;
for ( const t of types ) {
classes[t] ||= {};
const existingDefault = defaults[documentClass.documentName]?.[t];
const isDefault = existingDefault ? (existingDefault === id) : makeDefault;
if ( isDefault ) Object.values(classes[t]).forEach(s => s.default = false);
if ( label instanceof Function ) label = label();
else if ( label ) label = game.i18n.localize(label);
else label = id;
classes[t][id] = {
id, label, canBeDefault, canConfigure,
cls: sheetClass,
default: isDefault
};
}
}
/* -------------------------------------------- */
/**
* Unregister a sheet class, removing it from the list of available Applications to use for a Document type
* @param {typeof ClientDocument} documentClass The Document class for which to register a new Sheet option
* @param {string} scope Provide a unique namespace scope for this sheet
* @param {typeof DocumentSheet} sheetClass A defined DocumentSheet subclass used to render the sheet
* @param {object} [config]
* @param {object[]} [config.types] An Array of types for which this sheet should be removed
*/
static unregisterSheet(documentClass, scope, sheetClass, {types}={}) {
const id = `${scope}.${sheetClass.name}`;
const config = {documentClass, id, types};
if ( game.ready ) this.#unregisterSheet(config);
else {
config.action = "unregister";
this.#pending.push(config);
}
}
/**
* Perform the sheet de-registration.
* @param {object} config Configuration for how the sheet should be un-registered
* @param {typeof ClientDocument} config.documentClass The Document class being unregistered
* @param {string} config.id The sheet ID being unregistered
* @param {object[]} config.types An array of types for which this sheet is removed
*/
static #unregisterSheet({documentClass, id, types}={}) {
types = this._getDocumentTypes(documentClass, types);
const classes = CONFIG[documentClass.documentName]?.sheetClasses;
if ( typeof classes !== "object" ) return;
for ( let t of types ) {
delete classes[t][id];
}
}
/* -------------------------------------------- */
/**
* Update the current default Sheets using a new core world setting.
* @param {object} setting
*/
static updateDefaultSheets(setting={}) {
if ( !Object.keys(setting).length ) return;
for ( let cls of Object.values(foundry.documents) ) {
const documentName = cls.documentName;
const cfg = CONFIG[documentName];
const classes = cfg.sheetClasses;
const collection = cfg.collection?.instance ?? [];
const defaults = setting[documentName];
if ( !defaults ) continue;
// Update default preference for registered sheets
for ( let [type, sheetId] of Object.entries(defaults) ) {
const sheets = Object.values(classes[type] || {});
let requested = sheets.find(s => s.id === sheetId);
if ( requested ) sheets.forEach(s => s.default = s.id === sheetId);
}
// Close and de-register any existing sheets
for ( let document of collection ) {
for ( const [id, app] of Object.entries(document.apps) ) {
app.close();
delete document.apps[id];
}
document._sheet = null;
}
}
}
/* -------------------------------------------- */
/**
* Initialize default sheet configurations for all document types.
* @private
*/
static _registerDefaultSheets() {
const defaultSheets = {
// Documents
Actor: ActorSheet,
Adventure: AdventureImporter,
Folder: FolderConfig,
Item: ItemSheet,
JournalEntry: JournalSheet,
Macro: MacroConfig,
Playlist: PlaylistConfig,
RollTable: RollTableConfig,
Scene: SceneConfig,
User: foundry.applications.sheets.UserConfig,
// Embedded Documents
ActiveEffect: ActiveEffectConfig,
AmbientLight: foundry.applications.sheets.AmbientLightConfig,
AmbientSound: foundry.applications.sheets.AmbientSoundConfig,
Card: CardConfig,
Combatant: CombatantConfig,
Drawing: DrawingConfig,
MeasuredTemplate: MeasuredTemplateConfig,
Note: NoteConfig,
PlaylistSound: PlaylistSoundConfig,
Region: foundry.applications.sheets.RegionConfig,
RegionBehavior: foundry.applications.sheets.RegionBehaviorConfig,
Tile: TileConfig,
Token: TokenConfig,
Wall: WallConfig
};
Object.values(foundry.documents).forEach(base => {
const type = base.documentName;
const cfg = CONFIG[type];
cfg.sheetClasses = {};
const defaultSheet = defaultSheets[type];
if ( !defaultSheet ) return;
DocumentSheetConfig.registerSheet(cfg.documentClass, "core", defaultSheet, {
makeDefault: true,
label: () => game.i18n.format("SHEETS.DefaultDocumentSheet", {document: game.i18n.localize(`DOCUMENT.${type}`)})
});
});
DocumentSheetConfig.registerSheet(Cards, "core", CardsConfig, {
label: "CARDS.CardsDeck",
types: ["deck"],
makeDefault: true
});
DocumentSheetConfig.registerSheet(Cards, "core", CardsHand, {
label: "CARDS.CardsHand",
types: ["hand"],
makeDefault: true
});
DocumentSheetConfig.registerSheet(Cards, "core", CardsPile, {
label: "CARDS.CardsPile",
types: ["pile"],
makeDefault: true
});
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalTextTinyMCESheet, {
types: ["text"],
label: () => game.i18n.localize("EDITOR.TinyMCE")
});
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalImagePageSheet, {
types: ["image"],
makeDefault: true,
label: () =>
game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypeImage")})
});
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalVideoPageSheet, {
types: ["video"],
makeDefault: true,
label: () =>
game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypeVideo")})
});
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalPDFPageSheet, {
types: ["pdf"],
makeDefault: true,
label: () =>
game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {page: game.i18n.localize("JOURNALENTRYPAGE.TypePDF")})
});
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", JournalTextPageSheet, {
types: ["text"],
makeDefault: true,
label: () => {
return game.i18n.format("JOURNALENTRYPAGE.DefaultPageSheet", {
page: game.i18n.localize("JOURNALENTRYPAGE.TypeText")
});
}
});
DocumentSheetConfig.registerSheet(JournalEntryPage, "core", MarkdownJournalPageSheet, {
types: ["text"],
label: () => game.i18n.localize("EDITOR.Markdown")
});
}
}