350 lines
10 KiB
JavaScript
350 lines
10 KiB
JavaScript
|
|
/**
|
||
|
|
* A Note is an implementation of PlaceableObject which represents an annotated location within the Scene.
|
||
|
|
* Each Note links to a JournalEntry document and represents its location on the map.
|
||
|
|
* @category - Canvas
|
||
|
|
* @see {@link NoteDocument}
|
||
|
|
* @see {@link NotesLayer}
|
||
|
|
*/
|
||
|
|
class Note extends PlaceableObject {
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
static embeddedName = "Note";
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
static RENDER_FLAGS = {
|
||
|
|
redraw: {propagate: ["refresh"]},
|
||
|
|
refresh: {propagate: ["refreshState", "refreshPosition", "refreshTooltip", "refreshElevation"], alias: true},
|
||
|
|
refreshState: {propagate: ["refreshVisibility"]},
|
||
|
|
refreshVisibility: {},
|
||
|
|
refreshPosition: {},
|
||
|
|
refreshTooltip: {},
|
||
|
|
refreshElevation: {propagate: ["refreshVisibility"]},
|
||
|
|
/** @deprecated since v12 */
|
||
|
|
refreshText: {propagate: ["refreshTooltip"], deprecated: {since: 12, until: 14}, alias: true}
|
||
|
|
};
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The control icon.
|
||
|
|
* @type {ControlIcon}
|
||
|
|
*/
|
||
|
|
controlIcon;
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The tooltip.
|
||
|
|
* @type {PreciseText}
|
||
|
|
*/
|
||
|
|
tooltip;
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
get bounds() {
|
||
|
|
const {x, y, iconSize} = this.document;
|
||
|
|
const r = iconSize / 2;
|
||
|
|
return new PIXI.Rectangle(x - r, y - r, 2*r, 2*r);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The associated JournalEntry which is referenced by this Note
|
||
|
|
* @type {JournalEntry}
|
||
|
|
*/
|
||
|
|
get entry() {
|
||
|
|
return this.document.entry;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* The specific JournalEntryPage within the associated JournalEntry referenced by this Note.
|
||
|
|
*/
|
||
|
|
get page() {
|
||
|
|
return this.document.page;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determine whether the Note is visible to the current user based on their perspective of the Scene.
|
||
|
|
* Visibility depends on permission to the underlying journal entry, as well as the perspective of controlled Tokens.
|
||
|
|
* If Token Vision is required, the user must have a token with vision over the note to see it.
|
||
|
|
* @type {boolean}
|
||
|
|
*/
|
||
|
|
get isVisible() {
|
||
|
|
const accessTest = this.document.page ?? this.document.entry;
|
||
|
|
const access = accessTest?.testUserPermission(game.user, "LIMITED") ?? true;
|
||
|
|
if ( (access === false) || !canvas.visibility.tokenVision || this.document.global ) return access;
|
||
|
|
const point = {x: this.document.x, y: this.document.y};
|
||
|
|
const tolerance = this.document.iconSize / 4;
|
||
|
|
return canvas.visibility.testVisibility(point, {tolerance, object: this});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Rendering
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
async _draw(options) {
|
||
|
|
this.controlIcon = this.addChild(this._drawControlIcon());
|
||
|
|
this.tooltip = this.addChild(this._drawTooltip());
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Draw the control icon.
|
||
|
|
* @returns {ControlIcon}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_drawControlIcon() {
|
||
|
|
const {texture, iconSize} = this.document;
|
||
|
|
const icon = new ControlIcon({texture: texture.src, size: iconSize, tint: texture.tint});
|
||
|
|
icon.x -= (iconSize / 2);
|
||
|
|
icon.y -= (iconSize / 2);
|
||
|
|
return icon;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Draw the tooltip.
|
||
|
|
* @returns {PreciseText}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_drawTooltip() {
|
||
|
|
const tooltip = new PreciseText(this.document.label, this._getTextStyle());
|
||
|
|
tooltip.eventMode = "none";
|
||
|
|
return tooltip;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the tooltip.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshTooltip() {
|
||
|
|
this.tooltip.text = this.document.label;
|
||
|
|
this.tooltip.style = this._getTextStyle();
|
||
|
|
const halfPad = (0.5 * this.document.iconSize) + 12;
|
||
|
|
switch ( this.document.textAnchor ) {
|
||
|
|
case CONST.TEXT_ANCHOR_POINTS.CENTER:
|
||
|
|
this.tooltip.anchor.set(0.5, 0.5);
|
||
|
|
this.tooltip.position.set(0, 0);
|
||
|
|
break;
|
||
|
|
case CONST.TEXT_ANCHOR_POINTS.BOTTOM:
|
||
|
|
this.tooltip.anchor.set(0.5, 0);
|
||
|
|
this.tooltip.position.set(0, halfPad);
|
||
|
|
break;
|
||
|
|
case CONST.TEXT_ANCHOR_POINTS.TOP:
|
||
|
|
this.tooltip.anchor.set(0.5, 1);
|
||
|
|
this.tooltip.position.set(0, -halfPad);
|
||
|
|
break;
|
||
|
|
case CONST.TEXT_ANCHOR_POINTS.LEFT:
|
||
|
|
this.tooltip.anchor.set(1, 0.5);
|
||
|
|
this.tooltip.position.set(-halfPad, 0);
|
||
|
|
break;
|
||
|
|
case CONST.TEXT_ANCHOR_POINTS.RIGHT:
|
||
|
|
this.tooltip.anchor.set(0, 0.5);
|
||
|
|
this.tooltip.position.set(halfPad, 0);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Define a PIXI TextStyle object which is used for the tooltip displayed for this Note
|
||
|
|
* @returns {PIXI.TextStyle}
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_getTextStyle() {
|
||
|
|
const style = CONFIG.canvasTextStyle.clone();
|
||
|
|
|
||
|
|
// Positioning
|
||
|
|
if ( this.document.textAnchor === CONST.TEXT_ANCHOR_POINTS.LEFT ) style.align = "right";
|
||
|
|
else if ( this.document.textAnchor === CONST.TEXT_ANCHOR_POINTS.RIGHT ) style.align = "left";
|
||
|
|
|
||
|
|
// Font preferences
|
||
|
|
style.fontFamily = this.document.fontFamily || CONFIG.defaultFontFamily;
|
||
|
|
style.fontSize = this.document.fontSize;
|
||
|
|
|
||
|
|
// Toggle stroke style depending on whether the text color is dark or light
|
||
|
|
const color = this.document.textColor;
|
||
|
|
style.fill = color;
|
||
|
|
style.stroke = color.hsv[2] > 0.6 ? 0x000000 : 0xFFFFFF;
|
||
|
|
style.strokeThickness = 4;
|
||
|
|
return style;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Incremental Refresh */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_applyRenderFlags(flags) {
|
||
|
|
if ( flags.refreshState ) this._refreshState();
|
||
|
|
if ( flags.refreshVisibility ) this._refreshVisibility();
|
||
|
|
if ( flags.refreshPosition ) this._refreshPosition();
|
||
|
|
if ( flags.refreshTooltip ) this._refreshTooltip();
|
||
|
|
if ( flags.refreshElevation ) this._refreshElevation();
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the visibility.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshVisibility() {
|
||
|
|
const wasVisible = this.visible;
|
||
|
|
this.visible = this.isVisible;
|
||
|
|
if ( this.controlIcon ) this.controlIcon.refresh({
|
||
|
|
visible: this.visible,
|
||
|
|
borderVisible: this.hover || this.layer.highlightObjects
|
||
|
|
});
|
||
|
|
if ( wasVisible !== this.visible ) {
|
||
|
|
this.layer.hintMapNotes();
|
||
|
|
MouseInteractionManager.emulateMoveEvent();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the state of the Note. Called the Note enters a different interaction state.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshState() {
|
||
|
|
this.alpha = this._getTargetAlpha();
|
||
|
|
this.tooltip.visible = this.hover || this.layer.highlightObjects;
|
||
|
|
this.zIndex = this.hover ? 1 : 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the position of the Note. Called with the coordinates change.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshPosition() {
|
||
|
|
const {x, y} = this.document;
|
||
|
|
if ( (this.position.x !== x) || (this.position.y !== y) ) MouseInteractionManager.emulateMoveEvent();
|
||
|
|
this.position.set(this.document.x, this.document.y);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Refresh the elevation of the control icon.
|
||
|
|
* @protected
|
||
|
|
*/
|
||
|
|
_refreshElevation() {
|
||
|
|
this.controlIcon.elevation = this.document.elevation;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Document Event Handlers */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritDoc */
|
||
|
|
_onUpdate(changed, options, userId) {
|
||
|
|
super._onUpdate(changed, options, userId);
|
||
|
|
|
||
|
|
// Incremental Refresh
|
||
|
|
const positionChanged = ("x" in changed) || ("y" in changed);
|
||
|
|
this.renderFlags.set({
|
||
|
|
redraw: ("texture" in changed) || ("iconSize" in changed),
|
||
|
|
refreshVisibility: positionChanged || ["entryId", "pageId", "global"].some(k => k in changed),
|
||
|
|
refreshPosition: positionChanged,
|
||
|
|
refreshTooltip: ["text", "fontFamily", "fontSize", "textAnchor", "textColor", "iconSize"].some(k => k in changed),
|
||
|
|
refreshElevation: "elevation" in changed
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Interactivity */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_canHover(user) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_canView(user) {
|
||
|
|
const {entry, page} = this.document;
|
||
|
|
if ( !entry ) return false;
|
||
|
|
if ( game.user.isGM ) return true;
|
||
|
|
if ( page?.testUserPermission(game.user, "LIMITED", {exact: true}) ) {
|
||
|
|
// Special-case handling for image pages.
|
||
|
|
return page.type === "image";
|
||
|
|
}
|
||
|
|
const accessTest = page ?? entry;
|
||
|
|
return accessTest.testUserPermission(game.user, "OBSERVER");
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @override */
|
||
|
|
_canConfigure(user) {
|
||
|
|
return canvas.notes.active && this.document.canUserModify(game.user, "update");
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/** @inheritdoc */
|
||
|
|
_onClickLeft2(event) {
|
||
|
|
const {entry, page} = this.document;
|
||
|
|
if ( !entry ) return;
|
||
|
|
const options = {};
|
||
|
|
if ( page ) {
|
||
|
|
options.mode = JournalSheet.VIEW_MODES.SINGLE;
|
||
|
|
options.pageId = page.id;
|
||
|
|
}
|
||
|
|
const allowed = Hooks.call("activateNote", this, options);
|
||
|
|
if ( allowed === false ) return;
|
||
|
|
if ( page?.type === "image" ) {
|
||
|
|
return new ImagePopout(page.src, {
|
||
|
|
uuid: page.uuid,
|
||
|
|
title: page.name,
|
||
|
|
caption: page.image.caption
|
||
|
|
}).render(true);
|
||
|
|
}
|
||
|
|
entry.sheet.render(true, options);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
/* Deprecations and Compatibility */
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v12
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get text() {
|
||
|
|
const msg = "Note#text has been deprecated. Use Note#document#label instead.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
|
||
|
|
return this.document.label;
|
||
|
|
}
|
||
|
|
|
||
|
|
/* -------------------------------------------- */
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @deprecated since v12
|
||
|
|
* @ignore
|
||
|
|
*/
|
||
|
|
get size() {
|
||
|
|
const msg = "Note#size has been deprecated. Use Note#document#iconSize instead.";
|
||
|
|
foundry.utils.logCompatibilityWarning(msg, {since: 12, until: 14, once: true});
|
||
|
|
return this.document.iconSize;
|
||
|
|
}
|
||
|
|
}
|