Difference between revisions of "Cocoa Internals/Input"

From Lazarus wiki
Jump to navigationJump to search
 
(13 intermediate revisions by 3 users not shown)
Line 3: Line 3:
  
 
==Mouse==
 
==Mouse==
 +
 
The biggest difference between Apple and LCL ideology is the way of handling MouseMove events.
 
The biggest difference between Apple and LCL ideology is the way of handling MouseMove events.
  
According to Apple - mouse events are easily to flood the event queue and thus are disabled by default, unless a use presses and hold the button.
+
According to Apple - mouse events can easily flood the event queue and thus are disabled by default, unless a user presses and holds the button.
  
 
Apple provides a special NSWindow mode to allow MouseMove events to come through even with buttons unreleased.
 
Apple 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.  
 
However, such MouseMove events would be reported to the '''focused''' controls (aka FirstResponder), rather than the control immediately under the cursor.  
  
(MouseDown, MouseUp events are reported directly to the control under the cursor, similar to LCL).
+
(MouseDown, MouseUp events are reported directly to the control under the cursor, similar to the LCL).
  
Because of that, 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) the controls hierarchy.  
+
Because 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.  
  
Such propagation happens, when NSView inherited mouseMove event is called.
+
Such propagation happens, when the NSView inherited mouseMove event is called.
  
 
If the actual control is found, the propagation of the event is stopped.
 
If the actual control is found, the propagation of the event is stopped.
 +
 
===Implementation===
 
===Implementation===
 +
 
The whole set of methods ''should be'' overridden for each sub-classed Objective-C control:
 
The whole set of methods ''should be'' overridden for each sub-classed Objective-C control:
  
Line 36: Line 39:
  
 
Even 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.
 
Even 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 the design time, ALL of the Cocoa default handling should be blocked.
+
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.
  
Typical implementation for mouseDown/mouseUp events (including "right" and "other" versions) should look like this:  
+
The typical implementation for mouseDown/mouseUp events (including "right" and "other" versions) should look like this:  
  
 
<syntaxhighlight lang="pascal">
 
<syntaxhighlight lang="pascal">
Line 54: Line 57:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
Such implementation notifies LCL first (via callback) about a mouse event. If LCL doesn't block it (by returning "true" from MouseUpdownEvent) the event is passed further to Cocoa.
+
Such 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.
Actually there's no harm in non-blocking mouseUp and always inheriting it.
 
  
However, for some controls, what either implementing drag and drop functionality or some sort of dragging action (eg ScrollBar) requires mouseDown to be implemented in the following manner:
+
However, 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:
  
 
<syntaxhighlight lang="pascal">
 
<syntaxhighlight lang="pascal">
Line 71: Line 73:
 
</syntaxhighlight>
 
</syntaxhighlight>
  
The reason for that is within Cocoa (and Apple in genernal) implementation of mouse-hold button loops.
+
The 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.
+
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.
It's quite typical to do an event processing loop, until mouse-up event received.
 
  
The issue is that the actual mouseUp() selector would never be called, and must be emulated via callback.MouseUpDownEvent(, true) call.
+
The issue is that the actual mouseUp() selector would never be called, and must be emulated via a callback.MouseUpDownEvent(, true) call.
  
The implementation of mouseUp method should still remain.
+
The implementation of the mouseUp method should still remain.
  
Also, such approach doesn't apply to rightMouseDown and otherMouseDown. Apple doesn't make any event loops for those.
+
Also, this approach doesn't apply to rightMouseDown and otherMouseDown. Apple doesn't make any event loops for those.
  
 
===Ctrl+Left Click / Right Click===
 
===Ctrl+Left Click / Right Click===
Apple's classic mouse is a single button mouse. The additional actions are performed by holding the mouse pressed. (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).
 
  
However, context menus are also part of the user interface. In order to open up a context menu a user would need to hold "Control" button and click the mouse button.
+
Apple'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).
  
With the modern mouse device (where having 3 buttons+scroll is a norm) it ended up recognized as Ctrl+Left is a Right click.
+
However, 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.
 +
 
 +
