mirror of
https://github.com/Codeux-Software/Textual.git
synced 2026-06-16 13:24:32 +00:00
442 lines
14 KiB
Objective-C
442 lines
14 KiB
Objective-C
/* *********************************************************************
|
|
* _____ _ _
|
|
* |_ _|____ _| |_ _ _ __ _| |
|
|
* | |/ _ \ \/ / __| | | |/ _` | |
|
|
* | | __/> <| |_| |_| | (_| | |
|
|
* |_|\___/_/\_\\__|\__,_|\__,_|_|
|
|
*
|
|
* Copyright (c) 2010 - 2018 Codeux Software, LLC & respective contributors.
|
|
* Please see Acknowledgements.pdf for additional information.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * 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.
|
|
* * Neither the name of Textual, "Codeux Software, LLC", nor the
|
|
* names of its contributors may be used to endorse or promote products
|
|
* derived from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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 "TDCAlert.h"
|
|
#import "TPCApplicationInfo.h"
|
|
#import "TPCPathInfo.h"
|
|
#import "TLOLocalization.h"
|
|
#import "TPCResourceManagerPrivate.h"
|
|
|
|
NS_ASSUME_NONNULL_BEGIN
|
|
|
|
NSString * const TPCResourceManagerBundleDocumentTypeExtension = @".bundle";
|
|
NSString * const TPCResourceManagerBundleDocumentTypeExtensionWithoutPeriod = @"bundle";
|
|
|
|
NSString * const TPCResourceManagerScriptDocumentTypeExtension = @".scpt";
|
|
NSString * const TPCResourceManagerScriptDocumentTypeExtensionWithoutPeriod = @"scpt";
|
|
|
|
@implementation TPCResourceManager
|
|
|
|
+ (void)copyResourcesToApplicationSupportFolder
|
|
{
|
|
/* Add a system link for the unsupervised scripts folder if it exists. */
|
|
NSString *sourcePath = [TPCPathInfo customScripts];
|
|
|
|
NSString *destinationPath = [[TPCPathInfo groupContainerApplicationSupport] stringByAppendingPathComponent:@"/Custom Scripts/"];
|
|
|
|
if ([RZFileManager() fileExistsAtPath:sourcePath] &&
|
|
[RZFileManager() fileExistsAtPath:destinationPath] == NO)
|
|
{
|
|
[RZFileManager() createSymbolicLinkAtPath:destinationPath withDestinationPath:sourcePath error:NULL];
|
|
}
|
|
}
|
|
|
|
+ (NSCache *)sharedResourcesCache
|
|
{
|
|
static NSCache *cachedValue = nil;
|
|
|
|
static dispatch_once_t onceToken;
|
|
|
|
dispatch_once(&onceToken, ^{
|
|
cachedValue = [NSCache new];
|
|
});
|
|
|
|
return cachedValue;
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name
|
|
{
|
|
return [self dictionaryFromResources:name inDirectory:nil key:nil cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath
|
|
{
|
|
return [self dictionaryFromResources:name inDirectory:subpath key:nil cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self dictionaryFromResources:name inDirectory:subpath key:nil cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self dictionaryFromResources:name inDirectory:nil key:nil cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name key:(nullable NSString *)key
|
|
{
|
|
return [self dictionaryFromResources:name inDirectory:nil key:key cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name key:(nullable NSString *)key cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self dictionaryFromResources:name inDirectory:nil key:key cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key
|
|
{
|
|
return [self dictionaryFromResources:name inDirectory:subpath key:key cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSDictionary<NSString *, id> *)dictionaryFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self objectFromResources:name inDirectory:subpath key:key kindOf:[NSDictionary class] cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name
|
|
{
|
|
return [self arrayFromResources:name inDirectory:nil key:nil cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath
|
|
{
|
|
return [self arrayFromResources:name inDirectory:subpath key:nil cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self arrayFromResources:name inDirectory:subpath key:nil cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self arrayFromResources:name inDirectory:nil key:nil cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name key:(nullable NSString *)key
|
|
{
|
|
return [self arrayFromResources:name inDirectory:nil key:key cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name key:(nullable NSString *)key cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self arrayFromResources:name inDirectory:nil key:key cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key
|
|
{
|
|
return [self arrayFromResources:name inDirectory:subpath key:key cacheValue:YES];
|
|
}
|
|
|
|
+ (nullable NSArray *)arrayFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key cacheValue:(BOOL)cacheValue
|
|
{
|
|
return [self objectFromResources:name inDirectory:subpath key:key kindOf:[NSArray class] cacheValue:cacheValue];
|
|
}
|
|
|
|
+ (nullable id)objectFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key kindOf:(Class)class cacheValue:(BOOL)cacheValue
|
|
{
|
|
NSParameterAssert(name != nil);
|
|
|
|
/* Bypass cache? */
|
|
if (cacheValue == NO) {
|
|
return [self _objectFromResources:name inDirectory:subpath key:key kindOf:class];
|
|
}
|
|
|
|
/* Cache hit? */
|
|
NSString *cacheKey = [NSString stringWithFormat:@"%@.plist / %@ / %@",
|
|
name, ((subpath) ?: @"Root Folder"), ((key) ?: @"Root Object")];
|
|
|
|
NSCache *cache = self.sharedResourcesCache;
|
|
|
|
id cachedValue = [cache objectForKey:cacheKey];
|
|
|
|
if (cachedValue) {
|
|
return cachedValue;
|
|
}
|
|
|
|
/* No cache hit, generate */
|
|
cachedValue = [self _objectFromResources:name inDirectory:subpath key:key kindOf:class];
|
|
|
|
if (cachedValue) {
|
|
[cache setObject:cachedValue forKey:cacheKey];
|
|
}
|
|
|
|
return cachedValue;
|
|
}
|
|
|
|
+ (nullable id)_objectFromResources:(NSString *)name inDirectory:(nullable NSString *)subpath key:(nullable NSString *)key kindOf:(Class)class
|
|
{
|
|
/* Locate resource */
|
|
NSURL *resourceURL = [RZMainBundle() URLForResource:name withExtension:@"plist" subdirectory:subpath];
|
|
|
|
if (resourceURL == nil) {
|
|
LogToConsoleError("Resource '%{public}@' in subpath '%{public}@' was not found.",
|
|
name, ((subpath) ?: @"<No subpath>"));
|
|
|
|
return nil;
|
|
}
|
|
|
|
/* Read resource */
|
|
NSError *readError = nil;
|
|
|
|
NSData *fileContents = [NSData dataWithContentsOfURL:resourceURL options:0 error:&readError];
|
|
|
|
if (readError) {
|
|
LogToConsoleError("Resource '%{public}@' could not be read with error: %{public}@",
|
|
resourceURL.standardizedTildePath, readError.localizedDescription);
|
|
|
|
return nil;
|
|
}
|
|
|
|
/* Process as a property list */
|
|
NSError *parseError = nil;
|
|
|
|
id propertyList =
|
|
[NSPropertyListSerialization propertyListWithData:fileContents
|
|
options:NSPropertyListImmutable
|
|
format:NULL
|
|
error:&parseError];
|
|
|
|
if (parseError) {
|
|
LogToConsoleFault("Resource '%{public}@' could not be parsed as a property list with error: %{public}@",
|
|
resourceURL.standardizedTildePath, parseError.localizedDescription);
|
|
|
|
return nil;
|
|
}
|
|
|
|
/* Locate object */
|
|
/* Until we know otherwise, the value is the root object. */
|
|
id objectValue = nil;
|
|
|
|
if (key == nil) {
|
|
objectValue = propertyList;
|
|
} else {
|
|
/* Property list can be an array. */
|
|
if ([propertyList isKindOfClass:[NSDictionary class]] == NO) {
|
|
LogToConsoleError("Contents of resource '%{public}@' is not a dictionary. "
|
|
"Cannot locate value of 'key' in other formats.",
|
|
resourceURL.standardizedTildePath);
|
|
|
|
return nil;
|
|
}
|
|
|
|
objectValue = [propertyList objectForKey:key];
|
|
}
|
|
|
|
if ([objectValue isKindOfClass:class] == NO) {
|
|
LogToConsoleError("Contents of key '%{public}@' in resource '%{public}@' is not kind of class: %{public}@",
|
|
((key) ?: @"<Root Object>"), resourceURL.standardizedTildePath, NSStringFromClass(class));
|
|
|
|
return nil;
|
|
}
|
|
|
|
return objectValue;
|
|
}
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation TPCResourceManagerDocumentTypeImporter
|
|
|
|
+ (BOOL)autosavesInPlace
|
|
{
|
|
/* We are read-only. This suppresses a warning in console. */
|
|
return YES;
|
|
}
|
|
|
|
- (BOOL)readFromURL:(NSURL *)url ofType:(NSString *)typeName error:(NSError **)outError
|
|
{
|
|
NSString *filePath = url.filePathURL.absoluteString;
|
|
|
|
if ([filePath hasSuffix:TPCResourceManagerScriptDocumentTypeExtension]) {
|
|
[self performImportOfScriptFile:url];
|
|
|
|
return YES;
|
|
}
|
|
|
|
NSString *pluginSuffix = [TPCResourceManagerBundleDocumentTypeExtension stringByAppendingString:@"/"];
|
|
|
|
if ([filePath hasSuffix:pluginSuffix]) {
|
|
[self performImportOfPluginFile:url];
|
|
|
|
return YES;
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Custom Plugin Files
|
|
|
|
- (void)performImportOfPluginFile:(NSURL *)url
|
|
{
|
|
NSParameterAssert(url != nil);
|
|
|
|
NSString *filename = url.lastPathComponent;
|
|
|
|
BOOL performInstall = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[6tj-yp]")
|
|
title:TXTLS(@"Prompts[xfl-8e]", filename)
|
|
defaultButton:TXTLS(@"Prompts[mvh-ms]")
|
|
alternateButton:TXTLS(@"Prompts[99q-gg]")];
|
|
|
|
if (performInstall == NO) {
|
|
return;
|
|
}
|
|
|
|
NSURL *newPath = [[TPCPathInfo customExtensionsURL] URLByAppendingPathComponent:filename];
|
|
|
|
BOOL didImport = [self import:url into:newPath];
|
|
|
|
if (didImport) {
|
|
NSString *filenameWithoutExtension = filename.stringByDeletingPathExtension;
|
|
|
|
[TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[k69-q0]")
|
|
title:TXTLS(@"Prompts[xek-0t]", filenameWithoutExtension)
|
|
defaultButton:TXTLS(@"Prompts[c7s-dq]")
|
|
alternateButton:nil];
|
|
}
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark Custom Script Files
|
|
|
|
- (BOOL)panel:(id)sender validateURL:(NSURL *)url error:(NSError **)outError
|
|
{
|
|
NSString *scriptsPath = [TPCPathInfo customScripts];
|
|
|
|
if ([url.path hasPrefix:scriptsPath] == NO) {
|
|
if (outError) {
|
|
NSMutableDictionary<NSString *, id> *userInfo = [NSMutableDictionary dictionary];
|
|
|
|
userInfo[NSURLErrorKey] = url;
|
|
|
|
userInfo[NSLocalizedDescriptionKey] = TXTLS(@"Prompts[m2r-gv]");
|
|
userInfo[NSLocalizedRecoverySuggestionErrorKey] = TXTLS(@"Prompts[ztu-nv]");
|
|
|
|
*outError = [NSError errorWithDomain:TXErrorDomain code:27984 userInfo:userInfo];
|
|
}
|
|
|
|
return NO;
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
- (void)performImportOfScriptFile:(NSURL *)url
|
|
{
|
|
NSParameterAssert(url != nil);
|
|
|
|
NSString *filename = url.lastPathComponent;
|
|
|
|
/* Ask user before installing. */
|
|
BOOL performInstall = [TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[6tj-yp]")
|
|
title:TXTLS(@"Prompts[xfl-8e]", filename)
|
|
defaultButton:TXTLS(@"Prompts[mvh-ms]")
|
|
alternateButton:TXTLS(@"Prompts[99q-gg]")];
|
|
|
|
if (performInstall == NO) {
|
|
return; // Do not install.
|
|
}
|
|
|
|
#if TEXTUAL_BUILT_INSIDE_SANDBOX == 0
|
|
NSURL *newPath = [[TPCPathInfo customScriptsURL] URLByAppendingPathComponent:filename];
|
|
|
|
BOOL didImport = [self import:url into:newPath];
|
|
|
|
if (didImport) {
|
|
[self performImportOfScriptFilePostflight:filename];
|
|
}
|
|
#else
|
|
NSURL *folderRep = [TPCPathInfo customScriptsURL];
|
|
|
|
if ([RZFileManager() fileExistsAtURL:folderRep] == NO) {
|
|
folderRep = [TPCPathInfo userApplicationScriptsURL];
|
|
}
|
|
|
|
NSString *bundleID = [TPCApplicationInfo applicationBundleIdentifier];
|
|
|
|
NSSavePanel *d = [NSSavePanel savePanel];
|
|
|
|
d.delegate = (id)self;
|
|
|
|
d.canCreateDirectories = YES;
|
|
|
|
d.directoryURL = folderRep;
|
|
|
|
d.title = TXTLS(@"Prompts[6hx-ni]");
|
|
|
|
d.message = TXTLS(@"Prompts[0bj-ic]", bundleID);
|
|
|
|
d.nameFieldStringValue = url.lastPathComponent;
|
|
|
|
d.showsTagField = NO;
|
|
|
|
[d beginWithCompletionHandler:^(NSInteger returnCode) {
|
|
if (returnCode == NSModalResponseOK) {
|
|
if ([self import:url into:d.URL] == NO) {
|
|
return;
|
|
}
|
|
|
|
NSString *filename = d.URL.lastPathComponent;
|
|
|
|
XRPerformBlockAsynchronouslyOnMainQueue(^{
|
|
[self performImportOfScriptFilePostflight:filename];
|
|
});
|
|
}
|
|
}];
|
|
#endif
|
|
}
|
|
|
|
- (void)performImportOfScriptFilePostflight:(NSString *)filename
|
|
{
|
|
NSParameterAssert(filename != nil);
|
|
|
|
NSString *filenameWithoutExtension = filename.stringByDeletingPathExtension;
|
|
|
|
[TDCAlert modalAlertWithMessage:TXTLS(@"Prompts[3ze-xh]", filenameWithoutExtension)
|
|
title:TXTLS(@"Prompts[4ua-v5]", filenameWithoutExtension)
|
|
defaultButton:TXTLS(@"Prompts[c7s-dq]")
|
|
alternateButton:nil];
|
|
}
|
|
|
|
#pragma mark -
|
|
#pragma mark General Import Controller
|
|
|
|
- (BOOL)import:(NSURL *)url into:(NSURL *)destination
|
|
{
|
|
return [RZFileManager() replaceItemAtURL:destination
|
|
withItemAtURL:url
|
|
options:(CSFileManagerOptionsMoveToTrash |
|
|
CSFileManagerOptionsRemoveIfExists)];
|
|
}
|
|
|
|
@end
|
|
|
|
NS_ASSUME_NONNULL_END
|