如何在 OSX 上存储 HTML-based QuickLook 生成器的首选项?
How to store preferences for a HTML-based QuickLook generator on OSX?
我曾经使用必须单独安装的自定义 PrefPane 来完成,但这并不令人满意。
我有一个 HTML-based QuickLook 生成器,可以创建一些不均匀内容的缩略图和预览(具有长 ASCII header 的文件,以及各种二进制扩展名,每个扩展名都有一些header).
在 QL 方法 OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options)
中,我尝试使用 [NSUserDefaults standardUserDefaults]
,但它没有效果,并且没有写入首选项文件,可能是因为我们在一个包中,而不是一个应用程序中。
知道如何实现吗?我知道一些优秀的 QL 生成器可以做到这一点,例如 BetterZip QL。我试图对 BetterZip QL 进行逆向工程,但没有成功。
我直接联系了 BetterZip QL 的作者,我们一起想出了一个解决方案。在这里。
简而言之:
- 首先,创建一个小助手应用程序,它将被捆绑在
发电机。此应用程序将负责编写首选项
文件。
- 让这个应用程序注册一个自定义 URL 方案并实施与之关联的处理。
- 使基于 HTML 的 QL 打开一个特殊格式的 URL,使用该自定义方案,使用 Javascript。
好的,现在详细介绍。
首先在您的 Xcode workspace/project 中创建一个小的辅助应用程序目标。我的 QL 生成器名为 QLFits,我选择了 QLFitsConfig。
默认情况下,有一个 MainMenu.xib 与该应用相关联。留着它。它由 Info.plist 使用,对调试很有用。事实上,要调试自定义 URL 方案,您可以向该 xib 添加一个 NSWindow,并放置可用于显示调试消息的标签。调试此问题时,我发现没有真正的其他方法来记录或显示调试消息。
但最后,你有一个小 windowless 应用程序。这个应用程序必须有两个配置。
- 表示此应用程序是代理的标志(见图)。当 运行ning.
时,它会阻止应用程序出现在文档中
- 自定义 URL 方案的声明,具有
Editor
角色。另请参见图片示例(此处 qlfitsconfig
)
接下来,应用程序的实现需要注册URL方案来告诉系统有一个应用程序可以打开它。下面是应用程序 AppDelegate 中我的 "appDidFinishLaunching" 方法的实现。
分为三个部分:自定义URL方案的处理程序的注册。与 QL 生成器 共享的具有套件名称 的 NSUserDefaults
对象的实例化。最后,注册首选项的默认值(使用与应用程序捆绑在一起的 .plist 文件)。
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
[appleEventManager setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
[[NSUserDefaults standardUserDefaults] addSuiteNamed:suiteName];
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
NSString *optionsPath = [[NSBundle mainBundle] pathForResource:@"defaultOptions" ofType:@"plist"];
NSDictionary *defaultOptions = [NSDictionary dictionaryWithContentsOfFile:optionsPath];
[defaults registerDefaults:defaultOptions];
}
suiteName
变量是具有反向 DNS 格式的静态 NSString:static NSString *suiteName = @"com.onekiloparsec.qlfitsconfig.user-defaults-suite";
然后,应用程序需要根据事件的触发进行操作。因此,必须对事件做一些事情,并使用该事件来存储偏好。这是实现。请注意,方法的签名必须恰好是那个,不是因为我们在上面声明了它,而是因为它是系统唯一识别的。
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
{
NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
if (URLString) {
NSURL *URL = [NSURL URLWithString:URLString];
if (URL) {
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
for (NSString *component in [URL pathComponents]) {
if ([component containsString:@"="]) {
NSArray *keyValue = [component componentsSeparatedByString:@"="];
[defaults setObject:keyValue.lastObject forKey:keyValue.firstObject];
}
}
[defaults synchronize];
}
}
}
基本思想是我们将通过 URL 参数作为键值对提供偏好。因此,我们在这里将 URL 字符串转换为一对首选项,并将其存储为字符串。
这就是应用程序的全部内容。要测试和调试它,您需要构建并 运行 它(例如,检查 /Utilities/Activity Monitor.app 它是 运行ning)。您可以在终端中键入以下命令以查看会发生什么:
$ open qlfitsconfig://save/option1=value1/option2=value2
如果您保留了 window 和上面提到的标签,您可以使用它们来 display/debug 您的应用接收什么事件。
现在,回到 QL 生成器。将配置应用作为 "Target Dependency" 包含在生成器的 "Build Phases" 中。此外,添加一个新的 "Copy Files" 构建阶段(在 Copy Bundle Resources 构建阶段之后)以在 QL 包中复制该助手应用程序(见图)。
现在,在代码中,更准确地说,在方法预览方法内部:OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options)
。
在开始时,我确保配置帮助应用程序确实已在系统的启动服务中注册。要找到它,必须使用 QL 生成器的包标识符。请特别注意应用程序的 URL 是如何构造的,基于它在构建阶段(Helpers
目录)中的复制位置。
NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.onekiloparsec.QLFits3"];
NSURL *urlConfig = [NSURL fileURLWithPath:[[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/QLFitsConfig.app"]];
LSRegisterURL((__bridge CFURLRef) urlConfig, true);
最后一行使用的是遗留 API,但我无法使新 API 正常工作。这是一个弱点,在某些时候可能应该找到更好的方法。
现在,如果已经保存了一些首选项,则可以使用 NSUserDefaults
的实例访问它们,假设我们使用 中定义的相同套件名称 对其进行初始化帮助应用程序。示例:
static NSString *suiteName = @"com.onekiloparsec.qlfitsconfig.user-defaults-suite";
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
BOOL alwaysShowHeaders = YES;
if ([defaults stringForKey:@"alwaysShowHeaders"]) {
alwaysShowHeaders = [[defaults stringForKey:@"alwaysShowHeaders"] isEqualToString:@"1"];
}
Obj-C 代码就是这些。
最后一部分是Javascript代码。在我的 QL 生成器 (whose code can be checked on GitHub) 中,我使用了一个包含所有 html 和 JS 代码的 template.html
文件。您可以在这里以不同的方式组织自己。
我最初打算在切换复选框时更改 QL 首选项。但它似乎不起作用(没有触发任何事件)。我让它工作的唯一方法是,一旦设置了我的复选框,就要求用户使用按钮 "save" 首选项。单击该按钮后,我会保存首选项。这是我的 template.html
中的 JS 代码
<script>
function saveConfig (a) {
a.href = "qlfitsconfig://save";
a.href += "/alwaysShowHeaders=" + (document.getElementById("alwaysShowHeadersInput").checked ? "1" : "0");
a.href += "/showSummaryInThumbnails=" + (document.getElementById("showSummaryInThumbnailsInput").checked ? "1" : "0");
return true;
}
</script>
alwaysShowHeadersInput
和 showSummaryInThumbnailsInput
是 HTML 代码中我的复选框的 'id'。并且保存按钮触发了 saveConfig
函数。
并且按钮必须在 a
标记内声明:
<a href="#" onClick="saveConfig(this);return true;" style="float:right;"><input id="save" type="button" value="Save"></a>
这是我的 QL 中的偏好设置 window:
瞧瞧!
我曾经使用必须单独安装的自定义 PrefPane 来完成,但这并不令人满意。
我有一个 HTML-based QuickLook 生成器,可以创建一些不均匀内容的缩略图和预览(具有长 ASCII header 的文件,以及各种二进制扩展名,每个扩展名都有一些header).
在 QL 方法 OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options)
中,我尝试使用 [NSUserDefaults standardUserDefaults]
,但它没有效果,并且没有写入首选项文件,可能是因为我们在一个包中,而不是一个应用程序中。
知道如何实现吗?我知道一些优秀的 QL 生成器可以做到这一点,例如 BetterZip QL。我试图对 BetterZip QL 进行逆向工程,但没有成功。
我直接联系了 BetterZip QL 的作者,我们一起想出了一个解决方案。在这里。
简而言之:
- 首先,创建一个小助手应用程序,它将被捆绑在 发电机。此应用程序将负责编写首选项 文件。
- 让这个应用程序注册一个自定义 URL 方案并实施与之关联的处理。
- 使基于 HTML 的 QL 打开一个特殊格式的 URL,使用该自定义方案,使用 Javascript。
好的,现在详细介绍。
首先在您的 Xcode workspace/project 中创建一个小的辅助应用程序目标。我的 QL 生成器名为 QLFits,我选择了 QLFitsConfig。
默认情况下,有一个 MainMenu.xib 与该应用相关联。留着它。它由 Info.plist 使用,对调试很有用。事实上,要调试自定义 URL 方案,您可以向该 xib 添加一个 NSWindow,并放置可用于显示调试消息的标签。调试此问题时,我发现没有真正的其他方法来记录或显示调试消息。
但最后,你有一个小 windowless 应用程序。这个应用程序必须有两个配置。
- 表示此应用程序是代理的标志(见图)。当 运行ning. 时,它会阻止应用程序出现在文档中
- 自定义 URL 方案的声明,具有
Editor
角色。另请参见图片示例(此处qlfitsconfig
)
接下来,应用程序的实现需要注册URL方案来告诉系统有一个应用程序可以打开它。下面是应用程序 AppDelegate 中我的 "appDidFinishLaunching" 方法的实现。
分为三个部分:自定义URL方案的处理程序的注册。与 QL 生成器 共享的具有套件名称 的 NSUserDefaults
对象的实例化。最后,注册首选项的默认值(使用与应用程序捆绑在一起的 .plist 文件)。
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager];
[appleEventManager setEventHandler:self andSelector:@selector(handleAppleEvent:withReplyEvent:) forEventClass:kInternetEventClass andEventID:kAEGetURL];
[[NSUserDefaults standardUserDefaults] addSuiteNamed:suiteName];
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
NSString *optionsPath = [[NSBundle mainBundle] pathForResource:@"defaultOptions" ofType:@"plist"];
NSDictionary *defaultOptions = [NSDictionary dictionaryWithContentsOfFile:optionsPath];
[defaults registerDefaults:defaultOptions];
}
suiteName
变量是具有反向 DNS 格式的静态 NSString:static NSString *suiteName = @"com.onekiloparsec.qlfitsconfig.user-defaults-suite";
然后,应用程序需要根据事件的触发进行操作。因此,必须对事件做一些事情,并使用该事件来存储偏好。这是实现。请注意,方法的签名必须恰好是那个,不是因为我们在上面声明了它,而是因为它是系统唯一识别的。
- (void)handleAppleEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent
{
NSString *URLString = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
if (URLString) {
NSURL *URL = [NSURL URLWithString:URLString];
if (URL) {
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
for (NSString *component in [URL pathComponents]) {
if ([component containsString:@"="]) {
NSArray *keyValue = [component componentsSeparatedByString:@"="];
[defaults setObject:keyValue.lastObject forKey:keyValue.firstObject];
}
}
[defaults synchronize];
}
}
}
基本思想是我们将通过 URL 参数作为键值对提供偏好。因此,我们在这里将 URL 字符串转换为一对首选项,并将其存储为字符串。
这就是应用程序的全部内容。要测试和调试它,您需要构建并 运行 它(例如,检查 /Utilities/Activity Monitor.app 它是 运行ning)。您可以在终端中键入以下命令以查看会发生什么:
$ open qlfitsconfig://save/option1=value1/option2=value2
如果您保留了 window 和上面提到的标签,您可以使用它们来 display/debug 您的应用接收什么事件。
现在,回到 QL 生成器。将配置应用作为 "Target Dependency" 包含在生成器的 "Build Phases" 中。此外,添加一个新的 "Copy Files" 构建阶段(在 Copy Bundle Resources 构建阶段之后)以在 QL 包中复制该助手应用程序(见图)。
现在,在代码中,更准确地说,在方法预览方法内部:OSStatus GeneratePreviewForURL(void *thisInterface, QLPreviewRequestRef preview, CFURLRef url, CFStringRef contentTypeUTI, CFDictionaryRef options)
。
在开始时,我确保配置帮助应用程序确实已在系统的启动服务中注册。要找到它,必须使用 QL 生成器的包标识符。请特别注意应用程序的 URL 是如何构造的,基于它在构建阶段(Helpers
目录)中的复制位置。
NSBundle *bundle = [NSBundle bundleWithIdentifier:@"com.onekiloparsec.QLFits3"];
NSURL *urlConfig = [NSURL fileURLWithPath:[[bundle bundlePath] stringByAppendingPathComponent:@"Contents/Helpers/QLFitsConfig.app"]];
LSRegisterURL((__bridge CFURLRef) urlConfig, true);
最后一行使用的是遗留 API,但我无法使新 API 正常工作。这是一个弱点,在某些时候可能应该找到更好的方法。
现在,如果已经保存了一些首选项,则可以使用 NSUserDefaults
的实例访问它们,假设我们使用 中定义的相同套件名称 对其进行初始化帮助应用程序。示例:
static NSString *suiteName = @"com.onekiloparsec.qlfitsconfig.user-defaults-suite";
NSUserDefaults *defaults = [[NSUserDefaults alloc] initWithSuiteName:suiteName];
BOOL alwaysShowHeaders = YES;
if ([defaults stringForKey:@"alwaysShowHeaders"]) {
alwaysShowHeaders = [[defaults stringForKey:@"alwaysShowHeaders"] isEqualToString:@"1"];
}
Obj-C 代码就是这些。
最后一部分是Javascript代码。在我的 QL 生成器 (whose code can be checked on GitHub) 中,我使用了一个包含所有 html 和 JS 代码的 template.html
文件。您可以在这里以不同的方式组织自己。
我最初打算在切换复选框时更改 QL 首选项。但它似乎不起作用(没有触发任何事件)。我让它工作的唯一方法是,一旦设置了我的复选框,就要求用户使用按钮 "save" 首选项。单击该按钮后,我会保存首选项。这是我的 template.html
中的 JS 代码<script>
function saveConfig (a) {
a.href = "qlfitsconfig://save";
a.href += "/alwaysShowHeaders=" + (document.getElementById("alwaysShowHeadersInput").checked ? "1" : "0");
a.href += "/showSummaryInThumbnails=" + (document.getElementById("showSummaryInThumbnailsInput").checked ? "1" : "0");
return true;
}
</script>
alwaysShowHeadersInput
和 showSummaryInThumbnailsInput
是 HTML 代码中我的复选框的 'id'。并且保存按钮触发了 saveConfig
函数。
并且按钮必须在 a
标记内声明:
<a href="#" onClick="saveConfig(this);return true;" style="float:right;"><input id="save" type="button" value="Save"></a>
这是我的 QL 中的偏好设置 window:
瞧瞧!