With the modern mouse device (where having 3 buttons+scroll wheel is the norm) it ended up with {{keypress|Ctrl+Left}} being recognised as a Right click.
 +
 
 +
The Cocoa WidgetSet recognizes the {{keypress|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).
  
CocoaWS recognizes Ctrl+Left click as a Context Menu request only and forwards an appropriate to the control, which ends in a call to OnContextPopup event.
 
Yet, Cocoa doesn't alternate the information about left-click in any manner. (CarbonWS reports Button as TRightButton, but the shift state indicates the that Left mouse button is actually pressed)
 
 
===Capture===
 
===Capture===
 +
 
Only 1 handle (control) can have the mouse captured. This is typically happening on MouseDown and the capture is released on MouseUp.
 
Only 1 handle (control) can have the mouse captured. This is typically happening on MouseDown and the capture is released on MouseUp.
  
There are corresponding WS methods available:
+
There are corresponding WidgetSet methods available:
 
  GetCapture  
 
  GetCapture  
 
  SetCapture
 
  SetCapture
 
  ReleaseCapture
 
  ReleaseCapture
 +
 
There are a few tricks.  
 
There are a few tricks.  
* most of the Cocoa controls are doing "mouse loop" capture themselves on MouseDown key. (Mouse events are still propagated to LCL as if the loop is not happening)
+
* 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)
* if a control is disabled by EnableWindow() function call, the Capture MUST BE released. This is not documented in WinAPI, yet this is how it's working. (see #36491)
+
* 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)
* if a control is destroyed DestoryHandle, the Capture MUST BE released as well.
+
* if a control is destroyed with DestoryHandle, the Capture MUST BE released as well.
  
The captured control Handle is stored at '''CaptureControl''' (FCaptureControl) property of '''TCocoaWidgetSet'''.
+
The captured control Handle is stored in the '''CaptureControl''' (FCaptureControl) property of '''TCocoaWidgetSet'''.
  
The 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 not let the Cocoa control to process the event.
+
The 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.
 +
 
 +
===Scrolling Wheel===
 +
Scrolling is different between LCL (WinAPI) approach and macOS (Cocoa or Carbon)
 +
 
 +
* 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).
 +
* 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).
 +
:scrolling speed can be acquired from either Preferences dialog or via command line:
 +
defaults read .GlobalPreferences com.apple.scrollwheel.scaling
 +
:in runtime:
 +
NSUserDefaults.standardUserDefaults.doubleForKey(NSSTR('com.apple.scrollwheel.scaling'));
 +
Cocoa would increase "wheel" delta if the wheel is scrolled fast enough. WinAPI would continue to report 120.
 +
 
 +
Both WinAPI and Cocoa would "accumulate" the delta, if main thread is working slow. Thus the next wheel event might bring a larger delta.
 +
 
 +
If the hardware allows, WinAPI delta would be different than 120. Cocoa might also start report different amounts.
  
 
==Keyboard==
 
==Keyboard==
KeyDown event is handled through the NSWindow sendEvent method.
+
 
There's an explicit code written in order to pass an event to the focused control (firstResponder).
+
The KeyDown event is handled through the NSWindow sendEvent method.
 +
There's explicit code written to pass an event to the focused control (firstResponder).
  
 
===Virtual Key code===
 
===Virtual Key code===
LCL is using the concept of Virtual Key codes.
 
  
MacOSX APIs don't follow such concept. Instead Cocoa is using "characters" for such purpose. For example: Menu Short Cuts or special Key Combinations are typically specified by characters, rather than some special codes. For each key pressed MacOSX 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).
+
The LCL uses the concept of Virtual Key codes.
 +
 
 +
macOS 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).
  
For this particular reason, Virtual Keycodes are determined in the following approach:
+
For this particular reason, Virtual Keycodes are determined with the following approach:
* 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 keyboard is different than "/" on numpad)
+
* 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)
* otherwise Virtual Key code is determined based on keyCode.
+
* otherwise the Virtual Key code is determined based on keyCode.
  
