Haiku BChannelControl: Multichannel Values, Limits, and Invocation
Model related values in Haiku BChannelControl with explicit channel ranges, per-channel limits, change masks, and reliable invocation messages.
BChannelControl is the Interface Kit base for a control that owns several related integer values. A stereo balance panel, RGB adjustment widget, or multiband setting can expose several channels through one view and one invocation route. The important distinction is that this is not merely a BControl with a larger integer: it has a channel count, a current channel, per-channel values and limits, optional per-channel labels, and an invocation payload that can report more than one value.
The class is abstract. A subclass supplies drawing, mouse and key handling, preferred size, the maximum supported channel count, and whether each channel can have its own limits. Haiku also provides BChannelSlider, a concrete slider specialization. The public reference is sparse, so check the installed headers and the exact implementation for target-release behavior; do not turn undocumented UI details into a portable contract.
Separate channel identity from the selected channel
Channel indices are zero-based. CountChannels() reports the active count, and ValueFor(index) reads one channel. CurrentChannel() is different: it selects which channel the inherited single-value control methods and display state refer to. Code that treats it as a channel iterator can update the wrong value when user interaction changes the selection.
Keep an application-side model whose meaning is explicit. For example, a two-channel level control might store left and right gains in the same integer unit, while a color editor might store red, green, and blue bytes. The control does not decide whether 0..100 means a percentage, decibels, or a normalized fraction. Document the unit at the boundary, convert to the domain’s native representation in one place, and do not reuse a range from another feature merely because both use int32.
Configure count and limits before publishing the control to the window. Check every returned status and validate that the count is supported by the concrete subclass. The API exposes MaxChannelCount() as a virtual limit, but application code should still reject invalid user or configuration input itself so that an error does not leave a partially initialized panel.
enum : uint32 { kGainChanged = 'gain' };
BChannelSlider* MakeGainControl()
{
BChannelSlider* slider = new BChannelSlider(
BRect(10, 10, 300, 50), "output-gain", "Output gain",
new BMessage(kGainChanged), 2);
if (slider == NULL)
return NULL;
status_t status = slider->SetLimitsFor(0, 0, 100);
if (status == B_OK)
status = slider->SetLimitsFor(1, 0, 100);
if (status == B_OK)
status = slider->SetValueFor(0, 75);
if (status == B_OK)
status = slider->SetValueFor(1, 75);
if (status != B_OK) {
delete slider;
return NULL;
}
return slider;
}
This excerpt treats both values as a UI percentage, not as a media gain representation. Convert the model values before setting them and after receiving a change. A real view should also be added to a window only after its setup succeeds. The constructor accepts a message pointer using the inherited control model; verify message ownership and target behavior against the installed BControl contract when constructing dynamically.
Make limits part of the data model
The base API has shared limits (SetLimits) and indexed limits (SetLimitsFor). A subclass reports whether individual limits are supported. If the control supports only shared limits, designing the rest of the application around independent ranges creates a mismatch that the view cannot display faithfully. Check SupportsIndividualLimits() rather than assuming the virtual method exists only for implementers.
Do not assume that setting the range transforms existing data into the desired scale. In the current implementation, changing a channel’s limits clamps its stored value into the new interval; that is not the same as converting the model’s units. Decide whether the application should clamp, reject, or migrate its model before calling the control. When a device reports a new range, update the model and the control together; otherwise the thumb may suggest an allowed value that the backend refuses. If one channel is unavailable, disable or omit it in the application model instead of encoding availability as a magic numeric value.
Per-channel labels communicate more than decoration. A shared MinLimitLabel() or MaxLimitLabel() describes all channels, while the indexed label methods let a subclass distinguish, for example, Quiet from Full on one channel and Cool from Warm on another. Those labels need the same localization and layout review as ordinary control labels. If the control cannot render separate labels correctly, use a neighboring BStringView or a custom control rather than presenting ambiguous endpoints.
When changing channel count at runtime, treat it as a structural change. The public API returns a status from SetChannelCount(). First decide how removed values are persisted, what newly added channels mean, and whether the current channel remains valid. Only then resize the control and refresh any message bindings. A successful count update is not a promise that an external device or model has also changed; that requires a separate operation and result.
Route user intent with channel-aware messages
Invoke() follows the normal BControl path. InvokeChannel() and InvokeNotifyChannel() provide a multichannel path. The current implementation adds the be:current_channel field and repeated be:channel_value and be:channel_changed fields. Since fields repeat, recipients must enumerate each occurrence rather than reading only the first. Pass the changed-channel mask when the user edits only part of a larger control; without one, the API’s default path marks each included value as changed.
void GainWindow::MessageReceived(BMessage* message)
{
if (message->what == kGainChanged) {
int32 left = 0;
int32 right = 0;
if (message->FindInt32("be:channel_value", 0, &left) == B_OK
&& message->FindInt32("be:channel_value", 1, &right) == B_OK) {
ApplyGainModel(left, right);
}
return;
}
BWindow::MessageReceived(message);
}
Use an application-defined message code and validate each field before applying it. Messages are snapshots of UI intent, not proof that the hardware accepted a value. If applying the request can fail, update the displayed control from confirmed model state after the operation completes. Avoid blocking the window’s message loop while communicating with a device; use a worker and return a result message.
SetModificationMessage() and ModificationMessage() are separate from invocation. Their names alone do not specify when a particular concrete subclass sends them or which channel fields it includes. Inspect the subclass implementation and test the interaction before using modification notifications for persistence, undo, or expensive device writes. Keep InvokeChannel() as the explicit application commit boundary when that is the intended behavior.
Avoid inconsistent notification and drawing state
The control must render the same channel model that the message handler updates. If a drag changes one channel, update the displayed value and mark only that channel changed. If a keyboard event edits the current channel, do not silently broadcast a whole-channel array as if all channels changed. Conversely, an atomic operation such as “set all outputs to mute” should report all affected channels so observers can update accurately.
Use the BChannelControl setters from the window looper or whatever thread owns the control’s UI state. The class is not a thread-safe model store. A backend callback should send a message to the window rather than mutating the view from an arbitrary thread. On receipt, reconcile the event with any in-flight user edit to avoid a stale device update overwriting newer intent.
Resizing also affects the control’s interaction geometry. A subclass must implement preferred size and draw only the requested update region. Test narrow and wide frames, keyboard focus, channel changes, disabled state, and long localized labels. If an input gesture can select or move several channels at once, make that selection visible and ensure the emitted change mask matches it.
Failure-oriented acceptance checks
Test zero, one, and maximum supported channel counts; invalid indices; range changes while a value is outside the new limits; channels removed while selected; and a backend rejecting one channel in a multi-value update. Confirm that error paths leave the control and model in a known state. For messages, test one changed channel, several changed channels, a full update, a missing repeated field, and a message with the wrong what code.
For a concrete BChannelSlider, verify how the selected channel is rendered and how its keyboard and mouse paths affect values. For a custom subclass, test the pure virtual drawing, preferred-size, maximum-count, and individual-limit contracts directly. Do not infer behavior from a screenshot alone: capture the exact outgoing message and compare it with the model state.
BChannelControl is useful when multiple values share one interaction and lifecycle. Its channels still need explicit units, limits, identity, and update semantics. A reliable integration treats channel selection as view state, checks API status, emits deliberate change masks, and reconciles UI intent with confirmed application state.
Related:
- Haiku BControl: Values, Invocation Messages, and Enabled State
- Haiku BSlider: Value Mapping, Keyboard Steps, and Updates
Sources: