diff --git a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj index 0c39af914..8e6e5309b 100755 --- a/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj +++ b/Frameworks/RSCore/RSCore.xcodeproj/project.pbxproj @@ -71,6 +71,7 @@ 842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CB1ED623C7000A8B52 /* UniqueIdentifier.swift */; }; 8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432B1851DACA0E90057D6DF /* NSResponder-Extensions.swift */; }; 8432B1881DACA2060057D6DF /* NSWindow-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432B1871DACA2060057D6DF /* NSWindow-Extensions.swift */; }; + 8434D15C200BD6F400D6281E /* UserApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8434D15B200BD6F400D6281E /* UserApp.swift */; }; 84411E731FE5FFC3004B527F /* NSImage+RSCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E721FE5FFC3004B527F /* NSImage+RSCore.swift */; }; 844B5B571FE9D36000C7C76A /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B561FE9D36000C7C76A /* Keyboard.swift */; }; 844C915B1B65753E0051FC1B /* RSPlist.h in Headers */ = {isa = PBXBuildFile; fileRef = 844C91591B65753E0051FC1B /* RSPlist.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -191,6 +192,7 @@ 842E45CB1ED623C7000A8B52 /* UniqueIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniqueIdentifier.swift; sourceTree = ""; }; 8432B1851DACA0E90057D6DF /* NSResponder-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSResponder-Extensions.swift"; sourceTree = ""; }; 8432B1871DACA2060057D6DF /* NSWindow-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSWindow-Extensions.swift"; sourceTree = ""; }; + 8434D15B200BD6F400D6281E /* UserApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserApp.swift; path = AppKit/UserApp.swift; sourceTree = ""; }; 84411E721FE5FFC3004B527F /* NSImage+RSCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSImage+RSCore.swift"; path = "Images/NSImage+RSCore.swift"; sourceTree = ""; }; 844B5B561FE9D36000C7C76A /* Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Keyboard.swift; path = RSCore/Keyboard.swift; sourceTree = ""; }; 844C91591B65753E0051FC1B /* RSPlist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSPlist.h; path = RSCore/RSPlist.h; sourceTree = ""; }; @@ -460,6 +462,7 @@ 84C687311FBAA3DF00345C9E /* LogWindowController.swift */, 84C687341FBC025600345C9E /* Log.swift */, 84C687371FBC028900345C9E /* LogItem.swift */, + 8434D15B200BD6F400D6281E /* UserApp.swift */, 842DD7F91E1499FA00E061EB /* Views */, ); name = AppKit; @@ -794,6 +797,7 @@ 842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */, 84A8358A1D4EC7B80004C598 /* PlistProviderProtocol.swift in Sources */, 849A339E1AC90A0A0015BA09 /* NSTableView+RSCore.m in Sources */, + 8434D15C200BD6F400D6281E /* UserApp.swift in Sources */, 84CFF51B1AC3C77500CEA6C8 /* RSPlatform.m in Sources */, 845A291F1FC8BC49007B49E3 /* BinaryDiskCache.swift in Sources */, 84CFF52C1AC3CA9700CEA6C8 /* NSData+RSCore.m in Sources */, diff --git a/Frameworks/RSCore/RSCore/AppKit/UserApp.swift b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift new file mode 100644 index 000000000..2f10936d5 --- /dev/null +++ b/Frameworks/RSCore/RSCore/AppKit/UserApp.swift @@ -0,0 +1,123 @@ +// +// UserApp.swift +// RSCore +// +// Created by Brent Simmons on 1/14/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import Cocoa + +// Represents an app (the type of app mostly found in /Applications.) +// The app may or may not be running. It may or may not exist. + +final class UserApp { + + let bundleID: String + var icon: NSImage? = nil + var existsOnDisk = false + var path: String? = nil + var runningApplication: NSRunningApplication? = nil + + var isRunning: Bool { + + updateStatus() + if let runningApplication = runningApplication { + return !runningApplication.isTerminated + } + return false + } + + init(bundleID: String) { + + self.bundleID = bundleID + updateStatus() + } + + func updateStatus() { + + if let runningApplication = runningApplication, runningApplication.isTerminated { + self.runningApplication = nil + } + + let runningApplications = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID) + for app in runningApplications { + if let runningApplication = runningApplication { + if app == runningApplication { + break + } + } + else { + if !app.isTerminated { + runningApplication = app + break + } + } + } + + if let runningApplication = runningApplication { + existsOnDisk = true + icon = runningApplication.icon + if let bundleURL = runningApplication.bundleURL { + path = bundleURL.path + } + else { + path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) + } + if icon == nil, let path = path { + icon = NSWorkspace.shared.icon(forFile: path) + } + return + } + + path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID) + if let path = path { + if icon == nil { + icon = NSWorkspace.shared.icon(forFile: path) + } + existsOnDisk = true + } + else { + existsOnDisk = false + icon = nil + } + } + + func launchIfNeeded() -> Bool { + + // Return true if already running. + // Return true if not running and successfully gets launched. + + updateStatus() + if isRunning { + return true + } + + guard existsOnDisk, let path = path else { + return false + } + + let url = URL(fileURLWithPath: path) + if let app = try? NSWorkspace.shared.launchApplication(at: url, options: [.withErrorPresentation], configuration: [:]) { + runningApplication = app + if app.isFinishedLaunching { + return true + } + Thread.sleep(forTimeInterval: 0.5) // Give the app time to launch. This is ugly. + return true + } + + return false + } + + func bringToFront() -> Bool { + + // Activates the app, ignoring other apps. + // Does not automatically launch the app first. + + updateStatus() + return runningApplication?.activate(options: [.activateIgnoringOtherApps]) ?? false + } +} + +