It covers the most common cases. The problem remains with the 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 VK code would be different on Cocoa and Windows.
+
It 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.
  
 
===Special Keys===
 
===Special Keys===
Some controls wants special keys, such as Tabs and Arrows to handle.
 
(for example TMemo with WantsTab set to true, or TTrackBar to change thumb position).
 
  
Both tabs and arrows are keys used for switch focus within the application itself.
+
Some 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.
 +
 
 +
Both tabs and arrow keys are used for switching focus within the application itself.
  
Thus, it's critical for a control to let LCL know, that they would handle those keys and focus would not be switched.
+
Thus, it's critical for a control to let the LCL know that it will handle those keys so that focus is not switched.
  
 
For this purpose an additional method has been introduced:
 
For this purpose an additional method has been introduced:
 +
 
  lclExpectedKeys:::
 
  lclExpectedKeys:::
the method accepts variable 3 parameters:
+
 
* wantTabs (default value false), should be set to true, if control expects to process tab key  
+
This method accepts variable 3 parameters:
* wantArrows (default value false), should be set to true, if control expects to process arrow keys (all of them!)
+
* wantTabs (default value false), should be set to true, if the control expects to process tab key  
 +
* wantArrows (default value false), should be set to true, if the control expects to process arrow keys (all of them!)
 
* wantAllKeys (default value true?)... not sure how it's being used.
 
* wantAllKeys (default value true?)... not sure how it's being used.
 +
 
===Major Patch===
 
===Major Patch===
 +
 
See https://bugs.freepascal.org/view.php?id=35449 by Zoë Peterson
 
See https://bugs.freepascal.org/view.php?id=35449 by Zoë Peterson
  
Line 143: Line 169:
  
 
In short, Cocoa's key handling looks like this:
 
In short, Cocoa's key handling looks like this:
 +
 
:1. NSApplication.sendEvent (overridable)
 
:1. NSApplication.sendEvent (overridable)
::2. Local NSEvent Monitors (local event monitor can change the resulting event)
+
::2. Local NSEvent Monitors (local event monitor can change the resulting event. NSEvent. addLocalMonitorForEventsMatchingMask)
 
::3. If modifierFlags include Cmd or Ctrl, then Cocoa tries to perform KeyEquivalent first.  
 
::3. If modifierFlags include Cmd or Ctrl, then Cocoa tries to perform KeyEquivalent first.  
 
:::3.1 NSWindow.performKeyEquivalent (Key window)
 
:::3.1 NSWindow.performKeyEquivalent (Key window)
Line 152: Line 179:
 
::4. Check registered hot keys ("app command" keys) (e.g., Cmd~ for Window cycling or Ctrl+F2 to move focus to menu)
 
::4. Check registered hot keys ("app command" keys) (e.g., Cmd~ for Window cycling or Ctrl+F2 to move focus to menu)
 
::5. NSWindow.sendEvent (Key window)
 
::5. NSWindow.sendEvent (Key window)
:::NSResponder.keyDown (firstResponder)
+
::: 5.1 NSResponder.keyDown (firstResponder)
 
::::inherited calls keyDown up the responder chain until it reaches the NSWindow
 
::::inherited calls keyDown up the responder chain until it reaches the NSWindow
::::NSWindow.keyDown
+
::::5.2 NSWindow.keyDown
:::::NSWindow.performKeyEquivalent (Key Window, possibly for second time if Cmd or Ctrl)
+
:::::5.2.0 <undocumented magic> but for Control+\
:::::NSMenu.performKeyEquivalent (again, possibly the second time)
+
:::::5.2.1 NSWindow.performKeyEquivalent (Key Window, possibly for second time if Cmd or Ctrl)
:::::<undocumented magic>
+
:::::5.2.3 NSMenu.performKeyEquivalent (again, possibly the second time)
:::::Beep
+
:::::5.2.4 <undocumented magic>
 +
:::::5.2.5 Beep
  
 
===Dead Keys===
 
