Files
GDevelop/Extensions/BBText/JsExtension.js

640 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @flow
/**
* This is a declaration of an extension for GDevelop 5.
*
* Changes in this file are watched and automatically imported if the editor
* is running. You can also manually run `node import-GDJS-Runtime.js` (in newIDE/app/scripts).
*
* The file must be named "JsExtension.js", otherwise GDevelop won't load it.
* ⚠️ If you make a change and the extension is not loaded, open the developer console
* and search for any errors.
*
* More information on https://github.com/4ian/GDevelop/blob/master/newIDE/README-extensions.md
*/
/*::
// Import types to allow Flow to do static type checking on this file.
// Extensions declaration are typed using Flow (like the editor), but the files
// for the game engine are checked with TypeScript annotations.
import { type ObjectsRenderingService, type ObjectsEditorService } from '../JsExtensionTypes.flow.js'
*/
const stringifyOptions = (options) => '["' + options.join('","') + '"]';
module.exports = {
createExtension: function (
_ /*: (string) => string */,
gd /*: libGDevelop */
) {
const extension = new gd.PlatformExtension();
extension
.setExtensionInformation(
'BBText',
_('BBCode Text Object'),
'A BBText is an object displaying on the screen a rich text formatted using BBCode markup (allowing to set parts of the text as bold, italic, use different colors and shadows).',
'Todor Imreorov',
'Open source (MIT License)'
)
.setExtensionHelpPath('/objects/bbtext')
.setCategory('Text');
extension
.addInstructionOrExpressionGroupMetadata(_('BBCode Text Object'))
.setIcon('JsPlatform/Extensions/bbcode32.png');
var objectBBText = new gd.ObjectJsImplementation();
// $FlowExpectedError
objectBBText.updateProperty = function (
objectContent,
propertyName,
newValue
) {
if (propertyName in objectContent) {
if (typeof objectContent[propertyName] === 'boolean')
objectContent[propertyName] = newValue === '1';
else if (typeof objectContent[propertyName] === 'number')
objectContent[propertyName] = parseFloat(newValue);
else objectContent[propertyName] = newValue;
return true;
}
return false;
};
// $FlowExpectedError
objectBBText.getProperties = function (objectContent) {
const objectProperties = new gd.MapStringPropertyDescriptor();
objectProperties
.getOrCreate('text')
.setValue(objectContent.text)
.setType('textarea')
.setLabel(_('BBCode text'));
objectProperties
.getOrCreate('color')
.setValue(objectContent.color)
.setType('color')
.setLabel(_('Base color'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('opacity')
.setValue(objectContent.opacity.toString())
.setType('number')
.setLabel(_('Opacity (0-255)'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('fontSize')
.setValue(objectContent.fontSize.toString())
.setType('number')
.setLabel(_('Base size'))
.setGroup(_('Font'));
objectProperties
.getOrCreate('align')
.setValue(objectContent.align)
.setType('choice')
.addExtraInfo('left')
.addExtraInfo('center')
.addExtraInfo('right')
.setLabel(_('Base alignment'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('fontFamily')
.setValue(objectContent.fontFamily)
.setType('resource')
.addExtraInfo('font')
.setLabel(_('Font'))
.setGroup(_('Font'));
objectProperties
.getOrCreate('wordWrap')
.setValue(objectContent.wordWrap ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Word wrapping'))
.setGroup(_('Appearance'));
objectProperties
.getOrCreate('visible')
.setValue(objectContent.visible ? 'true' : 'false')
.setType('boolean')
.setLabel(_('Visible on start'))
.setGroup(_('Appearance'));
return objectProperties;
};
objectBBText.setRawJSONContent(
JSON.stringify({
text: '[b]bold[/b] [i]italic[/i] [size=15]smaller[/size] [font=times]times[/font] font\n[spacing=12]spaced out[/spacing]\n[outline=yellow]outlined[/outline] [shadow=red]DropShadow[/shadow] ',
opacity: 255,
fontSize: 20,
visible: true,
color: '0;0;0',
fontFamily: 'Arial',
align: 'left',
wordWrap: true,
})
);
// $FlowExpectedError
objectBBText.updateInitialInstanceProperty = function (
objectContent,
instance,
propertyName,
newValue,
project,
layout
) {
return false;
};
// $FlowExpectedError
objectBBText.getInitialInstanceProperties = function (
content,
instance,
project,
layout
) {
var instanceProperties = new gd.MapStringPropertyDescriptor();
return instanceProperties;
};
const object = extension
.addObject(
'BBText',
_('BBText'),
_(
'Displays a rich text label using BBCode markup (allowing to set parts of the text as bold, italic, use different colors and shadows).'
),
'JsPlatform/Extensions/bbcode32.png',
objectBBText
)
.setIncludeFile('Extensions/BBText/bbtextruntimeobject.js')
.addIncludeFile('Extensions/BBText/bbtextruntimeobject-pixi-renderer.js')
.addIncludeFile(
'Extensions/BBText/pixi-multistyle-text/dist/pixi-multistyle-text.umd.js'
)
.setCategoryFullName(_('Text'));
/**
* Utility function to add both a setter and a getter to a property from a list.
* Useful for setting multiple generic properties.
*/
const addSettersAndGettersToObject = (gdObject, properties, objectName) => {
properties.forEach((property) => {
const parameterType =
property.type === 'boolean' ? 'yesorno' : property.type;
// Add the expression
if (parameterType === 'number') {
gdObject
.addExpression(
`Get${property.functionName}`,
property.expressionLabel,
property.expressionDescription,
'',
property.iconPath
)
.addParameter('object', objectName, objectName, false)
.getCodeExtraInformation()
.setFunctionName(`get${property.functionName}`);
} else if (
parameterType === 'string' ||
parameterType === 'stringWithSelector'
) {
gdObject
.addStrExpression(
`Get${property.functionName}`,
property.expressionLabel,
property.expressionDescription,
'',
property.iconPath
)
.addParameter('object', objectName, objectName, false)
.getCodeExtraInformation()
.setFunctionName(`get${property.functionName}`);
}
// Add the action
if (
parameterType === 'number' ||
parameterType === 'string' ||
parameterType === 'stringWithSelector'
) {
const parameterOptions =
gd.ParameterOptions.makeNewOptions().setDescription(
property.paramLabel
);
if (property.options) {
parameterOptions.setTypeExtraInfo(
stringifyOptions(property.options)
);
}
gdObject
.addAction(
`Set${property.functionName}`,
property.instructionLabel,
property.actionDescription,
property.actionSentence,
'',
property.iconPath,
property.iconPath
)
.addParameter('object', objectName, objectName, false)
.useStandardOperatorParameters(parameterType, parameterOptions)
.getCodeExtraInformation()
.setFunctionName(`set${property.functionName}`)
.setGetter(`get${property.functionName}`);
} else {
gdObject
.addAction(
`Set${property.functionName}`,
property.instructionLabel,
property.actionDescription,
property.actionSentence,
'',
property.iconPath,
property.iconPath
)
.addParameter('object', objectName, objectName, false)
.addParameter(
parameterType,
property.paramLabel,
'', // There should not be options for the property if it's not a stringWithSelector
false
)
.getCodeExtraInformation()
.setFunctionName(`set${property.functionName}`)
.setGetter(`get${property.functionName}`);
}
// Add condition
if (
parameterType === 'string' ||
parameterType === 'number' ||
parameterType === 'stringWithSelector'
) {
const parameterOptions =
gd.ParameterOptions.makeNewOptions().setDescription(
property.paramLabel
);
if (property.options) {
parameterOptions.setTypeExtraInfo(
stringifyOptions(property.options)
);
}
gdObject
.addCondition(
`Is${property.functionName}`,
property.instructionLabel,
property.conditionDescription,
property.conditionSentence,
'',
property.iconPath,
property.iconPath
)
.addParameter('object', objectName, objectName, false)
.useStandardRelationalOperatorParameters(
parameterType,
parameterOptions
)
.getCodeExtraInformation()
.setFunctionName(`get${property.functionName}`);
} else if (parameterType === 'yesorno') {
gdObject
.addCondition(
`Is${property.functionName}`,
property.instructionLabel,
property.conditionDescription,
property.conditionSentence,
'',
property.iconPath,
property.iconPath
)
.addParameter('object', objectName, objectName, false)
.getCodeExtraInformation()
.setFunctionName(`get${property.functionName}`);
}
});
};
const setterAndGetterProperties = [
{
functionName: 'BBText',
iconPath: 'res/actions/text24_black.png',
type: 'string',
instructionLabel: _('BBCode text'),
paramLabel: _('Text'),
conditionDescription: _('Compare the value of the BBCode text.'),
conditionSentence: _('the BBCode text'),
actionDescription: _('Set BBCode text'),
actionSentence: _('the BBCode text'),
expressionLabel: _('Get BBCode text'),
expressionDescription: _('Get BBCode text'),
},
{
functionName: 'Color',
iconPath: 'res/actions/color24.png',
type: 'color',
instructionLabel: _('Color'),
paramLabel: _('Color (R;G;B)'),
conditionDescription: '', // No conditions for a "color" property
conditionSentence: '', // No conditions for a "color" property
actionDescription: _('Set base color'),
actionSentence: _('Set base color of _PARAM0_ to _PARAM1_'),
expressionLabel: '', // No expression for a "color" property
expressionDescription: '', // No expression for a "color" property
},
{
functionName: 'Opacity',
iconPath: 'res/actions/opacity24.png',
type: 'number',
instructionLabel: _('Opacity'),
paramLabel: _('Opacity (0-255)'),
conditionDescription: _(
'Compare the value of the base opacity of the text.'
),
conditionSentence: _('the base opacity'),
actionDescription: _('Set base opacity'),
actionSentence: _('the base opacity'),
expressionLabel: _('Get the base opacity'),
expressionDescription: _('Get the base opacity'),
},
{
functionName: 'FontSize',
iconPath: 'res/actions/characterSize24.png',
type: 'number',
instructionLabel: _('Font size'),
paramLabel: _('Font size'),
conditionDescription: _('Compare the base font size of the text.'),
conditionSentence: _('the base font size'),
actionDescription: _('Set base font size'),
actionSentence: _('the base font size'),
expressionLabel: _('Get the base font size'),
expressionDescription: _('Get the base font size'),
},
{
functionName: 'FontFamily',
iconPath: 'res/actions/font24.png',
type: 'string',
instructionLabel: _('Font family'),
paramLabel: _('Font family'),
conditionDescription: _('Compare the value of font family'),
conditionSentence: _('the base font family'),
actionDescription: _('Set font family'),
actionSentence: _('the base font family'),
expressionLabel: _('Get the base font family'),
expressionDescription: _('Get the base font family'),
},
{
functionName: 'Alignment',
iconPath: 'res/actions/textAlign24.png',
type: 'stringWithSelector',
instructionLabel: _('Alignment'),
paramLabel: _('Alignment'),
options: ['left', 'right', 'center'],
conditionDescription: _('Check the current text alignment.'),
conditionSentence: _('The text alignment of _PARAM0_ is _PARAM1_'),
actionDescription: _('Change the alignment of the text.'),
actionSentence: _('Set text alignment of _PARAM0_ to _PARAM1_'),
expressionLabel: _('Get the text alignment'),
expressionDescription: _('Get the text alignment'),
},
{
functionName: 'WordWrap',
iconPath: 'res/actions/scaleWidth24_black.png',
type: 'boolean',
instructionLabel: _('Word wrap'),
paramLabel: _('Word wrap'),
conditionDescription: _('Check if word wrap is enabled.'),
conditionSentence: _('Word wrap is enabled'),
actionDescription: _('Set word wrap'),
actionSentence: _('Activate word wrap for _PARAM0_: _PARAM1_'),
expressionLabel: '',
expressionDescription: '',
},
{
functionName: 'WrappingWidth',
iconPath: 'res/actions/scaleWidth24_black.png',
type: 'number',
instructionLabel: _('Wrapping width'),
paramLabel: _('Wrapping width'),
conditionDescription: _(
'Compare the width, in pixels, after which the text is wrapped on next line.'
),
conditionSentence: _('the wrapping width'),
actionDescription: _(
'Change the width, in pixels, after which the text is wrapped on next line.'
),
actionSentence: _('the wrapping width'),
expressionLabel: _('Get the wrapping width'),
expressionDescription: _('Get the wrapping width'),
},
];
addSettersAndGettersToObject(object, setterAndGetterProperties, 'BBText');
return extension;
},
/**
* You can optionally add sanity tests that will check the basic working
* of your extension behaviors/objects by instantiating behaviors/objects
* and setting the property to a given value.
*
* If you don't have any tests, you can simply return an empty array.
*
* But it is recommended to create tests for the behaviors/objects properties you created
* to avoid mistakes.
*/
runExtensionSanityTests: function (
gd /*: libGDevelop */,
extension /*: gdPlatformExtension*/
) {
return [];
},
/**
* Register editors for objects.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
*/
registerEditorConfigurations: function (
objectsEditorService /*: ObjectsEditorService */
) {
objectsEditorService.registerEditorConfiguration(
'BBText::BBText',
objectsEditorService.getDefaultObjectJsImplementationPropertiesEditor({
helpPagePath: '/objects/bbtext',
})
);
},
/**
* Register renderers for instance of objects on the scene editor.
*
* Run `node import-GDJS-Runtime.js` (in newIDE/app/scripts) if you make any change.
*/
registerInstanceRenderers: function (
objectsRenderingService /*: ObjectsRenderingService */
) {
const RenderedInstance = objectsRenderingService.RenderedInstance;
const PIXI = objectsRenderingService.PIXI;
const MultiStyleText = objectsRenderingService.requireModule(
__dirname,
'pixi-multistyle-text/dist/pixi-multistyle-text.umd'
);
/**
* Renderer for instances of BBText inside the IDE.
*
* @extends RenderedBBTextInstance
* @class RenderedBBTextInstance
* @constructor
*/
function RenderedBBTextInstance(
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
) {
RenderedInstance.call(
this,
project,
layout,
instance,
associatedObjectConfiguration,
pixiContainer,
pixiResourcesLoader
);
const bbTextStyles = {
default: {
// Use a default font family the time for the resource font to be loaded.
fontFamily: 'Arial',
fontSize: '24px',
fill: '#cccccc',
tagStyle: 'bbcode',
wordWrap: true,
wordWrapWidth: 250, // This value is the default wrapping width of the runtime object.
align: 'left',
},
};
this._pixiObject = new MultiStyleText('', bbTextStyles);
this._pixiObject.anchor.x = 0.5;
this._pixiObject.anchor.y = 0.5;
this._pixiContainer.addChild(this._pixiObject);
this.update();
}
RenderedBBTextInstance.prototype = Object.create(
RenderedInstance.prototype
);
/**
* Return the path to the thumbnail of the specified object.
*/
RenderedBBTextInstance.getThumbnail = function (
project,
resourcesLoader,
objectConfiguration
) {
return 'JsPlatform/Extensions/bbcode24.png';
};
/**
* This is called to update the PIXI object on the scene editor
*/
RenderedBBTextInstance.prototype.update = function () {
const properties = this._associatedObjectConfiguration.getProperties();
const rawText = properties.get('text').getValue();
if (rawText !== this._pixiObject.text) {
this._pixiObject.text = rawText;
}
const opacity = properties.get('opacity').getValue();
this._pixiObject.alpha = opacity / 255;
const color = properties.get('color').getValue();
this._pixiObject.textStyles.default.fill =
objectsRenderingService.rgbOrHexToHexNumber(color);
const fontSize = properties.get('fontSize').getValue();
this._pixiObject.textStyles.default.fontSize = `${fontSize}px`;
const fontResourceName = properties.get('fontFamily').getValue();
if (this._fontResourceName !== fontResourceName) {
this._fontResourceName = fontResourceName;
this._pixiResourcesLoader
.loadFontFamily(this._project, fontResourceName)
.then((fontFamily) => {
// Once the font is loaded, we can use the given fontFamily.
this._pixiObject.textStyles.default.fontFamily = fontFamily;
this._pixiObject.dirty = true;
})
.catch((err) => {
// Ignore errors
console.warn(
'Unable to load font family for RenderedBBTextInstance',
err
);
});
}
const wordWrap = properties.get('wordWrap').getValue() === 'true';
if (wordWrap !== this._pixiObject._style.wordWrap) {
this._pixiObject._style.wordWrap = wordWrap;
this._pixiObject.dirty = true;
}
const align = properties.get('align').getValue();
if (align !== this._pixiObject._style.align) {
this._pixiObject._style.align = align;
this._pixiObject.dirty = true;
}
this._pixiObject.position.x =
this._instance.getX() + this._pixiObject.width / 2;
this._pixiObject.position.y =
this._instance.getY() + this._pixiObject.height / 2;
this._pixiObject.rotation = RenderedInstance.toRad(
this._instance.getAngle()
);
if (this._instance.hasCustomSize() && this._pixiObject) {
const customWidth = this.getCustomWidth();
if (
this._pixiObject &&
this._pixiObject._style.wordWrapWidth !== customWidth
) {
this._pixiObject._style.wordWrapWidth = customWidth;
this._pixiObject.dirty = true;
}
}
};
/**
* Return the width of the instance, when it's not resized.
*/
RenderedBBTextInstance.prototype.getDefaultWidth = function () {
return this._pixiObject.width;
};
/**
* Return the height of the instance, when it's not resized.
*/
RenderedBBTextInstance.prototype.getDefaultHeight = function () {
return this._pixiObject.height;
};
objectsRenderingService.registerInstanceRenderer(
'BBText::BBText',
RenderedBBTextInstance
);
},
};