diff --git a/.gitignore b/.gitignore index 568d109..ae5aa16 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store */xcuserdata/* -Quick Caption.xcodeproj/xcuserdata \ No newline at end of file +Quick Caption.xcodeproj/xcuserdata/* +/Quick Caption.xcodeproj/xcuserdata/* \ No newline at end of file diff --git a/Caption/.DS_Store b/Caption/.DS_Store index 96594c8..be6f733 100644 Binary files a/Caption/.DS_Store and b/Caption/.DS_Store differ diff --git a/Caption/Credits/QuickCaption_Acknowledgements.pages b/Caption/Credits/QuickCaption_Acknowledgements.pages new file mode 100755 index 0000000..912e539 Binary files /dev/null and b/Caption/Credits/QuickCaption_Acknowledgements.pages differ diff --git a/Caption/Credits/QuickCaption_Acknowledgements.pdf b/Caption/Credits/QuickCaption_Acknowledgements.pdf new file mode 100644 index 0000000..d67d1ba Binary files /dev/null and b/Caption/Credits/QuickCaption_Acknowledgements.pdf differ diff --git a/Caption/Model/Exporter.swift b/Caption/Model/Exporter.swift index c61ef00..6f58f52 100644 --- a/Caption/Model/Exporter.swift +++ b/Caption/Model/Exporter.swift @@ -25,7 +25,7 @@ enum FileType { srtString = srtString + "\(i+1)\n\(str)\n\n" } } - print(srtString) + // print(srtString) return srtString } @@ -189,7 +189,7 @@ enum FileType { } } } - print(txtString) + // print(txtString) return txtString } diff --git a/Caption/Model/Saver.swift b/Caption/Model/Saver.swift index 363dcee..3657c07 100644 --- a/Caption/Model/Saver.swift +++ b/Caption/Model/Saver.swift @@ -44,6 +44,19 @@ class Saver { return } + if type == .fcpXML { + if !Helper.fcpxTemplateAlreadyInstalled() { + Helper.displayInteractiveSheet(title: "Install Final Cut Pro X Caption Template", text: "For Final Cut Pro X to correctly import your FCPXML, Final Cut Pro X Caption Template must be installed first.", firstButtonText: "Continue", secondButtonText: "Cancel") { (confirm) in + if confirm { + Helper.installFCPXCaptionFiles(callback: { + Saver.writeFileToDisk(type: type, text: text, episode: episode) + }) + } + } + return + } + } + guard let origonalVideoName = episode.videoURL?.lastPathComponent else { return } @@ -57,22 +70,26 @@ class Saver { newSubtitleName = "\(ogVN).fcpxml" } - guard let newPath = episode.videoURL?.deletingLastPathComponent().appendingPathComponent(newSubtitleName) else { + guard let directoryPath = episode.videoURL?.deletingLastPathComponent() else { return } - do { - try text.write(to: newPath, atomically: true, encoding: String.Encoding.utf8) - Helper.displayInteractiveSheet(title: "Saved successfully!", text: "Subtitle saved as \(newSubtitleName) under \(newPath.deletingLastPathComponent()).", firstButtonText: "Show in Finder", secondButtonText: "Dismiss") { (firstButtonReturn) in - if firstButtonReturn == true { - NSWorkspace.shared.activateFileViewerSelecting([newPath]) + let newPath = directoryPath.appendingPathComponent(newSubtitleName) + + Helper.displaySaveFileDialog(newSubtitleName, directoryPath: directoryPath, callback: { (success, url, string) in + if success { + do { + try text.write(to: url!, atomically: true, encoding: String.Encoding.utf8) + Helper.displayInteractiveSheet(title: "Saved successfully!", text: "Subtitle saved as \(newSubtitleName) under \(newPath.deletingLastPathComponent()).", firstButtonText: "Show in Finder", secondButtonText: "Dismiss") { (firstButtonReturn) in + if firstButtonReturn == true { + NSWorkspace.shared.activateFileViewerSelecting([newPath]) + } + } + } catch { + Helper.displayInformationalSheet(title: "Save failed!", text: "Save has failed. \(error)") } } - } - catch { - print("Error writing to file: \(error)") - Helper.displayInformationalSheet(title: "Saved failed!", text: "Save has failed. \(error)") - } + }) } } diff --git a/Caption/Others/AppDelegate.swift b/Caption/Others/AppDelegate.swift index 6dbbb6b..f4e9033 100644 --- a/Caption/Others/AppDelegate.swift +++ b/Caption/Others/AppDelegate.swift @@ -96,26 +96,19 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + @IBAction func installFCPXExtras(_ sender: Any) { + Helper.installFCPXCaptionFiles(callback: nil) + } + func applicationDidFinishLaunching(_ aNotification: Notification) { - #if DEBUG -// UserDefaults.standard.set(true, forKey: "NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints") - #else - PFMoveToApplicationsFolderIfNecessary() - #endif - SUUpdater.shared()?.checkForUpdatesInBackground() - UserDefaults.standard.set(true, forKey: "SUAutomaticallyUpdate") - MSAppCenter.start("c5be1193-d482-4d0e-99a9-b5901f40d6f3", withServices:[ - MSAnalytics.self, - MSCrashes.self, - ]) - Helper.installFCPXCaptionFiles() +// Helper.installFCPXCaptionFiles() Timer.scheduledTimer(withTimeInterval: 30, repeats: true) { (_) in self.saveEverything(self) } } func applicationWillTerminate(_ aNotification: Notification) { - UserDefaults.standard.set(true, forKey: "SUAutomaticallyUpdate") + // UserDefaults.standard.set(true, forKey: "SUAutomaticallyUpdate") do { let fetchRequest: NSFetchRequest = EpisodeProject.fetchRequest() @@ -368,6 +361,12 @@ class AppDelegate: NSObject, NSApplicationDelegate { } } + @IBAction func displayAcknowledgements(_ sender: Any) { + if let bundleURL = Bundle.main.url(forResource: "QuickCaption_Acknowledgements", withExtension: "pdf") { + NSWorkspace.shared.open(bundleURL) + } + } + @IBAction func projectNavigatorClicked(_ sender: Any) { AppDelegate.mainWindow()?.toggleSidebarList(self) } @@ -386,6 +385,11 @@ class AppDelegate: NSObject, NSApplicationDelegate { @IBAction func replaceCaptionsWithSRT(_ sender: Any) { } + + @IBAction func checkForUpdates(_ sender: NSMenuItem) { + NSWorkspace.shared.open(URL(string: "macappstore://itunes.apple.com/us/app/quick-caption/id1363610340?mt=12")!) + } + } diff --git a/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccess.h b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccess.h new file mode 100755 index 0000000..cdbc9bc --- /dev/null +++ b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccess.h @@ -0,0 +1,195 @@ +// +// AppSandboxFileAccess.h +// AppSandboxFileAccess +// +// Created by Leigh McCulloch on 23/11/2013. +// +// Copyright (c) 2013, Leigh McCulloch +// All rights reserved. +// +// BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +#import +@import AppKit; + +#pragma mark - +#pragma mark AppSandboxFileAccessProtocol + +@protocol AppSandboxFileAccessProtocol + +@required +- (NSData *)bookmarkDataForURL:(NSURL *)url; +- (void)setBookmarkData:(NSData *)data forURL:(NSURL *)url; +- (void)clearBookmarkDataForURL:(NSURL *)url; + +@end + +#pragma mark - +#pragma mark AppSandboxFileAccess + +typedef void (^AppSandboxFileAccessBlock)(void); +typedef void (^AppSandboxFileSecurityScopeBlock)(NSURL *securityScopedFileURL, NSData *bookmarkData); + +@interface AppSandboxFileAccess : NSObject + +/*! @brief The title of the NSOpenPanel displayed when asking permission to access a file. + Default: "Allow Access" + */ +@property (readwrite, copy, nonatomic) NSString *title; +/*! @brief The message contained on the the NSOpenPanel displayed when asking permission to access a file. + Default: "[Application Name] needs to access this path to continue. Click Allow to continue." + */ +@property (readwrite, copy, nonatomic) NSString *message; +/*! @brief The prompt button on the the NSOpenPanel displayed when asking permission to access a file. + Default: "Allow" + */ +@property (readwrite, copy, nonatomic) NSString *prompt; + +/*! @brief This is an optional delegate object that can be provided to customize the persistance of bookmark data (e.g. in a Core Data database). + Default: nil (Default uses the AppSandboxFileAccessPersist class.) + */ +@property (nonatomic, weak) id bookmarkPersistanceDelegate; + +/*! @brief Create the object with the default values. */ ++ (AppSandboxFileAccess *)fileAccess; + +/*! @brief Initialise the object with the default values. */ +- (instancetype)init; + +/*! @brief Access a file path to read or write, automatically gaining permission from the user with NSOpenPanel if required + and using persisted permissions if possible. + + @see accessFile:persistPermission:withBlock: + @see securityScopedURLForFilePath:persistPermission:bookmark: + + @param path A file path, either a file or folder, that the caller needs access to. + @param persist If YES will save the permission for future calls. + @param block The block that will be given access to the file or folder. + @return YES if permission was granted or already available, NO otherwise. + */ +- (BOOL)accessFilePath:(NSString *)path persistPermission:(BOOL)persist withBlock:(AppSandboxFileAccessBlock)block; + +/*! + @warning Deprecated. + + @see accessFilePath:persistPermission:withBlock: + + @param path A file path, either a file or folder, that the caller needs access to. + @param block The block that will be given access to the file or folder. + @param persist If YES will save the permission for future calls. + @return YES if permission was granted or already available, NO otherwise. + */ +- (BOOL)accessFilePath:(NSString *)path withBlock:(AppSandboxFileAccessBlock)block persistPermission:(BOOL)persist __attribute__((deprecated("Use 'accessFilePath:persistPermission:withBlock:' instead."))); + +/*! @brief Access a file URL to read or write, automatically gaining permission from the user with NSOpenPanel if required + and using persisted permissions if possible. + + @see requestAccessPermissionsForFileURL:persistPermission:withBlock: + + @discussion Internally calls `requestAccessPermissionsForFileURL:persistPermission:withBlock:` and accesses the returned scoped URL if successful. + + @discussion See `requestAccessPermissionsForFileURL:persistPermission:withBlock:` for detailed behaviour. + + @param fileURL A file URL, either a file or folder, that the caller needs access to. + @param persist If YES will save the permission for future calls. + @param block The block that will be given access to the file or folder. + @return YES if permission was granted or already available, NO otherwise. + */ +- (BOOL)accessFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(AppSandboxFileAccessBlock)block; + +/*! + @warning Deprecated. + + @see accessFileURL:persistPermission:withBlock: + + @param fileURL A file URL, either a file or folder, that the caller needs access to. + @param persist If YES will save the permission for future calls. + @param block The block that will be given access to the file or folder. + @return YES if permission was granted or already available, NO otherwise. + */ +- (BOOL)accessFileURL:(NSURL *)fileURL withBlock:(AppSandboxFileAccessBlock)block persistPermission:(BOOL)persist __attribute__((deprecated("Use 'accessFileURL:persistPermission:withBlock:' instead."))); + +/*! @brief Request access permission for a file path to read or write, automatically with NSOpenPanel if required + and using persisted permissions if possible. + + @see securityScopedURLForFilePath:persistPermission:bookmark: + + @param filePath A file path, either a file or folder, that the caller needs access to. + @param persist If YES will save the permission for future calls. + @return YES if permission was granted or already available, NO otherwise. + */ +- (BOOL)requestAccessPermissionsForFilePath:(NSString *)filePath persistPermission:(BOOL)persist withBlock:(AppSandboxFileSecurityScopeBlock)block; + +/*! @brief Request access permission for a file path to read or write, automatically with NSOpenPanel if required + and using persisted permissions if possible. + + @discussion Use this function to access a file URL to either read or write in an application restricted by the App Sandbox. + This function will ask the user for permission if necessary using a well formed NSOpenPanel. The user will + have the option of approving access to the URL you specify, or a parent path for that URL. If persist is YES + the permission will be stored as a bookmark in NSUserDefaults and further calls to this function will + load the saved permission and not ask for permission again. + + @discussion If the file URL does not exist, it's parent directory will be asked for permission instead, since permission + to the directory will be required to write the file. If the parent directory doesn't exist, it will ask for + permission of whatever part of the parent path exists. + + @discussion Note: If the caller has permission to access a file because it was dropped onto the application or introduced + to the application in some other way, this function will not be aware of that permission and still prompt + the user. To prevent this, use the persistPermission function to persist a permission you've been given + whenever a user introduces a file to the application. E.g. when dropping a file onto the application window + or dock or when using an NSOpenPanel. + + @param fileURL A file URL, either a file or folder, that the caller needs access to. + @param persist If YES will save the permission for future calls. + @param block The block that will be given access to the file or folder. + @return YES if permission was granted or already available, NO otherwise. + */ +- (BOOL)requestAccessPermissionsForFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(AppSandboxFileSecurityScopeBlock)block; + +/*! @brief Persist a security bookmark for the given path. The calling application must already have permission. + + @see persistPermissionURL: + + @param path The path with permission that will be persisted. + @return Bookmark data if permission was granted or already available, nil otherwise. + */ +- (NSData *)persistPermissionPath:(NSString *)path; + +/*! @brief Persist a security bookmark for the given URL. The calling application must already have permission. + + @discussion Use this function to persist permission of a URL that has already been granted when a user introduced + a file to the calling application. E.g. by dropping the file onto the application window, or dock icon, + or when using an NSOpenPanel. + + Note: If the calling application does not have access to this file, this call will do nothing. + + @param url The URL with permission that will be persisted. + @return Bookmark data if permission was granted or already available, nil otherwise. + */ +- (NSData *)persistPermissionURL:(NSURL *)url; + +@end diff --git a/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccess.m b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccess.m new file mode 100755 index 0000000..7773e20 --- /dev/null +++ b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccess.m @@ -0,0 +1,240 @@ +// +// AppSandboxFileAccess.m +// AppSandboxFileAccess +// +// Created by Leigh McCulloch on 23/11/2013. +// +// Copyright (c) 2013, Leigh McCulloch +// All rights reserved. +// +// BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +#import "AppSandboxFileAccess.h" +#import "AppSandboxFileAccessPersist.h" +#import "AppSandboxFileAccessOpenSavePanelDelegate.h" + +#if !__has_feature(objc_arc) +#error ARC must be enabled! +#endif + +#define CFBundleDisplayName @"CFBundleDisplayName" +#define CFBundleName @"CFBundleName" + +@interface AppSandboxFileAccess () +@property (nonatomic, strong) AppSandboxFileAccessPersist *defaultDelegate; +@end + +@implementation AppSandboxFileAccess + ++ (AppSandboxFileAccess *)fileAccess { + return [[AppSandboxFileAccess alloc] init]; +} + +- (instancetype)init { + self = [super init]; + if (self) { + NSString *applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:CFBundleDisplayName]; + if (!applicationName) { + applicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:CFBundleName]; + } + + self.title = NSLocalizedString(@"Allow Access", @"Sandbox Access panel title."); + NSString *formatString = NSLocalizedString(@"%@ needs to access this path to continue. Click Allow to continue.", @"Sandbox Access panel message."); + self.message = [NSString stringWithFormat:formatString, applicationName]; + self.prompt = NSLocalizedString(@"Allow", @"Sandbox Access panel prompt."); + + // create default delegate object that persists bookmarks to user defaults + self.defaultDelegate = [[AppSandboxFileAccessPersist alloc] init]; + self.bookmarkPersistanceDelegate = _defaultDelegate; + } + return self; +} + +- (NSURL *)askPermissionForURL:(NSURL *)url { + NSParameterAssert(url); + + // this url will be the url allowed, it might be a parent url of the url passed in + __block NSURL *allowedURL = nil; + + // create delegate that will limit which files in the open panel can be selected, to ensure only a folder + // or file giving permission to the file requested can be selected + AppSandboxFileAccessOpenSavePanelDelegate *openPanelDelegate = [[AppSandboxFileAccessOpenSavePanelDelegate alloc] initWithFileURL:url]; + + // check that the url exists, if it doesn't, find the parent path of the url that does exist and ask permission for that + NSFileManager *fileManager = [NSFileManager defaultManager]; + NSString *path = [url path]; + while (path.length > 1) { // give up when only '/' is left in the path or if we get to a path that exists + if ([fileManager fileExistsAtPath:path isDirectory:NULL]) { + break; + } + path = [path stringByDeletingLastPathComponent]; + } + url = [NSURL fileURLWithPath:path]; + + // display the open panel + dispatch_block_t displayOpenPanelBlock = ^{ + NSOpenPanel *openPanel = [NSOpenPanel openPanel]; + [openPanel setMessage:self.message]; + [openPanel setCanCreateDirectories:NO]; + [openPanel setCanChooseFiles:YES]; + [openPanel setCanChooseDirectories:YES]; + [openPanel setAllowsMultipleSelection:NO]; + [openPanel setPrompt:self.prompt]; + [openPanel setTitle:self.title]; + [openPanel setShowsHiddenFiles:NO]; + [openPanel setExtensionHidden:NO]; + [openPanel setDirectoryURL:url]; + [openPanel setDelegate:openPanelDelegate]; + [[NSApplication sharedApplication] activateIgnoringOtherApps:YES]; + NSInteger openPanelButtonPressed = [openPanel runModal]; + if (openPanelButtonPressed == NSModalResponseOK) { + allowedURL = [openPanel URL]; + } + }; + if ([NSThread isMainThread]) { + displayOpenPanelBlock(); + } else { + dispatch_sync(dispatch_get_main_queue(), displayOpenPanelBlock); + } + + return allowedURL; +} + +- (NSData *)persistPermissionPath:(NSString *)path { + NSParameterAssert(path); + + return [self persistPermissionURL:[NSURL fileURLWithPath:path]]; +} + +- (NSData *)persistPermissionURL:(NSURL *)url { + NSParameterAssert(url); + + // store the sandbox permissions + NSData *bookmarkData = [url bookmarkDataWithOptions:NSURLBookmarkCreationWithSecurityScope includingResourceValuesForKeys:nil relativeToURL:nil error:NULL]; + if (bookmarkData) { + [self.bookmarkPersistanceDelegate setBookmarkData:bookmarkData forURL:url]; + } + return bookmarkData; +} + +- (BOOL)accessFilePath:(NSString *)path withBlock:(AppSandboxFileAccessBlock)block persistPermission:(BOOL)persist { + // Deprecated. Use 'accessFilePath:persistPermission:withBlock:' instead. + return [self accessFilePath:path persistPermission:persist withBlock:block]; +} + +- (BOOL)accessFileURL:(NSURL *)fileURL withBlock:(AppSandboxFileAccessBlock)block persistPermission:(BOOL)persist { + // Deprecated. Use 'accessFileURL:persistPermission:withBlock:' instead. + return [self accessFileURL:fileURL persistPermission:persist withBlock:block]; +} + +- (BOOL)accessFilePath:(NSString *)path persistPermission:(BOOL)persist withBlock:(AppSandboxFileAccessBlock)block { + return [self accessFileURL:[NSURL fileURLWithPath:path] persistPermission:persist withBlock:block]; +} + +- (BOOL)accessFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(AppSandboxFileAccessBlock)block { + NSParameterAssert(fileURL); + NSParameterAssert(block); + + BOOL success = [self requestAccessPermissionsForFileURL:fileURL persistPermission:persist withBlock:^(NSURL *securityScopedFileURL, NSData *bookmarkData) { + // execute the block with the file access permissions + @try { + [securityScopedFileURL startAccessingSecurityScopedResource]; + block(); + } @finally { + [securityScopedFileURL stopAccessingSecurityScopedResource]; + } + }]; + + return success; +} + +- (BOOL)requestAccessPermissionsForFilePath:(NSString *)filePath persistPermission:(BOOL)persist withBlock:(AppSandboxFileSecurityScopeBlock)block { + NSParameterAssert(filePath); + + NSURL *fileURL = [NSURL fileURLWithPath:filePath]; + return [self requestAccessPermissionsForFileURL:fileURL persistPermission:persist withBlock:block]; +} + +- (BOOL)requestAccessPermissionsForFileURL:(NSURL *)fileURL persistPermission:(BOOL)persist withBlock:(AppSandboxFileSecurityScopeBlock)block { + NSParameterAssert(fileURL); + + NSURL *allowedURL = nil; + + // standardize the file url and remove any symlinks so that the url we lookup in bookmark data would match a url given by the askPermissionForURL method + fileURL = [[fileURL URLByStandardizingPath] URLByResolvingSymlinksInPath]; + + // lookup bookmark data for this url, this will automatically load bookmark data for a parent path if we have it + NSData *bookmarkData = [self.bookmarkPersistanceDelegate bookmarkDataForURL:fileURL]; + if (bookmarkData) { + // resolve the bookmark data into an NSURL object that will allow us to use the file + BOOL bookmarkDataIsStale; + allowedURL = [NSURL URLByResolvingBookmarkData:bookmarkData options:NSURLBookmarkResolutionWithSecurityScope|NSURLBookmarkResolutionWithoutUI relativeToURL:nil bookmarkDataIsStale:&bookmarkDataIsStale error:NULL]; + // if the bookmark data is stale we'll attempt to recreate it with the existing url object if possible (not guaranteed) + if (bookmarkDataIsStale) { + bookmarkData = nil; + [self.bookmarkPersistanceDelegate clearBookmarkDataForURL:fileURL]; + if (allowedURL) { + bookmarkData = [self persistPermissionURL:allowedURL]; + if (!bookmarkData) { + allowedURL = nil; + } + } + } + } + + // if allowed url is nil, we need to ask the user for permission + if (!allowedURL) { + allowedURL = [self askPermissionForURL:fileURL]; + if (!allowedURL) { + // if the user did not give permission, exit out here + return NO; + } + } + + // if we have no bookmark data and we want to persist, we need to create it + if (persist && !bookmarkData) { + bookmarkData = [self persistPermissionURL:allowedURL]; + } + + if (block) { + block(allowedURL, bookmarkData); + } + + return YES; +} + +- (void)setBookmarkPersistanceDelegate:(NSObject *)bookmarkPersistanceDelegate +{ + // revert to default delegate object if no delegate provided + if (bookmarkPersistanceDelegate == nil) { + _bookmarkPersistanceDelegate = self.defaultDelegate; + } else { + _bookmarkPersistanceDelegate = bookmarkPersistanceDelegate; + } +} + +@end diff --git a/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessOpenSavePanelDelegate.h b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessOpenSavePanelDelegate.h new file mode 100755 index 0000000..3786982 --- /dev/null +++ b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessOpenSavePanelDelegate.h @@ -0,0 +1,44 @@ +// +// AppSandboxFileAccessOpenSavePanelDelegate.h +// AppSandboxFileAccess +// +// Created by Leigh McCulloch on 23/11/2013. +// +// Copyright (c) 2013, Leigh McCulloch +// All rights reserved. +// +// BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + + +#import +@import AppKit; + +@interface AppSandboxFileAccessOpenSavePanelDelegate : NSObject + +- (instancetype)initWithFileURL:(NSURL *)fileURL; + +@end diff --git a/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessOpenSavePanelDelegate.m b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessOpenSavePanelDelegate.m new file mode 100755 index 0000000..d07b7ce --- /dev/null +++ b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessOpenSavePanelDelegate.m @@ -0,0 +1,87 @@ +// +// AppSandboxFileAccessOpenSavePanelDelegate.m +// AppSandboxFileAccess +// +// Created by Leigh McCulloch on 23/11/2013. +// +// Copyright (c) 2013, Leigh McCulloch +// All rights reserved. +// +// BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + + +#import "AppSandboxFileAccessOpenSavePanelDelegate.h" + +#if !__has_feature(objc_arc) +#error ARC must be enabled! +#endif + +@interface AppSandboxFileAccessOpenSavePanelDelegate () + +@property (readwrite, strong, nonatomic) NSArray *pathComponents; + +@end + +@implementation AppSandboxFileAccessOpenSavePanelDelegate + +- (instancetype)initWithFileURL:(NSURL *)fileURL { + self = [super init]; + if (self) { + NSParameterAssert(fileURL); + self.pathComponents = fileURL.pathComponents; + } + return self; +} + +#pragma mark -- NSOpenSavePanelDelegate + +- (BOOL)panel:(id)sender shouldEnableURL:(NSURL *)url { + NSParameterAssert(url); + + NSArray *pathComponents = self.pathComponents; + NSArray *otherPathComponents = url.pathComponents; + + // if the url passed in has more components, it could not be a parent path or a exact same path + if (otherPathComponents.count > pathComponents.count) { + return NO; + } + + // check that each path component in url, is the same as each corresponding component in self.url + for (NSUInteger i = 0; i < otherPathComponents.count; ++i) { + NSString *comp1 = otherPathComponents[i]; + NSString *comp2 = pathComponents[i]; + // not the same, therefore url is not a parent or exact match to self.url + if (![comp1 isEqualToString:comp2]) { + return NO; + } + } + + // there were no mismatches (or no components meaning url is root) + return YES; +} + +@end diff --git a/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessPersist.h b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessPersist.h new file mode 100755 index 0000000..6031ee1 --- /dev/null +++ b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessPersist.h @@ -0,0 +1,45 @@ +// +// AppSandboxFileAccessPersist.h +// AppSandboxFileAccess +// +// Created by Leigh McCulloch on 23/11/2013. +// +// Copyright (c) 2013, Leigh McCulloch +// All rights reserved. +// +// BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +#import +#import "AppSandboxFileAccess.h" + +@interface AppSandboxFileAccessPersist : NSObject + +- (NSData *)bookmarkDataForURL:(NSURL *)url; +- (void)setBookmarkData:(NSData *)data forURL:(NSURL *)url; +- (void)clearBookmarkDataForURL:(NSURL *)url; + +@end diff --git a/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessPersist.m b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessPersist.m new file mode 100755 index 0000000..cb09268 --- /dev/null +++ b/Caption/Others/AppSandboxFileAccess/AppSandboxFileAccessPersist.m @@ -0,0 +1,79 @@ +// +// AppSandboxFileAccessPersist.m +// AppSandboxFileAccess +// +// Created by Leigh McCulloch on 23/11/2013. +// +// Copyright (c) 2013, Leigh McCulloch +// All rights reserved. +// +// BSD-2-Clause License: http://opensource.org/licenses/BSD-2-Clause +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// 1. Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED +// TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +// PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +// TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +#import "AppSandboxFileAccessPersist.h" + +#if !__has_feature(objc_arc) +#error ARC must be enabled! +#endif + +@implementation AppSandboxFileAccessPersist + ++ (NSString *)keyForBookmarkDataForURL:(NSURL *)url { + NSString *urlStr = [url absoluteString]; + return [NSString stringWithFormat:@"bd_%1$@", urlStr]; +} + +- (NSData *)bookmarkDataForURL:(NSURL *)url { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + + // loop through the bookmarks one path at a time down the URL + NSURL *subURL = url; + while ([subURL path].length > 1) { // give up when only '/' is left in the path + NSString *key = [AppSandboxFileAccessPersist keyForBookmarkDataForURL:subURL]; + NSData *bookmark = [defaults dataForKey:key]; + if (bookmark) { // if a bookmark is found, return it + return bookmark; + } + subURL = [subURL URLByDeletingLastPathComponent]; + } + + // no bookmarks for the URL, or parent to the URL were found + return nil; +} + +- (void)setBookmarkData:(NSData *)data forURL:(NSURL *)url { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *key = [AppSandboxFileAccessPersist keyForBookmarkDataForURL:url]; + [defaults setObject:data forKey:key]; +} + +- (void)clearBookmarkDataForURL:(NSURL *)url { + NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; + NSString *key = [AppSandboxFileAccessPersist keyForBookmarkDataForURL:url]; + [defaults removeObjectForKey:key]; +} + +@end diff --git a/Caption/Others/ComboColorWell/ComboColorWell.swift b/Caption/Others/ComboColorWell/ComboColorWell.swift new file mode 100755 index 0000000..38efa66 --- /dev/null +++ b/Caption/Others/ComboColorWell/ComboColorWell.swift @@ -0,0 +1,1041 @@ +// +// ComboColorWell.swift +// Tasty Testy +// +// Created by Ernesto Giannotta on 16-08-18. +// Copyright © 2018 Apimac. All rights reserved. +// + +import Cocoa + +/** + A control to pick a color. + It has the look & feel of the color control of Apple apps like Pages, Numbers etc. + */ +class ComboColorWell: NSControl { + + // MARK: - public vars + + /** + The color currently represented by the control. + */ + @IBInspectable var color: NSColor { + get { + return comboColorWellCell.color + } + set { + comboColorWellCell.color = newValue + } + } + + /** + Set this to false if you don't want the popover to show the clear color in the grid. + */ + @IBInspectable var allowClearColor: Bool { + get { + return comboColorWellCell.allowClearColor + } + set { + comboColorWellCell.allowClearColor = newValue + } + } + + // MARK: - private vars + + /** + The action cell that will do the heavy lifting for the us. + */ + private var comboColorWellCell: ComboColorWellCell { + guard let cell = cell as? ComboColorWellCell else { fatalError("ComboColorWellCell not valid") } + return cell + } + + // MARK: - Overridden functions + + override func resignFirstResponder() -> Bool { + comboColorWellCell.state = .off + return super.resignFirstResponder() + } + + // MARK: - init & private functions + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + doInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + doInit() + } + + private func doInit() { + cell = ComboColorWellCell() + } + +} + +extension ComboColorWell: NSColorChanging { + func changeColor(_ sender: NSColorPanel?) { + if let sender = sender { + comboColorWellCell.colorAction(sender) + } + } +} + +/** + The action cell that will do the heavy lifting for the ComboColorWell control. + */ +class ComboColorWellCell: NSActionCell { + /** + Enumerate sensible areas of the control cell. + */ + enum ControlArea { + case nothing + case color + case button + } + /** + Enumerate possible mouse states. + */ + enum MouseState { + case outside + case over(ControlArea) + case down(ControlArea) + case up(ControlArea) + } + + // MARK: - public vars + + /** + The color we're representing. + */ + var color = NSColor.black { + didSet { + controlView?.needsDisplay = true + } + } + /** + Set this to false if you don't want the popover to show the clear color in the grid. + */ + var allowClearColor = true + + // MARK: - public functions + + func mouseEntered(with event: NSEvent) { + if isEnabled { + mouseMoved(with: event) + } + } + + func mouseExited(with event: NSEvent) { + if isEnabled { + mouseState = .outside + } + } + + func mouseMoved(with event: NSEvent) { + if isEnabled { + mouseState = .over(controlArea(for: event)) + } + } + + /** + The standard objc action function to handle color change messages from the Color panel and Color popover. + */ + @objc func colorAction(_ sender: ColorProvider) { + action(for: sender.color) + } + + /** + The function that will propagate the control action message to the control target. + */ + private func action(for color: NSColor) { + self.color = color + if let control = controlView as? NSControl { + control.sendAction(control.action, to: control.target) + } + } + + // MARK: - private vars + + /** + A NSResponder to handle mouse events. + */ + private lazy var mouseTracker = { + return MouseTracker(mouseEntered: { self.mouseEntered(with: $0) }, + mouseExited: { self.mouseExited(with: $0) }, + mouseMoved: { self.mouseMoved(with: $0) }) + }() + + /** + The current mouse state. + */ + private var mouseState = MouseState.outside { + didSet { + if mouseState != oldValue { + handleMouseState(mouseState) + controlView?.needsDisplay = true + } + } + } + + /** + Keep track of the colors popover visibility. + */ + private var colorsPopoverVisible = false + + /** + How much we want to inset the images (down arrow and color wheel) in the control. + */ + private let imageInset = CGFloat(3.5) + + // MARK: - overrided vars + + override var controlView: NSView? { + didSet { + // add a tracking area to let our mouse tracker handle significant events + controlView?.addTrackingArea(NSTrackingArea(rect: NSZeroRect, + options: [.mouseEnteredAndExited, + .mouseMoved, + .activeInKeyWindow, + .inVisibleRect], + owner: mouseTracker, + userInfo: nil)) + } + } + + override var state: NSControl.StateValue { + didSet { + // handle the new state + handleStateChange() + } + } + + // MARK: - overrided functions + + override func setNextState() { + // disable next state default setting, called mainly by the default cell mouse tracking + return + } + + override func draw(withFrame cellFrame: NSRect, in controlView: NSView) { + // helper functions + + /** + Fill the passed path with the passed color. + */ + func fill(path: NSBezierPath, withColor color: NSColor = .controlColor) { + color.setFill() + path.fill() + } + + /** + Fill the passed path with the passed gradient. + */ + func fill(path: NSBezierPath, withGradient gradient: NSGradient) { + gradient.draw(in: path, angle: 90.0) + } + + // hard coded colors and gradients + let buttonGradient: NSGradient = { + NSGradient(starting: NSColor(red: 17, green: 103, blue: 255), + ending: NSColor(red: 95, green: 165, blue: 255))! + }() + + NSColor.black.withAlphaComponent(0.25).setStroke() + + // give some space to the control rect for anti aliasing + let smoothRect = NSInsetRect(cellFrame, 0.5, 0.5) + + // the bezier path defining the control + let path = NSBezierPath(roundedRect: smoothRect, xRadius: 6.0, yRadius: 6.0) + path.lineWidth = 0.0 + + if state == .on { + // on state always draws a selected button + fill(path: path, withGradient: buttonGradient) + } else { + switch mouseState { + case .outside, + .up: + fill(path: path) + case let .over(controlArea): + switch controlArea { + case .button: + // mouse over button draws a darker background + fill(path: path, withColor: NSColor.lightGray.withAlphaComponent(0.25)) + default: + fill(path: path) + } + case let .down(controlArea): + switch controlArea { + case .button: + // clicked button draws selected + fill(path: path, withGradient: buttonGradient) + default: + fill(path: path) + } + } + } + + #imageLiteral(resourceName: "ColorWheel").draw(in: NSInsetRect(buttonArea(withFrame: cellFrame, smoothed: true), imageInset, imageInset)) + + // clip to fill the color area + NSBezierPath.clip(colorArea(withFrame: cellFrame)) + + if color == .clear { + // want a diagonal black & white split + // start filling all white + fill(path: path, withColor: .white) + // get the color area + let area = colorArea(withFrame: cellFrame) + // get an empty bezier path to draw the black portion + let blackPath = NSBezierPath() + // get the origin point of the color area + var point = area.origin + // set it the starting point of the black path + blackPath.move(to: point) + // draw a line to opposite diagonal + point = NSPoint(x: area.width, y: area.height) + blackPath.line(to: point) + // draw a line back to origin x + point.x = area.origin.x + blackPath.line(to: point) + // close the triangle + blackPath.close() + // add clip with the control shape + path.addClip() + // finally draw the black portion + fill(path: blackPath, withColor: .black) + } else { + fill(path: path, withColor: color) + } + + // reset the clipping area + path.setClip() + // draw the control border + path.stroke() + + if !isEnabled { + fill(path: path, withColor: NSColor(calibratedWhite: 1.0, alpha: 0.25)) + } + + switch mouseState { + case let .over(controlArea), + let .down(controlArea): + switch controlArea { + case .color: + #imageLiteral(resourceName: "CircledDownArrow").draw(in: popoverButtonArea(withFrame: cellFrame, smoothed: true)) + default: + break + } + default: + break + } + + } + + override func startTracking(at startPoint: NSPoint, in controlView: NSView) -> Bool { + switch controlArea(for: startPoint, in: controlView) { + case .color: +// print("color click") + mouseState = .down(.color) + case .button: +// print("button click") + mouseState = .down(.button) + default: +// print("nothing click") + mouseState = .outside + } + return true + } + + override func stopTracking(last lastPoint: NSPoint, current stopPoint: NSPoint, in controlView: NSView, mouseIsUp flag: Bool) { + if !flag { +// print("dragging outside") + mouseState = .outside + return + } + switch controlArea(for: stopPoint, in: controlView) { + case .color: +// print("color up") + mouseState = .up(.color) + case .button: +// print("button up") + mouseState = .up(.button) + default: +// print("nothing up") + mouseState = .up(.nothing) + } + } + + override func continueTracking(last lastPoint: NSPoint, current currentPoint: NSPoint, in controlView: NSView) -> Bool { + mouseState = .down(controlArea(for: currentPoint, in: controlView)) + + return true + } + + // MARK: - private functions + + /** + Handle mpuse state here, currently we're only interested in mouse ups. + */ + private func handleMouseState(_ state: MouseState) { + switch state { +// case .outside: +// print("outside") +// case let .over(controlArea): +// switch controlArea { +// case .color: +// print("over color") +// case .button: +// print("over button") +// default: +// print("over nothing") +// } +// case let .down(controlArea): +// switch controlArea { +// case .color: +// print("down color") +// case .button: +// print("down button") +// default: +// print("down nothing") +// } + case let .up(controlArea): +// switch controlArea { +// case .color: +// print("up color") +// case .button: +// print("up button") +// default: +// print("up nothing") +// } + handleMouseUp(in: controlArea) + mouseState = .over(controlArea) + default: + break + } + } + + /** + Handle mouse up clicks here. + */ + private func handleMouseUp(in controlArea: ControlArea) { + switch controlArea { + case .button: + // toggle on and of state + state = (state == .on ? .off : .on) + case .color: + // switch state off + state = .off + if colorsPopoverVisible { + // popover already visible, just bail out + return + } + // we need the control view to show the popove relative to it + guard let view = controlView else { break } + // create a popover + let popover = NSPopover() + popover.animates = false + popover.behavior = .semitransient + // make ourself its delegate + popover.delegate = self + // create a Color grid and set it as the popover content + popover.contentViewController = ColorGridController(color: color, + target: self, + action: #selector(colorAction(_:)), + allowClearColor: allowClearColor) + // show the popover + popover.show(relativeTo: popoverButtonArea(withFrame: view.bounds), of: view, preferredEdge: .minY) + // update the visible flag + colorsPopoverVisible = true + default: + mouseState = .over(controlArea) + } + } + + /** + Handle state change here. + */ + private func handleStateChange() { + let colorPanel = NSColorPanel.shared + switch state { + case .off: + if colorPanel.isVisible, + colorPanel.delegate === self { + colorPanel.delegate = nil + } + case .on: + if let window = controlView?.window, + window.makeFirstResponder(controlView) { + colorPanel.delegate = self + colorPanel.showsAlpha = allowClearColor + colorPanel.color = color + colorPanel.orderFront(self) + } + default: + break + } + } + + /** + Get the rect of the control that displays the selected color. + */ + private func colorArea(withFrame cellFrame: NSRect, smoothed: Bool = false) -> NSRect { + var rect = smoothed ? NSInsetRect(cellFrame, 0.5, 0.5) : cellFrame + rect.size.width -= rect.size.height + return rect + } + + /** + Get the rect of the control that displays the color panel button. + */ + private func buttonArea(withFrame cellFrame: NSRect, smoothed: Bool = false) -> NSRect { + var rect = smoothed ? NSInsetRect(cellFrame, 0.5, 0.5) : cellFrame + rect.origin.x += (rect.width - rect.height) + rect.size.width = rect.height + return rect + } + + /** + Get the rect of the control where a down arrow button should be drawn. + */ + private func popoverButtonArea(withFrame cellFrame: NSRect, smoothed: Bool = false) -> NSRect { + let buttonSize = CGFloat(15.0) + let rect = colorArea(withFrame: cellFrame, smoothed: smoothed) + return NSRect(x: rect.width - (buttonSize + imageInset), + y: ceil((rect.height - buttonSize) / 2), + width: buttonSize, height: buttonSize) + } + + /** + Get the area of the control where a mouse event has occurred. + */ + private func controlArea(for event: NSEvent) -> ControlArea { + guard let controlView = controlView else { return .nothing } + return controlArea(for: controlView.convert(event.locationInWindow, from: nil), in: controlView) + } + + /** + Get the area of the control where a point lies. + */ + private func controlArea(for point: NSPoint, in controlView: NSView) -> ControlArea { + if colorArea(withFrame: controlView.bounds).contains(point) { + return .color + } else if buttonArea(withFrame: controlView.bounds).contains(point) { + return .button + } else { + return .nothing + } + } + +} + +// handle Color panel delegate events here. +extension ComboColorWellCell: NSWindowDelegate { + func windowWillClose(_ notification: Notification) { + state = .off + controlView?.needsDisplay = true + } +} + +// handle Color popover delegate events here. +extension ComboColorWellCell: NSPopoverDelegate { + func popoverWillClose(_ notification: Notification) { + colorsPopoverVisible = false + } +} + +// removed the delegate approach +//extension ComboColorWellCell: ColorGridViewDelegate { +// func colorGridView(_ colorGridView: ColorGridView, didChoose color: NSColor) { +// doColorAction(color) +// } +//} + +/** + An NSResponder subclass to handle mouse events. + */ +class MouseTracker: NSResponder { + let mouseEnteredHandler: (_ : NSEvent) -> () + let mouseExitedHandler: (_ : NSEvent) -> () + let mouseMovedHandler: ((_ : NSEvent) -> ())? + + /** + The designated initializer. + Requires handlers for the entered and exited events. + Moved event handler is optional. + */ + init(mouseEntered enteredHandler: @escaping (_ event: NSEvent) -> (), + mouseExited exitedHandler: @escaping (_ event: NSEvent) -> (), + mouseMoved movedHandler: ((_ event: NSEvent) -> ())? = nil) { + mouseEnteredHandler = enteredHandler + mouseExitedHandler = exitedHandler + mouseMovedHandler = movedHandler + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func mouseEntered(with event: NSEvent) { + mouseEnteredHandler(event) + } + + override func mouseExited(with event: NSEvent) { + mouseExitedHandler(event) + } + + override func mouseMoved(with event: NSEvent) { + mouseMovedHandler?(event) + } + +} + +/** + A controller for a grid view to show and select colors. + */ +class ColorGridController: NSViewController { + + // MARK: - public vars + + /** + The color we want to show as selected in the grid. + */ + var color = NSColor.black { + didSet { + // try to select the color in the grid view. + (view as? ColorGridView)?.selectColor(color) + } + } + + /** + Set this to false if you don't want the popover to show the clear color in the grid. + */ + var allowClearColor = true { + didSet { + // propagate setting to the grid view. + (view as? ColorGridView)?.allowClearColor = allowClearColor + } + } + + // MARK: - private vars + + /** + The target that will receive the action message, only it neither is nil, when color has been chosen. + */ + private weak var target: AnyObject? + /** + The action that will be sent to the target, only it neither is nil, when color has been chosen. + */ + private var action: Selector? + /** + The delegate that will be notified when color has been chosen. + Deprecated approach. + */ + private weak var delegate: ColorGridViewDelegate? + + // MARK: - init & overrided functions + + init() { + super.init(nibName: nil, bundle: nil) + } + + convenience init(delegate: ColorGridViewDelegate) { + self.init() + self.delegate = delegate + } + + convenience init(color: NSColor, target: AnyObject, action: Selector, allowClearColor: Bool = true) { + self.init() + self.color = color + self.target = target + self.action = action + self.allowClearColor = allowClearColor + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func loadView() { + // create here our color grid + let colorGrid = ColorGridView() + view = colorGrid + colorGrid.delegate = self + colorGrid.allowClearColor = allowClearColor + colorGrid.selectColor(color) + } + +} + +// conform to the ColorGridViewDelegate protocol. +extension ColorGridController: ColorGridViewDelegate { + /** + Handle the color choice. + */ + func colorGridView(_ colorGridView: ColorGridView, didChoose color: NSColor) { + self.color = color + view.window?.performClose(self) + if let target = target, + let action = action { + let _ = target.perform(action, with: self) + } + delegate?.colorGridView(colorGridView, didChoose: color) + } +} + +/** + Add ColorProvider conformance to NSPanel + */ +extension ColorGridController: ColorProvider {} + +/** + The protocol for a delegate to handle color choice. + */ +protocol ColorGridViewDelegate: AnyObject { + func colorGridView(_ colorGridView: ColorGridView, didChoose color: NSColor) +} + +/** + A grid of selectable color view objects. + */ +class ColorGridView: NSGridView { + // MARK: - public vars + + weak var delegate: ColorGridViewDelegate? + + /** + An array of NSColor arrays, meant to be presented as columns in the grid. + */ + var colorArrays: [[NSColor]] = [[NSColor(red: 72, green: 179, blue: 255), + NSColor(red: 18, green: 141, blue: 254), + NSColor(red: 12, green: 96, blue: 172), + NSColor(red: 7, green: 59, blue: 108), + .white], + [NSColor(red: 102, green: 255, blue: 228), + NSColor(red: 36, green: 228, blue: 196), + NSColor(red: 20, green: 154, blue: 140), + NSColor(red: 14, green: 105, blue: 99), + NSColor(red: 205, green: 203, blue: 203)], + [NSColor(red: 122, green: 255, blue: 62), + NSColor(red: 83, green: 212, blue: 42), + NSColor(red: 32, green: 166, blue: 3), + NSColor(red: 13, green: 97, blue: 2), + NSColor(red: 128, green: 128, blue: 128)], + [NSColor(red: 255, green: 255, blue: 85), + NSColor(red: 249, green: 222, blue: 40), + NSColor(red: 245, green: 173, blue: 9), + NSColor(red: 253, green: 128, blue: 8), + NSColor(red: 76, green: 76, blue: 76)], + [NSColor(red: 253, green: 129, blue: 122), + NSColor(red: 251, green: 76, blue: 62), + NSColor(red: 230, green: 0, blue: 14), + NSColor(red: 164, green: 0, blue: 2), + .black], + [NSColor(red: 252, green: 116, blue: 185), + NSColor(red: 232, green: 67, blue: 151), + NSColor(red: 189, green: 14, blue: 104), + NSColor(red: 133, green: 1, blue: 76), + .clear]] { + didSet { + setupGrid() + } + } + + /** + Set this to false if you don't want the popover to show the clear color in the grid. + */ + var allowClearColor = true { + didSet { + if let colorView = colorView(for: .clear) { + colorView.isHidden = !allowClearColor + } + } + } + + // MARK: - public functions + + /** + Try to select the element in the grid that represents the passed color. + */ + @discardableResult func selectColor(_ color: NSColor) -> Bool { + if let colorView = colorView(for: color) { + colorView.selected = true + return true + } + return false + } + + // MARK: - init & overrided functions + + init() { + super.init(frame: NSZeroRect) + + rowSpacing = 1.0 + columnSpacing = 1.0 + + setupGrid() + + } + + convenience init(in view: NSView) { + self.init() + + // make sure to disable autoresizing mask translations + translatesAutoresizingMaskIntoConstraints = false + + // add the grid view programmatically (macOS 10.12 doesn't play well with IB instantiated grids) + view.addSubview(self) + + // hook the borders of the grid to the parent view + view.addConstraints([NSLayoutConstraint(equalAttribute: .top, for: (self, view)), + NSLayoutConstraint(equalAttribute: .bottom, for: (self, view)), + NSLayoutConstraint(equalAttribute: .trailing, for: (self, view)), + NSLayoutConstraint(equalAttribute: .leading, for: (self, view))]) + + + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + // MARK: - private functions + + /** + Build the colors grid here. + */ + private func setupGrid() { + // start with an empty grid + (0.. 0 else { return } + + row(at: 0).topPadding = padding + row(at: numberOfRows - 1).bottomPadding = padding + + guard numberOfColumns > 0 else { return } + + let firstCol = column(at: 0) + let lastCol = column(at: numberOfColumns - 1) + + firstCol.leadingPadding = padding + lastCol.trailingPadding = padding + + } + + /** + Try to find the element in the grid that represents the passed color. + */ + private func colorView(for color: NSColor) -> ColorView? { + for (columnIndex, colorArray) in colorArrays.enumerated() { + if let rowIndex = colorArray.index(of: color) { + return column(at: columnIndex).cell(at: rowIndex).contentView as? ColorView + } + } + return nil + } + + /** + User has selected a color, tell it to the delegate. + */ + fileprivate func colorSelected(_ color: NSColor) { + delegate?.colorGridView(self, didChoose: color) + } + +} + +/** + A view to represent a color in a grid. + */ +class ColorView: NSView { + + // MARK: - public vars + + let color: NSColor + + var selected = false { + didSet { + needsDisplay = true + } + } + + // MARK: - private vars + + private weak var colorGridView: ColorGridView? + + // MARK: - init & overrided functions + + init(color: NSColor, in colorGridView: ColorGridView) { + self.color = color + self.colorGridView = colorGridView + super.init(frame: NSZeroRect) // NSRect(origin: NSPoint(x: 0, y: 0), size: NSSize(width: 50, height: 30))) + } + + required init?(coder decoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + guard let context = NSGraphicsContext.current?.cgContext else { return } + + if color == .clear { + NSColor.white.setFill() + } else { + color.setFill() + } + + context.fill(dirtyRect) + + if color == .clear { + NSColor.red.setStroke() + context.beginPath() + context.move(to: dirtyRect.origin) + context.addLine(to: CGPoint(x: dirtyRect.width, y: dirtyRect.height)) + context.strokePath() + } + + if selected { + NSColor.white.setStroke() + } else { + NSColor.gray.withAlphaComponent(0.5).setStroke() + } + + context.stroke(dirtyRect, width: selected ? 2.0 : 1.0) + } + + override func mouseDown(with event: NSEvent) { + selected = true + } + + override func mouseDragged(with event: NSEvent) { + let point = self.convert(event.locationInWindow, from: nil) + selected = bounds.contains(point) + } + + override func mouseUp(with event: NSEvent) { + if selected { + colorGridView?.colorSelected(color) + } + } + +} + +// MARK: - Protocols & Extensions + +/** + handy protocol for classes that have a color var +*/ +@objc protocol ColorProvider { + var color: NSColor { get set } +} + +/** + Add ColorProvider conformance to NSPanel + */ +extension NSColorPanel: ColorProvider {} + +/** + Add equatable conformance to MouseState enum + */ +extension ComboColorWellCell.MouseState: Equatable { + static func == (lhs: ComboColorWellCell.MouseState, rhs: ComboColorWellCell.MouseState) -> Bool { + switch lhs { + case .outside: + switch rhs { + case .outside: + return true + default: + return false + } + case let .over(leftArea): + switch rhs { + case let .over(rightArea): + return leftArea == rightArea + default: + return false + } + case let .down(leftArea): + switch rhs { + case let .down(rightArea): + return leftArea == rightArea + default: + return false + } + case let .up(leftArea): + switch rhs { + case let .up(rightArea): + return leftArea == rightArea + default: + return false + } + } + } +} + +extension NSLayoutConstraint { + public convenience init(equalAttribute: NSLayoutConstraint.Attribute, + for items: (NSView, NSView?), + multiplier: CGFloat = 1.0, + constant: CGFloat = 0.0) { + + self.init(item: items.0, + attribute: equalAttribute, + relatedBy: .equal, + toItem: items.1, + attribute: (items.1 != nil) ? + equalAttribute : + .notAnAttribute, + multiplier: multiplier, + constant: constant) + } +} + +extension NSColor { + convenience init(red: Int, green: Int, blue: Int, alpha: CGFloat = 1.0) { + self.init(calibratedRed: CGFloat(red) / 255, green: CGFloat(green) / 255, blue: CGFloat(blue) / 255, alpha: alpha) + } +} + +class myTextView: NSTextView { +// override func changeColor(_ sender: Any?) { +// if let colorPanel = sender as? NSColorPanel, +// let _ = colorPanel.delegate as? ComboColorWellCell { +// return +// } +// super.changeColor(sender) +// } +} diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow.png b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow.png new file mode 100755 index 0000000..829430c Binary files /dev/null and b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow.png differ diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow@2x.png b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow@2x.png new file mode 100755 index 0000000..97be362 Binary files /dev/null and b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow@2x.png differ diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow@3x.png b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow@3x.png new file mode 100755 index 0000000..3a7ef18 Binary files /dev/null and b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/CircledDownArrow@3x.png differ diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/Contents.json b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/Contents.json new file mode 100755 index 0000000..945de0f --- /dev/null +++ b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/CircledDownArrow.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "CircledDownArrow.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "CircledDownArrow@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "CircledDownArrow@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel.png b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel.png new file mode 100755 index 0000000..0aa6977 Binary files /dev/null and b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel.png differ diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel@2x.png b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel@2x.png new file mode 100755 index 0000000..c040c8c Binary files /dev/null and b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel@2x.png differ diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel@3x.png b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel@3x.png new file mode 100755 index 0000000..eae7fd6 Binary files /dev/null and b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/ColorWheel@3x.png differ diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/Contents.json b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/Contents.json new file mode 100755 index 0000000..710da8a --- /dev/null +++ b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/ColorWheel.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "ColorWheel.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "ColorWheel@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "ColorWheel@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/Contents.json b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/Contents.json new file mode 100755 index 0000000..da4a164 --- /dev/null +++ b/Caption/Others/ComboColorWell/ComboColorWellImages.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/Caption/Others/Helper.swift b/Caption/Others/Helper.swift index 2030ffb..62f30f9 100644 --- a/Caption/Others/Helper.swift +++ b/Caption/Others/Helper.swift @@ -23,23 +23,45 @@ class Helper: NSObject { } } - static func installFCPXCaptionFiles() -> Bool { + static func fcpxTemplateAlreadyInstalled() -> Bool { + let path = "/Library/Application Support/Final Cut Pro/Templates.localized/Titles.localized/Captions/Caption/Caption.moti" + let pathB = "/Library/Application Support/Final Cut Pro/Templates.localized/Titles/Captions/Caption/Caption.moti" + let pathC = "/Library/Application Support/Final Cut Pro/Templates/Titles/Captions/Caption/Caption.moti" + let pathD = "/Library/Application Support/Final Cut Pro/Templates/Titles.localized/Captions/Caption/Caption.moti" + + if FileManager.default.fileExists(atPath: path) || FileManager.default.fileExists(atPath: pathB) || FileManager.default.fileExists(atPath: pathC) || FileManager.default.fileExists(atPath: pathD) { + return true + } + return false + } + + static func installFCPXCaptionFiles(callback: (()->Void)?) { + let firstLevelPath = "/Library/Application Support/Final Cut Pro/Templates.localized/Titles.localized/Captions" + let secondLevelPath = "/Library/Application Support/Final Cut Pro/Templates.localized/Titles.localized/Captions/Caption" let fileMgr = FileManager.default - // let userDocumentURL = fileMgr.urls(for: .documentDirectory, in: .userDomainMask).first! - let urlForCreation = URL(fileURLWithPath: "/Library/Application Support/Final Cut Pro/Templates.localized/Titles.localized/Captions", isDirectory: true) - let urlForCopy = URL(fileURLWithPath: "/Library/Application Support/Final Cut Pro/Templates.localized/Titles.localized/Captions/Caption", isDirectory: true) + let urlForCreation = URL(fileURLWithPath: firstLevelPath, isDirectory: true) + let urlForCopy = URL(fileURLWithPath: secondLevelPath, isDirectory: true) if let bundleURL = Bundle.main.url(forResource: "Caption", withExtension: "") { - do { - try fileMgr.createDirectory(at: urlForCreation, withIntermediateDirectories: true, attributes: nil) - try fileMgr.copyItem(at: bundleURL, to: urlForCopy) - return true - } catch let error as NSError { // Handle the error - print("copy failed! Error:\(error.localizedDescription)") - return false - } + AppSandboxFileAccess()?.accessFileURL(urlForCopy, persistPermission: true, with: { + do { + try fileMgr.createDirectory(at: urlForCreation, withIntermediateDirectories: true, attributes: nil) + try fileMgr.copyItem(at: bundleURL, to: urlForCopy) + Helper.displayInteractiveSheet(title: "Template successfully installed", text: "You have successfully installed Final Cut Pro X caption template on this Mac. Exported captions should now work correctly in Final Cut Pro X.\n\nIf Final Cut Pro X is unable to process your imported captions, please contact support with \"Contact → Contact Support\".\n\nTo import captions onto another Mac, install Quick Caption on your other Mac, and click on \"Help → Install Final Cut Pro X Caption Template\".", firstButtonText: "OK", secondButtonText: "", callback: { (clicked) in + callback?() + }) + } catch let error as NSError { // Handle the error + if error.code == 516 { + Helper.displayInteractiveSheet(title: "Template already successfully installed", text: "You have already installed Final Cut Pro X caption template on this Mac. Exported captions should now work correctly in Final Cut Pro X.\n\nIf Final Cut Pro X is unable to process your imported captions, please contact support with \"Contact → Contact Support\".\n\nTo import captions onto another Mac, install Quick Caption on your other Mac, and click on \"Help → Install Final Cut Pro X Caption Template\".", firstButtonText: "OK", secondButtonText: "", callback: { (clicked) in + callback?() + }) + } else { + Helper.displayInformationalSheet(title: "Failed to install template", text: "Unable to install template. \n\n\(error.localizedDescription).\n\nPlease contact support with Contact → Contact Support.\n\n") + print("Copy failed! Error: \(error.localizedDescription)") + } + } + }) } else { print("Folder doesn't not exist in bundle folder") - return false } } @@ -91,18 +113,53 @@ class Helper: NSObject { dialog.allowsMultipleSelection = false dialog.allowedFileTypes = movieTypes - dialog.beginSheetModal(for: NSApp.mainWindow!) { (result) in + dialog.beginSheetModal(for: Helper.appWindow()) { (result) in if result != .OK { callback(false, nil, nil) } else { if let result = dialog.url, let path = dialog.url?.path { + AppSandboxFileAccess().persistPermissionPath(path) callback(true, result, path) } } } + } + static func appWindow() -> NSWindow { + if let mainWindow = NSApp.mainWindow { + return mainWindow + } + for window in NSApp.windows { + if let typed = window as? QuickCaptionWindow { + return typed + } + } + fatalError("Unable to find a window.") } + static func displaySaveFileDialog(_ fileName: String, directoryPath: URL, callback: @escaping ((_ selectedFile: Bool, _ fileURL: URL?, _ filePath: String?)-> ())) { + let dialog = NSSavePanel() + dialog.directoryURL = directoryPath + dialog.title = "Save created caption file" + dialog.showsResizeIndicator = true + dialog.showsHiddenFiles = false + dialog.canCreateDirectories = true + dialog.nameFieldStringValue = fileName + +// dialog.allowedFileTypes = movieTypes + + dialog.beginSheetModal(for: Helper.appWindow()) { (result) in + if result != .OK { + callback(false, nil, nil) + } else { + if let result = dialog.url, let path = dialog.url?.path { + callback(true, result, path) + } + } + } + } + + static func displayInformationalSheet(title: String, text: String) { let alert = NSAlert() alert.messageText = title @@ -136,7 +193,9 @@ class Helper: NSObject { } alert.addButton(withTitle: firstButtonText) - alert.addButton(withTitle: secondButtonText) + if secondButtonText.count > 0 { + alert.addButton(withTitle: secondButtonText) + } if let window = NSApp.mainWindow { alert.beginSheetModal(for: window) { (response) in callback(response == NSApplication.ModalResponse.alertFirstButtonReturn, dropdown?.indexOfSelectedItem ?? 0) diff --git a/Caption/Others/Info.plist b/Caption/Others/Info.plist index e058bc9..003f440 100644 --- a/Caption/Others/Info.plist +++ b/Caption/Others/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 2019.03.29.5 + 2.0 CFBundleVersion - 2019.03.29.5 + 201904270 ITSAppUsesNonExemptEncryption LSApplicationCategoryType @@ -32,13 +32,5 @@ Main NSPrincipalClass QuickCaptionApplication - SUAutomaticallyUpdate - - SUEnableAutomaticChecks - - SUFeedURL - https://raw.githubusercontent.com/LumingYin/Caption/master/Sparkle/appcast.xml - SUPublicEDKey - uteuH/b0YP45ZT28RTRH3rqmVS/FP1x+VCICt8EXesQ= diff --git a/Caption/Others/Quick Caption-Bridging-Header.h b/Caption/Others/Quick Caption-Bridging-Header.h index 9ead35a..f8adbe0 100644 --- a/Caption/Others/Quick Caption-Bridging-Header.h +++ b/Caption/Others/Quick Caption-Bridging-Header.h @@ -4,3 +4,4 @@ #import "NSObject+KVO.h" #import "GBDeviceInfo.h" +#import "AppSandboxFileAccess.h" diff --git a/Caption/Waveform/SamplesExtractor.swift b/Caption/Others/Waveform/SamplesExtractor.swift similarity index 100% rename from Caption/Waveform/SamplesExtractor.swift rename to Caption/Others/Waveform/SamplesExtractor.swift diff --git a/Caption/Waveform/WaveFormDrawer.swift b/Caption/Others/Waveform/WaveFormDrawer.swift similarity index 100% rename from Caption/Waveform/WaveFormDrawer.swift rename to Caption/Others/Waveform/WaveFormDrawer.swift diff --git a/Caption/Views/Base.lproj/Main.storyboard b/Caption/Views/Base.lproj/Main.storyboard index 381a1c4..6fdb85f 100644 --- a/Caption/Views/Base.lproj/Main.storyboard +++ b/Caption/Views/Base.lproj/Main.storyboard @@ -24,8 +24,8 @@ - - +