Add alert modal when manually inserting animations or skins.

This commit is contained in:
Davide Tantillo 2026-01-28 17:01:45 +01:00
parent 5e33132153
commit 7f092113d8
4 changed files with 157 additions and 79 deletions

View File

@ -27,7 +27,6 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
// Common theme type
interface ModalTheme { interface ModalTheme {
overlayBg: string; overlayBg: string;
captionBg: string; captionBg: string;
@ -43,38 +42,36 @@ interface ModalTheme {
itemSelectedBg: string; itemSelectedBg: string;
} }
// Helper: Create theme
function getTheme (darkMode: boolean): ModalTheme { function getTheme (darkMode: boolean): ModalTheme {
return darkMode ? { return darkMode ? {
overlayBg: 'rgba(0, 0, 0, 0.5)', overlayBg: 'rgba(0, 0, 0, 0.5)',
captionBg: 'rgb(71, 71, 71)', // gray9 captionBg: 'rgb(71, 71, 71)',
captionText: 'rgb(214, 214, 214)', // gray27 captionText: 'rgb(214, 214, 214)',
contentBg: 'rgb(87, 87, 87)', // gray11 contentBg: 'rgb(87, 87, 87)',
contentText: 'rgb(214, 214, 214)', // gray27 contentText: 'rgb(214, 214, 214)',
buttonBg: 'rgb(71, 71, 71)', // gray9 buttonBg: 'rgb(71, 71, 71)',
buttonText: 'rgb(214, 214, 214)', // gray27 buttonText: 'rgb(214, 214, 214)',
buttonBorder: 'rgb(56, 56, 56)', // gray7 buttonBorder: 'rgb(56, 56, 56)',
buttonHoverBg: 'rgb(79, 79, 79)', // gray10 buttonHoverBg: 'rgb(79, 79, 79)',
closeColor: 'rgb(168, 168, 168)', // gray21 closeColor: 'rgb(168, 168, 168)',
itemHoverBg: 'rgb(79, 79, 79)', // gray10 itemHoverBg: 'rgb(79, 79, 79)',
itemSelectedBg: 'rgb(56, 56, 56)', // gray7 itemSelectedBg: 'rgb(56, 56, 56)',
} : { } : {
overlayBg: 'rgba(0, 0, 0, 0.3)', overlayBg: 'rgba(0, 0, 0, 0.3)',
captionBg: 'rgb(247, 247, 247)', // gray31 captionBg: 'rgb(247, 247, 247)',
captionText: 'rgb(94, 94, 94)', // gray12 captionText: 'rgb(94, 94, 94)',
contentBg: 'rgb(232, 232, 232)', // gray29 contentBg: 'rgb(232, 232, 232)',
contentText: 'rgb(94, 94, 94)', // gray12 contentText: 'rgb(94, 94, 94)',
buttonBg: 'rgb(222, 222, 222)', // gray28 buttonBg: 'rgb(222, 222, 222)',
buttonText: 'rgb(94, 94, 94)', // gray12 buttonText: 'rgb(94, 94, 94)',
buttonBorder: 'rgb(199, 199, 199)', // gray25 buttonBorder: 'rgb(199, 199, 199)',
buttonHoverBg: 'rgb(214, 214, 214)', // gray27 buttonHoverBg: 'rgb(214, 214, 214)',
closeColor: 'rgb(94, 94, 94)', // gray12 closeColor: 'rgb(94, 94, 94)',
itemHoverBg: 'rgb(214, 214, 214)', // gray27 itemHoverBg: 'rgb(214, 214, 214)',
itemSelectedBg: 'rgb(199, 199, 199)', // gray25 itemSelectedBg: 'rgb(199, 199, 199)',
}; };
} }
// Helper: Create overlay
function createOverlay (theme: ModalTheme): HTMLDivElement { function createOverlay (theme: ModalTheme): HTMLDivElement {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.style.cssText = ` overlay.style.cssText = `
@ -94,7 +91,6 @@ function createOverlay (theme: ModalTheme): HTMLDivElement {
return overlay; return overlay;
} }
// Helper: Create dialog
function createDialog (theme: ModalTheme, minWidth: number): HTMLDivElement { function createDialog (theme: ModalTheme, minWidth: number): HTMLDivElement {
const dialog = document.createElement('div'); const dialog = document.createElement('div');
dialog.style.cssText = ` dialog.style.cssText = `
@ -109,7 +105,6 @@ function createDialog (theme: ModalTheme, minWidth: number): HTMLDivElement {
return dialog; return dialog;
} }
// Helper: Create caption with close button
function createCaption (title: string, theme: ModalTheme, onClose: () => void): HTMLDivElement { function createCaption (title: string, theme: ModalTheme, onClose: () => void): HTMLDivElement {
const caption = document.createElement('div'); const caption = document.createElement('div');
caption.style.cssText = ` caption.style.cssText = `
@ -149,7 +144,6 @@ function createCaption (title: string, theme: ModalTheme, onClose: () => void):
return caption; return caption;
} }
// Helper: Create footer
function createFooter (): HTMLDivElement { function createFooter (): HTMLDivElement {
const footer = document.createElement('div'); const footer = document.createElement('div');
footer.style.cssText = ` footer.style.cssText = `
@ -161,7 +155,6 @@ function createFooter (): HTMLDivElement {
return footer; return footer;
} }
// Helper: Create button
function createButton (text: string, theme: ModalTheme, onClick: () => void): HTMLButtonElement { function createButton (text: string, theme: ModalTheme, onClick: () => void): HTMLButtonElement {
const btn = document.createElement('button'); const btn = document.createElement('button');
btn.textContent = text; btn.textContent = text;
@ -181,7 +174,6 @@ function createButton (text: string, theme: ModalTheme, onClick: () => void): HT
return btn; return btn;
} }
// Helper: Setup modal event handlers
function setupModalHandlers (overlay: HTMLDivElement, onCancel: () => void, extraKeyHandler?: (e: KeyboardEvent) => boolean) { function setupModalHandlers (overlay: HTMLDivElement, onCancel: () => void, extraKeyHandler?: (e: KeyboardEvent) => boolean) {
const handleKeyDown = (e: KeyboardEvent) => { const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
@ -193,7 +185,9 @@ function setupModalHandlers (overlay: HTMLDivElement, onCancel: () => void, extr
} }
}; };
document.addEventListener('keydown', handleKeyDown); setTimeout(() => {
document.addEventListener('keydown', handleKeyDown);
}, 0);
overlay.addEventListener('click', (e) => { overlay.addEventListener('click', (e) => {
if (e.target === overlay) { if (e.target === overlay) {
@ -207,7 +201,6 @@ function setupModalHandlers (overlay: HTMLDivElement, onCancel: () => void, extr
}; };
} }
// Original interfaces
interface ModalButton<T> { interface ModalButton<T> {
text: string; text: string;
color?: string; color?: string;
@ -324,11 +317,9 @@ export function showListSelectionModal (options: ListSelectionOptions): Promise<
`; `;
itemDiv.addEventListener('click', () => { itemDiv.addEventListener('click', () => {
// Deselect all items
contents.querySelectorAll('div').forEach(div => { contents.querySelectorAll('div').forEach(div => {
div.style.background = 'transparent'; div.style.background = 'transparent';
}); });
// Select this item
itemDiv.style.background = theme.itemSelectedBg; itemDiv.style.background = theme.itemSelectedBg;
selectedItem = item; selectedItem = item;
}); });
@ -486,7 +477,6 @@ export function showMultiListSelectionModal (options: MultiListSelectionOptions)
gap: 8px; gap: 8px;
`; `;
// Create checkbox
const checkbox = document.createElement('input'); const checkbox = document.createElement('input');
checkbox.type = 'checkbox'; checkbox.type = 'checkbox';
checkbox.checked = selectedSet.has(item); checkbox.checked = selectedSet.has(item);
@ -515,7 +505,6 @@ export function showMultiListSelectionModal (options: MultiListSelectionOptions)
}; };
itemDiv.addEventListener('click', (e) => { itemDiv.addEventListener('click', (e) => {
// Don't toggle if clicking directly on checkbox (it handles itself)
if (e.target !== checkbox) { if (e.target !== checkbox) {
toggleSelection(); toggleSelection();
} }

View File

@ -126,8 +126,9 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
if (id === PLUGIN_CLASS.PROP_SKIN) { if (id === PLUGIN_CLASS.PROP_SKIN) {
this.skins = []; this.skins = [];
const validatedString = this.validateSkinString(); const skinString = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKIN) as string;
if (validatedString) { const validatedString = await this.validateSkinString(skinString);
if (validatedString !== undefined && validatedString !== skinString) {
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_SKIN, validatedString); this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_SKIN, validatedString);
return; return;
} }
@ -138,6 +139,14 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
} }
if (id === PLUGIN_CLASS.PROP_ANIMATION) { if (id === PLUGIN_CLASS.PROP_ANIMATION) {
const animationString = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_ANIMATION) as string;
const validatedString = await this.validateAnimationString(animationString);
if (validatedString !== undefined && validatedString !== this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_ANIMATION)) {
this._inst.SetPropertyValue(PLUGIN_CLASS.PROP_ANIMATION, validatedString);
return;
}
this.setAnimation(); this.setAnimation();
this.layoutView?.Refresh(); this.layoutView?.Refresh();
return; return;
@ -259,7 +268,6 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
} }
} }
return zoom === -1 ? 1 : zoom; return zoom === -1 ? 1 : zoom;
} }
private setAnimation () { private setAnimation () {
@ -267,14 +275,47 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
this.animation = propValue === "" ? undefined : propValue; this.animation = propValue === "" ? undefined : propValue;
} }
private validateSkinString () { private async validateSkinString (skins: string) {
const skins = this._inst.GetPropertyValue(PLUGIN_CLASS.PROP_SKIN) as string; if (skins === "" || !this.skeleton) return;
if (skins === "") return;
const split = skins.split(",");
if (!split.includes("")) return; const split = skins.split(",").filter(s => s !== "");
if (split.length === 0) return;
return split.filter(s => s !== "").join(","); const availableSkins = this.skeleton.data.skins.map(skin => skin.name).filter(s => s !== "default");
const invalidSkins = split.filter(skinName => !availableSkins.includes(skinName));
if (invalidSkins.length > 0) {
await spine.showAlertModal({
darkMode: false,
title: this.lang('invalid-skins.title'),
message: this.lang('invalid-skins.message', [invalidSkins.join(", ")])
});
const validSkins = split.filter(skinName => availableSkins.includes(skinName));
console.log(validSkins);
return validSkins.join(",");
}
return split.join(",");
}
private async validateAnimationString (animation: string) {
if (animation === "" || !this.skeleton) return;
const availableAnimations = this.skeleton.data.animations.map(anim => anim.name);
if (!availableAnimations.includes(animation)) {
await spine.showAlertModal({
darkMode: false,
title: this.lang('invalid-animation.title'),
message: this.lang('invalid-animation.message', [animation])
});
return "";
}
return animation;
} }
private setSkin () { private setSkin () {
@ -483,10 +524,10 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
const errorTextC3 = iRenderer.CreateRendererText() const errorTextC3 = iRenderer.CreateRendererText()
this.errorTextC3 = errorTextC3; this.errorTextC3 = errorTextC3;
this.errorTextC3.SetFontSize(12); errorTextC3.SetFontSize(12);
this.errorTextC3.SetColorRgb(1, 0, 0); errorTextC3.SetColorRgb(1, 0, 0);
this.errorTextC3.SetTextureUpdateCallback(() => iLayoutView.Refresh()); errorTextC3.SetTextureUpdateCallback(() => iLayoutView.Refresh());
return this.errorTextC3; return errorTextC3;
} }
private update (delta: number) { private update (delta: number) {
@ -566,15 +607,15 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
const action = await spine.showModal({ const action = await spine.showModal({
darkMode: false, darkMode: false,
maxWidth: 500, maxWidth: 500,
title: this.lang(`showModal.${type}.title`), title: this.lang(`${type}.title`),
text: this.lang(`showModal.${type}.text`, [collisionSpriteName]), text: this.lang(`${type}.message`, [collisionSpriteName]),
buttons: [ buttons: [
{ {
text: this.lang(`showModal.${type}.buttons.${0}`), text: this.lang(`${type}.buttons.${0}`),
value: 'disable' value: 'disable'
}, },
{ {
text: this.lang(`showModal.${type}.buttons.${1}`, [collisionSpriteName]), text: this.lang(`${type}.buttons.${1}`, [collisionSpriteName]),
color: '#d9534f', color: '#d9534f',
value: 'delete' value: 'delete'
} }
@ -597,8 +638,8 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
if (!this.skeleton) { if (!this.skeleton) {
await spine.showAlertModal({ await spine.showAlertModal({
darkMode: false, darkMode: false,
title: 'Error', title: this.lang('skeleton-not-loaded.title'),
message: 'Skeleton not loaded. Please ensure atlas and skeleton files are set.', message: this.lang('skeleton-not-loaded.message'),
}); });
return; return;
} }
@ -607,15 +648,15 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
if (animations.length === 0) { if (animations.length === 0) {
await spine.showAlertModal({ await spine.showAlertModal({
darkMode: false, darkMode: false,
title: 'No Animations', title: this.lang('no-animations.title'),
message: 'No animations found in the skeleton.', message: this.lang('no-animations.message'),
}); });
return; return;
} }
const selectedAnimation = await spine.showListSelectionModal({ const selectedAnimation = await spine.showListSelectionModal({
darkMode: false, darkMode: false,
title: 'Select Animation', title: this.lang('select-animation.title'),
items: animations, items: animations,
}); });
@ -628,8 +669,8 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
if (!this.skeleton) { if (!this.skeleton) {
await spine.showAlertModal({ await spine.showAlertModal({
darkMode: false, darkMode: false,
title: 'Error', title: this.lang('skeleton-not-loaded.title'),
message: 'Skeleton not loaded. Please ensure atlas and skeleton files are set.', message: this.lang('skeleton-not-loaded.message'),
}); });
return; return;
} }
@ -638,15 +679,15 @@ class SpineC3PluginInstance extends SDK.IWorldInstanceBase {
if (skins.length === 0) { if (skins.length === 0) {
await spine.showAlertModal({ await spine.showAlertModal({
darkMode: false, darkMode: false,
title: 'No Skins', title: this.lang('no-skins.title'),
message: 'No skins found in the skeleton.', message: this.lang('no-skins.message'),
}); });
return; return;
} }
const selectedSkins = await spine.showMultiListSelectionModal({ const selectedSkins = await spine.showMultiListSelectionModal({
darkMode: false, darkMode: false,
title: 'Select Skins', title: this.lang('select-skins.title'),
items: skins, items: skins,
selectedItems: this.skins, selectedItems: this.skins,
}); });

View File

@ -689,15 +689,39 @@
} }
}, },
"custom_ui": { "custom_ui": {
"showModal": { "spine-enable-collision": {
"spine-enable-collision": { "title": "Delete Sprite Object Type",
"title": "Delete Sprite Object Type", "message": "This is the last instance of {0}. You can delete the {0} or just disable the Sprite collision body. Deleting the Sprite collision body will remove all related ACEs in the event sheet, if there's any. How do you want to proceed?",
"text": "This is the last instance of {0}. You can delete the {0} or just disable the Sprite collision body. Deleting the Sprite collision body will remove all related ACEs in the event sheet, if there's any. How do you want to proceed?", "buttons": {
"buttons": { "0": "Just disable",
"0": "Just disable", "1": "Disable and delete {0}"
"1": "Disable and delete {0}"
}
} }
},
"skeleton-not-loaded": {
"title": "Error",
"message": "Skeleton not loaded. Please ensure atlas and skeleton files are set."
},
"no-animations": {
"title": "No Animations",
"message": "No animations found in the skeleton."
},
"no-skins": {
"title": "No Skins",
"message": "No skins found in the skeleton."
},
"select-animation": {
"title": "Select Animation"
},
"select-skins": {
"title": "Select Skins"
},
"invalid-skins": {
"title": "Invalid Skins",
"message": "The following skins do not exist in the skeleton: {0}.\n\nThey have been removed. Please use the \"Select skin\" button to choose from existing skins."
},
"invalid-animation": {
"title": "Invalid Animation",
"message": "The animation \"{0}\" does not exist in the skeleton.\n\nIt has been removed. Please use the \"Select animation\" button to choose from existing animations."
} }
} }
} }

View File

@ -689,15 +689,39 @@
} }
}, },
"custom_ui": { "custom_ui": {
"showModal": { "spine-enable-collision": {
"spine-enable-collision": { "title": "删除精灵对象类型",
"title": "删除精灵对象类型", "message": "这是{0}的最后一个实例。您可以删除{0}或仅禁用精灵碰撞体。删除精灵碰撞体将移除事件表中所有相关的ACE如果有。您想如何继续",
"text": "这是{0}的最后一个实例。您可以删除{0}或仅禁用精灵碰撞体。删除精灵碰撞体将移除事件表中所有相关的ACE如果有。您想如何继续", "buttons": {
"buttons": { "0": "仅禁用",
"0": "仅禁用", "1": "禁用并删除{0}"
"1": "禁用并删除{0}"
}
} }
},
"skeleton-not-loaded": {
"title": "错误",
"message": "骨架未加载。请确保已设置图集和骨架文件。"
},
"no-animations": {
"title": "无动画",
"message": "在骨架中未找到动画。"
},
"no-skins": {
"title": "无皮肤",
"message": "在骨架中未找到皮肤。"
},
"select-animation": {
"title": "选择动画"
},
"select-skins": {
"title": "选择皮肤"
},
"invalid-skins": {
"title": "无效皮肤",
"message": "以下皮肤在骨架中不存在:{0}。\n\n它们已被删除。请使用\"选择皮肤\"按钮从现有皮肤中选择。"
},
"invalid-animation": {
"title": "无效动画",
"message": "动画\"{0}\"在骨架中不存在。\n\n它已被删除。请使用\"选择动画\"按钮从现有动画中选择。"
} }
} }
} }