From 3c91608189184de8dd0774290b763d52bfc73e57 Mon Sep 17 00:00:00 2001 From: joylink_zhaoerwei Date: Wed, 16 Oct 2024 17:09:36 +0800 Subject: [PATCH] =?UTF-8?q?=E9=80=9A=E7=94=A8=E7=9A=84button-=E7=B1=BB?= =?UTF-8?q?=E4=BC=BC=E7=BB=84=E4=BB=B6button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- rtss-proto-msg | 2 +- .../draw-app/IscsDrawProperties.vue | 8 +- .../draw-app/properties/ButtonProperty.vue | 112 +++++++ .../properties/CCTV/CCTVButtonProperty.vue | 40 --- .../draw-app/properties/RectProperty.vue | 2 +- src/drawApp/commonApp.ts | 13 + src/drawApp/graphics/ButtonInteraction.ts | 92 ++++++ .../graphics/CCTV/CCTVButtonInteraction.ts | 47 --- src/drawApp/iscsApp.ts | 16 +- src/graphics/CCTV/cctvButton/CCTVButton.ts | 81 ------ .../cctvButton/CCTVButtonDrawAssistant.ts | 122 -------- src/graphics/button/Button.ts | 201 +++++++++++++ src/graphics/button/ButtonDrawAssistant.ts | 129 +++++++++ .../cctv-button-data.json | 0 .../cctv-button-spritesheet.png | Bin src/layouts/IscsDrawLayout.vue | 5 +- src/protos/iscs_graphic_data.ts | 273 +++++++++++++++--- src/protos/sync_data_message.ts | 2 +- 18 files changed, 790 insertions(+), 355 deletions(-) create mode 100644 src/components/draw-app/properties/ButtonProperty.vue delete mode 100644 src/components/draw-app/properties/CCTV/CCTVButtonProperty.vue create mode 100644 src/drawApp/graphics/ButtonInteraction.ts delete mode 100644 src/drawApp/graphics/CCTV/CCTVButtonInteraction.ts delete mode 100644 src/graphics/CCTV/cctvButton/CCTVButton.ts delete mode 100644 src/graphics/CCTV/cctvButton/CCTVButtonDrawAssistant.ts create mode 100644 src/graphics/button/Button.ts create mode 100644 src/graphics/button/ButtonDrawAssistant.ts rename src/graphics/{CCTV/cctvButton => button}/cctv-button-data.json (100%) rename src/graphics/{CCTV/cctvButton => button}/cctv-button-spritesheet.png (100%) diff --git a/rtss-proto-msg b/rtss-proto-msg index 4ce6d52..c3ebea5 160000 --- a/rtss-proto-msg +++ b/rtss-proto-msg @@ -1 +1 @@ -Subproject commit 4ce6d5206d9ac578adb4305df8cbff59746d8d2f +Subproject commit c3ebea56bd46e93f4bd5204bdc3966b5b9fb9d58 diff --git a/src/components/draw-app/IscsDrawProperties.vue b/src/components/draw-app/IscsDrawProperties.vue index fd95662..86af68a 100644 --- a/src/components/draw-app/IscsDrawProperties.vue +++ b/src/components/draw-app/IscsDrawProperties.vue @@ -39,8 +39,8 @@ - @@ -56,8 +56,6 @@ import { useDrawStore } from 'src/stores/draw-store'; import CanvasProperty from './properties/CanvasIscsProperty.vue'; import IscsTextProperty from './properties/IscsTextProperty.vue'; import { TextContent } from 'src/graphics/textContent/TextContent'; -import cctvButtonProperty from './properties/CCTV/CCTVButtonProperty.vue'; -import { CCTVButton } from 'src/graphics/CCTV/cctvButton/CCTVButton'; import RectProperty from './properties/RectProperty.vue'; import { Rect } from 'src/graphics/rect/Rect'; import LineTemplate from './templates/LineTemplate.vue'; @@ -65,6 +63,8 @@ import LineProperty from './properties/LineProperty.vue'; import { Line } from 'src/graphics/line/Line'; import CircleProperty from './properties/CircleProperty.vue'; import { Circle } from 'src/graphics/circle/Circle'; +import ButtonProperty from './properties/ButtonProperty.vue'; +import { Button } from 'src/graphics/button/Button'; import { watch } from 'vue'; const drawStore = useDrawStore(); diff --git a/src/components/draw-app/properties/ButtonProperty.vue b/src/components/draw-app/properties/ButtonProperty.vue new file mode 100644 index 0000000..29c5f7e --- /dev/null +++ b/src/components/draw-app/properties/ButtonProperty.vue @@ -0,0 +1,112 @@ + + + diff --git a/src/components/draw-app/properties/CCTV/CCTVButtonProperty.vue b/src/components/draw-app/properties/CCTV/CCTVButtonProperty.vue deleted file mode 100644 index 482b71b..0000000 --- a/src/components/draw-app/properties/CCTV/CCTVButtonProperty.vue +++ /dev/null @@ -1,40 +0,0 @@ - - - diff --git a/src/components/draw-app/properties/RectProperty.vue b/src/components/draw-app/properties/RectProperty.vue index 7c0833c..9b8f2bd 100644 --- a/src/components/draw-app/properties/RectProperty.vue +++ b/src/components/draw-app/properties/RectProperty.vue @@ -85,7 +85,7 @@ @blur="onUpdate" label="高度" lazy-rules - :rules="[(val) => (val && val > 0) || '宽度必须大于0']" + :rules="[(val) => (val && val > 0) || '高度必须大于0']" /> { datas.push(new CircleData(circle)); }); + storage.commonGraphicStorage.buttons.forEach((button) => { + datas.push(new ButtonData(button)); + }); return datas; } @@ -140,6 +148,11 @@ export function saveCommonDrawDatas(app: IDrawApp, storage: ICommonStorage) { storage.commonGraphicStorage.circles.push( (circleData as CircleData).data ); + } else if (g instanceof Button) { + const buttonData = g.saveData(); + storage.commonGraphicStorage.buttons.push( + (buttonData as ButtonData).data + ); } }); diff --git a/src/drawApp/graphics/ButtonInteraction.ts b/src/drawApp/graphics/ButtonInteraction.ts new file mode 100644 index 0000000..f96fe8c --- /dev/null +++ b/src/drawApp/graphics/ButtonInteraction.ts @@ -0,0 +1,92 @@ +import * as pb_1 from 'google-protobuf'; +import { GraphicDataBase } from './GraphicDataBase'; +import { Button, IButtonData } from 'src/graphics/button/Button'; +import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; + +export class ButtonData extends GraphicDataBase implements IButtonData { + constructor(data?: iscsGraphicData.Button) { + let button; + if (data) { + button = data; + } else { + button = new iscsGraphicData.Button({ + common: GraphicDataBase.defaultCommonInfo(Button.Type), + }); + } + super(button); + } + + public get data(): iscsGraphicData.Button { + return this.getData(); + } + + get code(): string { + return this.data.code; + } + set code(v: string) { + this.data.code = v; + } + get codeColor(): string { + return this.data.codeColor; + } + set codeColor(v: string) { + this.data.codeColor = v; + } + get codeFontSize(): number { + return this.data.codeFontSize; + } + set codeFontSize(v: number) { + this.data.codeFontSize = v; + } + get belongSubMenu(): string { + return this.data.codeColor; + } + set belongSubMenu(v: string) { + this.data.codeColor = v; + } + get buttonType(): iscsGraphicData.Button.ButtonType { + return this.data.buttonType; + } + set buttonType(v: iscsGraphicData.Button.ButtonType) { + this.data.buttonType = v; + } + get width(): number { + return this.data.width; + } + set width(v: number) { + this.data.width = v; + } + get height(): number { + return this.data.height; + } + set height(v: number) { + this.data.height = v; + } + get radius(): number { + return this.data.radius; + } + set radius(v: number) { + this.data.radius = v; + } + get fillColor(): string { + return this.data.fillColor; + } + set fillColor(v: string) { + this.data.fillColor = v; + } + get alpha(): number { + return this.data.alpha; + } + set alpha(v: number) { + this.data.alpha = v; + } + clone(): ButtonData { + return new ButtonData(this.data.cloneMessage()); + } + copyFrom(data: ButtonData): void { + pb_1.Message.copyInto(data.data, this.data); + } + eq(other: ButtonData): boolean { + return pb_1.Message.equals(this.data, other.data); + } +} diff --git a/src/drawApp/graphics/CCTV/CCTVButtonInteraction.ts b/src/drawApp/graphics/CCTV/CCTVButtonInteraction.ts deleted file mode 100644 index 0647fd8..0000000 --- a/src/drawApp/graphics/CCTV/CCTVButtonInteraction.ts +++ /dev/null @@ -1,47 +0,0 @@ -import * as pb_1 from 'google-protobuf'; -import { GraphicDataBase } from '../GraphicDataBase'; -import { - CCTVButton, - ICCTVButtonData, -} from 'src/graphics/CCTV/cctvButton/CCTVButton'; -import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; - -export class CCTVButtonData extends GraphicDataBase implements ICCTVButtonData { - constructor(data?: iscsGraphicData.CCTVButton) { - let cctvButton; - if (data) { - cctvButton = data; - } else { - cctvButton = new iscsGraphicData.CCTVButton({ - common: GraphicDataBase.defaultCommonInfo(CCTVButton.Type), - }); - } - super(cctvButton); - } - - public get data(): iscsGraphicData.CCTVButton { - return this.getData(); - } - - get code(): string { - return this.data.code; - } - set code(v: string) { - this.data.code = v; - } - get buttonType(): iscsGraphicData.CCTVButton.ButtonType { - return this.data.buttonType; - } - set buttonType(v: iscsGraphicData.CCTVButton.ButtonType) { - this.data.buttonType = v; - } - clone(): CCTVButtonData { - return new CCTVButtonData(this.data.cloneMessage()); - } - copyFrom(data: CCTVButtonData): void { - pb_1.Message.copyInto(data.data, this.data); - } - eq(other: CCTVButtonData): boolean { - return pb_1.Message.equals(this.data, other.data); - } -} diff --git a/src/drawApp/iscsApp.ts b/src/drawApp/iscsApp.ts index 8c74311..367a6a0 100644 --- a/src/drawApp/iscsApp.ts +++ b/src/drawApp/iscsApp.ts @@ -17,12 +17,6 @@ import { saveDrawToServer, handlerNoEditCommonData, } from './commonApp'; -import { CCTVButtonData } from './graphics/CCTV/CCTVButtonInteraction'; -import { CCTVButtonDraw } from 'src/graphics/CCTV/cctvButton/CCTVButtonDrawAssistant'; -import { - CCTVButton, - CCTVButtonTemplate, -} from 'src/graphics/CCTV/cctvButton/CCTVButton'; import { useDrawStore } from 'src/stores/draw-store'; import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; import { getDraft } from 'src/api/DraftApi'; @@ -66,8 +60,6 @@ export function initIscsDrawApp(): IDrawApp { const app = drawApp; initCommonDrawApp(app); - new CCTVButtonDraw(app, new CCTVButtonTemplate(new CCTVButtonData())); - app.addKeyboardListener( new KeyListener({ value: 'KeyS', @@ -238,9 +230,9 @@ export async function loadDrawDatas(): Promise { ) { canvasProperty = ctvOfStationControl.canvas; datas = loadCommonDrawDatas(ctvOfStationControl); - ctvOfStationControl.cctvButtons.forEach((cctvButton) => { + /* ctvOfStationControl.cctvButtons.forEach((cctvButton) => { datas.push(new CCTVButtonData(cctvButton)); - }); + }); */ break; } } @@ -328,14 +320,14 @@ export function saveDrawDatas(app: IDrawApp) { cctvOfStationControl ) as iscsGraphicData.CCTVOfStationControlStorage; - graphics.forEach((g) => { + /* graphics.forEach((g) => { if (g instanceof CCTVButton) { const cctvButtonData = g.saveData(); cctvStorage.cctvButtons.push( (cctvButtonData as CCTVButtonData).data ); } - }); + }); */ storage.cctvOfStationControlStorages[i] = cctvStorage; break; } diff --git a/src/graphics/CCTV/cctvButton/CCTVButton.ts b/src/graphics/CCTV/cctvButton/CCTVButton.ts deleted file mode 100644 index 5259237..0000000 --- a/src/graphics/CCTV/cctvButton/CCTVButton.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { GraphicData, JlGraphic, JlGraphicTemplate } from 'jl-graphic'; -import CCTV_Button_Assets from './cctv-button-spritesheet.png'; -import CCTV_Button_JSON from './cctv-button-data.json'; -import { Assets, Sprite, Spritesheet, Texture } from 'pixi.js'; -import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; - -interface CCTVButtonTextures { - rectPressBtn: Texture; - rectBtn: Texture; - monitorBtn: Texture; - semicircleBtn: Texture; -} - -export interface ICCTVButtonData extends GraphicData { - get code(): string; - set code(v: string); - get buttonType(): iscsGraphicData.CCTVButton.ButtonType; - set buttonType(v: iscsGraphicData.CCTVButton.ButtonType); -} - -export class CCTVButton extends JlGraphic { - static Type = 'CCTVButton'; - _cctvButton: Sprite; - cctvButtonTextures: CCTVButtonTextures; - __state = 0; - - constructor(cctvButtonTextures: CCTVButtonTextures) { - super(CCTVButton.Type); - this.cctvButtonTextures = cctvButtonTextures; - this._cctvButton = new Sprite(); - this._cctvButton.texture = this.cctvButtonTextures.rectBtn; - this._cctvButton.anchor.set(0.5); - this.addChild(this._cctvButton); - } - get code(): string { - return this.datas.code; - } - get datas(): ICCTVButtonData { - return this.getDatas(); - } - - doRepaint(): void { - if (this.datas.buttonType == iscsGraphicData.CCTVButton.ButtonType.rect) { - this._cctvButton.texture = this.cctvButtonTextures.rectBtn; - } else if ( - this.datas.buttonType == iscsGraphicData.CCTVButton.ButtonType.monitor - ) { - this._cctvButton.texture = this.cctvButtonTextures.monitorBtn; - } else { - this._cctvButton.texture = this.cctvButtonTextures.semicircleBtn; - } - } -} - -export class CCTVButtonTemplate extends JlGraphicTemplate { - cctvButtonTextures?: CCTVButtonTextures; - constructor(dataTemplate: ICCTVButtonData) { - super(CCTVButton.Type, { dataTemplate }); - this.loadAssets(); - } - new(): CCTVButton { - if (this.cctvButtonTextures) { - const g = new CCTVButton(this.cctvButtonTextures); - g.loadData(this.datas); - return g; - } - throw new Error('资源未加载/加载失败'); - } - async loadAssets(): Promise { - const texture = await Assets.load(CCTV_Button_Assets); - const cctvButtonSheet = new Spritesheet(texture, CCTV_Button_JSON); - const result = await cctvButtonSheet.parse(); - this.cctvButtonTextures = { - rectPressBtn: result['rect-press-btn.png'], - rectBtn: result['rect-btn.png'], - monitorBtn: result['monitor-btn.png'], - semicircleBtn: result['semicircle-btn.png'], - }; - return this.cctvButtonTextures as CCTVButtonTextures; - } -} diff --git a/src/graphics/CCTV/cctvButton/CCTVButtonDrawAssistant.ts b/src/graphics/CCTV/cctvButton/CCTVButtonDrawAssistant.ts deleted file mode 100644 index b5ed24d..0000000 --- a/src/graphics/CCTV/cctvButton/CCTVButtonDrawAssistant.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { DisplayObject, FederatedMouseEvent, Point } from 'pixi.js'; -import { - AbsorbableLine, - AbsorbablePosition, - GraphicDrawAssistant, - GraphicInteractionPlugin, - GraphicTransformEvent, - IDrawApp, - JlGraphic, -} from 'jl-graphic'; -import { ICCTVButtonData, CCTVButton, CCTVButtonTemplate } from './CCTVButton'; - -export class CCTVButtonDraw extends GraphicDrawAssistant< - CCTVButtonTemplate, - ICCTVButtonData -> { - _cctvButton: CCTVButton | null = null; - constructor(app: IDrawApp, template: CCTVButtonTemplate) { - super( - app, - template, - 'svguse:../drawIcon.svg#icon-psl-button', - 'cctv按钮' - ); - CCTVButtonInteraction.init(app); - } - - bind(): void { - super.bind(); - if (!this._cctvButton) { - this._cctvButton = this.graphicTemplate.new(); - this.container.addChild(this._cctvButton); - } - } - - public get cctvButton(): CCTVButton { - if (!this._cctvButton) { - this._cctvButton = this.graphicTemplate.new(); - this.container.addChild(this._cctvButton); - } - return this._cctvButton; - } - - redraw(cp: Point): void { - this.cctvButton.position.copyFrom(cp); - } - onLeftUp(e: FederatedMouseEvent): void { - this.cctvButton.position.copyFrom(this.toCanvasCoordinates(e.global)); - this.createAndStore(true); - } - prepareData(data: ICCTVButtonData): boolean { - data.transform = this.cctvButton.saveTransform(); - return true; - } - onEsc(): void { - this.finish(); - } -} - -/** - * 构建吸附线 - * @param cctvButton - */ -function buildAbsorbablePositions(cctvButton: CCTVButton): AbsorbablePosition[] { - const aps: AbsorbablePosition[] = []; - const cctvButtons = cctvButton.queryStore.queryByType( - CCTVButton.Type - ); - const canvas = cctvButton.getCanvas(); - cctvButtons.forEach((item) => { - if (item.id === cctvButton.id) { - return; - } - const ala = new AbsorbableLine( - new Point(item.x, 0), - new Point(item.x, canvas.height) - ); - const alb = new AbsorbableLine( - new Point(0, item.y), - new Point(canvas.width, item.y) - ); - aps.push(ala); - aps.push(alb); - }); - - return aps; -} - -export class CCTVButtonInteraction extends GraphicInteractionPlugin { - static Name = 'cctv_button_transform'; - constructor(app: IDrawApp) { - super(CCTVButtonInteraction.Name, app); - } - static init(app: IDrawApp) { - return new CCTVButtonInteraction(app); - } - filter(...grahpics: JlGraphic[]): CCTVButton[] | undefined { - return grahpics - .filter((g) => g.type === CCTVButton.Type) - .map((g) => g as CCTVButton); - } - bind(g: CCTVButton): void { - g.eventMode = 'static'; - g.cursor = 'pointer'; - g.scalable = true; - g.rotatable = true; - g.on('transformstart', this.transformstart, this); - } - unbind(g: CCTVButton): void { - g.eventMode = 'none'; - g.scalable = false; - g.rotatable = false; - g.off('transformstart', this.transformstart, this); - } - transformstart(e: GraphicTransformEvent) { - const target = e.target as DisplayObject; - const cctvButton = target.getGraphic() as CCTVButton; - cctvButton.getGraphicApp().setOptions({ - absorbablePositions: buildAbsorbablePositions(cctvButton), - }); - } -} diff --git a/src/graphics/button/Button.ts b/src/graphics/button/Button.ts new file mode 100644 index 0000000..5139b37 --- /dev/null +++ b/src/graphics/button/Button.ts @@ -0,0 +1,201 @@ +import { + getRectangleCenter, + GraphicData, + JlGraphic, + JlGraphicTemplate, + VectorText, +} from 'jl-graphic'; +import CCTV_Button_Assets from './cctv-button-spritesheet.png'; +import CCTV_Button_JSON from './cctv-button-data.json'; +import { + Assets, + Color, + Graphics, + Rectangle, + Sprite, + Spritesheet, + Texture, +} from 'pixi.js'; +import { iscsGraphicData } from 'src/protos/iscs_graphic_data'; + +interface ButtonTextures { + rectPressBtn: Texture; + rectBtn: Texture; + monitorBtn: Texture; + semicircleBtn: Texture; +} + +export const buttonConsts = { + width: 60, + height: 30, + radius: 2, + fillColor: '0x1976D2', + alpha: 1, + codeFontSize: 16, +}; + +export interface IButtonData extends GraphicData { + get code(): string; + set code(v: string); + get codeColor(): string; // 填充色 + set codeColor(v: string); + get codeFontSize(): number; // 宽度 + set codeFontSize(v: number); + get belongSubMenu(): string; // 所属子菜单 + set belongSubMenu(v: string); + get buttonType(): iscsGraphicData.Button.ButtonType; + set buttonType(v: iscsGraphicData.Button.ButtonType); + get width(): number; // 宽度 + set width(v: number); + get height(): number; // 高度 + set height(v: number); + get radius(): number; // 圆角半径 + set radius(v: number); + get fillColor(): string; // 填充色 + set fillColor(v: string); + get alpha(): number; // 透明度 + set alpha(v: number); +} + +export class Button extends JlGraphic { + static Type = 'Button'; + _buttonIcon: Sprite; + labelGraphic = new VectorText(); + buttonTextures: ButtonTextures; + buttonBackground: Graphics = new Graphics(); + __state = 0; + + constructor(buttonTextures: ButtonTextures) { + super(Button.Type); + this.buttonTextures = buttonTextures; + this._buttonIcon = new Sprite(); + this._buttonIcon.anchor.set(0.5); + this.addChild(this.buttonBackground); + } + get code(): string { + return this.datas.code; + } + get datas(): IButtonData { + return this.getDatas(); + } + + doRepaint(): void { + this.buttonBackground.clear(); + if (this.datas.width > 0 && this.datas.height > 0) { + this.drawRect(); + } else { + this.removeChild(this.buttonBackground); + } + + if (this.datas.buttonType !== iscsGraphicData.Button.ButtonType.noIcon) { + this.addChild(this._buttonIcon); + this._buttonIcon.transformSave = true; + this._buttonIcon.name = 'buttonIcon'; + if (this.datas.buttonType == iscsGraphicData.Button.ButtonType.cctvRect) { + this._buttonIcon.texture = this.buttonTextures.rectBtn; + } else if ( + this.datas.buttonType == iscsGraphicData.Button.ButtonType.cctvMonitor + ) { + this._buttonIcon.texture = this.buttonTextures.monitorBtn; + } + if ( + this.datas.buttonType == + iscsGraphicData.Button.ButtonType.cctvSemicircle + ) { + this._buttonIcon.texture = this.buttonTextures.semicircleBtn; + } + const iconPosition = this.datas.childTransforms?.find( + (t) => t.name === this._buttonIcon.name + )?.transform.position; + if (iconPosition) { + this._buttonIcon.position.set(iconPosition.x, iconPosition.y); + } else { + this._buttonIcon.position.set(0, 0); + } + const iconScale = this.datas.childTransforms?.find( + (t) => t.name === this._buttonIcon.name + )?.transform.scale; + if (iconScale) { + this._buttonIcon.scale.set(iconScale.x, iconScale.y); + } else { + this._buttonIcon.scale.set(1, 1); + } + } else { + this.removeChild(this._buttonIcon); + } + + if (this.datas.code) { + this.setTextGraphic(this.labelGraphic, 'label'); + this.labelGraphic.text = this.datas.code; + this.datas.codeFontSize = + this.datas.codeFontSize || buttonConsts.codeFontSize; + const labelPosition = this.datas.childTransforms?.find( + (t) => t.name === this.labelGraphic.name + )?.transform.position; + if (labelPosition) { + this.labelGraphic.position.set(labelPosition.x, labelPosition.y); + } else { + this.labelGraphic.position.set(0, 0); + } + this.addChild(this.labelGraphic); + } else { + this.removeChild(this.labelGraphic); + } + } + + drawRect(): void { + const buttonBackground = this.buttonBackground; + const width = this.datas.width || buttonConsts.width; + const height = this.datas.height || buttonConsts.height; + const radius = this.datas?.radius || buttonConsts.radius; + const fillColor = this.datas.fillColor || buttonConsts.fillColor; + buttonBackground.lineStyle(1, new Color(fillColor)); + buttonBackground.beginFill( + fillColor, + this.datas.alpha || buttonConsts.alpha + ); + buttonBackground.drawRoundedRect(0, 0, width, height, radius); + buttonBackground.endFill; + buttonBackground.pivot = getRectangleCenter( + new Rectangle(0, 0, width, height) + ); + } + + setTextGraphic(g: VectorText, name: string) { + const fontSize = this.datas.codeFontSize || buttonConsts.codeFontSize; + g.setVectorFontSize(fontSize); + g.anchor.set(0.5); + g.style.fill = '#000'; + g.transformSave = true; + g.name = name; + } +} + +export class ButtonTemplate extends JlGraphicTemplate