Add delete buttons and bar add/create to sub-objects pane

This commit is contained in:
c0dev0id
2026-06-30 13:18:29 +02:00
parent e011c9e7d8
commit 66c1f2c151
8 changed files with 261 additions and 39 deletions

View File

@@ -2,8 +2,8 @@ import { h } from 'vue';
// One-line summary row with a drill-down chevron.
export const ObjectShort = {
props: ['label', 'typeTag', 'focused', 'hasChildren', 'readOnly'],
emits: ['focus', 'drillDown'],
props: ['label', 'typeTag', 'focused', 'hasChildren', 'readOnly', 'deletable'],
emits: ['focus', 'drillDown', 'delete'],
setup(props, { emit }) {
return () => h('li', {
class: ['se-object-item', props.focused ? 'focused' : null, props.readOnly ? 'read-only' : null],
@@ -11,6 +11,13 @@ export const ObjectShort = {
}, [
props.typeTag ? h('span', { class: 'se-object-type' }, props.typeTag) : null,
h('span', { class: 'se-object-label' }, props.label),
props.deletable
? h('button', {
class: 'se-btn-delete',
title: 'Delete',
onClick: e => { e.stopPropagation(); emit('delete'); },
}, '×')
: null,
props.hasChildren
? h('button', {
class: 'se-chevron',

View File

@@ -88,7 +88,9 @@ export const PaneCP = {
const model = props.store.scoreModel;
const { text: patched, log } = patchScore(
raw, model.instruments, model.bars, model.info, model.articles ?? [],
{ articlesModified: !!model.articlesModified },
);
model.articlesModified = false;
props.store.exportLog = log;
await putScoreText(patched);
props.store.synthesisStatus = { frozen: false, currently_rendered_notes: 0, notes_in_total: 0 };

View File

@@ -217,11 +217,12 @@ export const PaneFO = {
h('h4', H4, `Bar: ${node.id}`),
h(ObjectExtended, {
fields: [
{ key: 'id', value: node.id, editable: false },
{ key: 'id', value: node.id, editable: node.isNew ?? false },
{ key: 'beats_per_minute', value: node.tempoLevels ?? '', editable: true, type: 'number' },
{ key: 'stress_pattern', value: stressorToString(node.stressor), editable: true },
],
onChange: ({ key, value }) => {
if (key === 'id') { node.id = value; markBarDirty(); return; }
if (key === 'beats_per_minute') node.tempoLevels = isNaN(value) ? null : value;
if (key === 'stress_pattern') node.stressor = parseStressor(value);
markBarDirty();

View File

@@ -9,6 +9,15 @@ function barGroupKey(id) {
return m ? id.slice(0, m.index) : id;
}
// Increment the trailing numeric run of an ID, preserving zero-padding width.
function incrementId(id) {
const m = id.match(/(\d+)$/);
if (!m) return id + '1';
const num = parseInt(m[1], 10) + 1;
const padded = String(num).padStart(m[1].length, '0');
return id.slice(0, m.index) + padded;
}
export const PaneSubObjects = {
props: ['store', 'kind', 'onFocusFO'],
setup(props) {
@@ -17,6 +26,67 @@ export const PaneSubObjects = {
return fp.length ? fp[fp.length - 1] : props.store.scoreModel;
}
function deleteItem(kind, node) {
const store = props.store;
const model = store.scoreModel;
if (!model) return;
if (kind === 'instrument') {
const idx = model.instruments.indexOf(node);
if (idx !== -1) { model.instruments.splice(idx, 1); store.markDirty(); }
} else if (kind === 'articles') {
const idx = (model.articles ?? []).indexOf(node);
if (idx !== -1) { model.articles.splice(idx, 1); model.articlesModified = true; store.markDirty(); }
} else if (kind === 'bar') {
node.deleted = true;
node.isDirty = true;
store.markDirty();
} else if (kind === 'variation') {
for (const instr of model.instruments) {
const idx = instr.variations.indexOf(node);
if (idx !== -1) { instr.variations.splice(idx, 1); instr.isDirty = true; store.markDirty(); return; }
for (const v of instr.variations) {
const sidx = v.subvariations.indexOf(node);
if (sidx !== -1) { v.subvariations.splice(sidx, 1); instr.isDirty = true; store.markDirty(); return; }
}
}
} else if (kind === 'label_spec') {
for (const instr of model.instruments) {
for (const v of instr.variations) {
const idx = v.labelSpecs.indexOf(node);
if (idx !== -1) { v.labelSpecs.splice(idx, 1); instr.isDirty = true; store.markDirty(); return; }
for (const sv of v.subvariations) {
const idx2 = sv.labelSpecs.indexOf(node);
if (idx2 !== -1) { sv.labelSpecs.splice(idx2, 1); instr.isDirty = true; store.markDirty(); return; }
}
}
}
}
}
function addBar(afterId) {
const store = props.store;
const model = store.scoreModel;
if (!model) return;
let newId;
if (afterId) {
newId = incrementId(afterId);
} else {
const liveBars = (model.bars ?? []).filter(b => !b.deleted);
newId = liveBars.length ? incrementId(liveBars[liveBars.length - 1].id) : 'bar001';
}
const newBar = {
type: 'bar', id: newId, isDirty: true, isNew: true,
stressor: null, tempoLevels: null,
upperStressBound: null, lowerStressBound: null, tempoShape: null,
voices: {},
};
model.bars.push(newBar);
store.markDirty();
store.pushFocus(newBar);
props.onFocusFO?.();
}
return () => {
const node = focused();
const groups = getKindGroups(node);
@@ -25,13 +95,14 @@ export const PaneSubObjects = {
: groups[0];
const items = group?.items ?? [];
if (!items.length) return h('div', null, h('em', null, 'No sub-objects'));
if (!items.length && props.kind !== 'bar') return h('div', null, h('em', null, 'No sub-objects'));
if (props.kind === 'bar') {
// Group bars by shared P?L? key; render each group as a row of inline chips.
const visibleItems = items.filter(item => !item.node.deleted);
const barGroups = [];
const seen = new Map();
for (const item of items) {
for (const item of visibleItems) {
const key = barGroupKey(item.label);
if (!seen.has(key)) {
const g = { key, items: [] };
@@ -41,17 +112,39 @@ export const PaneSubObjects = {
seen.get(key).items.push(item);
}
return h('div', { class: 'se-bar-groups' }, barGroups.map(g =>
h('div', { key: g.key, class: 'se-bar-group' },
g.items.map((item, idx) =>
h('button', {
const groupEls = barGroups.map(g => {
const lastId = g.items[g.items.length - 1].label;
return h('div', { key: g.key, class: 'se-bar-group' }, [
...g.items.map((item, idx) =>
h('span', {
key: idx,
class: ['se-bar-chip', props.store.focusPath.includes(item.node) ? 'focused' : null],
onClick: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
}, item.label)
)
)
));
}, [
h('span', {
class: 'se-bar-chip-label',
onClick: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
}, item.label),
h('button', {
class: 'se-bar-chip-delete',
title: 'Delete bar',
onClick: e => { e.stopPropagation(); deleteItem('bar', item.node); },
}, '×'),
])
),
h('button', {
class: 'se-bar-add',
title: `Add bar after ${lastId}`,
onClick: () => addBar(lastId),
}, '+ add'),
]);
});
return h('div', { class: 'se-bar-groups' }, [
...groupEls,
h('div', { class: 'se-bar-group' },
h('button', { class: 'se-bar-add', onClick: () => addBar(null) }, '+ add bar')
),
]);
}
return h('div', null,
@@ -63,8 +156,10 @@ export const PaneSubObjects = {
focused: props.store.focusPath.includes(item.node),
hasChildren: item.hasChildren,
readOnly: item.readOnly ?? false,
onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
deletable: item.deletable ?? false,
onFocus: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
onDrillDown: () => { props.store.pushFocus(item.node); props.onFocusFO?.(); },
onDelete: item.deletable ? () => deleteItem(item.kind, item.node) : undefined,
})
))
);