Skip to content

Commit 2f63de4

Browse files
committed
Added global notifications system
1 parent 1f6c33a commit 2f63de4

8 files changed

Lines changed: 195 additions & 13 deletions

File tree

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,42 @@
527527
6CAAF69429BCD78600A1F48A /* (null) in Sources */,
528528
58F2EB03292FB2B0004A9BDE /* Documentation.docc in Sources */,
529529
6CB9144B29BEC7F100BC47F2 /* (null) in Sources */,
530+
587B9E7429301D8F00AC7927 /* URL+URLParameters.swift in Sources */,
531+
61538B902B111FE800A88846 /* String+AppearancesOfSubstring.swift in Sources */,
532+
581BFB6B2926431000D251EC /* RecentProjectListItem.swift in Sources */,
533+
587FB99029C1246400B519DD /* EditorTabView.swift in Sources */,
534+
587B9DA429300ABD00AC7927 /* SearchPanel.swift in Sources */,
535+
58D01C95293167DC00C5B6B4 /* Bundle+Info.swift in Sources */,
536+
B6C6A42A297716A500A3D28F /* EditorTabCloseButton.swift in Sources */,
537+
B60718422B17DB93009CDAB4 /* SourceControlNavigatorRepositoryView+outlineGroupData.swift in Sources */,
538+
58A5DF7D2931787A00D1BD5D /* ShellClient.swift in Sources */,
539+
5879821A292D92370085B254 /* SearchResultModel.swift in Sources */,
540+
B6F0517729D9E3AD00D72287 /* SourceControlGeneralView.swift in Sources */,
541+
587B9E8929301D8F00AC7927 /* GitHubGist.swift in Sources */,
542+
0485EB1F27E7458B00138301 /* EditorAreaFileView.swift in Sources */,
543+
6C092EDA2A53A58600489202 /* EditorLayout+StateRestoration.swift in Sources */,
544+
6C092EE02A53BFCF00489202 /* WorkspaceStateKey.swift in Sources */,
545+
618725A82C29F05500987354 /* OptionMenuItemView.swift in Sources */,
546+
613899B52B6E700300A5CAF6 /* FuzzySearchModels.swift in Sources */,
547+
6C9AE66F2D148DD200FAE8D2 /* URL+FindWorkspace.swift in Sources */,
548+
58D01C94293167DC00C5B6B4 /* Color+HEX.swift in Sources */,
549+
6C578D8729CD345900DC73B2 /* ExtensionSceneView.swift in Sources */,
550+
617DB3D02C25AFAE00B58BFE /* TaskNotificationHandler.swift in Sources */,
551+
77EF6C052C57DE4B00984B69 /* URL+ResouceValues.swift in Sources */,
552+
B68DE5E72D5A7D62009A43EF /* NotificationBannerEnvironment.swift in Sources */,
553+
B640A9A129E2188F00715F20 /* View+NavigationBarBackButtonVisible.swift in Sources */,
554+
587B9E7929301D8F00AC7927 /* GitHubIssueRouter.swift in Sources */,
555+
B67700F92D2A2662004FD61F /* WorkspacePanelView.swift in Sources */,
556+
587B9E8029301D8F00AC7927 /* GitHubConfiguration.swift in Sources */,
557+
B616EA8F2D662E9800DF9029 /* InternalDevelopmentNotificationsView.swift in Sources */,
558+
58822524292C280D00E83CDE /* StatusBarView.swift in Sources */,
559+
581550D429FBD37D00684881 /* ProjectNavigatorToolbarBottom.swift in Sources */,
560+
66AF6CE72BF17FFB00D83C9D /* UpdateStatusBarInfo.swift in Sources */,
561+
587B9E7E29301D8F00AC7927 /* GitHubGistRouter.swift in Sources */,
562+
B6E38E022CD3E63A00F4E65A /* GitConfigClient.swift in Sources */,
563+
6C3E12D62CC8388000DD12F1 /* URL+componentCompare.swift in Sources */,
564+
B6AB09A52AAAC00F0003A3A6 /* EditorTabBarTrailingAccessories.swift in Sources */,
565+
04BA7C0B2AE2A2D100584E1C /* GitBranch.swift in Sources */,
530566
6CAAF69229BCC71C00A1F48A /* (null) in Sources */,
531567
6CAAF68A29BC9C2300A1F48A /* (null) in Sources */,
532568
);

