Add delete buttons and bar add/create to sub-objects pane
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user