How Hallelujah IM Handles Candidate Window Pagination and Scrolling
Hallelujah IM implements candidate window pagination by storing the full candidate list in _candidates and calculating page offsets with a fixed page size of 9, while delegating visual scrolling to IMKCandidates’ moveUp: and moveDown: methods.
The Hallelujah IM input method, maintained in the dongyuwei/hallelujahim repository, converts Latin character sequences into Chinese candidates using a custom InputController paired with Apple’s Input Method Kit. Its approach to candidate window pagination and scrolling combines manual index tracking with macOS-provided UI components to manage lists that exceed the visible page capacity.
Pagination Logic and Page Size Configuration
Fixed Page Size of 9 Candidates
The controller enforces a strict page size limit of 9 entries per visible page. When the candidate list exceeds this threshold, the full set remains stored in the mutable array _candidates, while only a subset renders in the floating window. The currently highlighted position across the entire list—regardless of which page is visible—is tracked by the integer _currentCandidateIndex, which uses 1-based indexing.
In src/InputController.mm at lines 155–162, the logic handles digit-key selection (1–9) by computing the correct array offset based on the current page:
int pageSize = 9;
if (_currentCandidateIndex <= pageSize) {
candidate = _candidates[pressedNumber - 1];
} else {
// page‑size‑based offset calculation
candidate = _candidates[pageSize * (_currentCandidateIndex / pageSize - 1)
+ (_currentCandidateIndex % pageSize)
+ pressedNumber - 1];
}
This arithmetic determines which element to pull from _candidates when the user has scrolled beyond the first page. After selection, the controller resets the composition buffers and commits the chosen word.
Array Index Calculation
The pagination formula uses integer division and modulo operations to map the visible page number and the pressed digit to an absolute index in the master candidate array. The term (_currentCandidateIndex / pageSize - 1) identifies the current page offset, while (_currentCandidateIndex % pageSize) accounts for the remainder within the active page.
Arrow Key Scrolling Implementation
Delegating to IMKCandidates
Visual scrolling is handled by the IMKCandidates instance named sharedCandidates, which the controller instantiates during initialization. When the user presses the ↑ or ↓ arrow keys while the candidate window is visible, the controller delegates the UI update to the framework and manually synchronizes the logical index.
Lines 33–44 of src/InputController.mm implement this behavior:
if (keyCode == KEY_ARROW_DOWN) {
[sharedCandidates moveDown:self];
_currentCandidateIndex++;
return NO;
}
if (keyCode == KEY_ARROW_UP) {
[sharedCandidates moveUp:self];
_currentCandidateIndex--;
return NO;
}
The moveDown: and moveUp: methods automatically scroll the candidate view when the highlight moves past the visible boundary. The controller increments or decrements _currentCandidateIndex to maintain consistency between the UI state and the internal selection tracker.
Candidate List Synchronization
Updating the UI Buffer
Whenever the user inserts or deletes characters, the controller refreshes the candidate window by calling updateCandidates followed by show: on the shared instance. This occurs in src/InputController.mm at lines 125–127 and again in the deleteBackward: method around lines 200–204:
[sharedCandidates updateCandidates];
[sharedCandidates show:kIMKLocateCandidatesBelowHint];
The updateCandidates message triggers the controller’s candidates: delegate method, which fetches fresh results from the ConversionEngine:
- (NSArray *)candidates:(id)sender {
NSString *originalInput = [self originalBuffer];
NSArray *candidateList = [engine getCandidates:originalInput];
_candidates = [NSMutableArray arrayWithArray:candidateList];
return candidateList;
}
This method, located at lines 12–16 of src/InputController.mm, repopulates _candidates and returns the array to the Input Method Kit for rendering.
User Interaction Flow
The complete interaction flow for pagination and scrolling follows these steps:
- Input Detection:
onKeyEvent:appends typed Latin characters to the original buffer and invokesupdateCandidatesandshowto display the window. - Initial Render: The candidate window appears with up to 9 items from page 0.
- Scrolling: Pressing ↑ or ↓ calls
moveUp:ormoveDown:onsharedCandidates, which scrolls the view while the controller adjusts_currentCandidateIndexacross the entire candidate set. - Pagination Selection: Pressing a digit (1–9) executes the page-math formula to select the correct entry from
_candidatesbased on the current page derived from_currentCandidateIndex. - Commit: The controller replaces the composition buffer with the selected candidate, commits the composition, and hides the candidate UI.
Summary
- Hallelujah IM stores all candidates in
_candidatesbut displays only 9 per page. - Pagination math in
src/InputController.mm(lines 155–162) uses integer division and modulo to map digit keys to the correct array index across multiple pages. - Arrow-key scrolling delegates to
IMKCandidates’moveUp:andmoveDown:methods at lines 33–44, while_currentCandidateIndextracks the logical position. - UI synchronization occurs through
updateCandidatesand thecandidates:data source method, which queriesConversionEnginefor fresh results. - The 1-based index
_currentCandidateIndexspans the entire candidate list, not just the visible page, enabling consistent selection across pagination boundaries.
Frequently Asked Questions
How does Hallelujah IM calculate which candidate is selected when a user presses a number key?
The input method uses the 1-based _currentCandidateIndex to determine the current page. If the index exceeds the page size of 9, it calculates the absolute array position using the formula pageSize * (_currentCandidateIndex / pageSize - 1) + (_currentCandidateIndex % pageSize) + pressedNumber - 1 to retrieve the correct entry from _candidates.
Which macOS framework renders the candidate window and handles its visual scrolling?
Hallelujah IM uses the Input Method Kit (IMKit) framework. Specifically, it instantiates IMKCandidates as sharedCandidates and calls moveUp: and moveDown: to handle visual scrolling, while the framework manages the actual window rendering and paging animations.
How many candidates does Hallelujah IM display per page?
The implementation enforces a fixed page size of 9 candidates. This constant is hardcoded in src/InputController.mm and determines both the UI layout and the mathematical pagination logic used for number-key selection.
What happens to the internal state after a user selects a candidate from a paginated list?
Upon selection, the controller calls cancelComposition, updates the composed and original buffers with the selected string, invokes commitComposition: to send the text to the host application, and resets the candidate index. This sequence clears the candidate window and prepares the input method for the next composition session.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →