Initial
This commit is contained in:
423
resources/app/client/apps/hud/hotbar.js
Normal file
423
resources/app/client/apps/hud/hotbar.js
Normal file
@@ -0,0 +1,423 @@
|
||||
/**
|
||||
* The global action bar displayed at the bottom of the game view.
|
||||
* The Hotbar is a UI element at the bottom of the screen which contains Macros as interactive buttons.
|
||||
* The Hotbar supports 5 pages of global macros which can be dragged and dropped to organize as you wish.
|
||||
*
|
||||
* Left-clicking a Macro button triggers its effect.
|
||||
* Right-clicking the button displays a context menu of Macro options.
|
||||
* The number keys 1 through 0 activate numbered hotbar slots.
|
||||
* Pressing the delete key while hovering over a Macro will remove it from the bar.
|
||||
*
|
||||
* @see {@link Macros}
|
||||
* @see {@link Macro}
|
||||
*/
|
||||
class Hotbar extends Application {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
game.macros.apps.push(this);
|
||||
|
||||
/**
|
||||
* The currently viewed macro page
|
||||
* @type {number}
|
||||
*/
|
||||
this.page = 1;
|
||||
|
||||
/**
|
||||
* The currently displayed set of macros
|
||||
* @type {Macro[]}
|
||||
*/
|
||||
this.macros = [];
|
||||
|
||||
/**
|
||||
* Track collapsed state
|
||||
* @type {boolean}
|
||||
*/
|
||||
this._collapsed = false;
|
||||
|
||||
/**
|
||||
* Track which hotbar slot is the current hover target, if any
|
||||
* @type {number|null}
|
||||
*/
|
||||
this._hover = null;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
static get defaultOptions() {
|
||||
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||
id: "hotbar",
|
||||
template: "templates/hud/hotbar.html",
|
||||
popOut: false,
|
||||
dragDrop: [{ dragSelector: ".macro-icon", dropSelector: "#macro-list" }]
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Whether the hotbar is locked.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get locked() {
|
||||
return game.settings.get("core", "hotbarLock");
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
getData(options={}) {
|
||||
this.macros = this._getMacrosByPage(this.page);
|
||||
return {
|
||||
page: this.page,
|
||||
macros: this.macros,
|
||||
barClass: this._collapsed ? "collapsed" : "",
|
||||
locked: this.locked
|
||||
};
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the Array of Macro (or null) values that should be displayed on a numbered page of the bar
|
||||
* @param {number} page
|
||||
* @returns {Macro[]}
|
||||
* @private
|
||||
*/
|
||||
_getMacrosByPage(page) {
|
||||
const macros = game.user.getHotbarMacros(page);
|
||||
for ( let [i, slot] of macros.entries() ) {
|
||||
slot.key = i<9 ? i+1 : 0;
|
||||
slot.icon = slot.macro ? slot.macro.img : null;
|
||||
slot.cssClass = slot.macro ? "active" : "inactive";
|
||||
slot.tooltip = slot.macro ? slot.macro.name : null;
|
||||
}
|
||||
return macros;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Collapse the Hotbar, minimizing its display.
|
||||
* @returns {Promise} A promise which resolves once the collapse animation completes
|
||||
*/
|
||||
async collapse() {
|
||||
if ( this._collapsed ) return true;
|
||||
const toggle = this.element.find("#bar-toggle");
|
||||
const icon = toggle.children("i");
|
||||
const bar = this.element.find("#action-bar");
|
||||
return new Promise(resolve => {
|
||||
bar.slideUp(200, () => {
|
||||
bar.addClass("collapsed");
|
||||
icon.removeClass("fa-caret-down").addClass("fa-caret-up");
|
||||
this._collapsed = true;
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Expand the Hotbar, displaying it normally.
|
||||
* @returns {Promise} A promise which resolves once the expand animation completes
|
||||
*/
|
||||
async expand() {
|
||||
if ( !this._collapsed ) return true;
|
||||
const toggle = this.element.find("#bar-toggle");
|
||||
const icon = toggle.children("i");
|
||||
const bar = this.element.find("#action-bar");
|
||||
return new Promise(resolve => {
|
||||
bar.slideDown(200, () => {
|
||||
bar.css("display", "");
|
||||
bar.removeClass("collapsed");
|
||||
icon.removeClass("fa-caret-up").addClass("fa-caret-down");
|
||||
this._collapsed = false;
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Change to a specific numbered page from 1 to 5
|
||||
* @param {number} page The page number to change to.
|
||||
*/
|
||||
changePage(page) {
|
||||
this.page = Math.clamp(page ?? 1, 1, 5);
|
||||
this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Change the page of the hotbar by cycling up (positive) or down (negative)
|
||||
* @param {number} direction The direction to cycle
|
||||
*/
|
||||
cyclePage(direction) {
|
||||
direction = Number.isNumeric(direction) ? Math.sign(direction) : 1;
|
||||
if ( direction > 0 ) {
|
||||
this.page = this.page < 5 ? this.page+1 : 1;
|
||||
} else {
|
||||
this.page = this.page > 1 ? this.page-1 : 5;
|
||||
}
|
||||
this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
/* Event Listeners and Handlers
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
activateListeners(html) {
|
||||
super.activateListeners(html);
|
||||
|
||||
// Macro actions
|
||||
html.find("#bar-toggle").click(this._onToggleBar.bind(this));
|
||||
html.find("#macro-directory").click(ev => ui.macros.renderPopout(true));
|
||||
html.find(".macro").click(this._onClickMacro.bind(this));
|
||||
html.find(".page-control").click(this._onClickPageControl.bind(this));
|
||||
|
||||
// Activate context menu
|
||||
this._contextMenu(html);
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @inheritdoc */
|
||||
_contextMenu(html) {
|
||||
ContextMenu.create(this, html, ".macro", this._getEntryContextOptions());
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Get the Macro entry context options
|
||||
* @returns {object[]} The Macro entry context options
|
||||
* @private
|
||||
*/
|
||||
_getEntryContextOptions() {
|
||||
return [
|
||||
{
|
||||
name: "MACRO.Edit",
|
||||
icon: '<i class="fas fa-edit"></i>',
|
||||
condition: li => {
|
||||
const macro = game.macros.get(li.data("macro-id"));
|
||||
return macro ? macro.isOwner : false;
|
||||
},
|
||||
callback: li => {
|
||||
const macro = game.macros.get(li.data("macro-id"));
|
||||
macro.sheet.render(true);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "MACRO.Remove",
|
||||
icon: '<i class="fas fa-times"></i>',
|
||||
condition: li => !!li.data("macro-id"),
|
||||
callback: li => game.user.assignHotbarMacro(null, Number(li.data("slot")))
|
||||
},
|
||||
{
|
||||
name: "MACRO.Delete",
|
||||
icon: '<i class="fas fa-trash"></i>',
|
||||
condition: li => {
|
||||
const macro = game.macros.get(li.data("macro-id"));
|
||||
return macro ? macro.isOwner : false;
|
||||
},
|
||||
callback: li => {
|
||||
const macro = game.macros.get(li.data("macro-id"));
|
||||
return Dialog.confirm({
|
||||
title: `${game.i18n.localize("MACRO.Delete")} ${macro.name}`,
|
||||
content: `<h4>${game.i18n.localize("AreYouSure")}</h4><p>${game.i18n.localize("MACRO.DeleteWarning")}</p>`,
|
||||
yes: macro.delete.bind(macro)
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle left-click events to
|
||||
* @param {MouseEvent} event The originating click event
|
||||
* @protected
|
||||
*/
|
||||
async _onClickMacro(event) {
|
||||
event.preventDefault();
|
||||
const li = event.currentTarget;
|
||||
|
||||
// Case 1 - create a temporary Macro
|
||||
if ( li.classList.contains("inactive") ) {
|
||||
const cls = getDocumentClass("Macro");
|
||||
const macro = new cls({name: cls.defaultName({type: "chat"}), type: "chat", scope: "global"});
|
||||
macro.sheet._hotbarSlot = li.dataset.slot;
|
||||
macro.sheet.render(true);
|
||||
}
|
||||
|
||||
// Case 2 - trigger a Macro
|
||||
else {
|
||||
const macro = game.macros.get(li.dataset.macroId);
|
||||
return macro.execute();
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle pagination controls
|
||||
* @param {Event} event The originating click event
|
||||
* @private
|
||||
*/
|
||||
_onClickPageControl(event) {
|
||||
const action = event.currentTarget.dataset.action;
|
||||
switch ( action ) {
|
||||
case "page-up":
|
||||
this.cyclePage(1);
|
||||
break;
|
||||
|
||||
case "page-down":
|
||||
this.cyclePage(-1);
|
||||
break;
|
||||
|
||||
case "lock":
|
||||
this._toggleHotbarLock();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_canDragStart(selector) {
|
||||
return !this.locked;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_onDragStart(event) {
|
||||
const li = event.currentTarget.closest(".macro");
|
||||
const macro = game.macros.get(li.dataset.macroId);
|
||||
if ( !macro ) return false;
|
||||
const dragData = foundry.utils.mergeObject(macro.toDragData(), {slot: li.dataset.slot});
|
||||
event.dataTransfer.setData("text/plain", JSON.stringify(dragData));
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
_canDragDrop(selector) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/** @override */
|
||||
async _onDrop(event) {
|
||||
event.preventDefault();
|
||||
const li = event.target.closest(".macro");
|
||||
const slot = Number(li.dataset.slot);
|
||||
const data = TextEditor.getDragEventData(event);
|
||||
if ( Hooks.call("hotbarDrop", this, data, slot) === false ) return;
|
||||
|
||||
// Forbid overwriting macros if the hotbar is locked.
|
||||
const existingMacro = game.macros.get(game.user.hotbar[slot]);
|
||||
if ( existingMacro && this.locked ) return ui.notifications.warn("MACRO.CannotOverwrite", { localize: true });
|
||||
|
||||
// Get the dropped document
|
||||
const cls = getDocumentClass(data.type);
|
||||
const doc = await cls?.fromDropData(data);
|
||||
if ( !doc ) return;
|
||||
|
||||
// Get the Macro to add to the bar
|
||||
let macro;
|
||||
if ( data.type === "Macro" ) macro = game.macros.has(doc.id) ? doc : await cls.create(doc.toObject());
|
||||
else if ( data.type === "RollTable" ) macro = await this._createRollTableRollMacro(doc);
|
||||
else macro = await this._createDocumentSheetToggle(doc);
|
||||
|
||||
// Assign the macro to the hotbar
|
||||
if ( !macro ) return;
|
||||
return game.user.assignHotbarMacro(macro, slot, {fromSlot: data.slot});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a Macro which rolls a RollTable when executed
|
||||
* @param {Document} table The RollTable document
|
||||
* @returns {Promise<Macro>} A created Macro document to add to the bar
|
||||
* @private
|
||||
*/
|
||||
async _createRollTableRollMacro(table) {
|
||||
const command = `const table = await fromUuid("${table.uuid}");\nawait table.draw();`;
|
||||
return Macro.implementation.create({
|
||||
name: `${game.i18n.localize("TABLE.Roll")} ${table.name}`,
|
||||
type: "script",
|
||||
img: table.img,
|
||||
command
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Create a Macro document which can be used to toggle display of a Journal Entry.
|
||||
* @param {Document} doc A Document which should be toggled
|
||||
* @returns {Promise<Macro>} A created Macro document to add to the bar
|
||||
* @protected
|
||||
*/
|
||||
async _createDocumentSheetToggle(doc) {
|
||||
const name = doc.name || `${game.i18n.localize(doc.constructor.metadata.label)} ${doc.id}`;
|
||||
return Macro.implementation.create({
|
||||
name: `${game.i18n.localize("Display")} ${name}`,
|
||||
type: CONST.MACRO_TYPES.SCRIPT,
|
||||
img: "icons/svg/book.svg",
|
||||
command: `await Hotbar.toggleDocumentSheet("${doc.uuid}");`
|
||||
});
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle click events to toggle display of the macro bar
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
_onToggleBar(event) {
|
||||
event.preventDefault();
|
||||
if ( this._collapsed ) return this.expand();
|
||||
else return this.collapse();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Toggle the hotbar's lock state.
|
||||
* @returns {Promise<Hotbar>}
|
||||
* @protected
|
||||
*/
|
||||
async _toggleHotbarLock() {
|
||||
await game.settings.set("core", "hotbarLock", !this.locked);
|
||||
return this.render();
|
||||
}
|
||||
|
||||
/* -------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Handle toggling a document sheet.
|
||||
* @param {string} uuid The Document UUID to display
|
||||
* @returns {Promise<void>|Application|*}
|
||||
*/
|
||||
static async toggleDocumentSheet(uuid) {
|
||||
const doc = await fromUuid(uuid);
|
||||
if ( !doc ) {
|
||||
return ui.notifications.warn(game.i18n.format("WARNING.ObjectDoesNotExist", {
|
||||
name: game.i18n.localize("Document"),
|
||||
identifier: uuid
|
||||
}));
|
||||
}
|
||||
const sheet = doc.sheet;
|
||||
return sheet.rendered ? sheet.close() : sheet.render(true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user