Sign in Join
Wiki copytemporary

Cocoa Internals/Application: what changed

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

OldNewLine
11Cocoa's Application API pretty much matches what LCL provides. It could be possible to simply call NSApp apis, that correspond to LCL APIs. However, there's a major difference, that makes a simple solution not possible. LCL Application provides a methods **HandleMessages** and **ProcessMessagess** allowing a manual control over the event-loop.
22
33NSApp doesn't provide any corresponding counter parts, thus it's necessary to process events loops manually.
44
55## Event Loop
66
77The technical decisions over TCocoaApplication implementation are described below.
88
99### HiJacking (COCOALOOPHIJACK)
1010
1111macOS 10.15 beta is throwing a KVO inconsistency error. The issue <https://bugs.freepascal.org/view.php?id=35702>
1212
1313`*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: `\
1414`'Cannot update for observer <NSFrontmostDocumentWindowObserver 0x7fff8ec52208> for the key path "mainWindow.representedURL" `\
1515`from <TCocoaApplication 0x100d089a0>, most likely because the value for the key "mainWindow" has changed without an appropriate `\
1616`KVO notification being sent. Check the KVO-compliance of the TCocoaApplication class.'`
1717
1818Crash can occur on on startup (for our app) or also reported to happen when a two form app when opening second form as modal dialog.
1919
2020Error is caused because Cocoa currently doesn't run through base level NSApplication.run and thus appropriate ._isrunning flag is not set (notice that this also required the override of NSApplication.isRunning done for Mantis 32177)
2121
2222NSFrontmostDocumentWindowObserver registers for all changes to mainWindow.representedURL. During initialization of NSApplication (down inside Cocoa code) calls to willChangeValueForKey() and didChangevalueForKey() are made to '_mainWindow' or 'mainWindow' depending on state of _isRunning flag (not from calls to .isRunning but from the memory locaiton where it is stored) and NSapplication.isActive. Because we don't go through NSApplication.run the bit for .isRunning is never set and calls to willChangevalueForKey(), didChangevalueForKey() do not happen appropriately for 'mainWindow'.
2323
2424The crash occurs when _handleApplicationActivated calls removeObserverForKeyPath() on an observer object that is supposed to be set up for 'mainWindow' but it was never actually set up i.e. addObserverForKeyPath() was never called. Documentation for removeObserverForKeyPath() indicates that removing an object that hasn't been set up with addObserverForKeyPath() is an error condition. That is the condition we are hitting.
2525
2626Suggested fix (patch attached) is to run through NSApplication.run first and then jump into our aloop().
2727
2828Accomplished by removing TCocoaApplication.isRunning and TCocoaApplication.run methods thus allowing NSApplication.run to be called and set up NSRunloop and make first call to nextEventMatching:unitDate, etc. On first pass through nextEventMatching. (as caught by isRun = false) we grab control and call aloop.
2929
3030### Native Loop (COCOALOOPNATIVE)
3131
3232The approach doesn't interfere with Cocoa loop processing. The approach also DOES NOT use LCL event processing loop (as LCL event loop blocks Cocoa loop from running). Instead approach is catching FPC exceptions and handles them through LCL routines. Also end of event processing should cause Idle event to be performed. That lets Cocoa loop to run without interruptions.
3333
3434**Downside**
3535
3636- Since **Application.RunLoop** is not used, every time it's updated, CocoaWS side needs to be updated also. It's possible however, to Modify LCL to make the task easier. (which might look like a ultimate solution).
3737
3838**todo:**
3939
40-- <s>test application termination with Modal loops involved. (**stop:** needs to be called for EVERY modal loop started)</s> passed
41-- <s>check exception catching for "sync" methods. (as those are not really events)</s> passed
40+- test application termination with Modal loops involved. (**stop:** needs to be called for EVERY modal loop started) passed
41+- check exception catching for "sync" methods. (as those are not really events) passed
4242
4343### Manual Event Loop (COCOALOOPOVERRIDE)
4444
4545
4646**WARNING** the method described below is no longer used.
4747
4848Cocoa Widgetset implements even loops manually. Apple documentation advises that it's possible, though could be complicated.
4949
5050The actual event processing code could be found at AppWaitMessage and AppProcessMessage.
5151
5252```delphi
5353procedure TCocoaWidgetSet.AppWaitMessage;
5454var
5555 event : NSEvent;
5656 pool:NSAutoReleasePool;
5757begin
5858 pool := NSAutoreleasePool.alloc.init;
5959 event := NSApp.nextEventMatchingMask_untilDate_inMode_dequeue(NSAnyEventMask, NSDate.distantFuture, NSDefaultRunLoopMode, true);
6060 if event <> nil then
6161 begin
6262 if (event.type_ = NSApplicationDefined) and (event.subtype = LCLEventSubTypeMessage) and (event.data1 = LM_NULL) and (event.data2 = AppHandle) then
6363 CheckSynchronize
6464 else
6565 NSApp.sendEvent(event);
6666 NSApp.updateWindows;
6767 end;
6868 pool.release;
6969end;
7070```
7171
7272### Sub-classing NSApplication
7373
7474
7575**WARNING** the method described below is no longer used.
7676
7777With a manual event loop implementation, there's another problem showed up.
7878
7979NSWindows are behaving differently depending if NSApplication has started or not. (running property returning TRUE or FALSE). The Apple documentation doesn't mention that specifically, except for the following note:
8080
8181
8282
8383**Important**
8484
8585Many AppKit classes rely on the NSApplication class and may not work properly until this class is fully initialized.
8686
8787As a result, you should not, for example, attempt to invoke methods of other AppKit classes from an initialization method of an NSApplication subclass.
8888
8989If NSApplication is not running, then NSWindows would not pass the focus to another Window with the application. Instead they would keep the focus (if hidden) or make no window a focused window (if closed). (see [\#32177](https://bugs.freepascal.org/view.php?id=32177))
9090
9191There are two options to make NSApplication [running](https://developer.apple.com/documentation/appkit/nsapplication/1428759-running?language=objc) flag to set to true:
9292
9393- use NSApp default [run](https://developer.apple.com/documentation/appkit/nsapplication/1428631-run?language=objc) method (which could not be used, due to the need of manual event processing)
9494- subclass NSApplication, override "isRunning" (getter of "running" property)
9595
9696The sub-classing option is used, introducing TCocoaApplication class. It overrides two methods:
9797
9898- **run** - the code is actually LCL Loop procedure, which would process ObjC events loop manually
9999- **isRunning** - returns true, if **run** method was actually called.
100100
101101## Termination
102102
103103Normally, **NSApplication.run:** method never returns after being called.
104104
105105NSApplication provides a special **terminate:** method to terminate Cocoa App. The method terminates the running process (allowing **run:** never to return). It should **NEVER** be used for FPC environment, as FreePascal runtime should call unit finalization.
106106
107107The way to make **run:** method to return is use **stop:** method. Which **stops** *the current* event loop, and should be called multiple times with multiple event loops (such as modal) are used.
108108
109109## Sync (MainThreadWake), PostMessage and SendMessage
110110
111111Originally the syncing with the main thread, as well as Post and SendMessage implementations were based on creating a custom **NSApplicationDefined** event with sub type set to **LCLEventSubTypeMessage**
112112
113113Posting the custom event into an event loop on Cocoa might have a certain undesired consequences. The major problem is with event processing for tracking mouse actions. Unlike WinAPI, where it is expected for to process standalone events, Cocoa developer should actually run a while loop. The loop should catch mouse events and stop as soon as mouse is released.
114114
115115Each tracking loop however, is written differently. And some places reacting on "unexpected" events differently.
116116
117117Trying to hide "unexpected" events from a loop might work for one implementation, but not the other.
118118
119119The only solution that satisfies every loop is NOT to post any custom events at all.
120120
121121The implementation was changed from using a custom event to **performSelectorOnMainThread** method.
122122
123123For syncing with the main thread there's **lclSyncCheck:** method introduced for **TCocoaApplication**.
124124
125125For post and send events, there's a message **TLCLEventMessage** class, which **performSelectorOnMainThread:** method is called. The class provides fields to store WParam, LParam and Result (for SendMessage).
126126
127127### Clogging with Dialogs
128128
129129There's an issue with a clogged sync processing. If there are two messages sent to the main thread, where both are causing a modal window to be shown. (i.e. an error message). Then if the second one arrives, while the first one is still showing the modal window, the application terminates.
130130
131131## Post Event Processing
132132
133133There a number of routines performed after processing each event.
134134
135135### Clipboard
136136
137137In Cocoa, clipboard is named Pasteboard (class NSPasteboard);
138138
139139The global (system-wide) pasteboard is available via NSPasteboard.generalPasteboard method. Primary and Secondary selection boards are just unique named NSPasteboards allocated at start.
140140
141141The whole implementation of API is in TCocoaWidgetSet class. (todo: could/should be separated)
142142
143143The older (10.5 and earlier) APIs are used for operations. (starting with 10.6 Apple suggests using the new API, but the old one has not been deprecated so far)
144144
145145There's not NSPasteboard change notification in place (unlike Windows), thus the only way to track, if the content got changed (in LCL/Windows terms - clipboard ownership was lost) is through checking "changeCount" property. The property is checked on **EVERY** event processed (which might impact efficiency). The only way to get rid of that is to rewrite Clipboard LCL object. (instead of relying on a notification - it should check for the change at "Get" data calls)
146146
147147### ToCollect
148148
149149On certain conditions a control can destroy itself while processing its own event. (For example an Edit field of a "authentication" dialog. If enter is pressed, the OnKeyDown event to close the form, causing its all children controls, including the edit field itself).
150150
151151If such things occur, the **callback** object (linking NSObject and LCL Target) can be released (in DestoryHandle call). However, the **callback** object must not be freed, as Key processing routine (the one that started OnKeyDown) is not yet over. The same concept applies for other processing routines (i.e. Mouse handling, etc).
152152
153153In order to prevent crashed due to access of freed object, the destruction of Callbacks objects is delayed until the end of event processing. Cocoa widgetset interface provides following method **AddToCollect** to add an object to be released after the event processing.
154154
155155Keep in mind that events handling might be recursive, thus a destruction of an object only occurs when the processing of the event that added object to the collection is finished.
156156
157157### Focus Change
158158
159159The change if control focus is made post event basis to prevent endless loops of focus-fight.
160160
161161Focus-fight occurs, a focus change notification is sent at the time when the focus is actually switched. It's typical for Cocoa controls to temporary switch focus to Window. (in Cocoa world, Window is used a "back-up" focus target. As it always allows to be focused... even with canBecomeKeyWindow false?). If LCL is notified by Windows getting the focus, it would attempt to switch the focus to one of its child control. This would typically be the same control as it's now. Eventually Cocoa would try to switch the focus back to Window, causing the recursive focus-fight with application responsiveness and eventual crash.
162162
163163To avoid that, the focus switch is verified after event processing.
164164
165165## System Wide Events
166166
167167There's a number of system-wide events that are reported via Application object.
168168
169169### End of User Session
170170
171171Usually triggered by a user logging out or the entire system shutdown.
172172
173173Supported since <https://gitlab.com/freepascal.org/lazarus/lazarus/-/issues/34798>
174174
175175- **IntfQueryEndSession**(var Cancel: Boolean);
176176- **IntfEndSession**;
177177
178178### Application Active Status
179179
180180- **IntfAppActivate**(const Async: Boolean = False); - the event when application gets "active" by gaining the keyboard focus.
181181
182182
183183
184184supported by processing of **applicationDidBecomeActive** message
185185
186186- **IntfAppDeactivate**(const Async: Boolean = False); - the even when application gets "deactivated" by losing keybaord focus.
187187
188188
189189
190190supported by processing of **applicationDidResignActive** message
191191
192192### Application maximization
193193
194194- **IntfAppMinimize**;
195195- **IntfAppRestore**;
196196
197197### Files Drop
198198
199199Issued by the system file exchange (drag/drop mechanism). Files can be dropped on the Application icon (shown in the system Dock)
200200
201201- **IntfDropFiles**(const FileNames: Array of String);
202202
203203
204204
205205supported by processing **application:openFiles** files messages.
206206
207207the list of files is also passed to the main form of the application (similar IntfDropFiles event), by TApplication method is called first.
208208
209209### System Settings Changes
210210
211211- **IntfSettingsChange**
212212
213213
214214
215215not supported
216216
217217- **IntfThemeOptionChange**
218218
219219
220220
221221not supported (need to be supported for Light/Dark theme changes?)
222222
223223## See Also
224224
225225- [[Cocoa Internals]]
226226- [NSApplication](https://developer.apple.com/documentation/appkit/nsapplication?language=objc) - apple reference documentation for NSApplication class.
227227
228228---
229229*From the Free Pascal wiki, page "Cocoa Internals/Application", as it stood on 2022-02-21 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).*
230230Category: Cocoa
231231Category: macOS

Back to history