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

View File

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

View File

@ -689,15 +689,39 @@
}
},
"custom_ui": {
"showModal": {
"spine-enable-collision": {
"title": "Delete Sprite Object Type",
"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": {
"0": "Just disable",
"1": "Disable and delete {0}"
}
"spine-enable-collision": {
"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?",
"buttons": {
"0": "Just disable",
"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": {
"showModal": {
"spine-enable-collision": {
"title": "删除精灵对象类型",
"text": "这是{0}的最后一个实例。您可以删除{0}或仅禁用精灵碰撞体。删除精灵碰撞体将移除事件表中所有相关的ACE如果有。您想如何继续",
"buttons": {
"0": "仅禁用",
"1": "禁用并删除{0}"
}
"spine-enable-collision": {
"title": "删除精灵对象类型",
"message": "这是{0}的最后一个实例。您可以删除{0}或仅禁用精灵碰撞体。删除精灵碰撞体将移除事件表中所有相关的ACE如果有。您想如何继续",
"buttons": {
"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它已被删除。请使用\"选择动画\"按钮从现有动画中选择。"
}
}
}