===Dead Keys===
By default Cocoa event handling is ignoring dead-keys (on national keyboards, i.e. Spanish).
 
In order the event to be ready NSTextInputContext needs to be created and events "translated" through it.
 
  
The context does exist for any NSTextView field(s). Thus Cocoa native TMemo, TEdit or TcomboBox would not be affected.
+
By default Cocoa event handling ignores dead-keys (on national keyboards, eg Spanish).
Custom controls however are.
+
For the event to be ready, NSTextInputContext needs to be created and events "translated" through it.
 +
 
 +
The context exists for any NSTextView field(s). Thus native Cocoa TMemo, TEdit or TcomboBox would not be affected.
 +
Custom controls however are affected.
  
 
In 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
 
In 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
 +
 
  ´ e
 
  ´ e
 +
 
into
 
into
 +
 
  é
 
  é
  
However, if a sequence is invalid, i.e.
+
However, if a sequence is invalid, eg:
 +
 
 
  ´ s
 
  ´ s
Cocoa reports a single string of character "'s". CocoaWS breaks the string into separate characters and report them as a standalone characters, rather than the entire string. (This is matches WinAPI)
+
 
 
+
Cocoa 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.)
 +
 
 
==See Also==
 
==See Also==
 
* [[Cocoa Internals]]
 
* [[Cocoa Internals]]
Line 182: Line 216:
 
[[Category:macOS]]
 
[[Category:macOS]]
 
[[Category:Keyboard Input]]
 
[[Category:Keyboard Input]]
 +
[[category:Mouse Input]]

Latest revision as of 03:59, 7 May 2022

LCL Event Handling

For general "event loop" handling you might want to refer to application event loop article.

Mouse

The biggest difference between Apple and LCL ideology is the way of handling MouseMove events.

According to Apple - mouse events can easily flood the event queue and thus are disabled by default, unless a user presses and holds the button.

Apple 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.

(MouseDown, MouseUp events are reported directly to the control under the cursor, similar to the LCL).

Because 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.

Such propagation happens, when the NSView inherited mouseMove event is called.

If the actual control is found, the propagation of the event is stopped.

Implementation

The whole set of methods should be overridden for each sub-classed Objective-C control:

    function acceptsFirstMouse(event: NSEvent): Boolean; override;
    procedure mouseDown(event: NSEvent); override;
    procedure mouseUp(event: NSEvent); override;
    procedure rightMouseDown(event: NSEvent); override;
    procedure rightMouseUp(event: NSEvent); override;
    procedure rightMouseDragged(event: NSEvent); override;
    procedure otherMouseDown(event: NSEvent); override;
    procedure otherMouseUp(event: NSEvent); override;
    procedure otherMouseDragged(event: NSEvent); override;
    procedure mouseDragged(event: NSEvent); override;
    procedure mouseMoved(event: NSEvent); override;
    procedure scrollWheel(event: NSEvent); override;

Even 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.

The typical implementation for mouseDown/mouseUp events (including "right" and "other" versions) should look like this:

procedure TCocoaControl.mouseDown(event: NSEvent);
begin
  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
    inherited mouseDown(event);
end;

procedure TCocoaControl.mouseUp(event: NSEvent);
begin
  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
    inherited mouseUp(event);
end;

Such 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.

However, 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:

procedure TCocoaScrollBar.mouseDown(event: NSEvent);
begin
  if not Assigned(callback) or not callback.MouseUpDownEvent(event) then
  begin
    inherited mouseDown(event);
 
    callback.MouseUpDownEvent(event, true); // forced mouse-up event
  end;
end;

The 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.

The issue is that the actual mouseUp() selector would never be called, and must be emulated via a callback.MouseUpDownEvent(, true) call.

The implementation of the mouseUp method should still remain.

Also, this approach doesn't apply to rightMouseDown and otherMouseDown. Apple doesn't make any event loops for those.

Ctrl+Left Click / Right Click

Apple'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).

However, 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.

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.

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).

Capture

Only 1 handle (control) can have the mouse captured. This is typically happening on MouseDown and the capture is released on MouseUp.

