diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..7d73e66 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: https://www.paypal.me/JeffJohnsonWI diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7962502 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +DerivedData/ +DEVELOPMENT_TEAM.xcconfig +project.xcworkspace/ +xcshareddata/ +xcuserdata/ diff --git a/Debug.xcconfig b/Debug.xcconfig new file mode 100644 index 0000000..d6d48c1 --- /dev/null +++ b/Debug.xcconfig @@ -0,0 +1,6 @@ +DEBUG_INFORMATION_FORMAT = dwarf +DEPLOYMENT_POSTPROCESSING = NO +GCC_OPTIMIZATION_LEVEL = 0 +ONLY_ACTIVE_ARCH = YES +STRIP_INSTALLED_PRODUCT = NO +VALIDATE_PRODUCT = NO diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..6897ce8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,15 @@ +1. StartTheZoom is Copyright © 2022 Jeff Johnson. All rights reserved. + +2. You may make copies of the StartTheZoom source code and redistribute copies of the source code. + +3. All copies of the source code must include this license file unmodified. + +4. You may modify the source code, but you must not modify or remove this license file. + +5. You must not charge money for the unmodified or modified source code. + +6. You may compile the unmodified or modified source code and run the compiled products yourself. + +7. You must not redistribute the compiled products of the unmodified or modified source code without express written permission from Jeff Johnson. + +8. The spirit of this license agreement is that Jeff Johnson provides StartTheZoom to the public for free, and nobody is to make money from selling Jeff's work as if it were their own. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..7ff9a98 --- /dev/null +++ b/README.md @@ -0,0 +1,44 @@ +# StartTheZoom + +StartTheZoom is an app for macOS (10.13 High Sierra or later) that opens http and https URLs in the Zoom app and then quits. + +The Zoom app does not declare that it can open http and https URLs. Thus, sandboxed Mac App Store apps such as [Link Unshortener](https://underpassapp.com/LinkUnshortener/) and [StopTheMadness](https://underpassapp.com/StopTheMadness/) cannot open these URLs in Zoom. StartTheZoom can, because it is not sandboxed. + +## Installing + +1. Download the [latest release](https://github.com/lapcat/StartTheZoom/releases/latest). +2. Unzip the downloaded `.zip` file. +3. Move `StartTheZoom.app` to your Applications folder. +4. Open `StartTheZoom.app`. +5. Quit `StartTheZoom.app`. + +## Uninstalling + +1. Move `StartTheZoom.app` to the Trash. + +## Building + +Building StartTheZoom from source requires Xcode 13 or later. + +Before building, you need to create a file named `DEVELOPMENT_TEAM.xcconfig` in the project folder (the same folder as `Shared.xcconfig`). This file is excluded from version control by the project's `.gitignore` file, and it's not referenced in the Xcode project either. The file specifies the build setting for your Development Team, which is needed by Xcode to code sign the app. The entire contents of the file should be of the following format: +``` +DEVELOPMENT_TEAM = [Your TeamID] +``` + +## Author + +[Jeff Johnson](https://lapcatsoftware.com/) + +To support the author, you can [PayPal.Me](https://www.paypal.me/JeffJohnsonWI) or buy [my App Store apps](https://underpassapp.com/). + +## Copyright + +StartTheZoom is Copyright © 2022 Jeff Johnson. All rights reserved. + +Zoom is Copyright © 2012 Zoom Video Communications, Inc. All rights reserved. + +Neither StartTheZoom nor Jeff Johnson is associated in any way with Zoom Video Communications, Inc. + +## License + +See the [LICENSE.txt](LICENSE.txt) file for details. diff --git a/Release.xcconfig b/Release.xcconfig new file mode 100644 index 0000000..e1ed3ca --- /dev/null +++ b/Release.xcconfig @@ -0,0 +1,7 @@ +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym +DEPLOYMENT_POSTPROCESSING = YES +GCC_OPTIMIZATION_LEVEL = s +ONLY_ACTIVE_ARCH = NO +STRIP_INSTALLED_PRODUCT = YES +STRIP_STYLE = all +VALIDATE_PRODUCT = YES diff --git a/Shared.xcconfig b/Shared.xcconfig new file mode 100644 index 0000000..3b87eda --- /dev/null +++ b/Shared.xcconfig @@ -0,0 +1,78 @@ +#include "DEVELOPMENT_TEAM.xcconfig" +//Create the file DEVELOPMENT_TEAM.xcconfig in the project directory +//with the following build setting: +//DEVELOPMENT_TEAM = [Your TeamID] + +STARTTHEZOOM_SHORT_VERSION = 1.0 +STARTTHEZOOM_VERSION = 1 + +ALWAYS_SEARCH_USER_PATHS = NO +CLANG_ENABLE_MODULES = YES +CLANG_ENABLE_OBJC_ARC = YES +CLANG_MODULES_AUTOLINK = YES +CODE_SIGN_ENTITLEMENTS = nonsource/StartTheZoom.entitlements +CODE_SIGN_IDENTITY = Mac Developer +CODE_SIGN_STYLE = Automatic +COMBINE_HIDPI_IMAGES = YES +COPY_PHASE_STRIP = NO +DEPLOYMENT_LOCATION = NO +ENABLE_HARDENED_RUNTIME = YES +ENABLE_STRICT_OBJC_MSGSEND = YES +GCC_GENERATE_DEBUGGING_SYMBOLS = YES +GCC_NO_COMMON_BLOCKS = YES +GCC_SYMBOLS_PRIVATE_EXTERN = YES +INFOPLIST_EXPAND_BUILD_SETTINGS = YES +INFOPLIST_FILE = nonsource/Info.plist +MACH_O_TYPE = mh_execute +MACOSX_DEPLOYMENT_TARGET = 10.13 +PRODUCT_BUNDLE_IDENTIFIER = com.lapcatsoftware.StartTheZoom +PRODUCT_NAME = StartTheZoom +SDKROOT = macosx +WRAPPER_EXTENSION = app + +//Warnings +WARNING_CFLAGS = -Wall -Wextra -Wno-unused-parameter +CLANG_WARN_ASSIGN_ENUM = YES +CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES +CLANG_WARN_BOOL_CONVERSION = YES +CLANG_WARN_COMMA = YES +CLANG_WARN_CONSTANT_CONVERSION = YES +CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR +CLANG_WARN_DOCUMENTATION_COMMENTS = YES +CLANG_WARN_EMPTY_BODY = YES +CLANG_WARN_ENUM_CONVERSION = YES +CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES +CLANG_WARN_INFINITE_RECURSION = YES +CLANG_WARN_INT_CONVERSION = YES +CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = NO +CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR +CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES +CLANG_WARN_RANGE_LOOP_ANALYSIS = YES +CLANG_WARN_SEMICOLON_BEFORE_METHOD_BODY = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES +CLANG_WARN_SUSPICIOUS_MOVE = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES +GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES +GCC_WARN_ABOUT_MISSING_NEWLINE = YES +GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES +GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES +GCC_WARN_ABOUT_RETURN_TYPE = YES +GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES +GCC_WARN_CHECK_SWITCH_STATEMENTS = YES +GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES +GCC_WARN_MISSING_PARENTHESES = YES +GCC_WARN_SIGN_COMPARE = YES +GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES +GCC_WARN_UNDECLARED_SELECTOR = YES +GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE +GCC_WARN_UNUSED_FUNCTION = YES +GCC_WARN_UNUSED_LABEL = YES +GCC_WARN_UNUSED_PARAMETER = NO +GCC_WARN_UNUSED_VALUE = YES +GCC_WARN_UNUSED_VARIABLE = YES +GCC_WARN_64_TO_32_BIT_CONVERSION = YES diff --git a/StartTheZoom.xcodeproj/project.pbxproj b/StartTheZoom.xcodeproj/project.pbxproj new file mode 100644 index 0000000..54fb994 --- /dev/null +++ b/StartTheZoom.xcodeproj/project.pbxproj @@ -0,0 +1,227 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 528D987D2934EF330089DE79 /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 528D987C2934EF210089DE79 /* LICENSE.txt */; }; + 528D988F2934F03F0089DE79 /* JJApplicationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 528D988B2934EFEF0089DE79 /* JJApplicationDelegate.m */; }; + 528D98902934F0420089DE79 /* JJLicenseWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 528D988E2934EFEF0089DE79 /* JJLicenseWindow.m */; }; + 528D98912934F0440089DE79 /* JJMainMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 528D988D2934EFEF0089DE79 /* JJMainMenu.m */; }; + 528D98922934F0480089DE79 /* JJMainWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 528D98892934EFEF0089DE79 /* JJMainWindow.m */; }; + 528D98942934F04D0089DE79 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 528D988A2934EFEF0089DE79 /* main.m */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 528D98652934EA900089DE79 /* StartTheZoom.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StartTheZoom.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 528D98782934EF210089DE79 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + 528D98792934EF210089DE79 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 528D987A2934EF210089DE79 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; + 528D987B2934EF210089DE79 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + 528D987C2934EF210089DE79 /* LICENSE.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; + 528D98822934EFD30089DE79 /* StartTheZoom.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = StartTheZoom.entitlements; sourceTree = ""; }; + 528D98832934EFD30089DE79 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 528D98852934EFEF0089DE79 /* JJMainWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JJMainWindow.h; sourceTree = ""; }; + 528D98862934EFEF0089DE79 /* JJApplicationDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JJApplicationDelegate.h; sourceTree = ""; }; + 528D98872934EFEF0089DE79 /* JJLicenseWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JJLicenseWindow.h; sourceTree = ""; }; + 528D98882934EFEF0089DE79 /* JJMainMenu.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JJMainMenu.h; sourceTree = ""; }; + 528D98892934EFEF0089DE79 /* JJMainWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JJMainWindow.m; sourceTree = ""; }; + 528D988A2934EFEF0089DE79 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; + 528D988B2934EFEF0089DE79 /* JJApplicationDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JJApplicationDelegate.m; sourceTree = ""; }; + 528D988D2934EFEF0089DE79 /* JJMainMenu.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JJMainMenu.m; sourceTree = ""; }; + 528D988E2934EFEF0089DE79 /* JJLicenseWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JJLicenseWindow.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 528D98622934EA900089DE79 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 528D985C2934EA900089DE79 = { + isa = PBXGroup; + children = ( + 528D987B2934EF210089DE79 /* README.md */, + 528D987C2934EF210089DE79 /* LICENSE.txt */, + 528D98782934EF210089DE79 /* Debug.xcconfig */, + 528D98792934EF210089DE79 /* Release.xcconfig */, + 528D987A2934EF210089DE79 /* Shared.xcconfig */, + 528D98812934EFD30089DE79 /* nonsource */, + 528D98842934EFEF0089DE79 /* source */, + 528D98662934EA900089DE79 /* Products */, + ); + sourceTree = ""; + }; + 528D98662934EA900089DE79 /* Products */ = { + isa = PBXGroup; + children = ( + 528D98652934EA900089DE79 /* StartTheZoom.app */, + ); + name = Products; + sourceTree = ""; + }; + 528D98812934EFD30089DE79 /* nonsource */ = { + isa = PBXGroup; + children = ( + 528D98832934EFD30089DE79 /* Info.plist */, + 528D98822934EFD30089DE79 /* StartTheZoom.entitlements */, + ); + path = nonsource; + sourceTree = SOURCE_ROOT; + }; + 528D98842934EFEF0089DE79 /* source */ = { + isa = PBXGroup; + children = ( + 528D98862934EFEF0089DE79 /* JJApplicationDelegate.h */, + 528D988B2934EFEF0089DE79 /* JJApplicationDelegate.m */, + 528D98872934EFEF0089DE79 /* JJLicenseWindow.h */, + 528D988E2934EFEF0089DE79 /* JJLicenseWindow.m */, + 528D98882934EFEF0089DE79 /* JJMainMenu.h */, + 528D988D2934EFEF0089DE79 /* JJMainMenu.m */, + 528D98852934EFEF0089DE79 /* JJMainWindow.h */, + 528D98892934EFEF0089DE79 /* JJMainWindow.m */, + 528D988A2934EFEF0089DE79 /* main.m */, + ); + path = source; + sourceTree = SOURCE_ROOT; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 528D98642934EA900089DE79 /* StartTheZoom */ = { + isa = PBXNativeTarget; + buildConfigurationList = 528D98752934EA910089DE79 /* Build configuration list for PBXNativeTarget "StartTheZoom" */; + buildPhases = ( + 528D98612934EA900089DE79 /* Sources */, + 528D98622934EA900089DE79 /* Frameworks */, + 528D98632934EA900089DE79 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = StartTheZoom; + productName = StartTheZoom; + productReference = 528D98652934EA900089DE79 /* StartTheZoom.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 528D985D2934EA900089DE79 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastUpgradeCheck = 1410; + TargetAttributes = { + 528D98642934EA900089DE79 = { + CreatedOnToolsVersion = 14.1; + }; + }; + }; + buildConfigurationList = 528D98602934EA900089DE79 /* Build configuration list for PBXProject "StartTheZoom" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 528D985C2934EA900089DE79; + productRefGroup = 528D98662934EA900089DE79 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 528D98642934EA900089DE79 /* StartTheZoom */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 528D98632934EA900089DE79 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 528D987D2934EF330089DE79 /* LICENSE.txt in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 528D98612934EA900089DE79 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 528D988F2934F03F0089DE79 /* JJApplicationDelegate.m in Sources */, + 528D98922934F0480089DE79 /* JJMainWindow.m in Sources */, + 528D98912934F0440089DE79 /* JJMainMenu.m in Sources */, + 528D98942934F04D0089DE79 /* main.m in Sources */, + 528D98902934F0420089DE79 /* JJLicenseWindow.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 528D98732934EA910089DE79 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 528D987A2934EF210089DE79 /* Shared.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 528D98742934EA910089DE79 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 528D987A2934EF210089DE79 /* Shared.xcconfig */; + buildSettings = { + }; + name = Release; + }; + 528D98762934EA910089DE79 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 528D98782934EF210089DE79 /* Debug.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + 528D98772934EA910089DE79 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 528D98792934EF210089DE79 /* Release.xcconfig */; + buildSettings = { + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 528D98602934EA900089DE79 /* Build configuration list for PBXProject "StartTheZoom" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 528D98732934EA910089DE79 /* Debug */, + 528D98742934EA910089DE79 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 528D98752934EA910089DE79 /* Build configuration list for PBXNativeTarget "StartTheZoom" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 528D98762934EA910089DE79 /* Debug */, + 528D98772934EA910089DE79 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 528D985D2934EA900089DE79 /* Project object */; +} diff --git a/nonsource/Info.plist b/nonsource/Info.plist new file mode 100644 index 0000000..01a6e85 --- /dev/null +++ b/nonsource/Info.plist @@ -0,0 +1,44 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + Web site URL + CFBundleURLSchemes + + http + https + + + + CFBundleShortVersionString + $(STARTTHEZOOM_SHORT_VERSION) + CFBundleVersion + $(STARTTHEZOOM_VERSION) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + © 2022 Jeff Johnson. All rights reserved. + NSPrincipalClass + NSApplication + NSSupportsSuddenTermination + + + diff --git a/nonsource/StartTheZoom.entitlements b/nonsource/StartTheZoom.entitlements new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/nonsource/StartTheZoom.entitlements @@ -0,0 +1,5 @@ + + + + + diff --git a/source/JJApplicationDelegate.h b/source/JJApplicationDelegate.h new file mode 100644 index 0000000..221667b --- /dev/null +++ b/source/JJApplicationDelegate.h @@ -0,0 +1,10 @@ +@import Cocoa; + +@interface JJApplicationDelegate:NSObject +-(void)openLicense:(nullable id)sender; +-(void)openMacAppStore:(nullable id)sender; +-(void)openMainWindow:(nullable id)sender; +-(void)openWebSite:(nullable id)sender; +@end + +extern NSString*_Null_unspecified JJApplicationName; diff --git a/source/JJApplicationDelegate.m b/source/JJApplicationDelegate.m new file mode 100644 index 0000000..4bfe277 --- /dev/null +++ b/source/JJApplicationDelegate.m @@ -0,0 +1,150 @@ +#import "JJApplicationDelegate.h" + +#import "JJLicenseWindow.h" +#import "JJMainMenu.h" +#import "JJMainWindow.h" + +NSString* JJApplicationName; + +@implementation JJApplicationDelegate { + BOOL _didOpenURLs; + NSUInteger _urlCount; + NSWindow* _licenseWindow; + NSWindow* _mainWindow; +} + +#pragma mark Private + +-(void)terminateIfNecessary { + NSArray* windows = [NSApp windows]; + for (NSWindow* window in windows) { + if ([window isVisible]) + return; // Don't terminate if there are visible windows + } + [NSApp terminate:nil]; +} + +-(void)openMacAppStoreURL:(nonnull NSURL*)url { + [[NSWorkspace sharedWorkspace] openURLs:@[url] withAppBundleIdentifier:@"com.apple.AppStore" options:NSWorkspaceLaunchAsync additionalEventParamDescriptor:nil launchIdentifiers:nil]; +} + +#pragma mark NSApplicationDelegate + +-(void)applicationWillFinishLaunching:(nonnull NSNotification *)notification { + JJApplicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; + if (JJApplicationName == nil) { + NSLog(@"CFBundleName nil!"); + JJApplicationName = @"StartTheZoom"; + } + [JJMainMenu populateMainMenu]; +} + +-(void)applicationDidFinishLaunching:(nonnull NSNotification*)notification { + if (_didOpenURLs) + return; + + static NSString*_Nonnull const FirstRunWindowShown = @"FirstRunWindowShown"; + NSUserDefaults* standardUserDefaults = [NSUserDefaults standardUserDefaults]; + if ([standardUserDefaults boolForKey:FirstRunWindowShown]) + return; + [standardUserDefaults setBool:YES forKey:FirstRunWindowShown]; + + [self openMainWindow:nil]; +} + +-(void)applicationDidResignActive:(nonnull NSNotification*)notification { + if (_urlCount > 0) + return; + + [self terminateIfNecessary]; +} + +-(void)application:(nonnull NSApplication*)application openURLs:(nonnull NSArray*)urls { + _didOpenURLs = YES; + NSUInteger count = [urls count]; + _urlCount += count; + NSURL* zoomURL = [[NSWorkspace sharedWorkspace] URLForApplicationWithBundleIdentifier:@"us.zoom.xos"]; + if (zoomURL == nil) { + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:@"Zoom Not Installed"]; + NSMutableString* informativeText = [NSMutableString stringWithString:@"The URL"]; + if (count > 1u) + [informativeText appendString:@"s"]; + [informativeText appendString:@" could not be opened, because the Zoom app is not installed.\n"]; + for (NSURL* url in urls) { + [informativeText appendFormat:@"\n%@", [url absoluteString]]; + } + [alert setInformativeText:informativeText]; + [alert runModal]; + _urlCount -= count; + } else { + NSError* error = nil; + NSRunningApplication* zoomApp = [[NSWorkspace sharedWorkspace] openURLs:urls withApplicationAtURL:zoomURL options:0 configuration:@{} error:&error]; + if (zoomApp != nil) { + _urlCount -= count; + if (_urlCount == 0) + [self performSelector:@selector(terminateIfNecessary) withObject:nil afterDelay:2.0]; + } else { + NSAlert* alert = [[NSAlert alloc] init]; + [alert setMessageText:@"URL Opening Error"]; + NSMutableString* informativeText = [NSMutableString stringWithString:@"An error occurred opening the URL"]; + if (count > 1u) + [informativeText appendString:@"s"]; + [informativeText appendFormat:@":\n\n%@\n", [error localizedDescription]]; + for (NSURL* url in urls) { + [informativeText appendFormat:@"\n%@", [url absoluteString]]; + } + [alert setInformativeText:informativeText]; + [alert runModal]; + _urlCount -= count; + } + } +} + +#pragma mark JJApplicationDelegate + +-(void)windowWillClose:(nonnull NSNotification*)notification { + id object = [notification object]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:[notification name] object:object]; + if (object == _licenseWindow) + _licenseWindow = nil; + else if (object == _mainWindow) + _mainWindow = nil; +} + +-(void)openLicense:(nullable id)sender { + if (_licenseWindow != nil) { + [_licenseWindow makeKeyAndOrderFront:self]; + } else { + _licenseWindow = [JJLicenseWindow window]; + if (_licenseWindow != nil) + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowWillClose:) name:NSWindowWillCloseNotification object:_licenseWindow]; + } +} + +-(void)openMacAppStore:(id)sender { + NSURL* url = [NSURL URLWithString:@"macappstore://apps.apple.com/developer/jeff-johnson/id1176742298"]; + if (url != nil) + [self openMacAppStoreURL:url]; + else + NSLog(@"MAS URL nil!"); +} + +-(void)openMainWindow:(nullable id)sender { + if (_mainWindow != nil) { + [_mainWindow makeKeyAndOrderFront:self]; + } else { + _mainWindow = [JJMainWindow window]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowWillClose:) name:NSWindowWillCloseNotification object:_mainWindow]; + } +} + +-(void)openWebSite:(nullable id)sender { + NSURL* url = [NSURL URLWithString:@"https://github.com/lapcat/StartTheZoom"]; + if (url != nil) + [[NSWorkspace sharedWorkspace] openURL:url]; + else + NSLog(@"Support URL nil!"); +} + +@end diff --git a/source/JJLicenseWindow.h b/source/JJLicenseWindow.h new file mode 100644 index 0000000..0e1bf6f --- /dev/null +++ b/source/JJLicenseWindow.h @@ -0,0 +1,5 @@ +@import Cocoa; + +@interface JJLicenseWindow:NSObject ++(nullable NSWindow*)window; +@end diff --git a/source/JJLicenseWindow.m b/source/JJLicenseWindow.m new file mode 100644 index 0000000..b7f5a12 --- /dev/null +++ b/source/JJLicenseWindow.m @@ -0,0 +1,44 @@ +#import "JJLicenseWindow.h" + +@implementation JJLicenseWindow + ++(nullable NSWindow*)window { + NSURL* url = [[NSBundle mainBundle] URLForResource:@"LICENSE" withExtension:@"txt"]; + if (url == nil) { + NSLog(@"LICENSE.txt not found"); + return nil; + } + NSError* error = nil; + NSString* license = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error]; + if (license == nil) { + NSLog(@"LICENSE.txt error: %@", error); + return nil; + } + + NSTextField* label = [NSTextField wrappingLabelWithString:license]; + [label setTranslatesAutoresizingMaskIntoConstraints:NO]; + + NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0.0, 0.0, 630.0, 100.0) styleMask:style backing:NSBackingStoreBuffered defer:YES]; + [window setExcludedFromWindowsMenu:YES]; + [window setReleasedWhenClosed:NO]; // Necessary under ARC to avoid a crash. + [window setTabbingMode:NSWindowTabbingModeDisallowed]; + [window setTitle:NSLocalizedString(@"License", nil)]; + + NSView* contentView = [window contentView]; + [contentView addSubview:label]; + [NSLayoutConstraint activateConstraints:@[ + [[label topAnchor] constraintEqualToAnchor:[contentView topAnchor] constant:15.0], + [[label bottomAnchor] constraintEqualToAnchor:[contentView bottomAnchor] constant:-15.0], + [[label leadingAnchor] constraintEqualToAnchor:[contentView leadingAnchor] constant:15.0], + [[label trailingAnchor] constraintEqualToAnchor:[contentView trailingAnchor] constant:-15.0], + [[label widthAnchor] constraintEqualToConstant:600.0] + ]]; + + [window makeKeyAndOrderFront:nil]; + [window center]; // Wait until after makeKeyAndOrderFront so the window sizes properly first + + return window; +} + +@end diff --git a/source/JJMainMenu.h b/source/JJMainMenu.h new file mode 100644 index 0000000..62b34d2 --- /dev/null +++ b/source/JJMainMenu.h @@ -0,0 +1,5 @@ +#import "JJApplicationDelegate.h" + +@interface JJMainMenu:NSObject ++(void)populateMainMenu; +@end diff --git a/source/JJMainMenu.m b/source/JJMainMenu.m new file mode 100644 index 0000000..247b9da --- /dev/null +++ b/source/JJMainMenu.m @@ -0,0 +1,215 @@ +#import "JJMainMenu.h" + +// Apparently these aren't declared anywhere +@interface NSObject(JJMainMenu) +-(void)redo:(nullable id)sender; +-(void)undo:(nullable id)sender; +@end + +@implementation JJMainMenu + ++(void)populateMainMenu { + NSMenu* mainMenu = [[NSMenu alloc] initWithTitle:@"Main Menu"]; + + NSMenuItem* menuItem; + NSMenu* submenu; + + // The titles of the menu items are for identification purposes only and shouldn't be localized. + // The strings in the menu bar come from the submenu titles, + // except for the application menu, whose title is ignored at runtime. + menuItem = [mainMenu addItemWithTitle:@"Application" action:NULL keyEquivalent:@""]; + submenu = [[NSMenu alloc] initWithTitle:@"Application"]; + [self populateApplicationMenu:submenu]; + [mainMenu setSubmenu:submenu forItem:menuItem]; + + menuItem = [mainMenu addItemWithTitle:@"Edit" action:NULL keyEquivalent:@""]; + submenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Edit", @"The Edit menu")]; + [self populateEditMenu:submenu]; + [mainMenu setSubmenu:submenu forItem:menuItem]; + + menuItem = [mainMenu addItemWithTitle:@"Window" action:NULL keyEquivalent:@""]; + submenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Window", @"The Window menu")]; + [self populateWindowMenu:submenu]; + [mainMenu setSubmenu:submenu forItem:menuItem]; + [NSApp setWindowsMenu:submenu]; + + menuItem = [mainMenu addItemWithTitle:@"Help" action:NULL keyEquivalent:@""]; + submenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Help", @"The Help menu")]; + [self populateHelpMenu:submenu]; + [mainMenu setSubmenu:submenu forItem:menuItem]; + + [NSApp setMainMenu:mainMenu]; +} + ++(void)populateApplicationMenu:(NSMenu*)menu { + NSMenuItem* menuItem; + + menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"About", nil), JJApplicationName] + action:@selector(orderFrontStandardAboutPanel:) + keyEquivalent:@""]; + [menuItem setTarget:NSApp]; + + [menu addItem:[NSMenuItem separatorItem]]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Services", nil) + action:NULL + keyEquivalent:@""]; + NSMenu* servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; + [menu setSubmenu:servicesMenu forItem:menuItem]; + [NSApp setServicesMenu:servicesMenu]; + + [menu addItem:[NSMenuItem separatorItem]]; + + menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Hide", nil), JJApplicationName] + action:@selector(hide:) + keyEquivalent:@"h"]; + [menuItem setTarget:NSApp]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Hide Others", nil) + action:@selector(hideOtherApplications:) + keyEquivalent:@"h"]; + [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand | NSEventModifierFlagOption]; + [menuItem setTarget:NSApp]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Show All", nil) + action:@selector(unhideAllApplications:) + keyEquivalent:@""]; + [menuItem setTarget:NSApp]; + + [menu addItem:[NSMenuItem separatorItem]]; + + menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Quit", nil), JJApplicationName] + action:@selector(terminate:) + keyEquivalent:@"q"]; + [menuItem setTarget:NSApp]; +} + ++(void)populateEditMenu:(NSMenu*)menu { + NSMenuItem* menuItem; + + [menu addItemWithTitle:NSLocalizedString(@"Undo", nil) + action:@selector(undo:) + keyEquivalent:@"z"]; + + [menu addItemWithTitle:NSLocalizedString(@"Redo", nil) + action:@selector(redo:) + keyEquivalent:@"Z"]; + + [menu addItem:[NSMenuItem separatorItem]]; + + [menu addItemWithTitle:NSLocalizedString(@"Cut", nil) + action:@selector(cut:) + keyEquivalent:@"x"]; + + [menu addItemWithTitle:NSLocalizedString(@"Copy", nil) + action:@selector(copy:) + keyEquivalent:@"c"]; + + [menu addItemWithTitle:NSLocalizedString(@"Paste", nil) + action:@selector(paste:) + keyEquivalent:@"v"]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Paste and Match Style", nil) + action:@selector(pasteAsPlainText:) + keyEquivalent:@"V"]; + [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand | NSEventModifierFlagOption]; + + [menu addItemWithTitle:NSLocalizedString(@"Delete", nil) + action:@selector(delete:) + keyEquivalent:[NSString stringWithFormat:@"%C", (unichar)NSBackspaceCharacter]]; + + [menu addItemWithTitle:NSLocalizedString(@"Select All", nil) + action:@selector(selectAll:) + keyEquivalent:@"a"]; + + [menu addItem:[NSMenuItem separatorItem]]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find", nil) + action:NULL + keyEquivalent:@""]; + NSMenu* findMenu = [[NSMenu alloc] initWithTitle:@"Find"]; + [self populateFindMenu:findMenu]; + [menu setSubmenu:findMenu forItem:menuItem]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Spelling", nil) + action:NULL + keyEquivalent:@""]; + NSMenu* spellingMenu = [[NSMenu alloc] initWithTitle:@"Spelling"]; + [self populateSpellingMenu:spellingMenu]; + [menu setSubmenu:spellingMenu forItem:menuItem]; +} + ++(void)populateFindMenu:(NSMenu*)menu { + NSMenuItem* menuItem; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find…", nil) + action:@selector(performFindPanelAction:) + keyEquivalent:@"f"]; + [menuItem setTag:NSFindPanelActionShowFindPanel]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find Next", nil) + action:@selector(performFindPanelAction:) + keyEquivalent:@"g"]; + [menuItem setTag:NSFindPanelActionNext]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find Previous", nil) + action:@selector(performFindPanelAction:) + keyEquivalent:@"G"]; + [menuItem setTag:NSFindPanelActionPrevious]; + + menuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Selection for Find", nil) + action:@selector(performFindPanelAction:) + keyEquivalent:@"e"]; + [menuItem setTag:NSFindPanelActionSetFindString]; + + [menu addItemWithTitle:NSLocalizedString(@"Jump to Selection", nil) + action:@selector(centerSelectionInVisibleArea:) + keyEquivalent:@"j"]; +} + ++(void)populateSpellingMenu:(NSMenu*)menu { + [menu addItemWithTitle:NSLocalizedString(@"Spelling…", nil) + action:@selector(showGuessPanel:) + keyEquivalent:@":"]; + + [menu addItemWithTitle:NSLocalizedString(@"Check Spelling", nil) + action:@selector(checkSpelling:) + keyEquivalent:@";"]; + + [menu addItemWithTitle:NSLocalizedString(@"Check Spelling as You Type", nil) + action:@selector(toggleContinuousSpellChecking:) + keyEquivalent:@""]; +} + ++(void)populateWindowMenu:(NSMenu*)menu { + [menu addItemWithTitle:NSLocalizedString(@"Close Window", nil) + action:@selector(performClose:) + keyEquivalent:@"w"]; + [menu addItemWithTitle:NSLocalizedString(@"Minimize", nil) + action:@selector(performMiniaturize:) + keyEquivalent:@"m"]; + + [menu addItem:[NSMenuItem separatorItem]]; + + [menu addItemWithTitle:NSLocalizedString(@"Bring All to Front", nil) + action:@selector(arrangeInFront:) + keyEquivalent:@""]; + + [menu addItem:[NSMenuItem separatorItem]]; + + [menu addItemWithTitle:JJApplicationName + action:@selector(openMainWindow:) + keyEquivalent:@""]; +} + ++(void)populateHelpMenu:(NSMenu*)menu { + [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", JJApplicationName, NSLocalizedString(@"Web Site", nil)] + action:@selector(openWebSite:) + keyEquivalent:@""]; + + [menu addItemWithTitle:NSLocalizedString(@"License", nil) + action:@selector(openLicense:) + keyEquivalent:@""]; +} + +@end diff --git a/source/JJMainWindow.h b/source/JJMainWindow.h new file mode 100644 index 0000000..181c2e3 --- /dev/null +++ b/source/JJMainWindow.h @@ -0,0 +1,5 @@ +#import "JJApplicationDelegate.h" + +@interface JJMainWindow:NSObject ++(nonnull NSWindow*)window; +@end diff --git a/source/JJMainWindow.m b/source/JJMainWindow.m new file mode 100644 index 0000000..6a98107 --- /dev/null +++ b/source/JJMainWindow.m @@ -0,0 +1,45 @@ +#import "JJMainWindow.h" + +@implementation JJMainWindow + ++(nonnull NSWindow*)window { + NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; + NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0.0, 0.0, 480.0, 300.0) styleMask:style backing:NSBackingStoreBuffered defer:YES]; + [window setExcludedFromWindowsMenu:YES]; + [window setReleasedWhenClosed:NO]; // Necessary under ARC to avoid a crash. + [window setTabbingMode:NSWindowTabbingModeDisallowed]; + [window setTitle:JJApplicationName]; + NSView* contentView = [window contentView]; + + NSString* intro = NSLocalizedString(@"StartTheZoom opens http and https URLs in the Zoom app and then quits.\n\nThe Zoom app does not declare that it can open http and https URLs. Thus, sandboxed Mac App Store apps such as Link Unshortener and StopTheMadness cannot open these URLs in Zoom. StartTheZoom can, because it is not sandboxed.\n\nStartTheZoom is free and open source. To support the developer Jeff Johnson, please consider buying my App Store apps. Thanks!", nil); + NSTextField* label = [NSTextField wrappingLabelWithString:intro]; + [label setTranslatesAutoresizingMaskIntoConstraints:NO]; + [contentView addSubview:label]; + + NSButton* buyButton = [[NSButton alloc] init]; + [buyButton setButtonType:NSButtonTypeMomentaryLight]; + [buyButton setBezelStyle:NSBezelStyleRounded]; + [buyButton setTitle:NSLocalizedString(@"Mac App Store", nil)]; + [buyButton setAction:@selector(openMacAppStore:)]; + [buyButton setTranslatesAutoresizingMaskIntoConstraints:NO]; + [contentView addSubview:buyButton]; + [window setDefaultButtonCell:[buyButton cell]]; + [window setInitialFirstResponder:buyButton]; + + [NSLayoutConstraint activateConstraints:@[ + [[label topAnchor] constraintEqualToAnchor:[contentView topAnchor] constant:15.0], + [[label leadingAnchor] constraintEqualToAnchor:[contentView leadingAnchor] constant:15.0], + [[label trailingAnchor] constraintEqualToAnchor:[contentView trailingAnchor] constant:-15.0], + [[label widthAnchor] constraintEqualToConstant:450.0], + [[buyButton topAnchor] constraintEqualToAnchor:[label bottomAnchor] constant:15.0], + [[buyButton bottomAnchor] constraintEqualToAnchor:[contentView bottomAnchor] constant:-15.0], + [[buyButton trailingAnchor] constraintEqualToAnchor:[contentView trailingAnchor] constant:-15.0] + ]]; + + [window makeKeyAndOrderFront:nil]; + [window center]; // Wait until after makeKeyAndOrderFront so the window sizes properly first + + return window; +} + +@end diff --git a/source/main.m b/source/main.m new file mode 100644 index 0000000..dcc63c6 --- /dev/null +++ b/source/main.m @@ -0,0 +1,12 @@ +#import "JJApplicationDelegate.h" + +int main(int argc, const char* argv[]) { + @autoreleasepool { + NSApplication* application = [NSApplication sharedApplication]; + JJApplicationDelegate*NS_VALID_UNTIL_END_OF_SCOPE delegate = [[JJApplicationDelegate alloc] init]; + [application setDelegate:delegate]; + [application run]; + [application setDelegate:nil]; + } + return EXIT_SUCCESS; +}