CodeEdit/AppDelegate.swift

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
2525
checkForFilesToOpen()
2626

2727
NSApp.closeWindow(.welcome, .about)
28-
28+
29+
// Add test notification
30+
DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {
31+
NotificationManager.shared.post(
32+
icon: "bell.badge",
33+
title: "Welcome to CodeEdit",
34+
description: "This is a test notification to demonstrate the notification system.",
35+
actionButtonTitle: "Learn More",
36+
action: {
37+
print("Action button clicked!")
38+
}
39+
)
40+
}
41+
2942
DispatchQueue.main.async {
3043
var needToHandleOpen = true
31-
44+
3245
// If no windows were reopened by NSQuitAlwaysKeepsWindows, do default behavior.
3346
// Non-WindowGroup SwiftUI Windows are still in NSApp.windows when they are closed,
3447
// So we need to think about those.
@@ -60,30 +73,30 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
6073
}
6174

6275
func applicationWillTerminate(_ aNotification: Notification) {
63-
76+
6477
}
65-
78+
6679
func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool {
6780
true
6881
}
69-
82+
7083
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
7184
guard flag else {
7285
handleOpen()
7386
return false
7487
}
75-
88+
7689
/// Check if all windows are either miniaturized or not visible.
7790
/// If so, attempt to find the first miniaturized window and deminiaturize it.
7891
guard sender.windows.allSatisfy({ $0.isMiniaturized || !$0.isVisible }) else { return false }
7992
sender.windows.first(where: { $0.isMiniaturized })?.deminiaturize(sender)
8093
return false
8194
}
82-
95+
8396
func applicationShouldOpenUntitledFile(_ sender: NSApplication) -> Bool {
8497
false
8598
}
86-
99+
87100
func handleOpen() {
88101
let behavior = Settings.shared.preferences.general.reopenBehavior
89102
switch behavior {
@@ -97,15 +110,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
97110
CodeEditDocumentController.shared.newDocument(self)
98111
}
99112
}
100-
113+
101114
/// Handle urls with the form `codeedit://file/{filepath}:{line}:{column}`
102115
func application(_ application: NSApplication, open urls: [URL]) {
103116
for url in urls {
104117
let file = URL(fileURLWithPath: url.path).path.split(separator: ":")
105118
let filePath = URL(fileURLWithPath: String(file[0]))
106119
let line = file.count > 1 ? Int(file[1]) ?? 0 : 0
107120
let column = file.count > 2 ? Int(file[2]) ?? 1 : 1
108-
121+
109122
CodeEditDocumentController.shared
110123
.openDocument(withContentsOf: filePath, display: true) { document, _, error in
111124
if let error {
@@ -117,10 +130,23 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
117130
cursorPositions: [CursorPosition(line: line, column: column > 0 ? column : 1)]
118131
)
119132
}
133+
// Add notification when workspace is opened via URL
134+
if let workspaceDoc = document as? WorkspaceDocument {
135+
NotificationManager.shared.post(
136+
icon: "folder.badge.plus",
137+
title: "Workspace Opened",
138+
description: "Successfully opened workspace: \(workspaceDoc.fileURL?.lastPathComponent ?? "")",
139+
actionButtonTitle: "View Files",
140+
action: {
141+
// Ensure the workspace window is frontmost
142+
workspaceDoc.windowControllers.first?.window?.makeKeyAndOrderFront(nil)
143+
}
144+
)
145+
}
120146
}
121147
}
122148
}
123-
149+
124150
/// Defers the application terminate message until we've finished cleanup.
125151
///
126152
/// All paths _must_ call `NSApplication.shared.reply(toApplicationShouldTerminate: true)` as soon as possible.
@@ -138,9 +164,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, ObservableObject {
138164
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
139165
let projects: [String] = CodeEditDocumentController.shared.documents
140166
.compactMap { ($0 as? WorkspaceDocument)?.fileURL?.path }
141-
167+
142168
UserDefaults.standard.set(projects, forKey: AppDelegate.recoverWorkspacesKey)
143-
169+
144170
let areAllDocumentsClean = CodeEditDocumentController.shared.documents.allSatisfy { !$0.isDocumentEdited }
145171
guard areAllDocumentsClean else {
146172
CodeEditDocumentController.shared.closeAllDocuments(
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

CodeEdit/Features/InspectorArea/FileInspector/FileInspectorView.swift

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ struct FileInspectorView: View {
5959
widthOptions
6060
wrapLinesToggle
6161
}
62+
Section {
63+
Button("Add Test Notification") {
64+
addTestNotification()
65+
}
66+
Button("Add Test Notification After Delay") {
67+
addTestNotificationAfterDelay()
68+
}
69+
}
6270
}
6371
} else {
6472
NoSelectionInspectorView()
@@ -81,6 +89,24 @@ struct FileInspectorView: View {
8189
}
8290
}
8391

92+
func addTestNotification () {
93+
NotificationManager.shared.post(
94+
icon: "bell",
95+
title: "New Notification Created",
96+
description: "Successfully created new notification",
97+
actionButtonTitle: "Action",
98+
action: {
99+
print("Action taken")
100+
}
101+
)
102+
}
103+
104+
func addTestNotificationAfterDelay () {
105+
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
106+
addTestNotification()
107+
}
108+
}
109+
84110
@ViewBuilder private var fileNameField: some View {
85111
if let file {
86112
TextField("Name", text: $fileName)
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
struct FileInspector: View {
2+
var body: some View {
3+
List {
4+
Section("Testing") {
5+
Button("Test Notification (3s)") {
6+
NotificationManager.shared.testNotification()
7+
}
8+
.buttonStyle(.borderless)
9+
}
10+
}
11+
.listStyle(.inset)
12+
}
13+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import SwiftUI
2+
3+
struct NotificationListView: View {
4+
@ObservedObject private var notificationManager = NotificationManager.shared
5+
@Namespace private var animation
6+
7+
var body: some View {
8+
ScrollView {
9+
VStack(spacing: 10) {
10+
if notificationManager.notifications.isEmpty {
11+
Text("No notifications")
12+
.foregroundColor(.secondary)
13+
.padding()
14+
} else {
15+
ForEach(notificationManager.notifications) { notification in
16+
NotificationBannerView(
17+
notification: notification,
18+
namespace: animation,
19+
onDismiss: {
20+
withAnimation(.easeInOut(duration: 0.2)) {
21+
notificationManager.dismissNotification(notification)
22+
}
23+
},
24+
onAction: {
25+
withAnimation(.easeInOut(duration: 0.2)) {
26+
notification.action()
27+
notificationManager.dismissNotification(notification)
28+
}
29+
}
30+
)
31+
.environment(\.isOverlay, false)
32+
.environment(\.isSingleListItem, notificationManager.notifications.count == 1)
33+
.transition(.opacity.combined(with: .move(edge: .trailing)))
34+
}
35+
}
36+
}
37+
.padding(notificationManager.notifications.count == 1 ? 0 : 10)
38+
.animation(.easeInOut(duration: 0.2), value: notificationManager.notifications)
39+
}
40+
}
41+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import SwiftUI
2+
3+
struct NotificationOverlayView: View {
4+
@ObservedObject private var notificationManager = NotificationManager.shared
5+
@Namespace private var animation
6+
@Environment(\.controlActiveState) private var controlActiveState
7+
8+
var body: some View {
9+
VStack(spacing: 10) {
10+
ForEach(Array([notificationManager.activeNotification].compactMap { $0 }), id: \.id) { notification in
11+
if controlActiveState == .active || controlActiveState == .key {
12+
NotificationBannerView(
13+
notification: notification,
14+
namespace: animation,
15+
onDismiss: {
16+
notificationManager.dismissNotification(notification)
17+
},
18+
onAction: {
19+
notification.action()
20+
notificationManager.dismissNotification(notification)
21+
}
22+
)
23+
.environment(\.isOverlay, true)
24+
.transition(
25+
.asymmetric(
26+
insertion: .opacity.combined(with: .move(edge: .bottom)),
27+
removal: .opacity.combined(with: .move(edge: .top))
28+
)
29+
)
30+
}
31+
}
32+
}
33+
.padding(8)
34+
.animation(.easeInOut(duration: 0.2), value: notificationManager.activeNotification?.id)
35+
}
36+
}

CodeEdit/WorkspaceView.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ struct WorkspaceView: View {
5353
focus: $focusedEditor
5454
)
5555
.frame(maxWidth: .infinity, maxHeight: .infinity)
56+
.overlay(alignment: .topTrailing) {
57+
NotificationOverlayView()
58+
}
5659
.onChange(of: geo.size.height) { newHeight in
5760
editorsHeight = newHeight
5861
}

0 commit comments

Comments
 (0)