There are corresponding WidgetSet methods available:

GetCapture 
SetCapture
ReleaseCapture

There are a few tricks.

  • 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)
  • 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)
  • if a control is destroyed with DestoryHandle, the Capture MUST BE released as well.

The captured control Handle is stored in the CaptureControl (FCaptureControl) property of TCocoaWidgetSet.

The 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.

Scrolling Wheel

Scrolling is different between LCL (WinAPI) approach and macOS (Cocoa or Carbon)

  • 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).
  • 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).
scrolling speed can be acquired from either Preferences dialog or via command line:
defaults read .GlobalPreferences com.apple.scrollwheel.scaling
in runtime:
NSUserDefaults.standardUserDefaults.doubleForKey(NSSTR('com.apple.scrollwheel.scaling'));

Cocoa would increase "wheel" delta if the wheel is scrolled fast enough. WinAPI would continue to report 120.

Both WinAPI and Cocoa would "accumulate" the delta, if main thread is working slow. Thus the next wheel event might bring a larger delta.

If the hardware allows, WinAPI delta would be different than 120. Cocoa might also start report different amounts.

Keyboard

The KeyDown event is handled through the NSWindow sendEvent method. There's explicit code written to pass an event to the focused control (firstResponder).

Virtual Key code

The LCL uses the concept of Virtual Key codes.

macOS 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).

For this particular reason, Virtual Keycodes are determined with the following approach:

  • 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)
  • otherwise the Virtual Key code is determined based on keyCode.

It covers the most common cases. A problem remains for national characters (i.e. German ß). 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.

Special Keys

Some 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.

Both tabs and arrow keys are used for switching focus within the application itself.

Thus, it's critical for a control to let the LCL know that it will handle those keys so that focus is not switched.

For this purpose an additional method has been introduced:

lclExpectedKeys:::

This method accepts variable 3 parameters:

  • wantTabs (default value false), should be set to true, if the control expects to process tab key
  • wantArrows (default value false), should be set to true, if the control expects to process arrow keys (all of them!)
  • wantAllKeys (default value true?)... not sure how it's being used.

Major Patch

See https://bugs.freepascal.org/view.php?id=35449 by Zoë Peterson

Reference to https://asciiwwdc.com/2010/sessions/145 (video:

HD: https://developer.apple.com/devcenter/download.action?path=/videos/wwdc_2010__hd/session_145__key_event_handling_in_cocoa_applications.mov)

In short, Cocoa's key handling looks like this:

1. NSApplication.sendEvent (overridable)
2. Local NSEvent Monitors (local event monitor can change the resulting event. NSEvent. addLocalMonitorForEventsMatchingMask)
3. If modifierFlags include Cmd or Ctrl, then Cocoa tries to perform KeyEquivalent first.
3.1 NSWindow.performKeyEquivalent (Key window)
NSResponder.performKeyEquivalent (called on all views recursively)
3.2 NSWindow.performKeyEquivalent (Other "active" windows, but only for views that manage menus)
3.3 NSMenu.performKeyEquivalent
4. Check registered hot keys ("app command" keys) (e.g., Cmd~ for Window cycling or Ctrl+F2 to move focus to menu)
5. NSWindow.sendEvent (Key window)
5.1 NSResponder.keyDown (firstResponder)
inherited calls keyDown up the responder chain until it reaches the NSWindow
5.2 NSWindow.keyDown
5.2.0 <undocumented magic> but for Control+\
5.2.1 NSWindow.performKeyEquivalent (Key Window, possibly for second time if Cmd or Ctrl)
5.2.3 NSMenu.performKeyEquivalent (again, possibly the second time)
5.2.4 <undocumented magic>
5.2.5 Beep

Dead Keys

By 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.

The context exists for any NSTextView field(s). Thus native Cocoa TMemo, TEdit or TcomboBox would not be affected. Custom controls however are affected.

In 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

´ e

into

é

However, if a sequence is invalid, eg:

´ s

Cocoa 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.)

See Also