diff --git a/.gitignore b/.gitignore index 86de6aa..a8daa35 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,5 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ +.DS_Store +*.dmg diff --git a/FastFinder.xcodeproj/project.pbxproj b/FastFinder.xcodeproj/project.pbxproj index 85e9a91..a80b8dd 100644 --- a/FastFinder.xcodeproj/project.pbxproj +++ b/FastFinder.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 56; objects = { /* Begin PBXBuildFile section */ @@ -177,14 +177,14 @@ 8ED3D89321B1B7420059AFF9 /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1010; + LastUpgradeCheck = 1600; ORGANIZATIONNAME = "Anthonin Cocagne"; TargetAttributes = { 8ED3D89A21B1B7420059AFF9 = { CreatedOnToolsVersion = 10.1; SystemCapabilities = { com.apple.HardenedRuntime = { - enabled = 0; + enabled = 1; }; com.apple.Sandbox = { enabled = 0; @@ -194,7 +194,7 @@ }; }; buildConfigurationList = 8ED3D89621B1B7420059AFF9 /* Build configuration list for PBXProject "FastFinder" */; - compatibilityVersion = "Xcode 9.3"; + compatibilityVersion = "Xcode 14.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -247,17 +247,16 @@ buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${SRCROOT}/Pods/Target Support Files/Pods-FastFinder/Pods-FastFinder-resources.sh", - "${PODS_CONFIGURATION_BUILD_DIR}/MASShortcut/MASShortcut.bundle", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FastFinder/Pods-FastFinder-resources-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Copy Pods Resources"; - outputPaths = ( - "${TARGET_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH}/MASShortcut.bundle", + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-FastFinder/Pods-FastFinder-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-FastFinder/Pods-FastFinder-resources.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-FastFinder/Pods-FastFinder-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -296,7 +295,7 @@ ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; @@ -322,7 +321,7 @@ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; - CODE_SIGN_IDENTITY = "Mac Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; @@ -341,7 +340,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -394,7 +393,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; + MACOSX_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; @@ -405,16 +404,18 @@ isa = XCBuildConfiguration; baseConfigurationReference = 3D09F07E1DAAECD214DAA955 /* Pods-FastFinder.debug.xcconfig */; buildSettings = { + ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = FastFinder/FastFinder.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6A546753D6; + DEVELOPMENT_TEAM = A63B2PPG38; INFOPLIST_FILE = FastFinder/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = com.acocagne.FastFinder; PRODUCT_NAME = "$(TARGET_NAME)"; }; @@ -424,16 +425,18 @@ isa = XCBuildConfiguration; baseConfigurationReference = 04FFAE9FB767C6D853FBDCBC /* Pods-FastFinder.release.xcconfig */; buildSettings = { + ARCHS = arm64; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_ENTITLEMENTS = FastFinder/FastFinder.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - DEVELOPMENT_TEAM = 6A546753D6; + DEVELOPMENT_TEAM = A63B2PPG38; INFOPLIST_FILE = FastFinder/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); - MACOSX_DEPLOYMENT_TARGET = 10.10; + MACOSX_DEPLOYMENT_TARGET = 13.0; PRODUCT_BUNDLE_IDENTIFIER = com.acocagne.FastFinder; PRODUCT_NAME = "$(TARGET_NAME)"; }; diff --git a/FastFinder/AppDelegate.m b/FastFinder/AppDelegate.m index 12f33c1..6693308 100644 --- a/FastFinder/AppDelegate.m +++ b/FastFinder/AppDelegate.m @@ -9,7 +9,6 @@ #import "AppDelegate.h" #import #import "FinderLogicHelper.h" -#import @interface AppDelegate () @@ -19,25 +18,94 @@ @implementation AppDelegate - (void)applicationDidFinishLaunching:(NSNotification *)aNotification { //no dock icon - [NSApp windows][0].canHide = NO; + NSWindow *mainWindow = [NSApp windows].firstObject; + if (mainWindow) { + mainWindow.canHide = NO; + } [NSApp setActivationPolicy: NSApplicationActivationPolicyAccessory]; - [[MASShortcutBinder sharedBinder] - bindShortcutWithDefaultsKey:kPreferenceGlobalShortcut - toAction:^{ - [[FinderLogicHelper getInstance] processFinderLogic]; - }]; + [self requestAccessibilityAccess]; + + [self registerGlobalShortcut]; +} + +- (void)registerGlobalShortcut { + [[NSUserDefaults standardUserDefaults] addObserver:self + forKeyPath:kPreferenceGlobalShortcut + options:NSKeyValueObservingOptionNew + context:NULL]; + [self updateGlobalShortcut]; } +- (void)observeValueForKeyPath:(NSString *)keyPath + ofObject:(id)object + change:(NSDictionary *)change + context:(void *)context { + if ([keyPath isEqualToString:kPreferenceGlobalShortcut]) { + [self updateGlobalShortcut]; + } else { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (void)updateGlobalShortcut { + id shortcutData = [[NSUserDefaults standardUserDefaults] objectForKey:kPreferenceGlobalShortcut]; + NSLog(@"[AppDelegate] Stored shortcut data: %@", shortcutData); + + MASShortcut *shortcut = nil; + if ([shortcutData isKindOfClass:[NSData class]]) { + @try { + NSError *error = nil; + shortcut = [NSKeyedUnarchiver unarchivedObjectOfClass:[MASShortcut class] + fromData:shortcutData + error:&error]; + if (error || !shortcut) { + NSLog(@"[AppDelegate] Secure unarchiving failed: %@", error); + // Fallback to legacy unarchiving + #pragma clang diagnostic push + #pragma clang diagnostic ignored "-Wdeprecated-declarations" + shortcut = [NSKeyedUnarchiver unarchiveObjectWithData:shortcutData]; + #pragma clang diagnostic pop + } + } @catch (NSException *exception) { + NSLog(@"[AppDelegate] Exception unarchiving shortcut: %@", exception); + } + } + + NSLog(@"[AppDelegate] Resolved shortcut: %@", shortcut); + + [[MASShortcutMonitor sharedMonitor] unregisterAllShortcuts]; + + if (shortcut) { + BOOL registered = [[MASShortcutMonitor sharedMonitor] registerShortcut:shortcut withAction:^{ + NSLog(@"[AppDelegate] Global shortcut triggered!"); + [[FinderLogicHelper getInstance] processFinderLogic]; + }]; + NSLog(@"[AppDelegate] MASShortcutMonitor registerShortcut result: %d", registered); + } +} + +- (void)dealloc { + @try { + [[NSUserDefaults standardUserDefaults] removeObserver:self forKeyPath:kPreferenceGlobalShortcut]; + } @catch (NSException *exception) {} +} + +- (void)requestAccessibilityAccess { + NSDictionary *options = @{(__bridge id)kAXTrustedCheckOptionPrompt: @YES}; + AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); +} - (void)applicationWillTerminate:(NSNotification *)aNotification { // Insert code here to tear down your application } - - (void)applicationDidBecomeActive:(NSNotification *)notification { //to reopen settings window - [[NSApp windows][0] makeKeyAndOrderFront:self]; + NSWindow *window = [NSApp windows].firstObject; + if (window) { + [window makeKeyAndOrderFront:self]; + } } @end diff --git a/FastFinder/Base.lproj/Main.storyboard b/FastFinder/Base.lproj/Main.storyboard index abc75b2..ff5ccb2 100644 --- a/FastFinder/Base.lproj/Main.storyboard +++ b/FastFinder/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -620,7 +620,7 @@ - + @@ -714,7 +714,7 @@ - + @@ -723,7 +723,7 @@ - + @@ -747,7 +747,7 @@ - + @@ -756,7 +756,7 @@ - + @@ -765,7 +765,7 @@ - + @@ -793,7 +793,7 @@ - + @@ -802,7 +802,7 @@ - + @@ -833,7 +833,7 @@ - + @@ -866,7 +866,7 @@ from Spotlight or your Application folder - + @@ -878,7 +878,7 @@ from Spotlight or your Application folder - + @@ -907,7 +907,7 @@ from Spotlight or your Application folder - + @@ -921,7 +921,7 @@ Animation is currently experimental and can cause quick glitch do to how MacOS h - + @@ -930,7 +930,7 @@ Animation is currently experimental and can cause quick glitch do to how MacOS h - + @@ -939,7 +939,7 @@ Animation is currently experimental and can cause quick glitch do to how MacOS h - + @@ -961,6 +961,6 @@ Animation is currently experimental and can cause quick glitch do to how MacOS h - + diff --git a/FastFinder/FastFinder.entitlements b/FastFinder/FastFinder.entitlements index 0c67376..49ad0bb 100644 --- a/FastFinder/FastFinder.entitlements +++ b/FastFinder/FastFinder.entitlements @@ -1,5 +1,8 @@ - + + com.apple.security.automation.apple-events + + diff --git a/FastFinder/Helper/FinderLogicHelper.h b/FastFinder/Helper/FinderLogicHelper.h index 80b8432..7385468 100644 --- a/FastFinder/Helper/FinderLogicHelper.h +++ b/FastFinder/Helper/FinderLogicHelper.h @@ -12,8 +12,8 @@ NS_ASSUME_NONNULL_BEGIN @interface FinderLogicHelper : NSObject -+(FinderLogicHelper*)getInstance; --(void) processFinderLogic; ++ (FinderLogicHelper*)getInstance; +- (void) processFinderLogic; @end diff --git a/FastFinder/Helper/FinderLogicHelper.m b/FastFinder/Helper/FinderLogicHelper.m index 1b4ecfc..83debf1 100644 --- a/FastFinder/Helper/FinderLogicHelper.m +++ b/FastFinder/Helper/FinderLogicHelper.m @@ -7,247 +7,424 @@ // #import "FinderLogicHelper.h" - -#import -#import "Finder.h" -#import "SystemEvents.h" - +#import #import "UserSettingsHelper.h" -//CONFIGURATION - -//for first launch static CGFloat const DEFAULT_FINDER_HEIGHT = 550; +@interface FinderLogicHelper () +@property (strong, nullable) NSRunningApplication *previousActiveApp; +@end @implementation FinderLogicHelper { - FinderApplication * finder; - CGFloat screenHeight; CGFloat screenWidth; - - NSInteger idOfMainVisorFinderWindow; - NSInteger idOfVisorFinderWindow; CGFloat lastFinderHeight; } - static FinderLogicHelper *instance = nil; +static dispatch_once_t onceToken; -+(FinderLogicHelper*)getInstance { - @synchronized(self) { - if(instance==nil) { - instance = [FinderLogicHelper new]; - - //defaults values - CGRect screenRect = [[NSScreen mainScreen] frame]; - instance->screenWidth = screenRect.size.width; - instance->screenHeight = screenRect.size.height; - } - } ++ (FinderLogicHelper*)getInstance { + dispatch_once(&onceToken, ^{ + instance = [FinderLogicHelper new]; + [instance updateScreenDimensions]; + [instance observeScreenChanges]; + }); return instance; } --(void) processFinderLogic { - finder = [SBApplication applicationWithBundleIdentifier:@"com.apple.Finder"]; -// SystemEventsApplication * systEvt = [SBApplication applicationWithBundleIdentifier:@"com.apple.SystemEvents"]; //to send shortcuts to the app (https://github.com/tcurdt/shellhere/blob/master/main.m#L114) - - - NSLog(@"Frontmost : %hhd | Visible : %hhd", finder.frontmost, finder.visible); - - [self show:!finder.frontmost finder:finder]; +- (void)updateScreenDimensions { + CGRect screenRect = [[NSScreen mainScreen] frame]; + screenWidth = screenRect.size.width; + screenHeight = screenRect.size.height; +} + +- (void)observeScreenChanges { + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(screenParametersDidChange:) + name:NSApplicationDidChangeScreenParametersNotification + object:nil]; } +- (void)screenParametersDidChange:(NSNotification *)notification { + [self updateScreenDimensions]; +} --(void) show:(BOOL)show finder:(FinderApplication*)finder { - SBElementArray * finderWindows = finder.FinderWindows; - FinderWindow *finderWindow; +- (void)processFinderLogic { + NSLog(@"[FinderLogicHelper] processFinderLogic called"); + NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"]; + NSRunningApplication *finderApp = apps.firstObject; + if (!finderApp) { + NSLog(@"[FinderLogicHelper] Finder application not found"); + return; + } -// NSLog(@"finderHeight : %f", finderHeight); + // Save the previously active application before we activate Finder + NSRunningApplication *frontApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *ownBundleId = [[NSBundle mainBundle] bundleIdentifier]; + if (frontApp && + ![frontApp.bundleIdentifier isEqualToString:@"com.apple.finder"] && + ![frontApp.bundleIdentifier isEqualToString:ownBundleId]) { + self.previousActiveApp = frontApp; + NSLog(@"[FinderLogicHelper] Saved previous active app: %@", frontApp.bundleIdentifier); + } - BOOL animated = [UserSettingsHelper getInstance].animated; + // Check if Finder is the frontmost application AND the visor window is currently active/on-screen + AXUIElementRef visor = [self copyVisorWindowElement]; + BOOL isVisorOnScreen = NO; + if (visor) { + CGRect bounds = [self getFinderWindowBounds:visor]; + // If the window's y position is above the bottom of the screen (offscreen threshold), it is on-screen + if (bounds.origin.y < self->screenHeight - 50) { + isVisorOnScreen = YES; + } + CFRelease(visor); + } + BOOL finderIsActive = finderApp.active; + NSLog(@"[FinderLogicHelper] Finder active: %d, visor on screen: %d", finderIsActive, isVisorOnScreen); - if (show) { - NSLog(@"Show finder"); - //[finder activate]; - [self runCommand:@"open -j -a Finder"]; //activate without moving window when it outside of screen - finder.visible = YES; + if (finderIsActive && isVisorOnScreen) { + [self show:NO]; + } else { + [self show:YES]; + } +} - finderWindow = [self getFinderWindowFromWindows:finderWindows]; //it has to be done after finder.visible = YES - - //set size on first time - if (lastFinderHeight == 0) { - lastFinderHeight = DEFAULT_FINDER_HEIGHT; - [finderWindow setBounds:NSMakeRect(0, screenHeight, screenWidth, lastFinderHeight)]; //we set following bounds : {toutAGaucheDeLecran, enDehorsDeLecranEnBas_pourEtreInvisible, tailleEcran, lastFinderHeight)} - } +- (AXUIElementRef)copyFinderWindowElement { + NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"]; + NSRunningApplication *finderApp = apps.firstObject; + if (!finderApp) return NULL; + + pid_t pid = finderApp.processIdentifier; + AXUIElementRef finderElement = AXUIElementCreateApplication(pid); + if (!finderElement) return NULL; + + AXUIElementRef windowElement = NULL; + + // 1. Try focused window first + AXUIElementRef focusedWindow = NULL; + AXError err = AXUIElementCopyAttributeValue(finderElement, kAXFocusedWindowAttribute, (CFTypeRef *)&focusedWindow); + if (err == kAXErrorSuccess && focusedWindow) { + CFStringRef subrole = NULL; + AXUIElementCopyAttributeValue(focusedWindow, kAXSubroleAttribute, (CFTypeRef *)&subrole); - if (animated) { - [self animateOffsetWindow:finderWindow directionUp:YES completionHandler:nil]; + if (subrole && CFStringCompare(subrole, kAXStandardWindowSubrole, 0) == kCFCompareEqualTo) { + windowElement = focusedWindow; + } else { + CFRelease(focusedWindow); } - } else { - NSLog(@"Hide finder"); - - //experimental : reopen last closed window/tab; unfortunately, there is no way to distinguish tabs from window… - if ([self frontmostWindowIsTabOfMainWindow:finderWindows]) { - idOfVisorFinderWindow = ((FinderWindow*)finderWindows[0]).id; //show tab of main window - } else { //frontmost window isn't mainWindow nor a tab of mainWindow -// ((FinderWindow*)finderWindows[0]).index++; - for (int i = 0; i < [self getMainWindow:finderWindows].index; i++) { - ((FinderWindow*)finderWindows[i]).index++; //send all windows which are above mainWindow to back + if (subrole) CFRelease(subrole); + } + + // 2. If not found, scan all windows for the first standard window + if (!windowElement) { + CFArrayRef windows = NULL; + err = AXUIElementCopyAttributeValue(finderElement, kAXWindowsAttribute, (CFTypeRef *)&windows); + if (err == kAXErrorSuccess && windows) { + CFIndex count = CFArrayGetCount(windows); + for (CFIndex i = 0; i < count; i++) { + AXUIElementRef win = (AXUIElementRef)CFArrayGetValueAtIndex(windows, i); + CFStringRef subrole = NULL; + AXUIElementCopyAttributeValue(win, kAXSubroleAttribute, (CFTypeRef *)&subrole); + + if (subrole && CFStringCompare(subrole, kAXStandardWindowSubrole, 0) == kCFCompareEqualTo) { + windowElement = win; + CFRetain(windowElement); + if (subrole) CFRelease(subrole); + break; + } + if (subrole) CFRelease(subrole); } - [self getMainWindow:finderWindows].index = 1; //send mainWindow to front - idOfVisorFinderWindow = idOfMainVisorFinderWindow; //restore main window + CFRelease(windows); } - - finderWindow = [self getFinderWindowFromWindows:finderWindows]; - CGFloat finderHeight = finderWindow.bounds.size.height; + } + + CFRelease(finderElement); + return windowElement; +} - if (finderHeight != 0) { - lastFinderHeight = finderHeight; - } - - if (animated) { - [self animateOffsetWindow:finderWindow directionUp:NO completionHandler:^{ - //if the frontmost window (not finder one) at the time of making the shortcut takes the whole screen (window.bounds == screen.bounds) +- (NSString *)getWindowTitle:(AXUIElementRef)window { + CFStringRef title = NULL; + AXUIElementCopyAttributeValue(window, kAXTitleAttribute, (CFTypeRef *)&title); + NSString *titleStr = title ? (__bridge NSString *)title : @""; + if (title) CFRelease(title); + return titleStr; +} -// finder.frontmost = NO; //use this to allow working "Show in Finder" but experience is not as well as with finder.visible - finder.visible = NO; //this is what causes the glitch that is visible when there is no other window behind, because when you reactivate the finder after hiding it, it restores position - - //if the frontmost window (not finder one) at the time of making the shortcut does NOT take the whole screen (window.bounds != screen.bounds), but the problem of this version is that the fact of not setting finder visible=NO, causes that if we click manually on Finder dock icon, it stays at its position on bottom… -// finder.frontmost = NO; -//// finder.visible = NO; -// [finderWindow setPosition:NSMakePoint(-1440, 900)]; - }]; - } else { - finder.visible = NO; +- (AXUIElementRef)copyVisorWindowElement { + NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"]; + NSRunningApplication *finderApp = apps.firstObject; + if (!finderApp) return NULL; + + pid_t pid = finderApp.processIdentifier; + AXUIElementRef finderElement = AXUIElementCreateApplication(pid); + if (!finderElement) return NULL; + + AXUIElementRef visorWindow = NULL; + + CFArrayRef windows = NULL; + AXError err = AXUIElementCopyAttributeValue(finderElement, kAXWindowsAttribute, (CFTypeRef *)&windows); + if (err == kAXErrorSuccess && windows) { + CFIndex count = CFArrayGetCount(windows); + for (CFIndex i = 0; i < count; i++) { + AXUIElementRef win = (AXUIElementRef)CFArrayGetValueAtIndex(windows, i); + + CFStringRef subrole = NULL; + AXUIElementCopyAttributeValue(win, kAXSubroleAttribute, (CFTypeRef *)&subrole); + if (subrole) { + if (CFStringCompare(subrole, kAXStandardWindowSubrole, 0) == kCFCompareEqualTo) { + CGRect bounds = [self getFinderWindowBounds:win]; + + // Match window spanning screenWidth at left coordinate 0 + CGFloat leftDiff = fabs(bounds.origin.x); + CGFloat widthDiff = fabs(bounds.size.width - screenWidth); + + if (leftDiff < 15 && bounds.size.width >= screenWidth - 250) { + visorWindow = win; + CFRetain(visorWindow); + CFRelease(subrole); + break; + } + } + CFRelease(subrole); + } } + CFRelease(windows); } + + CFRelease(finderElement); + return visorWindow; } --(FinderWindow*) getFinderWindowFromWindows:(SBElementArray*)finderWindows { - FinderWindow *finderWindow = nil; - - if (finderWindows.count > 1) { - finderWindow = [self getMainWindow:finderWindows]; - finderWindow.index = 1; +- (AXUIElementRef)ensureFinderWindowExists { + AXUIElementRef window = [self copyVisorWindowElement]; + if (window) { + return window; } - if (finderWindow == nil) { - NSLog(@"Window not found, taking the frontmost one"); - if (finderWindows.count > 0) { - finderWindow = finderWindows[0]; - } else { - //Code to open a new Finder window. Currently it doesn't work well so I remove it. -// NSURL *u = [NSURL fileURLWithPath:@"/tmp"]; -// FinderFinderWindow *w = [[[finder classForScriptingClass:@"Finder window"] alloc] init]; -// [[finder FinderWindows] addObject:w]; -// [w setTarget:u]; - -// [self runCommand:@"osascript -e \"tell application \"Finder\" to make new Finder window\""]; - } - idOfMainVisorFinderWindow = finderWindow.id; - idOfVisorFinderWindow = finderWindow.id; + NSLog(@"[FinderLogicHelper] No visor window found. Creating a new one..."); + NSTask *task = [[NSTask alloc] init]; + [task setLaunchPath:@"/usr/bin/osascript"]; + [task setArguments:@[@"-e", @"tell application \"Finder\" to make new Finder window"]]; + [task launch]; + [task waitUntilExit]; + + // Wait up to 500ms for the window to appear + AXUIElementRef newWin = NULL; + for (int i = 0; i < 10; i++) { + usleep(50000); // 50ms + newWin = [self copyFinderWindowElement]; + if (newWin) break; } -// NSLog(@"Window : id:%ld, name:%@, index:%ld, idOfVisorFinderWindow:%ld", finderWindow.id, finderWindow.name, finderWindow.index, idOfVisorFinderWindow); + if (newWin) { + if (self->lastFinderHeight == 0) { + self->lastFinderHeight = DEFAULT_FINDER_HEIGHT; + } + // Configure it to the visor bounds at the bottom of the screen (off-screen) + [self setFinderWindowBounds:NSMakeRect(0, self->screenHeight, self->screenWidth, self->lastFinderHeight) window:newWin]; + return newWin; + } - //idOfVisorFinderWindow = finderWindow.id; - return finderWindow; + return NULL; } +- (void)setFinderWindowPosition:(NSPoint)point window:(AXUIElementRef)window { + if (!window) return; + CGPoint cgPoint = CGPointMake(point.x, point.y); + AXValueRef positionValue = AXValueCreate(kAXValueTypeCGPoint, &cgPoint); + if (positionValue) { + AXUIElementSetAttributeValue(window, kAXPositionAttribute, positionValue); + CFRelease(positionValue); + } +} -- (void)animateOffsetWindow:(FinderWindow *)finderWindow directionUp:(BOOL)directionUp completionHandler:(void (^)(void))completionHandler { - NSTimeInterval t; - NSDate* date = [NSDate date]; - float animationSpeed = [UserSettingsHelper getInstance].animationVelocity; +- (void)setFinderWindowBounds:(NSRect)rect window:(AXUIElementRef)window { + if (!window) return; - while (animationSpeed >= (t = -[date timeIntervalSinceNow])) { - float k = t / animationSpeed; //k varie de 0 à 1 - float kAccordingToDirection = directionUp ? 1 - k : k; - -// NSLog(@"kAccordingToDirection = %f", kAccordingToDirection); - - float offset = kAccordingToDirection * lastFinderHeight + (screenHeight - lastFinderHeight); -// NSLog(@"offset = %f", offset); - [finderWindow setPosition:NSMakePoint(0, offset)]; - - usleep(3000); + CGPoint cgPoint = CGPointMake(rect.origin.x, rect.origin.y); + AXValueRef positionValue = AXValueCreate(kAXValueTypeCGPoint, &cgPoint); + if (positionValue) { + AXUIElementSetAttributeValue(window, kAXPositionAttribute, positionValue); + CFRelease(positionValue); + } + + CGSize cgSize = CGSizeMake(rect.size.width, rect.size.height); + AXValueRef sizeValue = AXValueCreate(kAXValueTypeCGSize, &cgSize); + if (sizeValue) { + AXUIElementSetAttributeValue(window, kAXSizeAttribute, sizeValue); + CFRelease(sizeValue); } +} + +- (CGRect)getFinderWindowBounds:(AXUIElementRef)window { + if (!window) return CGRectZero; - //a last move to be sure to be on the right place (in the end it only works for SHOW because when the finder is down, MacOS does not allow its frame to go lower than its status bar) - float kAccordingToDirection = directionUp ? 0 : 1; - float offset = kAccordingToDirection * lastFinderHeight + (screenHeight - lastFinderHeight); - NSLog(@"offset = %f", offset); - [finderWindow setPosition:NSMakePoint(0, offset)]; + CGPoint pos = CGPointZero; + AXValueRef positionValue = NULL; + AXError err = AXUIElementCopyAttributeValue(window, kAXPositionAttribute, (CFTypeRef *)&positionValue); + if (err == kAXErrorSuccess && positionValue) { + AXValueGetValue(positionValue, kAXValueTypeCGPoint, &pos); + CFRelease(positionValue); + } + CGSize size = CGSizeZero; + AXValueRef sizeValue = NULL; + err = AXUIElementCopyAttributeValue(window, kAXSizeAttribute, (CFTypeRef *)&sizeValue); + if (err == kAXErrorSuccess && sizeValue) { + AXValueGetValue(sizeValue, kAXValueTypeCGSize, &size); + CFRelease(sizeValue); + } - if (completionHandler) - completionHandler(); + return CGRectMake(pos.x, pos.y, size.width, size.height); } +- (void)show:(BOOL)show { + BOOL animated = [UserSettingsHelper getInstance].animated; + NSArray *apps = [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.finder"]; + NSRunningApplication *finderApp = apps.firstObject; + if (!finderApp) return; + + if (show) { + // 1. Ensure Finder is active + [finderApp activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + NSAppleScript *activateScript = [[NSAppleScript alloc] initWithSource:@"tell application \"Finder\" to activate"]; + [activateScript executeAndReturnError:nil]; + + // 2. Ensure a visor window exists and get it + AXUIElementRef window = [self ensureFinderWindowExists]; + if (!window) return; + + if (self->lastFinderHeight == 0) { + self->lastFinderHeight = DEFAULT_FINDER_HEIGHT; + } + + // 3. Bring window to front + AXUIElementSetAttributeValue(window, kAXMainAttribute, kCFBooleanTrue); --(FinderWindow*) getMainWindow:(SBElementArray*)finderWindows { - FinderWindow *mainWindow; - for (FinderWindow *finderWin in finderWindows) { - //NSLog(@"win : %@", finderWin.properties); //this can cause glitch… + // 4. Place initial bounds (off-screen bottom) if it's not already showing + CGRect bounds = [self getFinderWindowBounds:window]; + if (bounds.origin.y >= self->screenHeight - 10) { + [self setFinderWindowBounds:NSMakeRect(0, self->screenHeight, self->screenWidth, self->lastFinderHeight) window:window]; + } + + // 5. Animate or set final position + if (animated) { + [self animateFinderShowWithWindow:window]; + } else { + [self setFinderWindowPosition:NSMakePoint(0, self->screenHeight - self->lastFinderHeight) window:window]; + CFRelease(window); + } + } else { + AXUIElementRef window = [self copyVisorWindowElement]; + if (!window) { + return; + } - if (finderWin.id == idOfVisorFinderWindow) { - mainWindow = finderWin; - break; + CGRect windowBounds = [self getFinderWindowBounds:window]; + CGFloat finderHeight = windowBounds.size.height; + if (finderHeight != 0) { + self->lastFinderHeight = finderHeight; } - } - - //don't really know why i have to do this a second time, but without this it doesn't work - if (mainWindow.id != idOfVisorFinderWindow) { - NSLog(@"Window isn't the good one, searching for the good one…"); - for (FinderWindow *finderWin in finderWindows) { - if (finderWin.id == idOfVisorFinderWindow) { - NSLog(@"Window finally found !"); - mainWindow = finderWin; - break; + + if (animated) { + __weak typeof(self) weakSelf = self; + [self animateFinderHideWithWindow:window completion:^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (strongSelf && strongSelf.previousActiveApp) { + NSString *ownBundleId = [[NSBundle mainBundle] bundleIdentifier]; + if (![strongSelf.previousActiveApp.bundleIdentifier isEqualToString:ownBundleId]) { + [strongSelf.previousActiveApp activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + } + strongSelf.previousActiveApp = nil; + } + }]; + } else { + [self setFinderWindowPosition:NSMakePoint(0, self->screenHeight) window:window]; + CFRelease(window); + + if (self.previousActiveApp) { + NSString *ownBundleId = [[NSBundle mainBundle] bundleIdentifier]; + if (![self.previousActiveApp.bundleIdentifier isEqualToString:ownBundleId]) { + [self.previousActiveApp activateWithOptions:NSApplicationActivateIgnoringOtherApps]; + } + self.previousActiveApp = nil; } } } - - return mainWindow; } --(BOOL) frontmostWindowIsTabOfMainWindow:(SBElementArray*)finderWindows { - FinderWindow *frontmostWindow = (FinderWindow*)finderWindows[0]; - FinderWindow *mainWindow = [self getMainWindow:finderWindows]; - - if (CGRectEqualToRect(frontmostWindow.bounds, mainWindow.bounds) && CGPointEqualToPoint(frontmostWindow.position, mainWindow.position)) { - return YES; - } - - return NO; +- (void)animateFinderShowWithWindow:(AXUIElementRef)window { + float animationSpeed = [UserSettingsHelper getInstance].animationVelocity; + NSDate *startDate = [NSDate date]; + CGFloat targetHeight = lastFinderHeight; + CGFloat targetScreenHeight = screenHeight; + + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 8 * NSEC_PER_MSEC, 2 * NSEC_PER_MSEC); + + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(timer, ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + dispatch_source_cancel(timer); + return; + } + + double elapsed = -[startDate timeIntervalSinceNow]; + if (elapsed >= animationSpeed) { + dispatch_source_cancel(timer); + [strongSelf setFinderWindowPosition:NSMakePoint(0, targetScreenHeight - targetHeight) window:window]; + CFRelease(window); + return; + } + float k = elapsed / animationSpeed; + float offset = (1 - k) * targetHeight + (targetScreenHeight - targetHeight); + [strongSelf setFinderWindowPosition:NSMakePoint(0, offset) window:window]; + }); + + dispatch_resume(timer); } -#pragma mark - Misc +- (void)animateFinderHideWithWindow:(AXUIElementRef)window completion:(void (^)(void))completion { + float animationSpeed = [UserSettingsHelper getInstance].animationVelocity; + NSDate *startDate = [NSDate date]; + CGFloat targetHeight = lastFinderHeight; + CGFloat targetScreenHeight = screenHeight; -- (NSString *)runCommand:(NSString *)commandToRun { - NSTask *task = [[NSTask alloc] init]; - [task setLaunchPath:@"/bin/sh"]; - - NSArray *arguments = [NSArray arrayWithObjects: - @"-c" , - [NSString stringWithFormat:@"%@", commandToRun], - nil]; - NSLog(@"run command:%@", commandToRun); - [task setArguments:arguments]; - - NSPipe *pipe = [NSPipe pipe]; - [task setStandardOutput:pipe]; - - NSFileHandle *file = [pipe fileHandleForReading]; - - [task launch]; - - NSData *data = [file readDataToEndOfFile]; - - NSString *output = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - return output; + dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); + dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 8 * NSEC_PER_MSEC, 2 * NSEC_PER_MSEC); + + __weak typeof(self) weakSelf = self; + dispatch_source_set_event_handler(timer, ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) { + dispatch_source_cancel(timer); + return; + } + + double elapsed = -[startDate timeIntervalSinceNow]; + if (elapsed >= animationSpeed) { + dispatch_source_cancel(timer); + [strongSelf setFinderWindowPosition:NSMakePoint(0, targetScreenHeight) window:window]; + if (completion) { + completion(); + } + CFRelease(window); + return; + } + float k = elapsed / animationSpeed; + float offset = k * targetHeight + (targetScreenHeight - targetHeight); + [strongSelf setFinderWindowPosition:NSMakePoint(0, offset) window:window]; + }); + + dispatch_resume(timer); +} + +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; } @end diff --git a/FastFinder/Helper/UserSettingsHelper.h b/FastFinder/Helper/UserSettingsHelper.h index 6581f12..3449de2 100644 --- a/FastFinder/Helper/UserSettingsHelper.h +++ b/FastFinder/Helper/UserSettingsHelper.h @@ -12,12 +12,18 @@ NS_ASSUME_NONNULL_BEGIN @interface UserSettingsHelper : NSObject -@property(nonatomic) BOOL launchOnStartup; -@property(nonatomic) BOOL animated; -@property(nonatomic) double animationVelocity; +@property (nonatomic) BOOL launchOnStartup; +@property (nonatomic) BOOL animated; +@property (nonatomic) double animationVelocity; -+(UserSettingsHelper*)getInstance; --(void) setDefaultsSettings; ++ (UserSettingsHelper*)getInstance; +- (void) setDefaultsSettings; +- (void) restoreSettings; + +// Setters +- (void) setLaunchOnStartup:(BOOL)launchOnStartup; +- (void) setAnimated:(BOOL)animated; +- (void) setAnimationVelocity:(double)animationVelocity; @end diff --git a/FastFinder/Helper/UserSettingsHelper.m b/FastFinder/Helper/UserSettingsHelper.m index 9c5d50e..1542af6 100644 --- a/FastFinder/Helper/UserSettingsHelper.m +++ b/FastFinder/Helper/UserSettingsHelper.m @@ -7,7 +7,7 @@ // #import "UserSettingsHelper.h" -#import +#import static NSString *const kSettingsLaunchOnStartup = @"settings_launchOnStartup"; static NSString *const kSettingsAnimated = @"settings_animated"; @@ -16,51 +16,57 @@ @implementation UserSettingsHelper static UserSettingsHelper *instance = nil; +static dispatch_once_t onceToken; -+(UserSettingsHelper*)getInstance { - @synchronized(self) { - if(instance==nil) { - instance = [UserSettingsHelper new]; - [instance restoreSettings]; - } - } ++ (UserSettingsHelper*)getInstance { + dispatch_once(&onceToken, ^{ + instance = [[UserSettingsHelper alloc] init]; + [instance restoreSettings]; + }); return instance; } --(void) setDefaultsSettings { - self.launchOnStartup = NO; //should be YES but currently feature isn't working so I disable it +- (void) setDefaultsSettings { + self.launchOnStartup = NO; self.animated = YES; self.animationVelocity = 0.4; } --(void) restoreSettings { +- (void) restoreSettings { NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; _launchOnStartup = [defaults boolForKey:kSettingsLaunchOnStartup]; _animated = [defaults boolForKey:kSettingsAnimated]; _animationVelocity = [defaults doubleForKey:kSettingsAnimationVelocity]; } - #pragma mark - Setters --(void) setLaunchOnStartup:(BOOL)launchOnStartup { +- (void) setLaunchOnStartup:(BOOL)launchOnStartup { if (_launchOnStartup != launchOnStartup) { _launchOnStartup = launchOnStartup; [[NSUserDefaults standardUserDefaults] setBool:launchOnStartup forKey:kSettingsLaunchOnStartup]; - - //has to be fixed with : https://theswiftdev.com/2017/10/27/how-to-launch-a-macos-app-at-login/ . for now, please add it manually to login items in System Preferences - SMLoginItemSetEnabled ((__bridge CFStringRef)@"com.acocagne.FastFinder", launchOnStartup); // NO to cancel launch at login + + NSError *error = nil; + if (launchOnStartup) { + if (![SMAppService.mainAppService registerAndReturnError:&error]) { + NSLog(@"Failed to enable login item: %@", error); + } + } else { + if (![SMAppService.mainAppService unregisterAndReturnError:&error]) { + NSLog(@"Failed to disable login item: %@", error); + } + } } } --(void) setAnimated:(BOOL)animated { +- (void) setAnimated:(BOOL)animated { if (_animated != animated) { _animated = animated; [[NSUserDefaults standardUserDefaults] setBool:animated forKey:kSettingsAnimated]; } } --(void) setAnimationVelocity:(double)animationVelocity { +- (void) setAnimationVelocity:(double)animationVelocity { if (_animationVelocity != animationVelocity) { _animationVelocity = animationVelocity; [[NSUserDefaults standardUserDefaults] setDouble:animationVelocity forKey:kSettingsAnimationVelocity]; diff --git a/FastFinder/Info.plist b/FastFinder/Info.plist index 60d8a70..2d06cbc 100644 --- a/FastFinder/Info.plist +++ b/FastFinder/Info.plist @@ -17,15 +17,17 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.1.1 + 0.1.2 CFBundleVersion - 2 + 3 LSApplicationCategoryType public.app-category.utilities LSMinimumSystemVersion $(MACOSX_DEPLOYMENT_TARGET) NSAppleEventsUsageDescription Used to control the Finder window. + NSAccessibilityUsageDescription + FastFinder needs accessibility access to detect the Finder window state and bring it to the front. NSHumanReadableCopyright Copyright © 2018 Anthonin Cocagne. All rights reserved. NSMainStoryboardFile diff --git a/FastFinder/ViewController/InfosViewController.m b/FastFinder/ViewController/InfosViewController.m index 5bdb4bf..61a3051 100644 --- a/FastFinder/ViewController/InfosViewController.m +++ b/FastFinder/ViewController/InfosViewController.m @@ -24,7 +24,7 @@ - (void)viewDidLoad { -(void) setVersionNumber { NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; - NSString *appVersion = [infoDict objectForKey:@"CFBundleShortVersionString"]; // example: 1.0.0 + NSString *appVersion = infoDict[@"CFBundleShortVersionString"]; // example: 1.0.0 _versionNumberLabel.stringValue = appVersion; } diff --git a/FastFinder/ViewController/SettingsViewController.m b/FastFinder/ViewController/SettingsViewController.m index 945386b..138b981 100644 --- a/FastFinder/ViewController/SettingsViewController.m +++ b/FastFinder/ViewController/SettingsViewController.m @@ -12,9 +12,7 @@ #import "UserSettingsHelper.h" -@interface SettingsViewController () { - -} +@interface SettingsViewController () @property (strong) IBOutlet MASShortcutView *customShortcutView; @property (weak) IBOutlet NSButton *launchAtStartupCheckbox; @@ -73,15 +71,7 @@ -(void) restoreSettingsStates { - (IBAction)didChangeLaunchOnStartup:(NSButton*)button { NSLog(@"Setting launchOnStartup to %ld", (long)button.state); - //[UserSettingsHelper getInstance].launchOnStartup = button.state; - - NSAlert *alert = [NSAlert new]; - [alert setMessageText:@"⚠️ Feature not available currently"]; - [alert setInformativeText:@"Sorry, launch at startup isn't supported for now.\n\nFor now, please do it manually by going to System Preferences > Users and groups > Login Items and adding FastFinder there.\n\nThe feature will be available when I'll have time to implement it, or if you have some free time, feel free to make a PR on https://github.com/AnthoPakPak/FastFinder 😉"]; - [alert addButtonWithTitle:@"OK"]; - [alert runModal]; - - button.state = NSControlStateValueOff; + [UserSettingsHelper getInstance].launchOnStartup = (button.state == NSControlStateValueOn); } - (IBAction)didChangeAnimated:(NSButton*)button { diff --git a/Podfile b/Podfile index 75d7af3..9965f83 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,4 @@ -# Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' +platform :osx, '13.0' target 'FastFinder' do # Uncomment the next line if you're using Swift or would like to use dynamic frameworks diff --git a/Podfile.lock b/Podfile.lock index 8294f81..9072ee5 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,16 +1,16 @@ PODS: - - MASShortcut (2.3.6) + - MASShortcut (2.4.0) DEPENDENCIES: - MASShortcut SPEC REPOS: - https://github.com/cocoapods/specs.git: + trunk: - MASShortcut SPEC CHECKSUMS: - MASShortcut: 9c215e8a8a78f3d01ce56da48e2730ab66b538fa + MASShortcut: d9e4909e878661cc42877cc9d6efbe638273ab57 -PODFILE CHECKSUM: 1d97161e8cdbc40bbca6c75e604c727d7f36f3ec +PODFILE CHECKSUM: b5960c7a52b4bc08178deefdb3f9554db43d4f5b -COCOAPODS: 1.5.3 +COCOAPODS: 1.16.2