Skip to content
Haiku OSDeep Dive Published Updated 7 min readViews unavailable

Haiku BControl: Values, Invocation Messages, and Enabled State

Build reliable Haiku controls by separating their integer value, invocation message, target routing, enabled state, and application-level validation.

BControl is the shared base for many interactive Interface Kit controls, including buttons, checkboxes, radio buttons, sliders, and text controls. It combines a view with an integer value and invoker behavior. That combination is convenient, but production code should keep four concepts separate: the visible label, the current value, the message used to report an invocation, and the application model that decides what the value means.

A control is an input boundary, not the source of truth for business state. The user can change its value, the application can change it programmatically, a message can arrive after the view has changed again, and the control can become disabled while work is in flight. Convert the control’s current state into a typed application action and validate the action against the current model.

Separate integer value from action meaning

BControl::Value() returns an int32. Derived classes give that value a control-specific meaning: a checkbox commonly distinguishes off and on, a radio button represents selection, and a slider maps a numeric value to a range. Do not treat all controls as having a universal Boolean value or a universal range. Read the documentation for the concrete control, then map its value into a domain type such as an enum, bounded quantity, or explicit state.

enum : uint32 { kRetentionChanged = 'rtch' };

void SettingsWindow::MessageReceived(BMessage* message)
{
	if (message->what == kRetentionChanged) {
		const bool retain = fRetainCheckbox->Value() != 0;
		if (!CanChangeRetentionPolicy()) {
			RefreshRetentionControl();
			return;
		}
		ApplyRetentionPolicy(retain);
		return;
	}

	BWindow::MessageReceived(message);
}

This example reads the live control value when handling the command and checks the current model before applying it. If a queued message can be delayed while the control changes again, include a sequence or desired value in the message when the concrete control’s invocation contract supports it; otherwise coalesce to the latest value intentionally. Do not interpret an untranslated label or raw index as the state identifier.

The control’s integer value is not automatically a validated application object. A slider can be configured with a range the application no longer accepts; a radio selection can refer to a choice removed by a configuration update. Clamp or reject values at the model boundary, and refresh the view to reflect the accepted state. This keeps keyboard, mouse, scripting, and programmatic changes on one validation path.

Route invocations explicitly

BControl inherits BInvoker message and target behavior. Set a stable command code in the BMessage, then set the target deliberately. A target can be a handler in the current window or an invoker messenger route according to the BInvoker API. The message is a request to handle an action; it is not proof that the action succeeded or that the target is still in the same state.

When a control is attached, ensure the intended handler is available and that the receiver checks the command code before using it. If the target is detached or destroyed while a message is in transit, the messenger routing contract and handler lifetime determine whether the message can be delivered. Avoid caching raw target pointers outside the supported object lifetime, and never let a worker thread call into a view directly to simulate a control action.

BInvoker::SetMessage() takes ownership of the BMessage pointer passed to it, deleting the previous default message. Allocate or transfer messages accordingly; do not delete a message after handing it to the invoker. If each invocation needs fresh data, keep a template message and invoke with a deliberate copy or construct an operation message with the current model identity. Avoid mutating shared message objects from unrelated threads.

Do not swallow messages the control or window does not own. In a MessageReceived() override, handle known command codes and pass unknown messages to the base class. This preserves framework handling and future extensions. Use a distinct command code per operation instead of overloading one generic “changed” message whose meaning depends on a fragile set of optional fields.

Enabled state is part of the interaction contract

SetEnabled(false) disables the control. The API documents that disabled controls generally cannot be focused and do not post messages; derived controls should visually show the disabled state and ignore keyboard or mouse actions. This is useful while an operation is pending, but it does not cancel a message already sent or prevent another code path from calling the underlying action.

Update enabled state from the current model and restore it on every completion path, including errors and cancellation. A stuck disabled control often means the UI state machine did not receive a failure or cleanup event. Conversely, re-enabling too early can allow duplicate submission while the first operation is still committing. Use an explicit pending-operation state and make the receiver idempotent.

Disabling a control is not authorization. Validate permissions and current resource state in the operation handler, because commands can be triggered through scripting, keyboard shortcuts, stale messages, or alternate interface paths. The control communicates available interaction; the model or service enforces whether the action is allowed.

Understand keyboard and default-button behavior

The base BControl::KeyDown() behavior is designed for standard controls: when a control has focus, Space or Enter can toggle or invoke it. If the window has a default button, Enter can be routed to that default button instead of the focused control. Test each concrete control and the actual window configuration rather than assuming keyboard behavior from a mouse-only test.

For controls representing irreversible actions, review which button becomes the window default and what keyboard shortcut activates it. For text-entry workflows, verify whether Enter submits the form, activates the focused control, or selects a default button. Make focus visible and predictable when validation fails; moving focus to an invalid control can help, but it should not cause the user’s typed content to be lost.

Keyboard event behavior is part of accessibility and efficiency. Exercise tab order, Space, Enter, arrow keys for sliders or radio groups, disabled state, and focus restoration after dialogs. A control that is visually disabled but still handles a custom message path is not functionally disabled.

Use derived controls for their actual semantics

Use BCheckBox for independent toggles, BRadioButton or a radio-mode menu for mutually exclusive choices, BSlider for a bounded continuous or stepped value, and BTextControl for a single-line string. These controls share BControl but do not share all interaction, accessibility, or data semantics. A custom subclass should implement the expected keyboard, focus, drawing, archiving, and message behavior rather than only rendering a shape.

SetValueNoUpdate() is a protected method intended for derived control implementation; ordinary application code should use the public setter so the view redraws as required. A value change without invalidation can leave the display stale. If a derived control overrides setters, preserve the base class contract and test the visual result after both user and programmatic changes.

Test the message and state matrix

Test mouse activation, keyboard activation, programmatic value changes, disabled state, message target changes, delayed message handling, target teardown, and model rejection. Verify that the control value after a rejected operation matches the canonical model. Include rapid repeated clicks and a window closing while an operation is pending.

Log command code, model identity, old and new semantic values, pending operation ID, and result status. Do not log sensitive text from a control unless the data-handling policy permits it. This gives production diagnostics enough context to distinguish a UI event from a committed model update.

BControl provides a reusable interaction mechanism, not a complete application command architecture. Make the target, value mapping, disabled-state policy, and model validation explicit, and derived controls remain predictable when input arrives from more than one route.

Related:

Sources:

Comments