Sign in Join
Wiki copytemporary

Cocoa Internals/Input: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11## LCL Event Handling
22
33For general "event loop" handling you might want to refer to [[Cocoa Internals/Application|application event loop]] article.
44
55## Mouse
66
77The biggest difference between Apple and LCL ideology is the way of handling MouseMove events.
88
99According to Apple - mouse events can easily flood the event queue and thus are disabled by default, unless a user presses and holds the button.
1010
1111Apple provides a special NSWindow mode to allow MouseMove events to come through even with buttons unreleased. However, such MouseMove events would be reported to the **focused** controls (aka FirstResponder), rather than the control immediately under the cursor.
1212
1313(MouseDown, MouseUp events are reported directly to the control under the cursor, similar to the LCL).
1414
1515Because of that, the Widgetset mouse event processing routine first checks if cursor is actually above the focused control, and if it's not, it would let Cocoa propagate the mouse event, down (from child to parent) through the controls hierarchy.
1616
1717Such propagation happens, when the NSView inherited mouseMove event is called.
1818
1919If the actual control is found, the propagation of the event is stopped.
2020
2121### Implementation
2222
2323The whole set of methods *should be* overridden for each sub-classed Objective-C control:
2424
2525``` pascal
2626 function acceptsFirstMouse(event: NSEvent): Boolean; override;
2727 procedure mouseDown(event: NSEvent); override;
2828 procedure mouseUp(event: NSEvent); override;
2929 procedure rightMouseDown(event: NSEvent); override;
3030 procedure rightMouseUp(event: NSEvent); override;
3131 procedure rightMouseDragged(event: NSEvent); override;
3232 procedure otherMouseDown(event: NSEvent); override;
3333 procedure otherMouseUp(event: NSEvent); override;
3434 procedure otherMouseDragged(event: NSEvent); override;
3535 procedure mouseDragged(event: NSEvent); override;
3636 procedure mouseMoved(event: NSEvent); override;
3737 procedure scrollWheel(event: NSEvent); override;
3838```
3939
4040Even if a target LCL control doesn't publish the events (eg TScrollBar doesn't have any mouse events published), all those methods should still be overriden. The purpose is to give LCL control over that. For example, if a control is used at design time, ALL of the Cocoa default handling should be blocked.
4141
4242The typical implementation for mouseDown/mouseUp events (including "right" and "other" versions) should look like this:
4343
4444``` pascal
4545procedure TCocoaControl.mouseDown(event: NSEvent);
4646begin
4747 if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
4848 inherited mouseDown(event);
4949end;
5050
5151procedure TCocoaControl.mouseUp(event: NSEvent);
5252begin
5353 if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
5454 inherited mouseUp(event);
5555end;
5656```
5757
5858Such an implementation notifies the LCL first (via callback) about a mouse event. If the LCL doesn't block it (by returning "true" from MouseUpdownEvent) the event is passed further to Cocoa. Actually there's no harm in non-blocking mouseUp and always inheriting it.
5959
6060However, for some controls, when either implementing drag and drop functionality or some sort of dragging action (eg ScrollBar) mouseDown must be implemented in the following way:
6161
6262``` pascal
6363procedure TCocoaScrollBar.mouseDown(event: NSEvent);
6464begin
6565 if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
6666 begin
6767 inherited mouseDown(event);
6868
6969 callback.MouseUpDownEvent(event, true); // forced mouse-up event
7070 end;
7171end;
7272```
7373
7474The reason for that is due to the Cocoa (and Apple in general) implementation of mouse-hold button loops. Once inherited mouseDown is called, the procedure would not return until the mouse button is released or the action is cancelled. It's quite typical to do an event processing loop, until mouse-up event received.
7575
7676The issue is that the actual mouseUp() selector would never be called, and must be emulated via a callback.MouseUpDownEvent(, true) call.
7777
7878The implementation of the mouseUp method should still remain.
7979
8080Also, this approach doesn't apply to rightMouseDown and otherMouseDown. Apple doesn't make any event loops for those.
8181
8282### Ctrl+Left Click / Right Click
8383
8484Apple's classic mouse is a single button mouse. The additional actions are performed by holding the mouse button down. (One might observe the similar behavior for smartphone devices, where holding a finger on the screen for a certain period of time opens up an additional action).
8585
8686However, context menus are also part of the user interface. To open a context menu a user would need to hold the "Control" button down and click the mouse button.
8787
88-With the modern mouse device (where having 3 buttons+scroll wheel is the norm) it ended up with <span class="kbd">Ctrl+Left</span> being recognised as a Right click.
88+With the modern mouse device (where having 3 buttons+scroll wheel is the norm) it ended up with `Ctrl+Left` being recognised as a Right click.
8989
90-The Cocoa WidgetSet recognizes the <span class="kbd">Ctrl+Left</span> click as a Context Menu request only and forwards an appropriate message (LM_CONTEXTMENU) to the control which ends in a call to the OnContextPopup event. Yet, Cocoa doesn't alternate the information about left-click in any manner. (The Carbon WidgetSet reports the Button as TRightButton, but the shift state indicates the that Left mouse button is actually pressed).
90+The Cocoa WidgetSet recognizes the `Ctrl+Left` click as a Context Menu request only and forwards an appropriate message (LM_CONTEXTMENU) to the control which ends in a call to the OnContextPopup event. Yet, Cocoa doesn't alternate the information about left-click in any manner. (The Carbon WidgetSet reports the Button as TRightButton, but the shift state indicates the that Left mouse button is actually pressed).
9191
9292### Capture
9393
9494Only 1 handle (control) can have the mouse captured. This is typically happening on MouseDown and the capture is released on MouseUp.
9595
9696There are corresponding WidgetSet methods available:
9797
9898`GetCapture `\
9999`SetCapture`\
100100`ReleaseCapture`
101101
102102There are a few tricks.
103103
104104- most of the Cocoa controls are doing "mouse loop" capture themselves for the MouseDown key. (Mouse events are still propagated to the LCL as if the loop is not happening)
105105- if a control is disabled by an EnableWindow() function call, the Capture MUST BE released. This is not documented in WinAPI, yet this is how it's working. (see \#36491)
106106- if a control is destroyed with DestoryHandle, the Capture MUST BE released as well.
107107
108108The captured control Handle is stored in the **CaptureControl** (FCaptureControl) property of **TCocoaWidgetSet**.
109109
110110The code at **TLCLCommonCallback.MouseUpDownEvent** checks for the **CaptureControl** to be assigned. And if it's assigned then it would pass the mouse event to it and would prevent the Cocoa control from processing the event.
111111
112112### Scrolling Wheel
113113
114114Scrolling is different between LCL (WinAPI) approach and macOS (Cocoa or Carbon)
115115
116116- LCL - reports the HARDWARE mouse wheel scroll events (in 120-based deltas). The values ARE NOT a subject to the system configuration of "Number of Lines per scroll" (SPI_GETWHEELSCROLLLINES).
117117- Cocoa - reports wheel scrolls as adjustment to lines. (how many lines should be scrolled... and it doesn't matter if there are any kind of lines in the control or what's the size of lines. Just logical lines). The amounts of lines is adjusted by "Scrolling Speed" setting (and thus can be different depending on the setting for the same physical scroll of a line).
118118
119119
120120scrolling speed can be acquired from either Preferences dialog or via command line:
121121
122122`defaults read .GlobalPreferences com.apple.scrollwheel.scaling`
123123
124124
125125in runtime:
126126
127127`NSUserDefaults.standardUserDefaults.doubleForKey(NSSTR('com.apple.scrollwheel.scaling'));`
128128
129129Cocoa would increase "wheel" delta if the wheel is scrolled fast enough. WinAPI would continue to report 120.
130130
131131Both WinAPI and Cocoa would "accumulate" the delta, if main thread is working slow. Thus the next wheel event might bring a larger delta.
132132
133133If the hardware allows, WinAPI delta would be different than 120. Cocoa might also start report different amounts.
134134
135135## Keyboard
136136
137137The KeyDown event is handled through the NSWindow sendEvent method. There's explicit code written to pass an event to the focused control (firstResponder).
138138
139139### Virtual Key code
140140
141141The LCL uses the concept of Virtual Key codes.
142142
143143macOS APIs don't follow such a concept. Instead Cocoa is using "characters" for this purpose. For example: Menu Short Cuts or special Key Combinations are typically specified by characters, rather than some special codes. For each key pressed, macOS does provide a hardware key code (available through event.keyCode). The key represents a hardware code. Meaning that on a different keyboard layout the keyCode would remain the same, while character might be different. (For example for QWERTY vs Dvorak vs AZERTY layout, the same "q" key pressed, would produce the same keyCode, yet different character).
144144
145145For this particular reason, Virtual Keycodes are determined with the following approach:
146146
147147- the character (event.characterWithoutModifiers) is examined. If it's available and it can be mapped to a known VirtualKey code - then character to VK would be used and non-numpad key (i.e. "/" on the keyboard is different than "/" on number keypad)
148148- otherwise the Virtual Key code is determined based on keyCode.
149149
150150It covers the most common cases. A problem remains for national characters (i.e. German [ß](https://en.wikipedia.org/wiki/%C3%9F)). The character itself is not within the range of VK_(a-z) codes. It's encoded as VK_OEM_xx code. However such a VK code would be different on Cocoa and Windows.
151151
152152### Special Keys
153153
154154Some controls wants special keys, such as Tabs and Arrow (Cursor) keys. For example TMemo with WantsTab set to true, or TTrackBar to change thumb position.
155155
156156Both tabs and arrow keys are used for switching focus within the application itself.
157157
158158Thus, it's critical for a control to let the LCL know that it will handle those keys so that focus is not switched.
159159
160160For this purpose an additional method has been introduced:
161161
162162`lclExpectedKeys:::`
163163
164164This method accepts variable 3 parameters:
165165
166166- wantTabs (default value false), should be set to true, if the control expects to process tab key
167167- wantArrows (default value false), should be set to true, if the control expects to process arrow keys (all of them!)
168168- wantAllKeys (default value true?)... not sure how it's being used.
169169
170170### Major Patch
171171
172172See <https://bugs.freepascal.org/view.php?id=35449> by Zoë Peterson
173173
174174Reference to <https://asciiwwdc.com/2010/sessions/145> (video:
175175
176176
177177HD: <https://developer.apple.com/devcenter/download.action?path=/videos/wwdc_2010__hd/session_145__key_event_handling_in_cocoa_applications.mov>)
178178
179179In short, Cocoa's key handling looks like this:
180180
181181
1821821\. NSApplication.sendEvent (overridable)
183183
184184
1851852\. Local NSEvent Monitors (local event monitor can change the resulting event. NSEvent. addLocalMonitorForEventsMatchingMask)
186186
1871873\. If modifierFlags include Cmd or Ctrl, then Cocoa tries to perform KeyEquivalent first.
188188
189189
1901903.1 NSWindow.performKeyEquivalent (Key window)
191191
192192
193193NSResponder.performKeyEquivalent (called on all views recursively)
194194
1951953.2 NSWindow.performKeyEquivalent (Other "active" windows, but only for views that manage menus)
196196
1971973.3 NSMenu.performKeyEquivalent
198198
1991994\. Check registered hot keys ("app command" keys) (e.g., Cmd~ for Window cycling or Ctrl+F2 to move focus to menu)
200200
2012015\. NSWindow.sendEvent (Key window)
202202
203203
2042045.1 NSResponder.keyDown (firstResponder)
205205
206206
207207inherited calls keyDown up the responder chain until it reaches the NSWindow
208208
2092095.2 NSWindow.keyDown
210210
211211
2122125.2.0 \<undocumented magic\> but for Control+\\
213213
2142145.2.1 NSWindow.performKeyEquivalent (Key Window, possibly for second time if Cmd or Ctrl)
215215
2162165.2.3 NSMenu.performKeyEquivalent (again, possibly the second time)
217217
2182185.2.4 \<undocumented magic\>
219219
2202205.2.5 Beep
221221
222222### Dead Keys
223223
224224By default Cocoa event handling ignores dead-keys (on national keyboards, eg Spanish). For the event to be ready, NSTextInputContext needs to be created and events "translated" through it.
225225
226226The context exists for any NSTextView field(s). Thus native Cocoa TMemo, TEdit or TcomboBox would not be affected. Custom controls however are affected.
227227
228228In order to "translate" a keyboard event TCocoaApplication creates its own (global) NSTextInputContext and passes all events through it. In order to recognize a dead-key combination. Turning the sequence of
229229
230230`´ e`
231231
232232into
233233
234234`é`
235235
236236However, if a sequence is invalid, eg:
237237
238238`´ s`
239239
240240Cocoa reports a single string of character "'s". The Cocoa Widget Set breaks the string into separate characters and reports them as standalone characters, rather than the entire string. (This matches the WinAPI.)
241241
242242## See Also
243243
244244- [[Cocoa Internals]]
245245
246246---
247247*From the Free Pascal wiki, page "Cocoa Internals/Input", as it stood on 2022-05-07 after a change by Skalogryz, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
248248Category: Cocoa
249249Category: Keyboard Input
250250Category: Mouse Input
251251Category: macOS

Back to history