|
1 | 1 | // PreferencesWindowController.m |
2 | 2 | #import "PreferencesWindowController.h" |
3 | 3 | #import "GeneralPreferencesViewController.h" |
| 4 | +#import "NotificationsPreferencesViewController.h" |
| 5 | +#import "AppDelegate.h" |
| 6 | + |
| 7 | +static NSString * const kPrefsToolbarID = @"la.dubs.decline.preferences.toolbar"; |
| 8 | +static NSString * const kPaneIDGeneral = @"general"; |
| 9 | +static NSString * const kPaneIDNotifications = @"notifications"; |
| 10 | +static NSString * const kLastSelectedPaneDefaultsKey = @"prefs.lastSelectedPane"; |
| 11 | + |
| 12 | +@interface PreferencesWindowController () |
| 13 | +@property (nonatomic, strong) NSToolbar *toolbar; |
| 14 | +@property (nonatomic, strong) NSDictionary<NSString *, NSViewController *> *panes; |
| 15 | +@property (nonatomic, copy) NSString *currentPaneID; |
| 16 | +@end |
4 | 17 |
|
5 | 18 | @implementation PreferencesWindowController |
| 19 | + |
6 | 20 | + (instancetype)sharedController { |
7 | 21 | static PreferencesWindowController *shared; |
8 | 22 | static dispatch_once_t onceToken; |
9 | 23 | dispatch_once(&onceToken, ^{ |
10 | | - // Create your prefs view controller |
11 | | - GeneralPreferencesViewController *vc = [[GeneralPreferencesViewController alloc] init]; |
12 | | - |
13 | | - // Make an NSWindow with the desired size & style |
14 | | - NSRect frame = NSMakeRect(0, 0, 400, 250); |
15 | | - NSUInteger style = NSWindowStyleMaskTitled |
16 | | - | NSWindowStyleMaskClosable; |
17 | | - NSWindow *win = [[NSWindow alloc] initWithContentRect:frame |
18 | | - styleMask:style |
| 24 | + // Create the window itself |
| 25 | + NSWindow *win = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 400, 250) |
| 26 | + styleMask:(NSWindowStyleMaskTitled |
| 27 | + | NSWindowStyleMaskClosable |
| 28 | + | NSWindowStyleMaskMiniaturizable) |
19 | 29 | backing:NSBackingStoreBuffered |
20 | 30 | defer:NO]; |
| 31 | + if (@available(macOS 11.0, *)) { |
| 32 | + win.toolbarStyle = NSWindowToolbarStylePreference; // <<< gives the pill look |
| 33 | + win.titlebarSeparatorStyle = NSTitlebarSeparatorStyleAutomatic; // remove hairline |
| 34 | + } |
| 35 | + win.releasedWhenClosed = NO; |
| 36 | + win.animationBehavior = NSWindowAnimationBehaviorDocumentWindow; |
21 | 37 |
|
22 | | - // Embed your VC in that window |
23 | | - win.contentViewController = vc; |
24 | | - win.title = @"Preferences"; |
25 | | - [win center]; |
26 | | - |
27 | | - // Finally hook it up to your NSWindowController |
28 | 38 | shared = [[self alloc] initWithWindow:win]; |
| 39 | + [shared setupToolbarAndPanes]; |
| 40 | + [shared centerInitial]; |
29 | 41 | }); |
30 | 42 | return shared; |
31 | 43 | } |
| 44 | + |
| 45 | +- (void)setupToolbarAndPanes { |
| 46 | + // Build panes |
| 47 | + GeneralPreferencesViewController *generalVC = [[GeneralPreferencesViewController alloc] init]; |
| 48 | + NotificationsPreferencesViewController *notifVC = [[NotificationsPreferencesViewController alloc] init]; |
| 49 | + |
| 50 | + self.panes = @{ |
| 51 | + kPaneIDGeneral: generalVC, |
| 52 | + kPaneIDNotifications: notifVC, |
| 53 | + }; |
| 54 | + |
| 55 | + // Toolbar |
| 56 | + self.toolbar = [[NSToolbar alloc] initWithIdentifier:kPrefsToolbarID]; |
| 57 | + self.toolbar.delegate = self; |
| 58 | + self.toolbar.allowsUserCustomization = NO; |
| 59 | + self.toolbar.autosavesConfiguration = NO; |
| 60 | + self.toolbar.displayMode = NSToolbarDisplayModeIconAndLabel; // classic Xcode-like look |
| 61 | + self.toolbar.sizeMode = NSToolbarSizeModeDefault; |
| 62 | + self.window.toolbar = self.toolbar; |
| 63 | + |
| 64 | + // Select last pane or default to General |
| 65 | + NSString *last = [defaults stringForKey:kLastSelectedPaneDefaultsKey] ?: kPaneIDGeneral; |
| 66 | + [self switchToPane:last animate:NO]; |
| 67 | + [self.toolbar setSelectedItemIdentifier:last]; |
| 68 | +} |
| 69 | + |
| 70 | +- (void)centerInitial { |
| 71 | + [self.window center]; |
| 72 | +} |
| 73 | + |
| 74 | +#pragma mark - Public API |
| 75 | + |
| 76 | +- (void)showGeneralPane { |
| 77 | + [self showWindow:nil]; |
| 78 | + [self.toolbar setSelectedItemIdentifier:kPaneIDGeneral]; |
| 79 | + [self switchToPane:kPaneIDGeneral animate:YES]; |
| 80 | +} |
| 81 | + |
| 82 | +#pragma mark - Switching |
| 83 | + |
| 84 | +- (void)switchToPane:(NSString *)paneID animate:(BOOL)animate { |
| 85 | + NSViewController *vc = self.panes[paneID]; |
| 86 | + if (!vc) return; |
| 87 | + |
| 88 | + // Make sure the view exists and is laid out so preferredContentSize is meaningful |
| 89 | + (void)vc.view; |
| 90 | + [vc.view layoutSubtreeIfNeeded]; |
| 91 | + |
| 92 | + NSSize target = vc.preferredContentSize; |
| 93 | + if (NSEqualSizes(target, NSZeroSize)) { |
| 94 | + // Fallbacks if the VC hasn't reported one yet |
| 95 | + target = vc.view.translatesAutoresizingMaskIntoConstraints ? vc.view.bounds.size : vc.view.fittingSize; |
| 96 | + if (NSEqualSizes(target, NSZeroSize)) target = NSMakeSize(350, 250); |
| 97 | + } |
| 98 | + |
| 99 | + NSRect contentRect = NSMakeRect(0, 0, target.width, target.height); |
| 100 | + NSRect newFrame = [self.window frameRectForContentRect:contentRect]; |
| 101 | + |
| 102 | + // Keep top edge fixed |
| 103 | + NSRect cur = self.window.frame; |
| 104 | + newFrame.origin.x = cur.origin.x; |
| 105 | + newFrame.origin.y = NSMaxY(cur) - newFrame.size.height; |
| 106 | + |
| 107 | + if (animate) [self.window setFrame:newFrame display:YES animate:YES]; |
| 108 | + else [self.window setFrame:newFrame display:YES]; |
| 109 | + |
| 110 | + self.window.contentViewController = vc; |
| 111 | + |
| 112 | + if([paneID isEqualTo:kPaneIDNotifications]) { |
| 113 | + self.window.title = @"Notifications"; |
| 114 | + } |
| 115 | + |
| 116 | + else { |
| 117 | + self.window.title = @"General"; |
| 118 | + } |
| 119 | + |
| 120 | + [self.toolbar setSelectedItemIdentifier:paneID]; |
| 121 | +} |
| 122 | + |
| 123 | +#pragma mark - NSToolbarDelegate |
| 124 | + |
| 125 | +- (NSArray<NSToolbarItemIdentifier> *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar { |
| 126 | + return @[kPaneIDGeneral, kPaneIDNotifications]; |
| 127 | +} |
| 128 | + |
| 129 | +- (NSArray<NSToolbarItemIdentifier> *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar { |
| 130 | + return @[kPaneIDGeneral, kPaneIDNotifications]; |
| 131 | +} |
| 132 | + |
| 133 | +- (NSArray<NSToolbarItemIdentifier> *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar { |
| 134 | + return @[kPaneIDGeneral, kPaneIDNotifications]; |
| 135 | +} |
| 136 | + |
| 137 | +- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar |
| 138 | + itemForItemIdentifier:(NSToolbarItemIdentifier)itemIdentifier |
| 139 | + willBeInsertedIntoToolbar:(BOOL)flag |
| 140 | +{ |
| 141 | + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier]; |
| 142 | + |
| 143 | + if ([itemIdentifier isEqualToString:kPaneIDGeneral]) { |
| 144 | + item.label = @"General"; |
| 145 | + item.paletteLabel = @"General"; |
| 146 | + item.target = self; |
| 147 | + item.action = @selector(onToolbarSelect:); |
| 148 | + if (@available(macOS 11.0, *)) { |
| 149 | + item.image = [NSImage imageWithSystemSymbolName:@"gearshape" accessibilityDescription:@"General"]; |
| 150 | + } else { |
| 151 | + item.image = [NSImage imageNamed:NSImageNamePreferencesGeneral]; |
| 152 | + } |
| 153 | + } else if ([itemIdentifier isEqualToString:kPaneIDNotifications]) { |
| 154 | + item.label = @"Notifications"; |
| 155 | + item.paletteLabel = @"Notifications"; |
| 156 | + item.target = self; |
| 157 | + item.action = @selector(onToolbarSelect:); |
| 158 | + if (@available(macOS 11.0, *)) { |
| 159 | + item.image = [NSImage imageWithSystemSymbolName:@"bell" accessibilityDescription:@"Notifications"]; |
| 160 | + } else { |
| 161 | + item.image = [NSImage imageNamed:NSImageNameCaution]; // fallback icon |
| 162 | + } |
| 163 | + } |
| 164 | + |
| 165 | + return item; |
| 166 | +} |
| 167 | + |
| 168 | +- (void)onToolbarSelect:(NSToolbarItem *)sender { |
| 169 | + NSString *paneID = sender.itemIdentifier; |
| 170 | + [self switchToPane:paneID animate:YES]; |
| 171 | +} |
| 172 | + |
32 | 173 | @end |
0 commit comments