Skip to content

Runtime-populated comboboxes and avnd UI file paths (reviewed #2110 + #2114) - #2262

Merged
jcelerier merged 13 commits into
masterfrom
review/combobox-and-filepaths
Sep 7, 2026
Merged

jcelerier merged 13 commits into
masterfrom
review/combobox-and-filepaths

Conversation

@jcelerier

@jcelerier jcelerier commented Sep 7, 2026 •

Copy link
Copy Markdown
Member

Supersedes #2110 and #2114, which come from a fork and so could not take the
review fixes directly. Their commits are here unchanged, with authorship
intact; the four commits on top are the result of reviewing them, and the
avendish bump they depend on is first.

Depends on celtera/avendish#204 (merged), which supersedes celtera/avendish#171.

What the review found

The dynamic-combobox feature did not work at all. alternativesChanged was
declared with W_SIGNAL, which emits nothing for connections made from another
shared library — and every consumer is in one: the inspector and canvas
comboboxes live in score_lib_process's headers, the executor in
score_plugin_avnd. The item list would be replaced and nothing would redraw.
Every other signal on these ports already uses E_SIGNAL for exactly this
reason. The first new test catches it.

Folder-backed comboboxes listed nothing in ordinary documents. The sibling
folder port's value is a stored template (<PROJECT>:…, <LIBRARY>:…, or
document-relative) and was handed to QDir raw. Resolved now — via a walk up
the parent chain rather than IDocument::documentFromObject, which throws when
a process is not attached to a document yet.

#2114 resolved paths one way only. item.set writes straight back to the
inlet, so a custom widget echoing the absolute path it was just given would
store that path in the document and the project would stop being relocatable.
score's own file widgets have always paired locateFilePath with
relativizeFilePath; the other half is restored.

Extensions written as ".wav" matched nothing, becoming the glob "*..wav".
Both spellings are accepted now.

One commit from #2110 is not included. QGraphicsCombo: open a popup menu on click replaces drag-selection with QMenu::exec() — a nested event loop inside
mousePressEvent on a graphics item, which has no precedent in this codebase,
and which the QEvent::UngrabMouse handling in the sliders/knobs/choosers
exists precisely to avoid. It also silently neuters
TEST_CASE("degenerate combo boxes survive being dragged"), the regression test
for a divide-by-zero on single-entry combos. It is unrelated to the feature and
the remaining seven commits rebase cleanly without it, so it is left out; worth
its own PR if the popup behaviour is wanted.

On the DataStream change

ComboBox's DataStream layout gains two fields (folderPortName,
fileExtensions). I originally flagged this as a backwards-compatibility
hazard for .scorebin; that was wrong. The binary format is only used to
serialize in and out of a live score instance, so reader and writer are always
the same build and there is no older stream to desynchronise. No version guard
is needed. The JSON path is guarded with tryGet regardless.

Tests

tests/unit/ComboBoxAlternativesTest.cpp (7 cases, 41 assertions): the selected
value survives the list being replaced under it, falls back to init when it
disappears, and is left alone when it names a file an upstream process has not
written yet; folder listings are filtered, sorted, and degrade to a placeholder;
JSON round-trips preserve the new metadata and documents without it still load.

celtera/avendish#204 added the matching compile-time tests on the avendish side.

Verification

Built clean and run against master: 145/150. The five failures
(analysis_gist, beat_tracker, media_trim, unused_files,
explorer_editor_look) reproduce identically on an unmodified origin/master
build and are pre-existing. test_unit_graphics_combo still passes, which it
would not with the popup commit included.

🤖 Generated with Claude Code

https://claude.ai/code/session_018oUFSPD1bswcWtvrsq25t3

