Skip to content

Commit 2577da9

Browse files
committed
Added chat input history which can be cycled through with the up and down arrow keys. Added Notification preferences, you can now change the text size and base position of notifications and preview how they will be displayed. Some general code cleanup.
1 parent c03cf8b commit 2577da9

9 files changed

Lines changed: 337 additions & 41 deletions

decline/AppDelegate.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#import "PreferencesWindowController.h"
1212
#import "HotlineClient.h"
1313

14+
extern NSUserDefaults *defaults;
15+
1416
@interface AppDelegate : NSObject <NSApplicationDelegate>
1517

1618
@property (strong) PreferencesWindowController *preferencesWindowController;

decline/AppDelegate.m

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ @interface AppDelegate ()
2929

3030
@end
3131

32+
NSUserDefaults *defaults;
33+
3234
@implementation AppDelegate
3335

3436
- (IBAction)showPreferences:(id)sender {
@@ -233,21 +235,25 @@ - (void)applicationDidFinishLaunching:(NSNotification *)notification {
233235
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
234236
[NSApp activateIgnoringOtherApps:YES];
235237

236-
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
237-
238+
defaults = [NSUserDefaults standardUserDefaults];
239+
238240
[defaults registerDefaults:@{
239241
@"DefaultNick": @"decline n00b",
240242
@"DefaultIcon": @"148",
241243
@"ShowJoinLeaveMessages": @NO,
242244
@"ShowNickChangeMessages": @NO,
243245
@"ShowUserlistOnRightSide" : @YES,
244-
@"ShowChatSendButton" : @NO
246+
@"ShowChatSendButton" : @NO,
247+
@"NotificationsEnabled" : @YES,
248+
@"NotificationTextSize" : @(NotificationTextSizeMedium),
249+
@"NotificationPosition" : @(NotificationPositionCenter),
250+
@"NotificationSticky" : @YES
245251
}];
246252

247253
//Nuke prefs
248254
/*NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
249-
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];
250-
[[NSUserDefaults standardUserDefaults] synchronize];*/
255+
[defaults removePersistentDomainForName:appDomain];
256+
[defaults synchronize];*/
251257

252258
[AppIconManager updateDockIconToMatchCurrentAppearance];
253259

decline/HotlineClient.h

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
#import "FileTransferManager.h"
3636
#import "UserTransactions.h"
3737

38-
@interface HotlineClient : NSObject <NSStreamDelegate, NSToolbarDelegate, NSToolbarItemValidation, NSTableViewDataSource, NSTableViewDelegate, NSComboBoxDelegate, NSWindowDelegate, FileTransferManagerDelegate>
38+
@interface HotlineClient : NSObject <NSControlTextEditingDelegate, NSStreamDelegate, NSToolbarDelegate, NSToolbarItemValidation, NSTableViewDataSource, NSTableViewDelegate, NSComboBoxDelegate, NSWindowDelegate, FileTransferManagerDelegate>
3939

4040
// uuid
4141
@property (nonatomic, strong) NSUUID *uuid;
@@ -69,6 +69,11 @@
6969
@property (strong) NSTextView *chatTextView;
7070
@property (strong) NSTableView *userListView;
7171
@property (strong) NSTextField *messageField;
72+
73+
@property (nonatomic) NSMutableArray<NSString *> *history;
74+
@property (nonatomic) NSInteger historyIndex; // == history.count means "live buffer"
75+
@property (nonatomic) NSString *stash; // stash current typing before history
76+
7277
@property (strong) NSButton *sendButton;
7378

7479
// News UI

decline/Preferences/GeneralPreferencesViewController.m

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,25 @@ @interface GeneralPreferencesViewController ()
1111

1212
@implementation GeneralPreferencesViewController
1313

14+
- (void)viewDidLayout {
15+
[super viewDidLayout];
16+
17+
// Compute once the view has a real layout
18+
NSView *v = self.view;
19+
NSSize s = v.translatesAutoresizingMaskIntoConstraints ? v.bounds.size : v.fittingSize;
20+
if (s.width < 400) s.width = 400; // optional floor
21+
if (s.height < 250) s.height = 250; // optional floor
22+
23+
// Only update if it actually changed (prevents resize loops)
24+
if (!NSEqualSizes(self.preferredContentSize, s)) {
25+
self.preferredContentSize = s; // <-- use the property, don't override the method
26+
}
27+
}
28+
1429
- (void)loadView {
1530
NSView *v = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 400, 250)];
1631
v.translatesAutoresizingMaskIntoConstraints = NO;
1732
self.view = v;
18-
19-
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
20-
// ensure defaults are registered (in case showPreferences called first)
2133

2234
// --- Default Nick ---
2335
NSTextField *nickLabel = [NSTextField labelWithString:@"Default Nick:"];
@@ -113,56 +125,56 @@ - (void)viewWillAppear {
113125
[super viewWillAppear];
114126

115127
// Populate its initial state from defaults
116-
BOOL showJoin = [[NSUserDefaults standardUserDefaults] boolForKey:@"ShowJoinLeaveMessages"];
128+
BOOL showJoin = [defaults boolForKey:@"ShowJoinLeaveMessages"];
117129
self.joinLeaveCheckbox.state = showJoin ? NSControlStateValueOn : NSControlStateValueOff;
118130

119-
BOOL showNick = [[NSUserDefaults standardUserDefaults] boolForKey:@"ShowNickChangeMessages"];
131+
BOOL showNick = [defaults boolForKey:@"ShowNickChangeMessages"];
120132
self.nickChangeCheckbox.state = showNick ? NSControlStateValueOn : NSControlStateValueOff;
121133

122-
BOOL showRight = [[NSUserDefaults standardUserDefaults] boolForKey:@"ShowUserlistOnRightSide"];
134+
BOOL showRight = [defaults boolForKey:@"ShowUserlistOnRightSide"];
123135
self.showUserlistRightCheckbox.state = showRight ? NSControlStateValueOn : NSControlStateValueOff;
124136

125-
BOOL showSend = [[NSUserDefaults standardUserDefaults] boolForKey:@"ShowChatSendButton"];
137+
BOOL showSend = [defaults boolForKey:@"ShowChatSendButton"];
126138
self.showChatSendButtonCheckbox.state = showSend ? NSControlStateValueOn : NSControlStateValueOff;
127139
}
128140

129141
- (void)defaultNickChanged:(NSTextField *)sender {
130-
[[NSUserDefaults standardUserDefaults] setObject:sender.stringValue forKey:@"DefaultNick"];
131-
[[NSUserDefaults standardUserDefaults] synchronize];
142+
[defaults setObject:sender.stringValue forKey:@"DefaultNick"];
143+
[defaults synchronize];
132144
}
133145

134146
- (void)defaultIconChanged:(NSTextField *)sender {
135-
[[NSUserDefaults standardUserDefaults] setObject:sender.stringValue forKey:@"DefaultIcon"];
136-
[[NSUserDefaults standardUserDefaults] synchronize];
147+
[defaults setObject:sender.stringValue forKey:@"DefaultIcon"];
148+
[defaults synchronize];
137149
}
138150

139151
#pragma mark – Action
140152

141153
- (void)toggleJoinLeave:(NSButton*)sender {
142154
BOOL newVal = (sender.state == NSControlStateValueOn);
143-
[[NSUserDefaults standardUserDefaults] setBool:newVal forKey:@"ShowJoinLeaveMessages"];
144-
[[NSUserDefaults standardUserDefaults] synchronize];
155+
[defaults setBool:newVal forKey:@"ShowJoinLeaveMessages"];
156+
[defaults synchronize];
145157
}
146158

147159
- (void)toggleNickChange:(NSButton*)sender {
148160
BOOL newVal = (sender.state == NSControlStateValueOn);
149-
[[NSUserDefaults standardUserDefaults] setBool:newVal forKey:@"ShowNickChangeMessages"];
150-
[[NSUserDefaults standardUserDefaults] synchronize];
161+
[defaults setBool:newVal forKey:@"ShowNickChangeMessages"];
162+
[defaults synchronize];
151163
}
152164

153165
- (void)toggleUserlistSide:(NSButton*)sender {
154166
BOOL newVal = (sender.state == NSControlStateValueOn);
155-
[[NSUserDefaults standardUserDefaults] setBool:newVal forKey:@"ShowUserlistOnRightSide"];
156-
[[NSUserDefaults standardUserDefaults] synchronize];
167+
[defaults setBool:newVal forKey:@"ShowUserlistOnRightSide"];
168+
[defaults synchronize];
157169

158170
AppDelegate *appDel = (AppDelegate *)[NSApp delegate];
159171
[appDel updateChatView];
160172
}
161173

162174
- (void)toggleSendButton:(NSButton*)sender {
163175
BOOL newVal = (sender.state == NSControlStateValueOn);
164-
[[NSUserDefaults standardUserDefaults] setBool:newVal forKey:@"ShowChatSendButton"];
165-
[[NSUserDefaults standardUserDefaults] synchronize];
176+
[defaults setBool:newVal forKey:@"ShowChatSendButton"];
177+
[defaults synchronize];
166178

167179
AppDelegate *appDel = (AppDelegate *)[NSApp delegate];
168180
[appDel updateChatView];
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// NotificationsPreferencesViewController.h
3+
// decline
4+
//
5+
// Created by Derek Scott on 8/26/25.
6+
//
7+
8+
#import <Cocoa/Cocoa.h>
9+
10+
NS_ASSUME_NONNULL_BEGIN
11+
12+
@interface NotificationsPreferencesViewController : NSViewController
13+
@end
14+
15+
NS_ASSUME_NONNULL_END
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// PreferencesWindowController.h
22
#import <Cocoa/Cocoa.h>
33

4-
@interface PreferencesWindowController : NSWindowController
4+
NS_ASSUME_NONNULL_BEGIN
5+
6+
@interface PreferencesWindowController : NSWindowController <NSToolbarDelegate>
57
+ (instancetype)sharedController;
8+
- (void)showGeneralPane; // convenience if you want to jump directly
69
@end
10+
11+
NS_ASSUME_NONNULL_END
Lines changed: 156 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,173 @@
11
// PreferencesWindowController.m
22
#import "PreferencesWindowController.h"
33
#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
417

518
@implementation PreferencesWindowController
19+
620
+ (instancetype)sharedController {
721
static PreferencesWindowController *shared;
822
static dispatch_once_t onceToken;
923
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)
1929
backing:NSBackingStoreBuffered
2030
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;
2137

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
2838
shared = [[self alloc] initWithWindow:win];
39+
[shared setupToolbarAndPanes];
40+
[shared centerInitial];
2941
});
3042
return shared;
3143
}
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+
32173
@end

0 commit comments

Comments
 (0)