diff --git a/Switch/SWAccessibilityService.m b/Switch/SWAccessibilityService.m index 665934e0..7180f849 100644 --- a/Switch/SWAccessibilityService.m +++ b/Switch/SWAccessibilityService.m @@ -43,7 +43,7 @@ - (instancetype)init; { BailUnless(self = [super init], nil); - _haxQueue = dispatch_queue_create("foo", DISPATCH_QUEUE_SERIAL); + _haxQueue = dispatch_queue_create("haxQueue", DISPATCH_QUEUE_SERIAL); return self; } diff --git a/Switch/SWCoreWindowService.m b/Switch/SWCoreWindowService.m index e6926263..d685b043 100644 --- a/Switch/SWCoreWindowService.m +++ b/Switch/SWCoreWindowService.m @@ -65,11 +65,6 @@ - (instancetype)init; [self.stateMachine incrementWithInvoke:false direction:SWIncrementDirectionDecreasing isRepeating:true]; }]; - // update window group UI state with the state machine's updates of window groups - [RACObserve(self, stateMachine.windowList) subscribeNext:^(NSOrderedSet *windowList) { - @strongify(self); - [self.interface updateWindowList:windowList]; - }]; // update the selected window group with the state machine's update of selector.selectedWindow [RACObserve(self, stateMachine.selectedWindow) subscribeNext:^(SWWindow *window) { @strongify(self); @@ -257,6 +252,11 @@ - (void)stopService; - (oneway void)windowListService:(SWWindowListService *)service updatedList:(NSOrderedSet *)windows; { [self.stateMachine updateWindowList:windows]; + + // Do not update the interface if the state machine has an action pending. + if (!self.stateMachine.pendingSwitch) { + [self.interface updateWindowList:self.stateMachine.windowList]; + } } #pragma mark - SWStateMachineDelegate @@ -278,22 +278,17 @@ - (void)stateMachine:(SWStateMachine *)stateMachine wantsWindowRaised:(SWWindow if (!selectedWindow) { return; } + + NSUInteger selectedIndex = [self.stateMachine.windowList indexOfObject:self.stateMachine.selectedWindow]; + if (selectedIndex == 0 && [((SWWindow *)self.stateMachine.windowList.firstObject).application isActiveApplication]) { + [self.stateMachine cancelInvocation]; + return; + } + [self.interface disableWindow:selectedWindow]; - @weakify(self); - [[SWAccessibilityService sharedService] raiseWindow:selectedWindow completion:^(NSError *error) { - @strongify(self); - if (error) { - SWLog(@"Failed to raise window group %@: %@", selectedWindow, error); - } else if (self.stateMachine.pendingSwitch) { - // Sending a cancel invocation here is to work around #105 - [self.stateMachine cancelInvocation]; - Check(!self.stateMachine.wantsInterfaceVisible); - } - - [self.interface enableWindow:selectedWindow]; - }]; + [self private_raiseWindow]; } - (void)stateMachine:(SWStateMachine *)stateMachine wantsWindowClosed:(SWWindow *)window; @@ -404,4 +399,40 @@ - (void)private_hideInterface; [self.interface shouldShowInterface:false]; } +- (void)private_raiseWindow; +{ + SWWindow *selectedWindow = self.stateMachine.selectedWindow; + if (!selectedWindow) { return; } + + @weakify(self); + [[SWAccessibilityService sharedService] raiseWindow:selectedWindow completion:^(NSError *error) { + @strongify(self); + if (error) { + SWLog(@"Failed to raise window group %@: %@", selectedWindow, error); + } + + NSOrderedSet *windowList = self.stateMachine.windowList; + NSUInteger selectedIndex = [windowList indexOfObject:selectedWindow]; + + if (selectedIndex == 0) { + // If the selected index was 0, this action won't change the window order so this code must replay the last update event. + [self.stateMachine updateWindowList:self.stateMachine.windowList]; + } else { + // Otherwise, force a window list reload. + [[SWWindowListService sharedService] refreshWindowList]; + } + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + @strongify(self); + if (self.stateMachine.pendingSwitch) { + NSLog(@"Attempt to raise window failed, trying again..."); + [self private_raiseWindow]; + } else { + SWWindow *window = self.stateMachine.selectedWindow; + [self.interface enableWindow:window]; + } + }); + }]; +} + @end diff --git a/Switch/SWSelector.m b/Switch/SWSelector.m index 411ddc76..026d9f6f 100644 --- a/Switch/SWSelector.m +++ b/Switch/SWSelector.m @@ -13,6 +13,7 @@ // #import "SWSelector.h" +#import "SWWindow.h" @implementation SWSelector @@ -140,6 +141,7 @@ - (instancetype)updateWithWindowList:(NSOrderedSet *)windowList; { // Carry over the current selected index by default. NSInteger newSelectedIndex = self.selectedIndex; + NSUInteger selectedWindowIndex; if (windowList && !windowList.count) { // Empty set? Selected window not found. @@ -149,9 +151,9 @@ - (instancetype)updateWithWindowList:(NSOrderedSet *)windowList; } else if (self.windowList.count == 0) { // Previously empty list? Select the beginning. newSelectedIndex = 0; - } else if ([windowList containsObject:self.selectedWindow]) { + } else if ((selectedWindowIndex = [self private_indexOfWindow:self.selectedWindow inWindowList:windowList]) != NSNotFound) { // Select the same window group as was previously selected, if it's still there. - newSelectedIndex = (NSInteger)[windowList indexOfObject:self.selectedWindow]; + newSelectedIndex = (NSInteger)selectedWindowIndex; } else if ((NSInteger)windowList.count <= newSelectedIndex) { // Clamp the selected index to the end of the window list. newSelectedIndex = (NSInteger)windowList.count - 1; @@ -160,4 +162,23 @@ - (instancetype)updateWithWindowList:(NSOrderedSet *)windowList; return [[[self class] alloc] initWithWindowList:windowList selectedIndex:newSelectedIndex]; } +- (NSUInteger)private_indexOfWindow:(id)selectedWindow inWindowList:(NSOrderedSet *)windowList; +{ + if (selectedWindow == nil) { + return NSNotFound; + } + if (windowList == nil) { + return NSNotFound; + } + + // Most of the unit tests for this class just shove NSNumbers into the collections instead of SWWindow objects + if (![selectedWindow respondsToSelector:NNTypedSelector1(SWWindow, isSameWindow:)]) { + return [windowList indexOfObject:selectedWindow]; + } + + return [windowList indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { + return [selectedWindow isSameWindow:obj]; + }]; +} + @end diff --git a/Switch/SWStateMachine.m b/Switch/SWStateMachine.m index 04192d46..1f93ea47 100644 --- a/Switch/SWStateMachine.m +++ b/Switch/SWStateMachine.m @@ -160,6 +160,7 @@ - (void)setActive:(_Bool)active; } self.selector = nil; + self.pendingSwitch = false; } } @@ -182,8 +183,10 @@ - (void)setInvoked:(_Bool)invoked; - (void)displayTimerCompleted; { - StateLog(@"State machine display timer completed"); - self.displayTimer = false; + if (!self.pendingSwitch) { + StateLog(@"State machine display timer completed"); + self.displayTimer = false; + } } #pragma mark - Keyboard interactions @@ -229,9 +232,6 @@ - (void)cancelInvocation; { StateLog(@"State machine event cancel invocation"); - // Setting pendingSwitch here is to work around #105 - self.pendingSwitch = false; - if (self.invoked) { self.invoked = false; } diff --git a/Switch/SWWindow.h b/Switch/SWWindow.h index bd30835d..87bc2deb 100644 --- a/Switch/SWWindow.h +++ b/Switch/SWWindow.h @@ -34,6 +34,7 @@ typedef NSPoint NNVec2; - (NSScreen *)screen; - (CGWindowID)windowID; +- (BOOL)isSameWindow:(SWWindow *)window; - (BOOL)isRelatedToLowerWindow:(SWWindow *)window; - (NNVec2)offsetOfCenterToCenterOfWindow:(SWWindow *)window; - (NSSize)sizeDifferenceFromWindow:(SWWindow *)window; diff --git a/Switch/SWWindow.m b/Switch/SWWindow.m index 3d927a65..0da306a8 100644 --- a/Switch/SWWindow.m +++ b/Switch/SWWindow.m @@ -63,6 +63,19 @@ - (BOOL)isEqual:(id)object; return ([object isKindOfClass:[self class]] && [[self windowDescription] isEqual:[object windowDescription]]); } +- (BOOL)isSameWindow:(SWWindow *)window; +{ + if (window.windowID != self.windowID) { + return NO; + } + + if (![window.application.name isEqualToString:self.application.name]) { + return NO; + } + + return YES; +} + - (NSString *)description; { return [NSString stringWithFormat:@"%p <%u (%@)>", self, self.windowID, self.name]; diff --git a/Switch/SWWindowContentsService.m b/Switch/SWWindowContentsService.m index 134027b5..cdf7ea94 100644 --- a/Switch/SWWindowContentsService.m +++ b/Switch/SWWindowContentsService.m @@ -62,7 +62,7 @@ - (id)init; BailUnless(self = [super init], nil); _contentContainers = [NSMutableDictionary new]; - _queue = dispatch_queue_create([[NSString stringWithFormat:@""] UTF8String], DISPATCH_QUEUE_SERIAL); + _queue = dispatch_queue_create([[NSString stringWithFormat:@"SWWindowContentsService"] UTF8String], DISPATCH_QUEUE_SERIAL); [[NSNotificationCenter defaultCenter] addWeakObserver:self selector:NNSelfSelector1(private_windowUpdateNotification:) name:[SWWindowWorker notificationName] object:nil]; @@ -90,6 +90,8 @@ - (void)startService; { [super startService]; + self->_contentContainers = [NSMutableDictionary new]; + NSOrderedSet *windows = [SWWindowListService sharedService].windows; if (windows) { [self windowListService:nil updatedList:windows]; @@ -102,6 +104,7 @@ - (void)stopService; { dispatch_async(self.queue, ^{ [self.contentContainers removeAllObjects]; + self->_contentContainers = nil; }); [[NNServiceManager sharedManager] removeObserver:self forService:[SWWindowListService class]]; @@ -155,6 +158,11 @@ - (oneway void)windowListService:(SWWindowListService *)service updatedList:(NSO }); } +- (oneway void)windowListServiceStopped:(SWWindowListService *)service; +{ + [self windowListService:nil updatedList:[NSOrderedSet orderedSet]]; +} + #pragma mark - Internal - (void)private_windowUpdateNotification:(NSNotification *)notification; diff --git a/Switch/SWWindowListService.h b/Switch/SWWindowListService.h index 598c352c..6dfe020f 100644 --- a/Switch/SWWindowListService.h +++ b/Switch/SWWindowListService.h @@ -33,4 +33,6 @@ @property (nonatomic, copy, readonly) NSOrderedSet *windows; +- (void)refreshWindowList; + @end diff --git a/Switch/SWWindowListService.m b/Switch/SWWindowListService.m index 5f4c9230..58dc58c1 100644 --- a/Switch/SWWindowListService.m +++ b/Switch/SWWindowListService.m @@ -213,6 +213,11 @@ + (NSOrderedSet *)sortedWindowGroups:(NSOrderedSet *)windowGroups; return result; } +- (void)refreshWindowList; +{ + [self.worker refreshWindowList]; +} + #pragma mark - Internal - (void)private_workerUpdatedWindowList:(NSNotification *)notification; @@ -233,7 +238,6 @@ - (void)private_updateWindowList:(NSArray *)windowInfoList; NSOrderedSet *windowGroupList = [[self class] filterWindowObjectsToWindowGroups:windowObjectList]; NSOrderedSet *sortedWindowGroupList = [[self class] sortedWindowGroups:windowGroupList]; - // This probably needs to change for #105 to get fixed properly as well—there are two views of equality, window order and window order and activation. if (![self.windows isEqualToOrderedSet:sortedWindowGroupList]) { self.windows = sortedWindowGroupList; [(id)self.subscriberDispatcher windowListService:self updatedList:self.windows]; diff --git a/Switch/SWWindowListWorker.h b/Switch/SWWindowListWorker.h index ec5a3714..5c49305a 100644 --- a/Switch/SWWindowListWorker.h +++ b/Switch/SWWindowListWorker.h @@ -14,4 +14,6 @@ @interface SWWindowListWorker : NNPollingObject +- (void)refreshWindowList; + @end diff --git a/Switch/SWWindowListWorker.m b/Switch/SWWindowListWorker.m index 0008e653..3899cfdd 100644 --- a/Switch/SWWindowListWorker.m +++ b/Switch/SWWindowListWorker.m @@ -43,6 +43,13 @@ - (instancetype)init; #pragma mark - NNPollingObject - (oneway void)main; +{ + [self refreshWindowList]; +} + +#pragma mark - SWWindowListWorker + +- (void)refreshWindowList; { CFArrayRef cgWindowInfoList = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements, kCGNullWindowID); NSArray *windowInfoList = CFBridgingRelease(cgWindowInfoList); diff --git a/Switch/SWWindowWorker.m b/Switch/SWWindowWorker.m index d26d6b74..87ab587b 100644 --- a/Switch/SWWindowWorker.m +++ b/Switch/SWWindowWorker.m @@ -28,6 +28,7 @@ @interface SWWindowWorker () @property (nonatomic, copy, readonly) SWWindow *window; +@property (nonatomic, assign) _Bool firstUpdate; @property (nonatomic, strong) NSImage *previousCapture; @end @@ -43,7 +44,9 @@ - (instancetype)initWithModelObject:(SWWindow *)window; if (!(self = [super initWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)])) { return nil; } _window = window; - self.interval = NNPollingIntervalFast; + self.interval = NNPollingIntervalSlow; + + _firstUpdate = true; return self; } @@ -85,7 +88,12 @@ - (oneway void)main; if (!imageChanged) { self.interval = MIN(NNPollingIntervalSlow, self.interval * 2.0); } else { - self.interval = NNPollingIntervalFast; + if (self.firstUpdate) { + self.interval = NNPollingIntervalSlow; + } else { + self.interval = NNPollingIntervalFast; + } + self.previousCapture = image; [self postNotification:@{ @@ -100,6 +108,10 @@ - (oneway void)main; // Window does not exist. Stop the worker loop. self.interval = -1.0; } + + if (self.firstUpdate) { + self.firstUpdate = false; + } } #pragma mark - SWWindowWorker diff --git a/SwitchTests/SWStateMachineTests.m b/SwitchTests/SWStateMachineTests.m index 5ba9bf87..ae23578d 100644 --- a/SwitchTests/SWStateMachineTests.m +++ b/SwitchTests/SWStateMachineTests.m @@ -768,13 +768,13 @@ - (void)stateMachineInRaiseStateMonkey:(unsigned)iterations; ^{ // -> windowlist (with selectedwindow not first/active) -> NSMutableOrderedSet *windowList = rwgs(); + // If selectedWindow is firstWindow, make inactiveWindow and updateWindows if ([[windowList objectAtIndex:0] isEqual:self.stateMachineUnderTest.selectedWindow]) { SWWindow *tmp = windowList[0]; [windowList removeObjectAtIndex:0]; ((SWTestApplication *)tmp.application).active = false; [windowList insertObject:tmp atIndex:0]; } - // If selectedWindow is firstWindow, make inactiveWindow and updateWindows XCTAssertNoThrow([self.stateMachineUnderTest updateWindowList:windowList]); }, ^{ @@ -1274,14 +1274,7 @@ - (void)testInvokeWindowListKeyReleasedTimer; [self _keyReleased]; }]; - // Remember: if (!showingUI) expectWantsDisplay - if (!self.stateMachineUnderTest.interfaceVisible) { - [self stateMachineExpectShowInterface:true block:^{ - [self _timer]; - }]; - } else { - [self _timer]; - } + [self _timer]; } // invoke: -> keyEventModifierReleased -> windowlist -> raise -> timer -> wantsDisplay -> YYY @@ -1294,14 +1287,7 @@ - (void)testInvokeKeyReleasedWindowListTimer; [self _windowList:nil]; }]; - // Remember: if (!showingUI) expectWantsDisplay - if (!self.stateMachineUnderTest.interfaceVisible) { - [self stateMachineExpectShowInterface:true block:^{ - [self _timer]; - }]; - } else { - [self _timer]; - } + [self _timer]; } // invoke: -> keyEventModifierReleased -> timer -> windowlist -> wantsDisplay/raise -> YYY @@ -1314,14 +1300,7 @@ - (void)testInvokeKeyReleasedTimerWindowList; [self _timer]; [self stateMachineExpectRaise:^{ - // Remember: if (!showingUI) expectWantsDisplay - if (!self.stateMachineUnderTest.interfaceVisible) { - [self stateMachineExpectShowInterface:true block:^{ - [self _windowList:nil]; - }]; - } else { - [self _windowList:nil]; - } + [self _windowList:nil]; }]; }