jcelerier and others added 13 commits September 7, 2026 12:20
Brings in halp::dynamic_combobox and halp::folder_combobox, and the
avnd::dynamic_items_parameter / folder_items_parameter concepts the score
binding matches on (celtera/avendish#204, superseding #171).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018oUFSPD1bswcWtvrsq25t3
File-chooser port values are stored in the document as path templates
(<PROJECT>:..., <LIBRARY>:..., or document-relative). Score's own file
widgets resolve them via score::locateFilePath, but the avnd layout
builder pushed the raw template string into custom ui items' `value`, so
an add-on widget bound to a soundfile/file/folder port received a path it
could not use (e.g. a waveform widget that decodes the file in edit mode
could never find it after the document moved or used project-relative
paths).

Resolve through score::locateFilePath in setupControl, both for the
initial sync and in the valueChanged connections, whenever the inlet is a
FileChooserBase or FolderChooser and the value is a non-empty string.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvD2PFHZtW6pfsh3gGhayg
Address review: replace the per-widget qobject_cast checks with a
SetGUIValue struct specialized through avendish port concepts
(soundfile_port, midifile_port, file_port, folder widget), so the
file/folder-only path resolution is selected at compile time.

Co-Authored-By: Claude <noreply@anthropic.com>
Lets an avendish object repopulate a combobox port's choices at run time
(e.g. from the files it finds in a folder). Complements the avendish
halp::dynamic_combobox port.

- Process::ComboBox gains setAlternatives() + an alternativesChanged signal.
  setAlternatives refreshes the value domain and re-clamps the current value
  when the list shrinks. The alternatives were already serialized, so saved
  documents reopen with their last-known items.
- The inspector and canvas combobox widgets read their item list live and
  rebuild on alternativesChanged.
- The avnd executor injects the port's update_items callback, marshalling the
  object's runtime updates to the model inlet on the main thread
  (QPointer-guarded). Nodes with type-only inputs are skipped.

Objects that don't use the feature are unaffected.

Co-authored-by: Pia Baltazar <pia@baltazar.studio>
Adds the host side of avendish's halp::folder_combobox: a combobox whose
items are the files of a sibling folder/path port, listed at edit/load time.

- Process::ComboBox gains folderPortName + fileExtensions and
  repopulateFromFolder(): lists a folder's matching files as the items.
- The avnd binding (Concepts.hpp) stamps that metadata onto the ComboBox when
  the field is an avnd::folder_items_parameter.
- ProcessModel wires each folder-combobox to its sibling folder port after
  port creation (i.e. after deserialization): populates immediately and
  refreshes on the folder's valueChanged. Populated at document load, no
  execution needed — unlike the executor-pushed dynamic_combobox, this is the
  right shape for file pickers in an editor (FluCoMa's folder file ports,
  Granola's sound picker).

NOT yet compile-tested (needs a full score build).

Co-authored-by: Pia Baltazar <pia@baltazar.studio>
Make the folder-backed combobox string-valued (the selected file name) rather
than an index, so an object consumes it directly like a file-name string —
adopting it is then a pure port-type swap (lineedit -> folder_combobox) with
no object-code change. The folder binding builds (name, name) alternatives.

Co-authored-by: Pia Baltazar <pia@baltazar.studio>
folderPortName / fileExtensions were only set by the avnd port factory at
creation; the deserializing ProcessModel constructor restores ports from the
document instead of re-running the factory, so on load they were empty and
init_folder_comboboxes skipped the combobox — no folder re-scan and no live
refresh, only the stale saved alternatives.

Serialize both fields with the port (extensions as one space-joined string,
like FileChooser's filters; JSON reads guarded for older documents).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDQaHG5FbEyH9D8VsjEyXe
Folder-backed comboboxes now install a QFileSystemWatcher on their folder, so
files created while the document is open (an upstream process writing its
analysis, a recorder...) appear in the list without reloading.

Also change the refresh policy for the current value: keep it when still
listed, fall back to the port's init when that is listed, and otherwise leave
it untouched instead of clamping to an arbitrary entry - it may name a file
that simply does not exist yet, and becomes selected once the file appears.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDQaHG5FbEyH9D8VsjEyXe
When the sibling port named by folder_port() holds a file path rather than a
folder (e.g. a sound-file port), list the file's containing folder. Lets an
object expose a picker next to an existing file port without adding a
dedicated folder port.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DDQaHG5FbEyH9D8VsjEyXe
…value

Resolving templates on the way in only works if the way out reverses it:
item.set writes straight to the inlet, so a custom widget that hands back the
absolute path it was given would store that path in the document and the
project would stop being relocatable. score's own file widgets have always
paired locateFilePath with relativizeFilePath; this restores the other half.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018oUFSPD1bswcWtvrsq25t3
W_SIGNAL emits nothing for a connection made from another shared library, and
every consumer of this signal -- the inspector and canvas comboboxes in
score_lib_process's headers, the avnd executor in score_plugin_avnd -- is in a
different one. The item list would be replaced and nothing would redraw.

Every other signal on these ports already uses E_SIGNAL for that reason.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018oUFSPD1bswcWtvrsq25t3
…xtensions

Two things kept a folder-backed combobox empty in ordinary documents:

 - the sibling folder port's value is a stored template (<PROJECT>:...,
   <LIBRARY>:..., or document-relative), and it was handed to QDir raw, so a
   project-relative folder listed nothing. The document is found by walking up
   rather than through IDocument::documentFromObject, which throws when a
   process is not attached to one yet;
 - extensions() is written by hand and ".wav" is as natural as "wav", but only
   the second worked: the first became the glob "*..wav".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018oUFSPD1bswcWtvrsq25t3
Pins the behaviour the feature rests on: the selected value survives the list
being replaced under it, disappears to the init value when it is gone, and is
left alone when it names a file an upstream process has not written yet.
Folder listings are filtered, sorted and degrade to a placeholder rather than
to nothing. Documents written before any of this existed still load.

The missing alternativesChanged emission was found by the first of these.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018oUFSPD1bswcWtvrsq25t3
@jcelerier
jcelerier added this pull request to the merge queue Sep 7, 2026
Merged via the queue into master with commit b1659d3 Sep 7, 2026
38 of 43 checks passed
@coveralls

Copy link
Copy Markdown

Coverage Status

Coverage is 27.983% — review/combobox-and-filepaths into master. No base build found for master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants