Merge pull request #63 from PolarSeeker77/fix/x11-drag-and-drop

Drag and drop on X11: run drags in-process so CEF/Electron pages don't freeze
This commit is contained in:
Kalah authored and GitHub committed 2026-10-18 19:00:20 +02:00
commit 0184089710
4 files changed
+702 -35

No files matched your search

+6
View File
@@ -129,6 +129,12 @@ With clients closed, run `build/riot-client-resize-check --handoff` through Darl
The probe cycles through 1280×720, 1600×900, 1024×576 and 1280×720. Each stage changes the frame color and samples its own X11 window near the bottom-right corner, beyond the original frame's extent when growing. It waits up to 2.5 seconds for presentation and prints `RESIZE_PIXEL ... pass=1` for every stage. This verifies presented pixels rather than only view properties. Success does not reproduce League's own resize callbacks; the reported League UI failure remains open, with the opt-in [geometry trace](TROUBLESHOOTING.md#language-dropdown-closes-riot-or-client-resize-leaves-a-clipped-ui) needed to locate the remaining mismatch.
### X11 drag and drop
`python3 build/runtime/tools/build-riot-x11-drag-check.py` builds an independent AppKit check for the in-process drag loop that the X11 backend uses (`-[NSWindow dragImage:at:offset:event:pasteboard:source:slideBack:]`). It needs the cocotron AppKit built with the drag-and-drop patch. With clients closed, run `build/riot-x11-drag-check` through Darling with the same framework/library overrides as the other probes. It opens its own source and destination windows, posts synthetic events to its own application queue, and does not inject X11 input or touch other clients.
It prints `PASS` or `FAIL` for seven cases: a drop onto a destination without `prepareForDragOperation:` (the default must proceed), a drag ending outside any destination, Escape cancel, two drags back to back with a source lacking `pasteboardChangedOwner:`, main-queue blocks and run loop timers firing during the drag, a mouse-down reaching a view afterwards, and a destination view removed from its window mid-drag. Each drag must call `draggedImage:endedAt:operation:` exactly once. The process exits nonzero on any failure, and a case that makes no progress for 20 seconds is reported as a hang, which is the original failure mode. Success covers the AppKit drag loop only; it does not exercise Chromium's own drag source.
## Legacy operating-system version query
```sh
+399 -35
View File
@@ -1377,7 +1377,7 @@ index c3f5d056..dbd93174 100644
@"NSTouchBarItemIdentifierCharacterPicker";
diff --git a/AppKit/NSView.m b/AppKit/NSView.m
index 8ac2bbdc..08197fbc 100644
index 8ac2bbdc..a0fb4f37 100644
--- a/AppKit/NSView.m
+++ b/AppKit/NSView.m
@@ -46,6 +46,12 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
@@ -1590,11 +1590,47 @@ index 8ac2bbdc..08197fbc 100644
}
[_subviews
@@ -2687,6 +2786,17 @@ static NSView *viewBeingPrinted = nil;
slideBack: (BOOL) slideBack
{
location = [self convertPoint: location toView: nil];
+ if ([NSDraggingManager draggingManager] == nil) {
+ // darling-lol: no drag manager on X11; NSWindow runs the drag in-process.
+ [[self window] dragImage: image
+ at: location
+ offset: offset
+ event: event
+ pasteboard: pasteboard
+ source: source
+ slideBack: slideBack];
+ return;
+ }
[[NSDraggingManager draggingManager] dragImage: image
at: location
offset: offset
@@ -2783,7 +2893,8 @@ static NSView *viewBeingPrinted = nil;
}
- (BOOL) prepareForDragOperation: (id<NSDraggingInfo>) sender {
- return NO;
+ // darling-lol: AppKit goes ahead when a destination doesn't implement this (Chromium).
+ return YES;
}
- (BOOL) performDragOperation: (id<NSDraggingInfo>) sender {
diff --git a/AppKit/NSWindow.m b/AppKit/NSWindow.m
index d545120a..b2845478 100644
index d545120a..79760f47 100644
--- a/AppKit/NSWindow.m
+++ b/AppKit/NSWindow.m
@@ -45,6 +45,21 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
@@ -27,6 +27,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
#import <AppKit/NSEvent_CoreGraphics.h>
#import <AppKit/NSGraphics.h>
#import <AppKit/NSImage.h>
+#import <AppKit/NSImageView.h>
#import <AppKit/NSMainMenuView.h>
#import <AppKit/NSMenu.h>
#import <AppKit/NSMenuItem.h>
@@ -45,6 +46,21 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
#import <AppKit/NSWindowAnimationContext.h>
#import <ApplicationServices/ApplicationServices.h>
#import <CoreGraphics/CGWindow.h>
@@ -1616,7 +1652,7 @@ index d545120a..b2845478 100644
const NSNotificationName NSWindowDidBecomeKeyNotification =
@"NSWindowDidBecomeKeyNotification";
@@ -204,6 +219,25 @@ NSInteger NSBitsPerPixelFromDepth(NSWindowDepth depth) {
@@ -204,6 +220,122 @@ NSInteger NSBitsPerPixelFromDepth(NSWindowDepth depth) {
@end
@@ -1638,11 +1674,108 @@ index d545120a..b2845478 100644
+ }
+ return view;
+}
+
+/* darling-lol: the X11 backend has no NSDraggingManager, so drags run in-process
+ * (see -_localDragImage:...): this is the NSDraggingInfo handed to destinations. */
+@interface _NSLocalDraggingInfo : NSObject <NSDraggingInfo> {
+ @public
+ NSPasteboard *_pasteboard;
+ NSImage *_image;
+ id _source;
+ NSWindow *_destinationWindow;
+ NSPoint _screenPoint;
+ NSDragOperation _sourceMask;
+}
+@end
+
+@implementation _NSLocalDraggingInfo
+
+- (NSPasteboard *) draggingPasteboard {
+ return _pasteboard;
+}
+
+- (NSDragOperation) draggingSourceOperationMask {
+ return _sourceMask;
+}
+
+- (NSPoint) draggingLocation {
+ if (_destinationWindow == nil)
+ return _screenPoint;
+ return [_destinationWindow convertScreenToBase: _screenPoint];
+}
+
+- (NSImage *) draggedImage {
+ return _image;
+}
+
+- (NSPoint) draggedImageLocation {
+ return [self draggingLocation];
+}
+
+- (NSWindow *) draggingDestinationWindow {
+ return _destinationWindow;
+}
+
+- (id) draggingSource {
+ return _source;
+}
+
+- (void) slideDraggedImageTo: (NSPoint) point {
+}
+
+- (int) draggingSequenceNumber {
+ return 0;
+}
+
+- (NSArray *) namesOfPromisedFilesDroppedAtDestination: (NSURL *) destination {
+ return nil;
+}
+
+@end
+
+// Shows the drag image under the pointer. Can't become key, so X11 makes it
+// override-redirect and the window manager leaves it alone.
+@interface _NSDragImageWindow : NSWindow
+@end
+
+@implementation _NSDragImageWindow
+
+- (BOOL) canBecomeKeyWindow {
+ return NO;
+}
+
+- (BOOL) canBecomeMainWindow {
+ return NO;
+}
+
+@end
+
+/* One -nextEventMatchingMask: pass handles a single run loop source, and Chromium
+ * runs one task per pass: during a drag its drop-target replies fall behind.
+ * Run sources until idle (wait == 0) or until the deadline (wait > 0). */
+static void _NSLocalDragRunPending(NSTimeInterval budget, BOOL wait) {
+ CFAbsoluteTime deadline = CFAbsoluteTimeGetCurrent() + budget;
+ for (;;) {
+ CFTimeInterval left = deadline - CFAbsoluteTimeGetCurrent();
+ if (left <= 0)
+ break;
+ SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, wait ? MIN(left, 0.01) : 0, true);
+ if (!wait && result != kCFRunLoopRunHandledSource)
+ break;
+ }
+}
+
+static BOOL _NSLocalDragTrace(void) {
+ static int trace = -1;
+ if (trace < 0)
+ trace = getenv("DARLING_DRAG_TRACE") != NULL;
+ return trace;
+}
+
@implementation NSWindow
@synthesize identifier = _identifier;
@@ -218,7 +252,8 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -218,7 +350,8 @@ static BOOL _allowsAutomaticWindowTabbing;
/* This method is Cococtron specific and can be override by subclasses, do not
* change method name */
+ (BOOL) hasMainMenuForStyleMask: (NSWindowStyleMask) styleMask {
@@ -1652,7 +1785,7 @@ index d545120a..b2845478 100644
}
/* This method is Cococtron specific and can be override by subclasses, do not
@@ -318,6 +353,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -318,6 +451,7 @@ static BOOL _allowsAutomaticWindowTabbing;
[NSApplication sharedApplication];
}
_styleMask = styleMask;
@@ -1660,7 +1793,7 @@ index d545120a..b2845478 100644
_backingType = backing;
_isDeferred = defer;
@@ -351,7 +387,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -351,7 +485,7 @@ static BOOL _allowsAutomaticWindowTabbing;
[_backgroundView _setWindow: self];
[_backgroundView setNextResponder: self];
@@ -1669,7 +1802,7 @@ index d545120a..b2845478 100644
_contentView = [[NSView alloc] initWithFrame: contentViewFrame];
[_contentView setAutoresizesSubviews: YES];
[_contentView
@@ -399,6 +435,8 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -399,6 +533,8 @@ static BOOL _allowsAutomaticWindowTabbing;
}
- (void) dealloc {
@@ -1678,7 +1811,7 @@ index d545120a..b2845478 100644
[[NSNotificationCenter defaultCenter] removeObserver: self];
[_childWindows release];
[_title release];
@@ -934,6 +972,22 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -934,6 +1070,22 @@ static BOOL _allowsAutomaticWindowTabbing;
return _animationContext;
}
@@ -1701,7 +1834,7 @@ index d545120a..b2845478 100644
- (void) setFrame: (NSRect) newFrame
display: (BOOL) display
animate: (BOOL) animate
@@ -944,8 +998,19 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -944,8 +1096,19 @@ static BOOL _allowsAutomaticWindowTabbing;
_frame = newFrame;
_makeSureIsOnAScreen = YES;
@@ -1722,7 +1855,7 @@ index d545120a..b2845478 100644
if (didSize) {
[self resetCursorRects];
@@ -1025,7 +1090,34 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -1025,7 +1188,34 @@ static BOOL _allowsAutomaticWindowTabbing;
}
- (void) setMovable: (BOOL) movable {
@@ -1758,7 +1891,7 @@ index d545120a..b2845478 100644
}
- (void) setBackingType: (NSBackingStoreType) value {
@@ -1234,6 +1326,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -1234,6 +1424,7 @@ static BOOL _allowsAutomaticWindowTabbing;
- (void) setIgnoresMouseEvents: (BOOL) value {
_ignoresMouseEvents = value;
@@ -1766,7 +1899,7 @@ index d545120a..b2845478 100644
}
- (void) setAspectRatio: (NSSize) value {
@@ -1413,8 +1506,14 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -1413,8 +1604,14 @@ static BOOL _allowsAutomaticWindowTabbing;
}
- (NSButton *) standardWindowButton: (NSWindowButton) value {
@@ -1783,7 +1916,7 @@ index d545120a..b2845478 100644
}
- (NSButtonCell *) defaultButtonCell {
@@ -1425,6 +1524,17 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -1425,6 +1622,17 @@ static BOOL _allowsAutomaticWindowTabbing;
return [_sheetContext sheet];
}
@@ -1801,7 +1934,7 @@ index d545120a..b2845478 100644
- (id) windowController {
return _windowController;
}
@@ -1511,10 +1621,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -1511,10 +1719,7 @@ static BOOL _allowsAutomaticWindowTabbing;
return [_platformWindow isMiniaturized];
}
@@ -1813,7 +1946,7 @@ index d545120a..b2845478 100644
- (BOOL) inLiveResize {
return _isInLiveResize;
@@ -1561,6 +1668,117 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -1561,6 +1766,117 @@ static BOOL _allowsAutomaticWindowTabbing;
return point;
}
@@ -1931,7 +2064,7 @@ index d545120a..b2845478 100644
- (NSRect) frameRectForContentRect: (NSRect) contentRect {
// hasMainMenu is an instance method so we can't just use the class method
// frameRectForContentRect:styleMask:
@@ -1580,6 +1798,10 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -1580,6 +1896,10 @@ static BOOL _allowsAutomaticWindowTabbing;
}
- (NSRect) contentRectForFrameRect: (NSRect) frameRect {
@@ -1942,7 +2075,7 @@ index d545120a..b2845478 100644
NSRect result = [[NSDisplay currentDisplay] insetRect: frameRect
forNativeWindowBorderWithStyle: [self styleMask]];
@@ -2155,6 +2377,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -2155,6 +2475,7 @@ static BOOL _allowsAutomaticWindowTabbing;
- (void) orderWindow: (NSWindowOrderingMode) place
relativeTo: (NSInteger) relativeTo
{
@@ -1950,7 +2083,7 @@ index d545120a..b2845478 100644
// The move notifications are sent under unknown conditions around
// orderFront: in the Apple AppKit, we do them all the time here until it's
// figured out. I suspect it is a side effect of off-screen windows being at
@@ -2208,6 +2431,8 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -2208,6 +2529,8 @@ static BOOL _allowsAutomaticWindowTabbing;
break;
}
@@ -1959,7 +2092,7 @@ index d545120a..b2845478 100644
[self postNotificationName: NSWindowDidMoveNotification];
}
@@ -2257,6 +2482,12 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -2257,6 +2580,12 @@ static BOOL _allowsAutomaticWindowTabbing;
}
- (void) sendEvent: (NSEvent *) event {
@@ -1972,7 +2105,7 @@ index d545120a..b2845478 100644
// Some events can cause our window to be destroyed
// So make sure self lives at least through this current run loop...
[[self retain] autorelease];
@@ -2300,6 +2531,10 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -2300,6 +2629,10 @@ static BOOL _allowsAutomaticWindowTabbing;
case NSLeftMouseDown: {
NSView *view = [_backgroundView hitTest: [event locationInWindow]];
@@ -1983,7 +2116,7 @@ index d545120a..b2845478 100644
if ([view acceptsFirstResponder]) {
if ([view needsPanelToBecomeKey]) {
@@ -2307,6 +2542,27 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -2307,6 +2640,27 @@ static BOOL _allowsAutomaticWindowTabbing;
}
}
@@ -2011,7 +2144,7 @@ index d545120a..b2845478 100644
// Event goes to view, not first responder.
[view mouseDown: event];
_mouseDownLocationInWindow = [event locationInWindow];
@@ -2330,6 +2586,31 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -2330,6 +2684,31 @@ static BOOL _allowsAutomaticWindowTabbing;
_mouseDownLocationInWindow = NSMakePoint(NAN, NAN);
break;
@@ -2043,7 +2176,216 @@ index d545120a..b2845478 100644
case NSMouseMoved: {
NSView *hit = [_backgroundView hitTest: [event locationInWindow]];
@@ -2536,6 +2817,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -2478,6 +2857,197 @@ static BOOL _allowsAutomaticWindowTabbing;
_draggedTypes = nil;
}
+// The registered drag destination under screenPoint in this window, if any.
+- (NSView *) _localDragDestinationAt: (NSPoint) screenPoint types: (NSArray *) types {
+ NSView *root = [self contentView];
+ while ([root superview] != nil)
+ root = [root superview];
+ NSView *view = [root hitTest: [self convertScreenToBase: screenPoint]];
+ while (view != nil && [[view registeredDraggedTypes] firstObjectCommonWithArray: types] == nil)
+ view = [view superview];
+ return view;
+}
+
+/* darling-lol: drag loop for backends without an NSDraggingManager (X11). Covers drags
+ * between this app's own windows; nothing crosses to other X clients. Like AppKit's,
+ * it returns once the drag is over and always ends with -draggedImage:endedAt:operation:,
+ * which Chromium waits for before the page takes mouse input again. */
+- (void) _localDragImage: (NSImage *) image
+ at: (NSPoint) imageLocation
+ event: (NSEvent *) event
+ pasteboard: (NSPasteboard *) pasteboard
+ source: (id) source
+{
+ _NSLocalDraggingInfo *info = [[_NSLocalDraggingInfo alloc] init];
+ info->_pasteboard = pasteboard;
+ info->_image = image;
+ info->_source = source;
+ info->_sourceMask = NSDragOperationEvery;
+ if ([source respondsToSelector: @selector(draggingSourceOperationMaskForLocal:)])
+ info->_sourceMask = [source draggingSourceOperationMaskForLocal: YES];
+
+ NSPoint screenPoint;
+ if (event != nil && [event window] != nil)
+ screenPoint = [[event window] convertBaseToScreen: [event locationInWindow]];
+ else
+ screenPoint = [self convertBaseToScreen: [self mouseLocationOutsideOfEventStream]];
+
+ // The image keeps its offset from the pointer, as on macOS.
+ _NSDragImageWindow *imageWindow = nil;
+ NSPoint imageOffset = NSZeroPoint;
+ NSSize imageSize = [image size];
+ if (imageSize.width >= 1 && imageSize.height >= 1) {
+ NSPoint imageOrigin = [self convertBaseToScreen: imageLocation];
+ imageOffset = NSMakePoint(screenPoint.x - imageOrigin.x, screenPoint.y - imageOrigin.y);
+ imageWindow = [[_NSDragImageWindow alloc]
+ initWithContentRect: NSMakeRect(imageOrigin.x, imageOrigin.y, imageSize.width, imageSize.height)
+ styleMask: NSBorderlessWindowMask
+ backing: NSBackingStoreBuffered
+ defer: NO];
+ [imageWindow setReleasedWhenClosed: NO];
+ [imageWindow setIgnoresMouseEvents: YES];
+ [imageWindow setLevel: NSPopUpMenuWindowLevel];
+ [imageWindow setHasShadow: NO];
+ NSImageView *imageView = [[NSImageView alloc]
+ initWithFrame: NSMakeRect(0, 0, imageSize.width, imageSize.height)];
+ [imageView setImage: image];
+ [imageView setImageScaling: NSImageScaleNone];
+ [imageWindow setContentView: imageView];
+ [imageView release];
+ [imageWindow orderFront: nil];
+ }
+
+ NSArray *types = [pasteboard types];
+ id destination = nil;
+ NSDragOperation operation = NSDragOperationNone;
+ BOOL dropped = NO;
+ BOOL moved = YES;
+ const char *ending = "release";
+
+ if (_NSLocalDragTrace())
+ NSLog(@"DRAGTRACE start at %.0f,%.0f window=%@ image=%.0fx%.0f mask=0x%lx types=%@",
+ screenPoint.x, screenPoint.y, [self className], imageSize.width, imageSize.height,
+ (unsigned long) info->_sourceMask, types);
+
+ for (;;) {
+ if (moved) {
+ NSWindow *window = nil;
+ if (NSPointInRect(screenPoint, [self frame]))
+ window = self;
+ else {
+ for (NSWindow *candidate in [NSApp orderedWindows]) {
+ if (candidate != imageWindow && [candidate isVisible] &&
+ NSPointInRect(screenPoint, [candidate frame])) {
+ window = candidate;
+ break;
+ }
+ }
+ }
+ info->_screenPoint = screenPoint;
+ if (info->_destinationWindow != window) {
+ [info->_destinationWindow release];
+ info->_destinationWindow = [window retain];
+ }
+ NSView *view = [window _localDragDestinationAt: screenPoint types: types];
+
+ if (view != destination) {
+ if ([destination respondsToSelector: @selector(draggingExited:)])
+ [destination draggingExited: info];
+ [destination release];
+ destination = [view retain];
+ operation = NSDragOperationNone;
+ if ([destination respondsToSelector: @selector(draggingEntered:)])
+ operation = [destination draggingEntered: info];
+ if (_NSLocalDragTrace())
+ NSLog(@"DRAGTRACE destination %@ at %.0f,%.0f operation=0x%lx", [view className],
+ screenPoint.x, screenPoint.y, (unsigned long) operation);
+ } else if ([destination respondsToSelector: @selector(draggingUpdated:)]) {
+ operation = [destination draggingUpdated: info];
+ }
+ [imageWindow setFrameOrigin: NSMakePoint(screenPoint.x - imageOffset.x,
+ screenPoint.y - imageOffset.y)];
+ if ([source respondsToSelector: @selector(draggedImage:movedTo:)])
+ [source draggedImage: image movedTo: screenPoint];
+ _NSLocalDragRunPending(0.008, NO);
+ }
+ if (dropped)
+ break;
+
+ // Short timeout: the wait below doesn't wake for run loop sources, and
+ // a release delivered before the loop started never shows up as an event.
+ NSEvent *next = [NSApp nextEventMatchingMask: NSLeftMouseUpMask | NSLeftMouseDraggedMask |
+ NSMouseMovedMask | NSKeyDownMask
+ untilDate: [NSDate dateWithTimeIntervalSinceNow: 0.05]
+ inMode: NSDefaultRunLoopMode
+ dequeue: YES];
+ moved = NO;
+ if (next == nil) {
+ NSUInteger buttons = [NSEvent pressedMouseButtons];
+ if ((buttons & 1) == 0) {
+ dropped = moved = YES;
+ ending = "button up, no event";
+ }
+ continue;
+ }
+ if ([next type] == NSKeyDown) {
+ if ([next keyCode] == 53 || [[next characters] isEqual: @"\033"]) {
+ ending = "escape";
+ break;
+ }
+ continue;
+ }
+ if ([next window] != nil)
+ screenPoint = [[next window] convertBaseToScreen: [next locationInWindow]];
+ else
+ screenPoint = [next locationInWindow];
+ moved = YES;
+ if (_NSLocalDragTrace())
+ NSLog(@"DRAGTRACE event type=%d window=%@ at %.0f,%.0f", (int) [next type],
+ [[next window] className], screenPoint.x, screenPoint.y);
+ if ([next type] == NSLeftMouseUp)
+ dropped = YES;
+ }
+
+ [imageWindow orderOut: nil];
+ [imageWindow release];
+
+ // The operation for the release point arrives asynchronously (Chromium):
+ // let it come in, then ask again.
+ if (dropped && [destination respondsToSelector: @selector(draggingUpdated:)]) {
+ _NSLocalDragRunPending(0.1, YES);
+ operation = [destination draggingUpdated: info];
+ if (_NSLocalDragTrace())
+ NSLog(@"DRAGTRACE release operation=0x%lx", (unsigned long) operation);
+ }
+
+ BOOL performed = NO;
+ if (dropped && destination != nil && operation != NSDragOperationNone) {
+ performed = YES;
+ if ([destination respondsToSelector: @selector(prepareForDragOperation:)])
+ performed = [destination prepareForDragOperation: info];
+ if (performed)
+ performed = [destination respondsToSelector: @selector(performDragOperation:)] &&
+ [destination performDragOperation: info];
+ if (performed && [destination respondsToSelector: @selector(concludeDragOperation:)])
+ [destination concludeDragOperation: info];
+ } else if ([destination respondsToSelector: @selector(draggingExited:)]) {
+ [destination draggingExited: info];
+ }
+ if (!performed)
+ operation = NSDragOperationNone;
+
+ if (_NSLocalDragTrace())
+ NSLog(@"DRAGTRACE end (%s) at %.0f,%.0f destination=%@ performed=%d operation=0x%lx", ending,
+ screenPoint.x, screenPoint.y, [destination className], performed, (unsigned long) operation);
+
+ if ([source respondsToSelector: @selector(draggedImage:endedAt:operation:)])
+ [source draggedImage: image endedAt: screenPoint operation: operation];
+ [destination release];
+ [info->_destinationWindow release];
+ info->_destinationWindow = nil;
+ [info release];
+}
+
- (void) dragImage: (NSImage *) image
at: (NSPoint) location
offset: (NSSize) offset
@@ -2486,6 +3056,10 @@ static BOOL _allowsAutomaticWindowTabbing;
source: (id) source
slideBack: (BOOL) slideBack
{
+ if ([NSDraggingManager draggingManager] == nil) {
+ [self _localDragImage: image at: location event: event pasteboard: pasteboard source: source];
+ return;
+ }
[[NSDraggingManager draggingManager] dragImage: image
at: location
offset: offset
@@ -2536,6 +3110,7 @@ static BOOL _allowsAutomaticWindowTabbing;
} else {
[[self platformWindow] hideWindow];
}
@@ -2051,7 +2393,7 @@ index d545120a..b2845478 100644
}
- (void) orderFront: (id) sender {
@@ -3064,6 +3346,13 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -3064,6 +3639,13 @@ static BOOL _allowsAutomaticWindowTabbing;
if (didSize) {
// Don't redraw everything unless we really have to.
[_backgroundView setFrameSize: _frame.size];
@@ -2065,7 +2407,7 @@ index d545120a..b2845478 100644
[_backgroundView setNeedsDisplay: YES];
[self resetCursorRects];
[self saveFrameUsingName: _autosaveFrameName];
@@ -3260,8 +3549,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -3260,8 +3842,7 @@ static BOOL _allowsAutomaticWindowTabbing;
mouseWasInside && !mouseIsInside) {
[exited addObject: area];
}
@@ -2075,7 +2417,7 @@ index d545120a..b2845478 100644
[moved addObject: area];
}
if (options & NSTrackingCursorUpdate && !mouseWasInside &&
@@ -3459,7 +3747,7 @@ static BOOL _allowsAutomaticWindowTabbing;
@@ -3459,7 +4040,7 @@ static BOOL _allowsAutomaticWindowTabbing;
}
- (CGSubWindow *) _createSubWindowWithFrame: (CGRect) frame {
@@ -4130,7 +4472,7 @@ index 411903cc..4b196afd 100644
NSMutableDictionary<NSPasteboardType, NSData *> *_typeToData;
NSMutableDictionary<NSPasteboardType, id<NSPasteboardTypeOwner>>
diff --git a/AppKit/X11.backend/X11Pasteboard.m b/AppKit/X11.backend/X11Pasteboard.m
index 0e91a5d1..1e4cec1b 100644
index 0e91a5d1..9feeffa1 100644
--- a/AppKit/X11.backend/X11Pasteboard.m
+++ b/AppKit/X11.backend/X11Pasteboard.m
@@ -18,6 +18,8 @@ along with Darling. If not, see <http://www.gnu.org/licenses/>.
@@ -4161,17 +4503,39 @@ index 0e91a5d1..1e4cec1b 100644
if (_typeToData != nil) {
// Already an owner.
return;
@@ -93,7 +94,8 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
@@ -78,12 +79,19 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
_changeCount++;
}
+// pasteboardChangedOwner: is optional (Chromium's drag source lacks it); once per owner.
+- (void) notifyOwnersChanged {
+ NSArray *owners = [[NSSet setWithArray: [_typeToOwner allValues]] allObjects];
+ for (id owner in owners) {
+ if ([owner respondsToSelector: @selector(pasteboardChangedOwner:)])
+ [owner pasteboardChangedOwner: self];
+ }
+}
+
- (NSInteger) clearContents {
[self ensureSelectionOwner];
- for (id owner in [_typeToOwner allValues]) {
- [owner pasteboardChangedOwner: self];
- }
+ [self notifyOwnersChanged];
[_typeToOwner removeAllObjects];
[_typeToData removeAllObjects];
@@ -93,7 +101,7 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
- (void) giveUpSelectionOwner {
_changeCount++;
- [self clearContents];
+ for (id owner in [_typeToOwner allValues])
+ [owner pasteboardChangedOwner: self];
+ [self notifyOwnersChanged];
[_typeToOwner release];
_typeToOwner = nil;
@@ -115,12 +117,47 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
@@ -115,12 +123,47 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
[super dealloc];
}
@@ -4220,7 +4584,7 @@ index 0e91a5d1..1e4cec1b 100644
}
+ (NSArray<NSString *> *) targetsForType: (NSPasteboardType) type {
@@ -177,7 +214,7 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
@@ -177,7 +220,7 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
}
- (NSData *) dataForType: (NSPasteboardType) type {
@@ -4229,7 +4593,7 @@ index 0e91a5d1..1e4cec1b 100644
if (_typeToOwner != nil) {
// We're the owner; just fetch the data directly.
id<NSPasteboardTypeOwner> owner = _typeToOwner[type];
@@ -185,25 +222,26 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
@@ -185,25 +228,26 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
return _typeToData[type];
}
@@ -4270,7 +4634,7 @@ index 0e91a5d1..1e4cec1b 100644
NSStringEncoding encoding = NSUnicodeStringEncoding;
if ([type isEqual: NSStringPboardType]) {
@@ -252,8 +290,37 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
@@ -252,8 +296,37 @@ static NSMutableDictionary<NSPasteboardName, X11Pasteboard *> *nameToPboard;
}
- (NSArray<NSPasteboardType> *) types {
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env python3
"""Build an owned AppKit check for the in-process X11 drag-and-drop loop."""
from pathlib import Path
import shlex
import subprocess
root = Path(__file__).resolve().parent.parent
folder = root / 'build/darling-src/build/src/external/cocotron/AppKit'
flags = {}
for line in (folder / 'CMakeFiles/X11_backend.dir/flags.make').read_text().splitlines():
if line.startswith('C_') and ' = ' in line:
key, value = line.split(' = ', 1)
flags[key] = shlex.split(value)
obj = root / 'build/riot-x11-drag-check.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O2', '-fblocks', '-Wno-macro-redefined', '-c', str(root / 'tools/riot-x11-drag-check.m'),
'-o', str(obj)], check=True)
subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib',
'-isysroot', '@DARLING_SDK@', str(obj), '-lSystem', '-lobjc',
'-framework', 'AppKit', '-framework', 'CoreFoundation', '-framework', 'Foundation',
'-o', str(root / 'build/riot-x11-drag-check')], check=True)
+276
View File
@@ -0,0 +1,276 @@
/* Standalone check for the in-process X11 drag loop (-[NSWindow _localDragImage:...]).
* Opens its own windows, drives -dragImage:at:offset:event:pasteboard:source:slideBack:
* with synthetic NSEvents posted to this app's own queue, and asserts the outcome.
* It injects no X11 input and touches no other client. A hang is reported as FAIL. */
#import <AppKit/AppKit.h>
#include <dispatch/dispatch.h>
#include <pthread.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#define CASE_TIMEOUT_SECONDS 20.0
@interface DragLog : NSObject {
@public
int entered, updated, exited, prepared, performed, concluded, ended, mouseDowns;
NSDragOperation endOperation;
BOOL removeWhenUpdated;
}
@end
@implementation DragLog @end
@interface NSView (DragCheckDefaults)
- (BOOL)prepareForDragOperation:(id<NSDraggingInfo>)sender;
@end
/* Destination without -prepareForDragOperation:, so the NSView default applies. */
@interface DropView : NSView { @public DragLog *log; }
@end
@implementation DropView
- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)s { log->entered++; return NSDragOperationCopy; }
- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)s { log->updated++; return NSDragOperationCopy; }
- (void)draggingExited:(id<NSDraggingInfo>)s { log->exited++; }
- (BOOL)performDragOperation:(id<NSDraggingInfo>)s { log->performed++; return YES; }
- (void)concludeDragOperation:(id<NSDraggingInfo>)s { log->concluded++; }
- (void)mouseDown:(NSEvent *)e { log->mouseDowns++; }
@end
/* Source that, like Chromium's, does not implement -pasteboardChangedOwner:. */
@interface DragSource : NSObject { @public DragLog *log; }
@end
@implementation DragSource
- (NSDragOperation)draggingSourceOperationMask { return NSDragOperationEvery; }
- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)local { return NSDragOperationEvery; }
- (void)draggedImage:(NSImage *)image endedAt:(NSPoint)p operation:(NSDragOperation)op {
log->ended++; log->endOperation = op;
}
@end
static const char *currentCase = "startup";
static volatile time_t caseStart;
static int failures = 0;
static void *watchdog(void *unused) {
for (;;) {
usleep(100000);
if (time(NULL) - caseStart > CASE_TIMEOUT_SECONDS) {
printf("FAIL %s: no progress for %.0fs (drag hang)\n", currentCase, CASE_TIMEOUT_SECONDS);
fflush(stdout);
_exit(1);
}
}
return NULL;
}
static void beginCase(const char *name) { currentCase = name; caseStart = time(NULL); }
static void expect(BOOL ok, const char *what) {
if (!ok) { failures++; printf(" FAIL %s: %s\n", currentCase, what); }
}
static void endCase(int failuresBefore) {
printf("%s %s\n", failures == failuresBefore ? "PASS" : "FAIL", currentCase);
fflush(stdout);
}
static void pump(double seconds) {
NSDate *end = [NSDate dateWithTimeIntervalSinceNow:seconds];
while ([end timeIntervalSinceNow] > 0) {
NSEvent *e = [NSApp nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode dequeue:YES];
if (e) [NSApp sendEvent:e];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.005]];
}
}
static NSWindow *makeWindow(NSString *title, NSRect frame) {
NSWindow *w = [[NSWindow alloc] initWithContentRect:frame styleMask:NSBorderlessWindowMask
backing:NSBackingStoreBuffered defer:NO];
[w setTitle:title];
[w setReleasedWhenClosed:NO];
[w makeKeyAndOrderFront:nil];
return w;
}
/* Destination window holding a registered DropView that only its superview owns. */
static NSWindow *makeDestination(DragLog *log, DropView **viewOut) {
NSWindow *w = makeWindow(@"Darling drag check destination", NSMakeRect(500, 200, 240, 240));
DropView *v = [[DropView alloc] initWithFrame:NSMakeRect(20, 20, 200, 200)];
v->log = log;
[v registerForDraggedTypes:[NSArray arrayWithObject:NSStringPboardType]];
[[w contentView] addSubview:v];
if (viewOut) *viewOut = v;
[v release];
return w;
}
static NSEvent *mouse(NSEventType type, NSWindow *w, NSPoint p) {
return [NSEvent mouseEventWithType:type location:p modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:[w windowNumber] context:nil eventNumber:0
clickCount:1 pressure:1.0];
}
static NSEvent *escapeKey(NSWindow *w) {
return [NSEvent keyEventWithType:NSKeyDown location:NSMakePoint(0, 0) modifierFlags:0
timestamp:[[NSProcessInfo processInfo] systemUptime]
windowNumber:[w windowNumber] context:nil characters:@"\033"
charactersIgnoringModifiers:@"\033" isARepeat:NO keyCode:53];
}
static void post(NSEvent *e) { [NSApp postEvent:e atStart:NO]; }
static NSPasteboard *declarePasteboard(id owner) {
NSPasteboard *pb = [NSPasteboard pasteboardWithName:NSDragPboard];
[pb declareTypes:[NSArray arrayWithObject:NSStringPboardType] owner:owner];
[pb setString:@"darling drag check" forType:NSStringPboardType];
return pb;
}
static void runDrag(NSWindow *sourceWindow, DragSource *source) {
NSPasteboard *pb = declarePasteboard(source);
NSImage *image = [[[NSImage alloc] initWithSize:NSMakeSize(32, 32)] autorelease];
NSEvent *down = mouse(NSLeftMouseDown, sourceWindow, NSMakePoint(50, 50));
[sourceWindow dragImage:image at:NSMakePoint(50, 50) offset:NSZeroSize event:down
pasteboard:pb source:source slideBack:YES];
}
static NSPoint inside = {120, 120}; /* inside DropView, in destination window base coordinates */
static NSPoint outside = {-4000, -4000};
int main(int argc, const char **argv) {
@autoreleasepool {
[NSApplication sharedApplication];
pthread_t thread;
beginCase("startup");
pthread_create(&thread, NULL, watchdog, NULL);
NSWindow *sourceWindow = makeWindow(@"Darling drag check source", NSMakeRect(100, 200, 240, 240));
pump(0.3);
int before;
/* 1. Drop onto a destination that lacks prepareForDragOperation:. */
{
beginCase("1 drop onto destination"); before = failures;
DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log;
DropView *view; NSWindow *dw = makeDestination(log, &view); pump(0.2);
expect([view prepareForDragOperation:nil] == YES, "NSView default prepareForDragOperation: is not YES");
NSWindow *dest = dw;
post(mouse(NSLeftMouseDragged, dest, inside));
post(mouse(NSLeftMouseDragged, dest, NSMakePoint(inside.x + 2, inside.y)));
post(mouse(NSLeftMouseUp, dest, inside));
runDrag(sourceWindow, src);
expect(log->entered == 1, "draggingEntered: not called exactly once");
expect(log->performed == 1, "performDragOperation: not called exactly once");
expect(log->concluded == 1, "concludeDragOperation: not called exactly once");
expect(log->ended == 1, "draggedImage:endedAt:operation: not called exactly once");
expect(log->endOperation != NSDragOperationNone, "end operation is None after a successful drop");
[dw close]; [dw release]; [src release]; [log release];
endCase(before);
}
/* 2. Drag leaves the destination and ends outside any window. */
{
beginCase("2 end outside destination"); before = failures;
DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log;
NSWindow *dw = makeDestination(log, NULL); pump(0.2);
post(mouse(NSLeftMouseDragged, dw, inside));
post(mouse(NSLeftMouseDragged, sourceWindow, outside));
post(mouse(NSLeftMouseUp, sourceWindow, outside));
runDrag(sourceWindow, src);
expect(log->entered == 1, "draggingEntered: not called exactly once");
expect(log->exited >= 1, "draggingExited: not called");
expect(log->performed == 0, "performDragOperation: called for a drop outside");
expect(log->ended == 1, "draggedImage:endedAt:operation: not called exactly once");
expect(log->endOperation == NSDragOperationNone, "end operation is not None");
[dw close]; [dw release]; [src release]; [log release];
endCase(before);
}
/* 3. Escape cancels while over the destination. */
{
beginCase("3 escape cancel"); before = failures;
DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log;
NSWindow *dw = makeDestination(log, NULL); pump(0.2);
post(mouse(NSLeftMouseDragged, dw, inside));
post(escapeKey(sourceWindow));
runDrag(sourceWindow, src);
expect(log->performed == 0, "performDragOperation: called after Escape");
expect(log->ended == 1, "draggedImage:endedAt:operation: not called exactly once");
expect(log->endOperation == NSDragOperationNone, "end operation is not None");
[dw close]; [dw release]; [src release]; [log release];
endCase(before);
}
/* 4. Two drags back to back with sources lacking pasteboardChangedOwner:. */
{
beginCase("4 back-to-back drags"); before = failures;
DragLog *log1 = [[DragLog alloc] init], *log2 = [[DragLog alloc] init];
DragSource *s1 = [[DragSource alloc] init], *s2 = [[DragSource alloc] init];
s1->log = log1; s2->log = log2;
expect(![s1 respondsToSelector:@selector(pasteboardChangedOwner:)], "fixture source implements pasteboardChangedOwner:");
NSWindow *dw = makeDestination(log1, NULL); pump(0.2);
post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside));
runDrag(sourceWindow, s1);
post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside));
runDrag(sourceWindow, s2); /* declaring new owner notifies s1, which must be tolerated */
expect(log1->ended == 1, "first drag did not end exactly once");
expect(log2->ended == 1, "second drag did not end exactly once");
expect(log1->performed == 2, "destination did not receive both drops");
[dw close]; [dw release]; [s1 release]; [s2 release]; [log1 release]; [log2 release];
endCase(before);
}
/* 5. Main queue and run loop timers are serviced during the drag. */
{
beginCase("5 main queue and timer serviced"); before = failures;
DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log;
NSWindow *dw = makeDestination(log, NULL); pump(0.2);
__block BOOL queueFired = NO, timerFired = NO, inDrag = YES;
__block BOOL queueDuring = NO, timerDuring = NO;
CFRunLoopTimerRef timer = CFRunLoopTimerCreateWithHandler(NULL, CFAbsoluteTimeGetCurrent() + 0.03, 0, 0, 0,
^(CFRunLoopTimerRef t) { timerFired = YES; timerDuring = inDrag; });
CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopDefaultMode);
dispatch_async(dispatch_get_main_queue(), ^{ queueFired = YES; queueDuring = inDrag; });
post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside));
runDrag(sourceWindow, src);
inDrag = NO;
expect(queueDuring, queueFired ? "dispatch_async block ran only after the drag returned" : "dispatch_async block never ran");
expect(timerDuring, timerFired ? "run loop timer fired only after the drag returned" : "run loop timer never fired");
CFRunLoopTimerInvalidate(timer); CFRelease(timer);
expect(log->ended == 1, "drag did not end exactly once");
[dw close]; [dw release]; [src release]; [log release];
endCase(before);
}
/* 6. Mouse input reaches views normally after the drags. */
{
beginCase("6 input after drags"); before = failures;
DragLog *log = [[DragLog alloc] init];
DropView *view; NSWindow *dw = makeDestination(log, &view); pump(0.2);
post(mouse(NSLeftMouseDown, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside));
pump(0.3);
expect(log->mouseDowns == 1, "mouse-down was not delivered to the view after drags");
[dw close]; [dw release]; [log release];
endCase(before);
}
/* 7. Destination view removed (and window closed) from a main-queue block mid-drag. */
{
beginCase("7 destination removed mid-drag"); before = failures;
DragLog *log = [[DragLog alloc] init]; DragSource *src = [[DragSource alloc] init]; src->log = log;
DropView *view; NSWindow *dw = makeDestination(log, &view); pump(0.2);
post(mouse(NSLeftMouseDragged, dw, inside)); post(mouse(NSLeftMouseUp, dw, inside));
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 20 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{
[view removeFromSuperview]; /* the drag loop must have retained it */
[dw orderOut:nil];
[dw close];
});
runDrag(sourceWindow, src);
expect(log->ended == 1, "drag did not end exactly once after the destination went away");
pump(0.2);
[dw release]; [src release]; [log release];
endCase(before);
}
[sourceWindow close]; [sourceWindow release];
printf("%s x11 drag check (%d failure%s)\n", failures ? "FAIL" : "PASS", failures, failures == 1 ? "" : "s");
return failures ? 1 : 0;
}
}