CI/CD / preconditions (pull_request) Successful in 2s
Native JavaScript checks / Native launcher and release tests (pull_request) Successful in 1m4s
macOS checks / Native Rust tests and unsigned build (pull_request) Successful in 1m31s
CI/CD / test_and_check_app (pull_request) Successful in 6m44s
CI/CD / Publish immutable image (pull_request) Skipped
103 lines
4.5 KiB
Objective-C
103 lines
4.5 KiB
Objective-C
#import <AppKit/AppKit.h>
|
|
#import <WebKit/WebKit.h>
|
|
#import <objc/runtime.h>
|
|
|
|
// Keep AppKit's system swipe preferences and cancellation thresholds, but use
|
|
// script history traversal so asynchronously created Inertia entries aren't skipped.
|
|
@interface KanbanNavigation : NSObject
|
|
@property(nonatomic, weak) WKWebView *webview;
|
|
@property(nonatomic, strong) id monitor;
|
|
@property(nonatomic) BOOL pending;
|
|
@property(nonatomic) BOOL tracking;
|
|
@property(nonatomic) BOOL blocked;
|
|
@property(nonatomic) NSUInteger sequence;
|
|
@end
|
|
|
|
@implementation KanbanNavigation
|
|
- (BOOL)swipeTrackingEnabled {
|
|
return NSEvent.isSwipeTrackingFromScrollEventsEnabled;
|
|
}
|
|
|
|
- (void)dealloc {
|
|
if (_monitor) [NSEvent removeMonitor:_monitor];
|
|
}
|
|
|
|
- (NSEvent *)handleEvent:(NSEvent *)event {
|
|
WKWebView *webview = self.webview;
|
|
if (!webview || event.window != webview.window || !webview.window.isKeyWindow ||
|
|
webview.hidden || ![self swipeTrackingEnabled]) return event;
|
|
if (event.phase == NSEventPhaseBegan) {
|
|
self.sequence++;
|
|
self.blocked = NO;
|
|
self.pending = NO;
|
|
self.tracking = NO;
|
|
}
|
|
if (self.tracking) return nil;
|
|
if (event.phase == NSEventPhaseEnded || event.phase == NSEventPhaseCancelled) {
|
|
self.sequence++;
|
|
self.pending = NO;
|
|
return event;
|
|
}
|
|
if (self.blocked || self.pending || event.momentumPhase != NSEventPhaseNone ||
|
|
!(event.phase & (NSEventPhaseBegan | NSEventPhaseChanged)) || !event.hasPreciseScrollingDeltas) return event;
|
|
if (fabs(event.scrollingDeltaX) <= fabs(event.scrollingDeltaY)) {
|
|
if (fabs(event.scrollingDeltaY) > 0) self.blocked = YES;
|
|
return event;
|
|
}
|
|
NSPoint point = [webview convertPoint:event.locationInWindow fromView:nil];
|
|
if (!NSPointInRect(point, webview.bounds)) return event;
|
|
if (!webview.isFlipped) point.y = webview.bounds.size.height - point.y;
|
|
NSString *script = [NSString stringWithFormat:@"window.__kanbanNavigation?.canSwipe(%f, %f) === true", point.x, point.y];
|
|
self.pending = YES;
|
|
NSUInteger sequence = self.sequence;
|
|
NSURL *page = webview.URL;
|
|
__weak KanbanNavigation *weakSelf = self;
|
|
[webview evaluateJavaScript:script completionHandler:^(id allowed, NSError *error) {
|
|
KanbanNavigation *owner = weakSelf;
|
|
if (!owner || owner.sequence != sequence) return;
|
|
owner.pending = NO;
|
|
owner.blocked = YES;
|
|
if (error || ![allowed isEqual:@YES] || ![webview.URL isEqual:page] ||
|
|
(!webview.canGoBack && !webview.canGoForward)) return;
|
|
owner.tracking = YES;
|
|
[event trackSwipeEventWithOptions:NSEventSwipeTrackingLockDirection | NSEventSwipeTrackingClampGestureAmount
|
|
dampenAmountThresholdMin:webview.canGoForward ? -1 : 0
|
|
max:webview.canGoBack ? 1 : 0
|
|
usingHandler:^(CGFloat amount, NSEventPhase phase, BOOL complete, BOOL *stop) {
|
|
KanbanNavigation *current = weakSelf;
|
|
if (current.sequence != sequence) {
|
|
*stop = YES;
|
|
return;
|
|
}
|
|
if (!current || !webview.window.isKeyWindow || ![webview.URL isEqual:page]) {
|
|
current.tracking = NO;
|
|
*stop = YES;
|
|
return;
|
|
}
|
|
if (!complete) return;
|
|
current.tracking = NO;
|
|
if (phase == NSEventPhaseCancelled) return;
|
|
NSInteger direction = amount >= 1 ? -1 : amount <= -1 ? 1 : 0;
|
|
if ((direction == -1 && webview.canGoBack) || (direction == 1 && webview.canGoForward)) {
|
|
NSString *navigate = [NSString stringWithFormat:@"if (%@) window.__kanbanNavigation.step(%ld)", script, (long)direction];
|
|
[webview evaluateJavaScript:navigate completionHandler:nil];
|
|
}
|
|
}];
|
|
}];
|
|
return event;
|
|
}
|
|
@end
|
|
|
|
void install_workspace_navigation(void *pointer) {
|
|
WKWebView *webview = (__bridge WKWebView *)pointer;
|
|
webview.allowsBackForwardNavigationGestures = NO;
|
|
KanbanNavigation *navigation = [KanbanNavigation new];
|
|
navigation.webview = webview;
|
|
__weak KanbanNavigation *weakNavigation = navigation;
|
|
navigation.monitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskScrollWheel handler:^NSEvent *(NSEvent *event) {
|
|
KanbanNavigation *owner = weakNavigation;
|
|
return owner ? [owner handleEvent:event] : event;
|
|
}];
|
|
objc_setAssociatedObject(webview, @selector(handleEvent:), navigation, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
}
|