From b961a3a56c7324172da537dc5f98b722f136f833 Mon Sep 17 00:00:00 2001 From: relikd Date: Mon, 19 Aug 2019 23:30:56 +0200 Subject: [PATCH] Make me default RSS reader (sandbox compatible) --- CHANGELOG.md | 2 + baRSS/Constants.h | 2 + baRSS/Helper/NSView+Ext.h | 1 + baRSS/Helper/NSView+Ext.m | 8 ++ baRSS/Info.plist | 2 +- .../Preferences/General Tab/SettingsGeneral.h | 2 +- .../Preferences/General Tab/SettingsGeneral.m | 125 ++++++------------ .../General Tab/SettingsGeneralView.h | 2 +- .../General Tab/SettingsGeneralView.m | 22 ++- 9 files changed, 62 insertions(+), 104 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 49dd993..8e0042d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2 - *Settings, Feeds:* Drag & Drop feeds from / to OPML file - *Settings, Feeds:* Drag & Drop feed titles and urls as text - *Settings, Feeds:* OPML export with selected items only +- *Settings, General*: [Auxiliary application](https://github.com/relikd/URL-Scheme-Defaults) for changing default feed reader - *UI:* Accessibility hints for most UI elements - *UI*: Show welcome message upon first usage (empty db) - Welcome message also adds Github releases feed @@ -46,6 +47,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2 - *Settings, Feeds:* Single add button for feeds, groups, and separators - *Settings, Feeds:* Always append new items at the end - *Settings, General*: Moved `Fix cache` button to `About` text section +- *Settings, General*: Changing default feed reader is prohibited within sandbox - *Status Bar Menu*: Show `(no title)` instead of `(error)` - *Status Bar Menu*: `Update all feeds` will show error alerts for broken URLs - *UI:* Interface builder files replaced with code equivalent diff --git a/baRSS/Constants.h b/baRSS/Constants.h index e0665bf..24bd08d 100644 --- a/baRSS/Constants.h +++ b/baRSS/Constants.h @@ -34,6 +34,8 @@ static NSPasteboardType const UTI_OPML = @"org.opml"; /// URL with newest baRSS releases. Automatically added when user starts baRSS for the first time. static NSString* const versionUpdateURL = @"https://github.com/relikd/baRSS/releases.atom"; +/// URL to help page of auxiliary application "URL Scheme Defaults" +static NSString* const auxiliaryAppURL = @"https://github.com/relikd/URL-Scheme-Defaults#url-scheme-defaults"; #pragma mark - NSImageName constants diff --git a/baRSS/Helper/NSView+Ext.h b/baRSS/Helper/NSView+Ext.h index 3909fa5..29696c1 100644 --- a/baRSS/Helper/NSView+Ext.h +++ b/baRSS/Helper/NSView+Ext.h @@ -61,6 +61,7 @@ NS_INLINE CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.frame) + (NSButton*)button:(NSString*)text; + (NSButton*)buttonImageSquare:(nonnull NSImageName)name; + (NSButton*)buttonIcon:(nonnull NSImageName)name size:(CGFloat)size; ++ (NSButton*)helpButton; + (NSButton*)inlineButton:(NSString*)text; + (NSPopUpButton*)popupButton:(CGFloat)w; // UI: Others diff --git a/baRSS/Helper/NSView+Ext.m b/baRSS/Helper/NSView+Ext.m index 41acbca..3c76769 100644 --- a/baRSS/Helper/NSView+Ext.m +++ b/baRSS/Helper/NSView+Ext.m @@ -94,6 +94,14 @@ return btn; } +/// Create round button with question mark. @c 21x21px ++ (NSButton*)helpButton { + NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 21, 21)]; + btn.bezelStyle = NSBezelStyleHelpButton; + btn.title = @""; + return btn; +} + /// Create gray inline button with rounded corners. @c 16px height. + (NSButton*)inlineButton:(NSString*)text { NSButton *btn = [[NSButton alloc] initWithFrame: NSMakeRect(0, 0, 0, HEIGHT_INLINEBUTTON)]; diff --git a/baRSS/Info.plist b/baRSS/Info.plist index 4a513b7..8815b40 100644 --- a/baRSS/Info.plist +++ b/baRSS/Info.plist @@ -70,7 +70,7 @@ CFBundleVersion - 11159 + 11387 LSApplicationCategoryType public.app-category.news LSMinimumSystemVersion diff --git a/baRSS/Preferences/General Tab/SettingsGeneral.h b/baRSS/Preferences/General Tab/SettingsGeneral.h index d264578..b17f7db 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneral.h +++ b/baRSS/Preferences/General Tab/SettingsGeneral.h @@ -24,5 +24,5 @@ @interface SettingsGeneral : NSViewController - (void)changeHttpApplication:(NSPopUpButton *)sender; -- (void)changeDefaultRSSReader:(NSPopUpButton *)sender; +- (void)clickHowToDefaults:(NSButton *)sender; @end diff --git a/baRSS/Preferences/General Tab/SettingsGeneral.m b/baRSS/Preferences/General Tab/SettingsGeneral.m index 97f5a70..dbfee87 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneral.m +++ b/baRSS/Preferences/General Tab/SettingsGeneral.m @@ -36,102 +36,51 @@ - (void)loadView { self.view = [[SettingsGeneralView alloc] initWithController:self]; // Default http application for opening the feed urls - [self generateMenuForPopup:self.view.popupHttpApplication withScheme:@"https"]; - [self.view.popupHttpApplication insertItemWithTitle:NSLocalizedString(@"System Default", @"Default web browser application") atIndex:0]; - [self selectBundleID:[UserPrefs getHttpApplication] inPopup:self.view.popupHttpApplication]; + NSPopUpButton *pop = self.view.popupHttpApplication; + [pop removeAllItems]; + [pop addItemWithTitle:NSLocalizedString(@"System Default", @"Default web browser application")]; + NSArray *browsers = CFBridgingRelease(LSCopyAllHandlersForURLScheme(CFSTR("https"))); + for (NSString *bundleID in browsers) { + [pop addItemWithTitle: [self applicationNameForBundleId:bundleID]]; + pop.lastItem.representedObject = bundleID; + } + [pop selectItemAtIndex:[pop indexOfItemWithRepresentedObject:[UserPrefs getHttpApplication]]]; // Default RSS Reader application - [self generateMenuForPopup:self.view.popupDefaultRSSReader withScheme:@"feed"]; - [self selectBundleID:[self defaultBundleIdForScheme:@"feed"] inPopup:self.view.popupDefaultRSSReader]; + NSString *feedBundleId = CFBridgingRelease(LSCopyDefaultHandlerForURLScheme(CFSTR("feed"))); + self.view.defaultReader.objectValue = [self applicationNameForBundleId:feedBundleId]; } -#pragma mark - UI interaction with IBAction +/// Get human readable application name such as 'Safari' or 'baRSS' +- (nonnull NSString*)applicationNameForBundleId:(nonnull NSString*)bundleID { + NSString *name; + NSArray *urls = CFBridgingRelease(LSCopyApplicationURLsForBundleIdentifier((__bridge CFStringRef)bundleID, NULL)); + if (urls.count > 0) { + NSDictionary *info = CFBridgingRelease(CFBundleCopyInfoDictionaryForURL((CFURLRef)urls.firstObject)); + name = info[(NSString*)kCFBundleExecutableKey]; + } + return name ? name : bundleID; +} +#pragma mark - User interaction + +// Callback method fired when user selects a different item from popup list - (void)changeHttpApplication:(NSPopUpButton *)sender { [UserPrefs setHttpApplication:sender.selectedItem.representedObject]; } -- (void)changeDefaultRSSReader:(NSPopUpButton *)sender { - if ([self setDefaultRSSApplication:sender.selectedItem.representedObject] == NO) { - // in case anything went wrong, restore previous selection - [self selectBundleID:[self defaultBundleIdForScheme:@"feed"] inPopup:sender]; - } +// Callback method from round help button right of default feed reader text +- (void)clickHowToDefaults:(NSButton *)sender { + NSAlert *alert = [[NSAlert alloc] init]; + alert.alertStyle = NSAlertStyleInformational; + alert.messageText = NSLocalizedString(@"How to change default feed reader", nil); + alert.informativeText = NSLocalizedString(@"Unfortunately sandboxed applications are not allowed to change the default application. However, there is an auxiliary application.\n\nFollow the instructions to change the 'feed:' scheme.", nil); + [alert addButtonWithTitle:NSLocalizedString(@"Close", nil)]; + [alert addButtonWithTitle:NSLocalizedString(@"Go to download page", nil)].toolTip = auxiliaryAppURL; + [alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse returnCode) { + if (returnCode == NSAlertSecondButtonReturn) { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:auxiliaryAppURL]]; + } + }]; } -#pragma mark - Helper methods - -/** - Populate @c NSPopUpButton menu with all available application for that scheme. - - @param scheme URL scheme like @c 'feed' or @c 'https' - */ -- (void)generateMenuForPopup:(NSPopUpButton*)popup withScheme:(NSString*)scheme { - [popup removeAllItems]; - NSArray *apps = [self listOfBundleIdsForScheme:scheme]; - for (NSString *bundleID in apps) { - NSString *appName = [self applicationNameForBundleId:bundleID]; - if (!appName) - appName = bundleID; - [popup addItemWithTitle:appName]; - popup.lastItem.representedObject = bundleID; - } -} - -/** - For a given @c NSPopUpButton select the item which represents the @c bundleID. - */ -- (void)selectBundleID:(NSString*)bundleID inPopup:(NSPopUpButton*)popup { - [popup selectItemAtIndex:[popup indexOfItemWithRepresentedObject:bundleID]]; -} - -/** - Get human readable, application name from @c bundleID. - - @param bundleID as defined in @c Info.plist - @return Application name such as 'Safari' or 'baRSS' - */ -- (NSString*)applicationNameForBundleId:(NSString*)bundleID { - NSArray *urls = CFBridgingRelease(LSCopyApplicationURLsForBundleIdentifier((__bridge CFStringRef)bundleID, NULL)); - if (urls.count > 0) { - NSDictionary *info = CFBridgingRelease(CFBundleCopyInfoDictionaryForURL((CFURLRef)urls.firstObject)); - return info[(NSString*)kCFBundleExecutableKey]; - } - return nil; -} - -/** - Get a list of all installed applications supporting that URL scheme. - - @param scheme URL scheme like @c 'feed' or @c 'https' - @return Array of @c bundleIDs of installed applications supporting that url scheme. - */ -- (NSArray*)listOfBundleIdsForScheme:(NSString*)scheme { - return CFBridgingRelease(LSCopyAllHandlersForURLScheme((__bridge CFStringRef _Nonnull)(scheme))); -} - -/** - Get current default application for provided URL scheme. (e.g., ) - - @param scheme URL scheme like @c 'feed' or @c 'https' - @return @c bundleID of default application - */ -- (NSString*)defaultBundleIdForScheme:(NSString*)scheme { - return CFBridgingRelease(LSCopyDefaultHandlerForURLScheme((__bridge CFStringRef _Nonnull)(scheme))); -} - -/** - Sets the default application for @c feed:// urls. (system wide) - - @param bundleID as defined in @c Info.plist - @return Return @c YES if operation was successfull. @c NO otherwise. - */ -- (BOOL)setDefaultRSSApplication:(NSString*)bundleID { - // TODO: Does not work with sandboxing. - OSStatus s = LSSetDefaultHandlerForURLScheme(CFSTR("feed"), (__bridge CFStringRef _Nonnull)(bundleID)); - return s == 0; -} - -// Rebuild Launch Services cache -// https://eclecticlight.co/2017/08/11/launch-services-database-problems-correcting-and-rebuilding/ -// /System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -kill -r -v -apps u - @end diff --git a/baRSS/Preferences/General Tab/SettingsGeneralView.h b/baRSS/Preferences/General Tab/SettingsGeneralView.h index 13c975b..9c8258c 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneralView.h +++ b/baRSS/Preferences/General Tab/SettingsGeneralView.h @@ -25,7 +25,7 @@ @interface SettingsGeneralView : NSView @property (weak) IBOutlet NSPopUpButton* popupHttpApplication; -@property (weak) IBOutlet NSPopUpButton* popupDefaultRSSReader; +@property (weak) IBOutlet NSTextField *defaultReader; - (instancetype)initWithController:(SettingsGeneral*)controller NS_DESIGNATED_INITIALIZER; - (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE; diff --git a/baRSS/Preferences/General Tab/SettingsGeneralView.m b/baRSS/Preferences/General Tab/SettingsGeneralView.m index de98898..d8ce93c 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneralView.m +++ b/baRSS/Preferences/General Tab/SettingsGeneralView.m @@ -28,20 +28,16 @@ - (instancetype)initWithController:(SettingsGeneral*)controller { self = [super initWithFrame:NSZeroRect]; - - NSArray *lbls = @[NSLocalizedString(@"Open URLs with:", nil), - NSLocalizedString(@"Default RSS Reader:", nil)]; - NSView *labels = [[NSView labelColumn:lbls rowHeight:HEIGHT_POPUP padding:PAD_M] placeIn:self x:PAD_WIN yTop:PAD_WIN]; - CGFloat x = NSMaxX(labels.frame) + PAD_S; - - self.popupHttpApplication = [[self createPopup:x top: PAD_WIN + 1] action:@selector(changeHttpApplication:) target:controller]; - self.popupDefaultRSSReader = [[self createPopup:x top: YFromTop(self.popupHttpApplication) + PAD_M] action:@selector(changeDefaultRSSReader:) target:controller]; + // Change default feed reader application + NSTextField *l1 = [[NSView label:NSLocalizedString(@"Default feed reader:", nil)] placeIn:self x:PAD_WIN yTop:PAD_WIN + 3]; + NSButton *help = [[[NSView helpButton] action:@selector(clickHowToDefaults:) target:controller] placeIn:self xRight:PAD_WIN yTop:PAD_WIN]; + self.defaultReader = [[[[NSView label:@""] bold] placeIn:self x:NSMaxX(l1.frame) + PAD_S yTop:PAD_WIN + 3] sizeToRight:NSWidth(help.frame) + PAD_WIN]; + // Popup button 'Open URLs with:' + CGFloat y = YFromTop(help) + PAD_M; + NSTextField *l2 = [[NSView label:NSLocalizedString(@"Open URLs with:", nil)] placeIn:self x:PAD_WIN yTop:y + 1]; + self.popupHttpApplication = [[[[NSView popupButton:0] placeIn:self x:NSMaxX(l2.frame) + PAD_S yTop:y] sizeToRight:PAD_WIN] + action:@selector(changeHttpApplication:) target:controller]; return self; } -/// Helper method to create sizable popup button -- (NSPopUpButton*)createPopup:(CGFloat)x top:(CGFloat)y { - return [[[NSView popupButton:0] placeIn:self x:x yTop:y] sizeToRight:PAD_WIN]; -} - @end