diff --git a/langs/en-ca.json b/langs/en-ca.json index 02cd095..2ead115 100644 --- a/langs/en-ca.json +++ b/langs/en-ca.json @@ -11,7 +11,7 @@ }, "chatImageLinks": { "name": "Image Shortcuts", - "hint": "(v13+) When attempting to send an image/gif in chat, this allows you to easily embed the actual image in the text by changing \"http\"/\"https\" into \"image\", automatically displaying the image after sending the message." + "hint": "(v13+) Automatically embeds image and gif links when posted in the chat. The link must point to a specific image file (e.g. png, webp, gif), otherwise it will not be embedded at all." }, "chatSidebarBackground": { "name": "Chat Background", @@ -65,17 +65,20 @@ "label": "Configure Hotbar" } }, + "keybindings": { + "toggleMouseBroadcast": { + "name": "Toggle Show Cursor", + "hint": "(v13+) Temporarily turns off the mouse cursor position that other players can see. Hides the cursor until you activate this keybind again." + } + }, "apps": { "no-settings-to-display": "No settings to display", "make-global-reference": "Make Global Reference" }, - "dialogs": { - "chatImageLinks": { - "didYouKnowImageLink": "", - "convertAndDontShowAgain": "", - "justConvert": "", - "ignoreAndDontShowAgain": "", - "disableEntirely": "" + "notifs": { + "toggleMouseBroadcast": { + "hidingCursor": "Hiding your cursor from others!", + "showingCursor": "Showing your cursor to others!" } } } diff --git a/module.json b/module.json index 61d9791..92e7f6e 100644 --- a/module.json +++ b/module.json @@ -1,7 +1,7 @@ { "id": "oft", "title": "Oliver's Foundry Tweaks", - "version": "1.1.0", + "version": "1.2.0", "authors": [ { "name": "Oliver" } ], diff --git a/module/apps/DevSettingsMenu.mjs b/module/apps/DevSettingsMenu.mjs index 81a14e8..bee0b96 100644 --- a/module/apps/DevSettingsMenu.mjs +++ b/module/apps/DevSettingsMenu.mjs @@ -9,6 +9,7 @@ export class DevSettingsMenu extends OFTSettingsMenu { }; static get _SETTINGS() { + if (!categories.has(`dev`)) { return [] }; const devSettings = categories.get(`dev`); const settingIDs = []; for (const [settingID, shown] of devSettings.entries()) { diff --git a/module/apps/HotbarSettingsMenu.mjs b/module/apps/HotbarSettingsMenu.mjs index 0c7fb9e..5a722e3 100644 --- a/module/apps/HotbarSettingsMenu.mjs +++ b/module/apps/HotbarSettingsMenu.mjs @@ -9,6 +9,7 @@ export class HotbarSettingsMenu extends OFTSettingsMenu { }; static get _SETTINGS() { + if (!categories.has(`hotbar`)) { return [] }; const settings = categories.get(`hotbar`); const settingIDs = []; for (const [settingID, shown] of settings.entries()) { diff --git a/module/apps/OFTSettingsMenu.mjs b/module/apps/OFTSettingsMenu.mjs index 74ddf97..387dc0a 100644 --- a/module/apps/OFTSettingsMenu.mjs +++ b/module/apps/OFTSettingsMenu.mjs @@ -47,6 +47,10 @@ export class OFTSettingsMenu extends HAM(ApplicationV2) { }; static _SETTINGS = []; + + static get isEmpty() { + return this._SETTINGS.length === 0; + }; // #endregion Options // #region Data Prep diff --git a/module/conflicts/crucible.mjs b/module/conflicts/crucible.mjs new file mode 100644 index 0000000..6df24db --- /dev/null +++ b/module/conflicts/crucible.mjs @@ -0,0 +1,10 @@ +import { key as preventMovementHistoryKey } from "../tweaks/preventMovementHistory.mjs"; + +Hooks.on(`oft.preRegisterTweak`, (tweakID) => { + if (game.system.id !== `crucible`) { return }; + + switch (tweakID) { + case preventMovementHistoryKey: + return false; + }; +}); diff --git a/module/hooks/init.mjs b/module/hooks/init.mjs index 60cde1b..729a004 100644 --- a/module/hooks/init.mjs +++ b/module/hooks/init.mjs @@ -1,5 +1,6 @@ // Settings -import { preventMovementHistory } from "../settings/preventMovementHistory.mjs"; +import { preventMovementHistory } from "../tweaks/preventMovementHistory.mjs"; +import { toggleMouseBroadcast } from "../tweaks/toggleMouseBroadcast.mjs"; // Utils import { Logger } from "../utils/Logger.mjs"; @@ -14,4 +15,5 @@ Hooks.on(`init`, () => { Logger.log(`Initializing`); preventMovementHistory(); + toggleMouseBroadcast(); }); diff --git a/module/hooks/oft.preRegisterTweak.mjs b/module/hooks/oft.preRegisterTweak.mjs new file mode 100644 index 0000000..47de773 --- /dev/null +++ b/module/hooks/oft.preRegisterTweak.mjs @@ -0,0 +1,19 @@ +/* +This hook is used to give external modules or systems the ability to interact +with the tweak registration lifecycle to do something before a tweak is registered +or being able to prevent registration of incompatible tweaks. + +The hook receives a string indicating which tweak this hook is being called for +and a boolean value indicating if the tweak is considered invasive. Returning +explicit false prevents that tweak from being registered. + +Invasive tweaks are additions that manipulate or override Document or helper +classes. An example of an invasive tweak is the "toggleMouseBroadcast", +tweak which replaces the existing "CONFIG.Canvas.layers.controls.layerClass" +class, most of these tweaks do smartly extend from the same CONFIG class +that they replace, however if they override a part of the class that +other modules/systems rely on, then that is a good time to block that +specific tweak's registration. + +Call Signature: (tweakKey: string, isInvasive: boolean) => (void | boolean) +*/ diff --git a/module/hooks/oft.preventSetting.mjs b/module/hooks/oft.preventSetting.mjs deleted file mode 100644 index 500dc8e..0000000 --- a/module/hooks/oft.preventSetting.mjs +++ /dev/null @@ -1,12 +0,0 @@ -/* -This hook is used for invasive hooks that we want to provide the -option for systems and other modules to be able to disable in case -of incompatabilities for whatever reason. This can also be used -internally within this module if we discover incompatabilites with -systems and want to disable it on our side. - -This file is meant more documentation than anything at this point in -time. - -Call Signature: (settingKey: string) => (void | boolean) -*/ diff --git a/module/hooks/oft.settingStatuses.mjs b/module/hooks/oft.settingStatuses.mjs deleted file mode 100644 index 46bef18..0000000 --- a/module/hooks/oft.settingStatuses.mjs +++ /dev/null @@ -1,13 +0,0 @@ -/* -This hook is used to enable any modules that attempt to disable settings -or just want to investigate what settings are enabled to be able to get -a ping with information about which settings where registered entirely -and which weren't. The object that is passed to this is frozen and is -not meant to be edited as you cannot de-register nor prevent setting -registration from this hook. For that see the "oft.preventSetting" hook. - -This file is meant more documentation than anything at this point in -time. - -Call Signature: (settings: Record) => void -*/ diff --git a/module/hooks/oft.tweakStatuses.mjs b/module/hooks/oft.tweakStatuses.mjs new file mode 100644 index 0000000..622d6f4 --- /dev/null +++ b/module/hooks/oft.tweakStatuses.mjs @@ -0,0 +1,9 @@ +/* +This hook is used to broadcast the final status of all tweaks within the module, +allowing other modules to either confirm their registration didn't happen or to +do something once all of the module setup has been finalized. Tweak statuses +cannot be blocked or changed from this hook, to prevent a tweak from being +registered you should use the "oft.preRegisterTweak" hook. + +Call Signature: (settings: Record) => void +*/ diff --git a/module/hooks/renderSettingsConfig.mjs b/module/hooks/renderSettingsConfig.mjs index b4e28d8..b0ec83c 100644 --- a/module/hooks/renderSettingsConfig.mjs +++ b/module/hooks/renderSettingsConfig.mjs @@ -6,11 +6,19 @@ prevent it from being as attention-grabbing compared to being at the top of the list. */ Hooks.on(`renderSettingsConfig`, (app) => { + // MARK: Hide empty menus + for (const [key, config] of game.settings.menus) { + if (!key.startsWith(__ID__)) { continue }; + if (config.type.isEmpty) { + const entry = app.element.querySelector(`.form-group:has(button[data-key="${key}"])`); + entry?.remove(); + }; + }; + + // MARK: devSettings Menu /** @type {Node | undefined} */ const settingList = app.element.querySelector(`.tab[data-group="categories"][data-tab="oft"]`); - - // MARK: devSettings Menu /** @type {Node | undefined} */ const devSettingsMenu = app.element.querySelector(`.form-group:has(button[data-key="${__ID__}.devSettings"])`); if (settingList && devSettingsMenu) { diff --git a/module/hooks/setup.mjs b/module/hooks/setup.mjs index ae63ec6..206867a 100644 --- a/module/hooks/setup.mjs +++ b/module/hooks/setup.mjs @@ -1,15 +1,15 @@ // Settings -import { addGlobalDocReferrer } from "../settings/addGlobalDocReferrer.mjs"; -import { autoUnpauseOnLoad } from "../settings/autoUnpauseOnLoad.mjs"; -import { chatImageLinks } from "../settings/chatImageLinks.mjs"; -import { chatSidebarBackground } from "../settings/chatSidebarBackground.mjs"; -import { hotbarButtonGap } from "../settings/hotbarButtonGap.mjs"; -import { hotbarButtonSize } from "../settings/hotbarButtonSize.mjs"; -import { preventTokenRotation } from "../settings/preventTokenRotation.mjs"; -import { preventUserConfigOpen } from "../settings/preventUserConfigOpen.mjs"; -import { repositionHotbar } from "../settings/repositionHotbar.mjs"; -import { startingSidebarTab } from "../settings/startingSidebarTab.mjs"; -import { startSidebarExpanded } from "../settings/startSidebarExpanded.mjs"; +import { addGlobalDocReferrer } from "../tweaks/addGlobalDocReferrer.mjs"; +import { autoUnpauseOnLoad } from "../tweaks/autoUnpauseOnLoad.mjs"; +import { chatImageLinks } from "../tweaks/chatImageLinks.mjs"; +import { chatSidebarBackground } from "../tweaks/chatSidebarBackground.mjs"; +import { hotbarButtonGap } from "../tweaks/hotbarButtonGap.mjs"; +import { hotbarButtonSize } from "../tweaks/hotbarButtonSize.mjs"; +import { preventTokenRotation } from "../tweaks/preventTokenRotation.mjs"; +import { preventUserConfigOpen } from "../tweaks/preventUserConfigOpen.mjs"; +import { repositionHotbar } from "../tweaks/repositionHotbar.mjs"; +import { startingSidebarTab } from "../tweaks/startingSidebarTab.mjs"; +import { startSidebarExpanded } from "../tweaks/startSidebarExpanded.mjs"; // Apps import { DevSettingsMenu } from "../apps/DevSettingsMenu.mjs"; @@ -51,7 +51,16 @@ Hooks.on(`setup`, () => { preventTokenRotation(); preventUserConfigOpen(); - Hooks.callAll(`oft.settingStatuses`, deepFreeze(status)); + // Compatibility Code + if (Hooks.events[`oft.settingStatuses`] != null) { + foundry.utils.logCompatibilityWarning( + `The hook "${__ID__}.settingStatuses" has been renamed "${__ID__}.tweakStatuses".`, + { since: `v1.2.0`, until: `v2.0.0`, stack: false, once: true }, + ); + Hooks.callAll(`oft.settingStatuses`, deepFreeze(status)); + }; + + Hooks.callAll(`${__ID__}.tweakStatuses`, deepFreeze(status)); game.modules.get(__ID__).api = deepFreeze({ settings: status, }); diff --git a/module/oft.mjs b/module/oft.mjs index 8740d00..12a83bf 100644 --- a/module/oft.mjs +++ b/module/oft.mjs @@ -2,3 +2,6 @@ import "./hooks/init.mjs"; import "./hooks/setup.mjs"; import "./hooks/renderSettingsConfig.mjs"; + +// Compatibility w/ other packages +import "./conflicts/crucible.mjs"; diff --git a/module/settings/chatImageLinks.mjs b/module/settings/chatImageLinks.mjs deleted file mode 100644 index c25d56f..0000000 --- a/module/settings/chatImageLinks.mjs +++ /dev/null @@ -1,130 +0,0 @@ -import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; -import { __ID__ } from "../consts.mjs"; -import { Logger } from "../utils/Logger.mjs"; - -const { DialogV2 } = foundry.applications.api; - -const key = `chatImageLinks`; -const IMAGE_TYPES = [ - `png`, - `jpg`, - `jpeg`, - `webp`, - `svg`, -]; - -export function chatImageLinks() { - status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; - - // #region Registration - Logger.log(`Registering setting: ${key}`); - game.settings.register(__ID__, key, { - name: `OFT.setting.${key}.name`, - hint: `OFT.setting.${key}.hint`, - scope: `user`, - type: Boolean, - default: true, - config: true, - requiresReload: true, - }); - - game.settings.register(__ID__, key + `-showPromptAgain`, { - scope: `user`, - type: Boolean, - default: true, - config: false, - }); - // #endregion Registration - - // #region Implementation - if (game.settings.get(__ID__, key)) { - Logger.log(`setting:${key} | Adding text enricher`); - - // MARK: Enricher - const pattern = new RegExp( - `(? { - Logger.debug(url); - url = url[0].replace(/^image:\/\//, ``); - const secure = `https://${url}`; - const insecure = `http://${url}`; - - if (await isAcceptableImage(secure)) { - const img = document.createElement(`img`); - img.src = secure; - img.alt = secure; - return img; - }; - - if (await isAcceptableImage(insecure)) { - const img = document.createElement(`img`); - img.src = insecure; - img.alt = insecure; - return img; - }; - - return null; - }, - }); - - // MARK: Chat Input - // Hooks.on(`chatMessage`, (chatLog, message, options) => { - // if (!game.settings.get(__ID__, key)) { return }; - - // const match = message.match(pattern); - // if (!match) { return }; - - // DialogV2.wait({ - // rejectClose: false, - // content: game.i18n.localize(`OFT.dialogs.chatImageLinks.didYouKnowImageLink`), - // buttons: [ - // { action: ``, label: `OFT.dialogs.chatImageLinks.convertAndDontShowAgain` }, - // { action: ``, label: `OFT.dialogs.chatImageLinks.justConvert` }, - // { action: ``, label: `OFT.dialogs.chatImageLinks.ignoreAndDontShowAgain` }, - // { action: ``, label: `OFT.dialogs.chatImageLinks.disableEntirely` }, - // ], - // }) - // .then((selected) => { - // chatLog.processMessage(message, options); - // }); - - // return false; - // }); - } - // #endregion Implementation - - status[key] = SettingStatusEnum.Registered; -}; - -// #region Helpers -async function isAcceptableImage(url) { - try { - const response = await fetch(url, { method: `HEAD` }); - const contentType = response.headers.get(`Content-Type`); - Logger.debug(`Image data:`, { url, contentType }); - let [ superType, subtype ] = contentType.split(`/`); - if (superType !== `image`) { - return false; - }; - if (subtype.includes(`+`)) { - subtype = subtype.split(`+`, 2).at(0); - }; - return IMAGE_TYPES.includes(subtype); - } catch { - return false; - }; -}; -// #endregion Helpers diff --git a/module/settings/addGlobalDocReferrer.mjs b/module/tweaks/addGlobalDocReferrer.mjs similarity index 85% rename from module/settings/addGlobalDocReferrer.mjs rename to module/tweaks/addGlobalDocReferrer.mjs index d955d40..9ee5f81 100644 --- a/module/settings/addGlobalDocReferrer.mjs +++ b/module/tweaks/addGlobalDocReferrer.mjs @@ -1,12 +1,14 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; import { registerDevSetting } from "../utils/SubMenuSettings.mjs"; -const key = `addGlobalDocReferrer`; +export const key = `addGlobalDocReferrer`; export function addGlobalDocReferrer() { status[key] = SettingStatusEnum.Unknown; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/autoUnpauseOnLoad.mjs b/module/tweaks/autoUnpauseOnLoad.mjs similarity index 79% rename from module/settings/autoUnpauseOnLoad.mjs rename to module/tweaks/autoUnpauseOnLoad.mjs index de15fa4..db72e4d 100644 --- a/module/settings/autoUnpauseOnLoad.mjs +++ b/module/tweaks/autoUnpauseOnLoad.mjs @@ -1,19 +1,14 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; import { registerDevSetting } from "../utils/SubMenuSettings.mjs"; -const key = `autoUnpauseOnLoad`; +export const key = `autoUnpauseOnLoad`; export function autoUnpauseOnLoad() { status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/tweaks/chatImageLinks.mjs b/module/tweaks/chatImageLinks.mjs new file mode 100644 index 0000000..8d17d0b --- /dev/null +++ b/module/tweaks/chatImageLinks.mjs @@ -0,0 +1,125 @@ +import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; +import { __ID__ } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; + +export const key = `chatImageLinks`; +const IMAGE_TYPES = [ + `png`, + `jpg`, + `jpeg`, + `webp`, + `svg`, +]; + +export function chatImageLinks() { + status[key] = SettingStatusEnum.Unknown; + if (preventTweakRegistration(key)) { return }; + + /** @type {number|null} */ + let hookID = null; + + // #region Registration + Logger.log(`Registering setting: ${key}`); + game.settings.register(__ID__, key, { + name: `OFT.setting.${key}.name`, + hint: `OFT.setting.${key}.hint`, + scope: `user`, + type: Boolean, + default: true, + config: true, + requiresReload: false, + onChange: (newValue) => { + if (newValue) { + hookID = Hooks.on(`chatMessage`, chatMessageHandler); + } else if (hookID != null) { + Hooks.off(`chatMessage`, hookID); + }; + }, + }); + // #endregion Registration + + // #region Implementation + if (game.settings.get(__ID__, key)) { + Logger.log(`setting:${key} | Adding chat message listener`); + Hooks.on(`chatMessage`, chatMessageHandler); + }; + // #endregion Implementation + + status[key] = SettingStatusEnum.Registered; +}; + +// #region Helpers +const pattern = new RegExp( + `https?:\\/\\/\\S*\\.(?:${IMAGE_TYPES.join(`|`)})`, + `gi`, +); + +// MARK: Mutate & Resend +const handled = new Set(); +async function mutateAndResendMessage(chatLog, message, options) { + const validMatches = new Set(); + + const matches = message.match(pattern); + for (const match of matches) { + if (await isAcceptableImage(match)) { + validMatches.add(match); + }; + }; + + message = message.replaceAll( + pattern, + (url) => { + if (!validMatches.has(url)) { + return url; + }; + return `${url}`; + }, + ); + + handled.add(message); + chatLog.processMessage(message, options); +}; + +// MARK: Chat Message +/** + * Must be synchronous since it is a hook handler, but the mutation + + * resending can be done asynchronously since it doesn't matter how + * long it takes. + */ +function chatMessageHandler(chatLog, message, options) { + if (!game.settings.get(__ID__, key)) { return }; + + // Don't re-process the same message + if (handled.has(message)) { return }; + + // Prevent cancellation for non-matching messages + const match = message.match(pattern); + if (!match) { return }; + + mutateAndResendMessage(chatLog, message, options); + return false; +}; + +// MARK: isAcceptableImage +async function isAcceptableImage(url) { + if (!URL.canParse(url)) { return false }; + + try { + const response = await fetch(url, { method: `HEAD` }); + const contentType = response.headers.get(`Content-Type`); + Logger.debug(`Image data:`, { url, contentType }); + + let [ superType, subtype ] = contentType.split(`/`); + if (superType !== `image`) { + return false; + }; + if (subtype.includes(`+`)) { + subtype = subtype.split(`+`, 2).at(0); + }; + return IMAGE_TYPES.includes(subtype); + } catch { + return false; + }; +}; +// #endregion Helpers diff --git a/module/settings/chatSidebarBackground.mjs b/module/tweaks/chatSidebarBackground.mjs similarity index 85% rename from module/settings/chatSidebarBackground.mjs rename to module/tweaks/chatSidebarBackground.mjs index 5fce77a..8e98f55 100644 --- a/module/settings/chatSidebarBackground.mjs +++ b/module/tweaks/chatSidebarBackground.mjs @@ -1,11 +1,13 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; -const key = `chatSidebarBackground`; +export const key = `chatSidebarBackground`; export function chatSidebarBackground() { status[key] = SettingStatusEnum.Unknown; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/hotbarButtonGap.mjs b/module/tweaks/hotbarButtonGap.mjs similarity index 81% rename from module/settings/hotbarButtonGap.mjs rename to module/tweaks/hotbarButtonGap.mjs index 98c7cbf..96c8239 100644 --- a/module/settings/hotbarButtonGap.mjs +++ b/module/tweaks/hotbarButtonGap.mjs @@ -1,19 +1,14 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; import { registerCategorySetting } from "../utils/SubMenuSettings.mjs"; -const key = `hotbarButtonGap`; +export const key = `hotbarButtonGap`; export function hotbarButtonGap() { status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/hotbarButtonSize.mjs b/module/tweaks/hotbarButtonSize.mjs similarity index 82% rename from module/settings/hotbarButtonSize.mjs rename to module/tweaks/hotbarButtonSize.mjs index 7b4797b..829b0bd 100644 --- a/module/settings/hotbarButtonSize.mjs +++ b/module/tweaks/hotbarButtonSize.mjs @@ -1,19 +1,14 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; import { registerCategorySetting } from "../utils/SubMenuSettings.mjs"; -const key = `hotbarButtonSize`; +export const key = `hotbarButtonSize`; export function hotbarButtonSize() { status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/preventMovementHistory.mjs b/module/tweaks/preventMovementHistory.mjs similarity index 78% rename from module/settings/preventMovementHistory.mjs rename to module/tweaks/preventMovementHistory.mjs index 8f4d18c..782e7f3 100644 --- a/module/settings/preventMovementHistory.mjs +++ b/module/tweaks/preventMovementHistory.mjs @@ -1,18 +1,13 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; -const key = `preventMovementHistory`; +export const key = `preventMovementHistory`; export function preventMovementHistory() { status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; + if (preventTweakRegistration(key, true)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/preventTokenRotation.mjs b/module/tweaks/preventTokenRotation.mjs similarity index 82% rename from module/settings/preventTokenRotation.mjs rename to module/tweaks/preventTokenRotation.mjs index 258920c..53b8e3e 100644 --- a/module/settings/preventTokenRotation.mjs +++ b/module/tweaks/preventTokenRotation.mjs @@ -1,18 +1,13 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; -const key = `preventTokenRotation`; +export const key = `preventTokenRotation`; export function preventTokenRotation() { status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; + if (preventTweakRegistration(key)) { return }; /** @type {number|null} */ let hookID = null; diff --git a/module/settings/preventUserConfigOpen.mjs b/module/tweaks/preventUserConfigOpen.mjs similarity index 77% rename from module/settings/preventUserConfigOpen.mjs rename to module/tweaks/preventUserConfigOpen.mjs index 3b7646d..e13a237 100644 --- a/module/settings/preventUserConfigOpen.mjs +++ b/module/tweaks/preventUserConfigOpen.mjs @@ -1,18 +1,13 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; -const key = `preventUserConfigOpen`; +export const key = `preventUserConfigOpen`; export function preventUserConfigOpen() { status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/repositionHotbar.mjs b/module/tweaks/repositionHotbar.mjs similarity index 85% rename from module/settings/repositionHotbar.mjs rename to module/tweaks/repositionHotbar.mjs index cce0c5f..dd694d1 100644 --- a/module/settings/repositionHotbar.mjs +++ b/module/tweaks/repositionHotbar.mjs @@ -1,19 +1,14 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; import { registerCategorySetting } from "../utils/SubMenuSettings.mjs"; -const key = `repositionHotbar`; +export const key = `repositionHotbar`; export function repositionHotbar() { status[key] = SettingStatusEnum.Unknown; - - const prevented = Hooks.call(`${__ID__}.preventSetting`, key); - if (!prevented) { - Logger.log(`Preventing setting "${key}" from being registered`); - status[key] = SettingStatusEnum.Blocked; - return; - }; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/startSidebarExpanded.mjs b/module/tweaks/startSidebarExpanded.mjs similarity index 83% rename from module/settings/startSidebarExpanded.mjs rename to module/tweaks/startSidebarExpanded.mjs index 4cf9f2d..14b39f4 100644 --- a/module/settings/startSidebarExpanded.mjs +++ b/module/tweaks/startSidebarExpanded.mjs @@ -1,11 +1,13 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; -const key = `startSidebarExpanded`; +export const key = `startSidebarExpanded`; export function startSidebarExpanded() { status[key] = SettingStatusEnum.Unknown; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/settings/startingSidebarTab.mjs b/module/tweaks/startingSidebarTab.mjs similarity index 90% rename from module/settings/startingSidebarTab.mjs rename to module/tweaks/startingSidebarTab.mjs index 02fab17..aba7170 100644 --- a/module/settings/startingSidebarTab.mjs +++ b/module/tweaks/startingSidebarTab.mjs @@ -1,11 +1,13 @@ import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; import { __ID__ } from "../consts.mjs"; import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; -const key = `startingSidebarTab`; +export const key = `startingSidebarTab`; export function startingSidebarTab() { status[key] = SettingStatusEnum.Unknown; + if (preventTweakRegistration(key)) { return }; // #region Registration Logger.log(`Registering setting: ${key}`); diff --git a/module/tweaks/toggleMouseBroadcast.mjs b/module/tweaks/toggleMouseBroadcast.mjs new file mode 100644 index 0000000..0fa123d --- /dev/null +++ b/module/tweaks/toggleMouseBroadcast.mjs @@ -0,0 +1,122 @@ +import { SettingStatusEnum, status } from "../utils/SettingStatus.mjs"; +import { __ID__ } from "../consts.mjs"; +import { Logger } from "../utils/Logger.mjs"; +import { preventTweakRegistration } from "../utils/preRegisterTweak.mjs"; + +export const key = `toggleMouseBroadcast`; + +/** @type {number | null} */ +let notifID = null; + +export function toggleMouseBroadcast() { + status[key] = SettingStatusEnum.Unknown; + if (preventTweakRegistration(key, true)) { return }; + + // #region Registration + Logger.log(`Registering setting: ${key}`); + + // MARK: setting + game.settings.register(__ID__, key, { + scope: `client`, + config: false, + default: true, + }); + + // MARK: keybind + game.keybindings.register(__ID__, key, { + name: `OFT.keybindings.${key}.name`, + hint: `OFT.keybindings.${key}.hint`, + precedence: CONST.KEYBINDING_PRECEDENCE.NORMAL, + restricted: true, + editable: [ + { key: `KeyH` }, + ], + onDown: (event) => { + if (!game.user.hasPermission(`SHOW_CURSOR`)) { return }; + event.preventDefault?.(); + const current = game.settings.get(__ID__, key); + + if (current) { + notifID = ui.notifications.warn( + `OFT.notifs.${key}.hidingCursor`, + { console: false, localize: true, permanent: true }, + ).id; + } else { + if (notifID != null) { + ui.notifications.remove(notifID); + notifID = null; + }; + ui.notifications.success( + `OFT.notifs.${key}.showingCursor`, + { console: false, localize: true }, + ); + }; + + game.settings.set(__ID__, key, !current); + + // Hide the existing cursor + game.user.broadcastActivity({ cursor: null }); + return true; + }, + }); + // #endregion Registration + + // #region Implementation + Hooks.on(`renderControlsConfig`, renderControlsConfigHandler); + Hooks.once(`ready`, readyHandler); + + class OFTControlsLayer extends CONFIG.Canvas.layers.controls.layerClass { + _onMouseMove(currentPos) { + if (!game.settings.get(__ID__, key)) { + game.user.broadcastActivity({}); + }; + super._onMouseMove(currentPos); + }; + }; + CONFIG.Canvas.layers.controls.layerClass = OFTControlsLayer; + + // #endregion Implementation + + status[key] = SettingStatusEnum.Registered; +}; + +// #region Helpers +const tabGroup = `categories`; + +/** + * Handle showing the "hiding your cursor" notification when the user + * connects to the server initially, allowing the current state to be + * correctly described to the user. + */ +function readyHandler() { + const hideCursor = !game.settings.get(__ID__, key); + const canShowCursor = game.user.hasPermission(`SHOW_CURSOR`); + if (hideCursor && canShowCursor) { + notifID = ui.notifications.warn( + `OFT.notifs.${key}.hidingCursor`, + { console: false, localize: true, permanent: true }, + ).id; + }; +}; + +/** + * Handles hiding the keybinding from the configuration if and when the user + * does not have the SHOW_CURSOR permission since this keybinding is really + * useless if they can't even broadcast their cursor in the first place. + */ +function renderControlsConfigHandler(_app, element) { + + if (game.user.hasPermission(`SHOW_CURSOR`)) { return }; + + const keybindingList = element.querySelector(`section[data-group="${tabGroup}"][data-tab="${__ID__}"]`); + const tabButton = element.querySelector(`button[data-group="${tabGroup}"][data-tab="${__ID__}"]`); + + const keybind = keybindingList.querySelector(`.form-group[data-action-id="${__ID__}.${key}"]`); + keybind?.remove(); + + if (keybindingList.childElementCount === 0) { + keybindingList.remove(); + tabButton.remove(); + }; +}; +// #endregion Helpers diff --git a/module/utils/preRegisterTweak.mjs b/module/utils/preRegisterTweak.mjs new file mode 100644 index 0000000..f97bebb --- /dev/null +++ b/module/utils/preRegisterTweak.mjs @@ -0,0 +1,26 @@ +import { SettingStatusEnum, status } from "./SettingStatus.mjs"; +import { __ID__ } from "../consts.mjs"; +import { Logger } from "./Logger.mjs"; + +export function preventTweakRegistration(key, invasive = false) { + let prevented = Hooks.call(`${__ID__}.preRegisterTweak`, key, invasive); + + // Compatibility Code + if (Hooks.events[`${__ID__}.preventSetting`] != null) { + foundry.utils.logCompatibilityWarning( + `The hook "${__ID__}.preventSetting" has been renamed "${__ID__}.registerTweak".`, + { since: `v1.2.0`, until: `v2.0.0`, stack: false, once: true }, + ); + if (prevented !== false) { + prevented = Hooks.call(`${__ID__}.preventSetting`, key); + }; + }; + + if (!prevented) { + Logger.log(`Preventing setting "${key}" from being registered`); + status[key] = SettingStatusEnum.Blocked; + return true; + }; + + return false; +}; diff --git a/oft.lock b/oft.lock new file mode 100644 index 0000000..82ef623 --- /dev/null +++ b/oft.lock @@ -0,0 +1 @@ +🔒 \ No newline at end of file diff --git a/styles/main.css b/styles/main.css index 2312044..1d0fec5 100644 --- a/styles/main.css +++ b/styles/main.css @@ -6,4 +6,4 @@ @import url("./apps.css"); /* Make the chat sidebar the same width as all the other tabs */ -.chat-sidebar:not(.sidebar-popout) { width: unset; } +.chat-sidebar:not(.sidebar-popout) { width: var(--sidebar-width); }