Skip to content

Commit c9b32d7

Browse files
committed
Added global notifications system
1 parent abced05 commit c9b32d7

9 files changed

Lines changed: 181 additions & 13 deletions

File tree

CodeEdit.xcodeproj/project.pbxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4471,6 +4471,7 @@
44714471
6C578D8729CD345900DC73B2 /* ExtensionSceneView.swift in Sources */,
44724472
617DB3D02C25AFAE00B58BFE /* TaskNotificationHandler.swift in Sources */,
44734473
77EF6C052C57DE4B00984B69 /* URL+ResouceValues.swift in Sources */,
4474+
B68DE5E72D5A7D62009A43EF /* NotificationBannerEnvironment.swift in Sources */,
44744475
B640A9A129E2188F00715F20 /* View+NavigationBarBackButtonVisible.swift in Sources */,
44754476
587B9E7929301D8F00AC7927 /* GitHubIssueRouter.swift in Sources */,
44764477
B67700F92D2A2662004FD61F /* WorkspacePanelView.swift in Sources */,

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: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import SwiftUI
2+
3+
struct IsOverlayKey: EnvironmentKey {
4+
static let defaultValue: Bool = false
5+
}
6+
7+
struct IsSingleListItemKey: EnvironmentKey {
8+
static let defaultValue: Bool = false
9+
}
10+
11+
extension EnvironmentValues {
12+
var isOverlay: Bool {
13+
get { self[IsOverlayKey.self] }
14+
set { self[IsOverlayKey.self] = newValue }
15+
}
16+
17+
var isSingleListItem: Bool {
18+
get { self[IsSingleListItemKey.self] }
19+
set { self[IsSingleListItemKey.self] = newValue }
20+
}
21+
}
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)