Skip to content

Commit

Permalink
fix: Update Recently Opened Menu (#1919)
Browse files Browse the repository at this point in the history
* Duplicates Title Is Always On

* Sync With AppKit
  • Loading branch information
thecoolwinter authored Nov 9, 2024
1 parent 7fd614f commit adf6fa7
Show file tree
Hide file tree
Showing 11 changed files with 292 additions and 114 deletions.
67 changes: 47 additions & 20 deletions CodeEdit.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"originHash" : "5c4a5d433333474763817b9804d7f1856ab3b416ed87b190a2bd6e86c0c9834c",
"originHash" : "aef43d6aa0c467418565c574c33495a50d6e24057eb350c17704ab4ae2aead6c",
"pins" : [
{
"identity" : "anycodable",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,6 @@ final class CodeEditDocumentController: NSDocumentController {
return panel.url
}

override func noteNewRecentDocument(_ document: NSDocument) {
// The super method is run manually when opening new documents.
}

override func openDocument(_ sender: Any?) {
self.openDocument(onCompletion: { document, documentWasAlreadyOpen in
// TODO: handle errors
Expand All @@ -63,17 +59,16 @@ final class CodeEditDocumentController: NSDocumentController {
display displayDocument: Bool,
completionHandler: @escaping (NSDocument?, Bool, Error?) -> Void
) {
super.noteNewRecentDocumentURL(url)
super.openDocument(withContentsOf: url, display: displayDocument) { document, documentWasAlreadyOpen, error in

if let document {
self.addDocument(document)
self.updateRecent(url)
} else {
let errorMessage = error?.localizedDescription ?? "unknown error"
print("Unable to open document '\(url)': \(errorMessage)")
}

RecentProjectsStore.documentOpened(at: url)
completionHandler(document, documentWasAlreadyOpen, error)
}
}
Expand All @@ -98,11 +93,6 @@ final class CodeEditDocumentController: NSDocumentController {
}
}

override func clearRecentDocuments(_ sender: Any?) {
super.clearRecentDocuments(sender)
UserDefaults.standard.set([Any](), forKey: "recentProjectPaths")
}

override func addDocument(_ document: NSDocument) {
super.addDocument(document)
if let document = document as? CodeFileDocument {
Expand Down Expand Up @@ -138,7 +128,6 @@ extension NSDocumentController {
alert.runModal()
return
}
self.updateRecent(url)
onCompletion(document, documentWasAlreadyOpen)
print("Document:", document)
print("Was already open?", documentWasAlreadyOpen)
Expand All @@ -148,16 +137,4 @@ extension NSDocumentController {
}
}
}

final func updateRecent(_ url: URL) {
var recentProjectPaths: [String] = UserDefaults.standard.array(
forKey: "recentProjectPaths"
) as? [String] ?? []
if let containedIndex = recentProjectPaths.firstIndex(of: url.path) {
recentProjectPaths.move(fromOffsets: IndexSet(integer: containedIndex), toOffset: 0)
} else {
recentProjectPaths.insert(url.path, at: 0)
}
UserDefaults.standard.set(recentProjectPaths, forKey: "recentProjectPaths")
}
}
102 changes: 102 additions & 0 deletions CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//
// RecentProjectsUtil.swift
// CodeEdit
//
// Created by Khan Winter on 10/22/24.
//

import AppKit
import CoreSpotlight

/// Helper methods for managing the recent projects list and donating list items to CoreSpotlight.
///
/// Limits the number of remembered projects to 100 items.
///
/// If a UI element needs to listen to changes in this list, listen for the
/// ``RecentProjectsStore/didUpdateNotification`` notification.
enum RecentProjectsStore {
private static let defaultsKey = "recentProjectPaths"
static let didUpdateNotification = Notification.Name("RecentProjectsStore.didUpdate")

static func recentProjectPaths() -> [String] {
UserDefaults.standard.array(forKey: defaultsKey) as? [String] ?? []
}

static func recentProjectURLs() -> [URL] {
recentProjectPaths().map { URL(filePath: $0) }
}

private static func setPaths(_ paths: [String]) {
var paths = paths
// Remove duplicates
var foundPaths = Set<String>()
for (idx, path) in paths.enumerated().reversed() {
if foundPaths.contains(path) {
paths.remove(at: idx)
} else {
foundPaths.insert(path)
}
}

// Limit list to to 100 items after de-duplication
UserDefaults.standard.setValue(Array(paths.prefix(100)), forKey: defaultsKey)
setDocumentControllerRecents()
donateSearchableItems()
NotificationCenter.default.post(name: Self.didUpdateNotification, object: nil)
}

/// Notify the store that a url was opened.
/// Moves the path to the front if it was in the list already, or prepends it.
/// Saves the list to defaults when called.
/// - Parameter url: The url that was opened. Any url is accepted. File, directory, https.
static func documentOpened(at url: URL) {
var paths = recentProjectURLs()
if let containedIndex = paths.firstIndex(where: { $0.componentCompare(url) }) {
paths.move(fromOffsets: IndexSet(integer: containedIndex), toOffset: 0)
} else {
paths.insert(url, at: 0)
}
setPaths(paths.map { $0.path(percentEncoded: false) })
}

/// Remove all paths in the set.
/// - Parameter paths: The paths to remove.
/// - Returns: The remaining urls in the recent projects list.
static func removeRecentProjects(_ paths: Set<URL>) -> [URL] {
var recentProjectPaths = recentProjectURLs()
recentProjectPaths.removeAll(where: { paths.contains($0) })
setPaths(recentProjectPaths.map { $0.path(percentEncoded: false) })
return recentProjectURLs()
}

static func clearList() {
setPaths([])
}

/// Syncs AppKit's recent documents list with ours, keeping the dock menu and other lists up-to-date.
private static func setDocumentControllerRecents() {
CodeEditDocumentController.shared.clearRecentDocuments(nil)
for path in recentProjectURLs().prefix(10) {
CodeEditDocumentController.shared.noteNewRecentDocumentURL(path)
}
}

/// Donates all recent URLs to Core Search, making them searchable in Spotlight
private static func donateSearchableItems() {
let searchableItems = recentProjectURLs().map { entity in
let attributeSet = CSSearchableItemAttributeSet(contentType: .content)
attributeSet.title = entity.lastPathComponent
attributeSet.relatedUniqueIdentifier = entity.path()
return CSSearchableItem(
uniqueIdentifier: entity.path(),
domainIdentifier: "app.codeedit.CodeEdit.ProjectItem",
attributeSet: attributeSet
)
}
CSSearchableIndex.default().indexSearchableItems(searchableItems) { error in
if let error = error {
print(error)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// RecentProjectItem.swift
// RecentProjectListItem.swift
// CodeEditModules/WelcomeModule
//
// Created by Ziyuan Zhao on 2022/3/18.
Expand All @@ -13,7 +13,7 @@ extension String {
}
}

struct RecentProjectItem: View {
struct RecentProjectListItem: View {
let projectPath: URL

init(projectPath: URL) {
Expand Down
73 changes: 14 additions & 59 deletions CodeEdit/Features/Welcome/Views/RecentProjectsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,8 @@ struct RecentProjectsListView: View {
init(openDocument: @escaping (URL?, @escaping () -> Void) -> Void, dismissWindow: @escaping () -> Void) {
self.openDocument = openDocument
self.dismissWindow = dismissWindow

let recentProjectPaths: [String] = UserDefaults.standard.array(
forKey: "recentProjectPaths"
) as? [String] ?? []
let projectsURL = recentProjectPaths.map { URL(filePath: $0) }
_selection = .init(initialValue: Set(projectsURL.prefix(1)))
_recentProjects = .init(initialValue: projectsURL)
donateSearchableItems()
self._recentProjects = .init(initialValue: RecentProjectsStore.recentProjectURLs())
self._selection = .init(initialValue: Set(RecentProjectsStore.recentProjectURLs().prefix(1)))
}

var listEmptyView: some View {
Expand All @@ -41,7 +35,7 @@ struct RecentProjectsListView: View {

var body: some View {
List(recentProjects, id: \.self, selection: $selection) { project in
RecentProjectItem(projectPath: project)
RecentProjectListItem(projectPath: project)
}
.listStyle(.sidebar)
.contextMenu(forSelectionType: URL.self) { items in
Expand All @@ -60,33 +54,22 @@ struct RecentProjectsListView: View {
}

Button("Remove from Recents") {
removeRecentProjects(items)
removeRecentProjects()
}
}
} primaryAction: { items in
items.forEach {
openDocument($0, dismissWindow)
}
items.forEach { openDocument($0, dismissWindow) }
}
.onCopyCommand {
selection.map {
NSItemProvider(object: $0.path(percentEncoded: false) as NSString)
}
selection.map { NSItemProvider(object: $0.path(percentEncoded: false) as NSString) }
}
.onDeleteCommand {
removeRecentProjects(selection)
removeRecentProjects()
}
.background(EffectView(.underWindowBackground, blendingMode: .behindWindow))
.onReceive(NSApp.publisher(for: \.keyWindow)) { _ in
// Update the list whenever the key window changes.
// Ideally, this should be 'whenever a doc opens/closes'.
updateRecentProjects()
}
.background {
Button("") {
selection.forEach {
openDocument($0, dismissWindow)
}
selection.forEach { openDocument($0, dismissWindow) }
}
.keyboardShortcut(.defaultAction)
.hidden()
Expand All @@ -98,44 +81,16 @@ struct RecentProjectsListView: View {
}
}
}
}

func removeRecentProjects(_ items: Set<URL>) {
var recentProjectPaths: [String] = UserDefaults.standard.array(
forKey: "recentProjectPaths"
) as? [String] ?? []
items.forEach { url in
recentProjectPaths.removeAll { url == URL(filePath: $0) }
selection.remove(url)
.onReceive(NotificationCenter.default.publisher(for: RecentProjectsStore.didUpdateNotification)) { _ in
updateRecentProjects()
}
UserDefaults.standard.set(recentProjectPaths, forKey: "recentProjectPaths")
let projectsURL = recentProjectPaths.map { URL(filePath: $0) }
recentProjects = projectsURL
}

func updateRecentProjects() {
let recentProjectPaths: [String] = UserDefaults.standard.array(
forKey: "recentProjectPaths"
) as? [String] ?? []
let projectsURL = recentProjectPaths.map { URL(filePath: $0) }
recentProjects = projectsURL
func removeRecentProjects() {
recentProjects = RecentProjectsStore.removeRecentProjects(selection)
}

func donateSearchableItems() {
let searchableItems = recentProjects.map { entity in
let attributeSet = CSSearchableItemAttributeSet(contentType: .content)
attributeSet.title = entity.lastPathComponent
attributeSet.relatedUniqueIdentifier = entity.path()
return CSSearchableItem(
uniqueIdentifier: entity.path(),
domainIdentifier: "app.codeedit.CodeEdit.ProjectItem",
attributeSet: attributeSet
)
}
CSSearchableIndex.default().indexSearchableItems(searchableItems) { error in
if let error = error {
print(error)
}
}
func updateRecentProjects() {
recentProjects = RecentProjectsStore.recentProjectURLs()
}
}
6 changes: 4 additions & 2 deletions CodeEdit/Features/WindowCommands/FileCommands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import SwiftUI

struct FileCommands: Commands {
static let recentProjectsMenu = RecentProjectsMenu()

@Environment(\.openWindow)
private var openWindow

Expand All @@ -29,8 +31,8 @@ struct FileCommands: Commands {
.keyboardShortcut("o")

// Leave this empty, is done through a hidden API in WindowCommands/Utils/CommandsFixes.swift
// This can't be done in SwiftUI Commands yet, as they don't support images in menu items.
Menu("Open Recent") {}
// We set this with a custom NSMenu. See WindowCommands/Utils/RecentProjectsMenu.swift
Menu("Open Recent") { }

Button("Open Quickly") {
NSApp.sendAction(#selector(CodeEditWindowController.openQuickly(_:)), to: nil, from: nil)
Expand Down
6 changes: 1 addition & 5 deletions CodeEdit/Features/WindowCommands/Utils/CommandsFixes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ extension EventModifiers {
extension NSMenuItem {
@objc
fileprivate func fixAlternate(_ newValue: NSEvent.ModifierFlags) {

if newValue.contains(.numericPad) {
isAlternate = true
fixAlternate(newValue.subtracting(.numericPad))
Expand All @@ -23,10 +22,7 @@ extension NSMenuItem {
fixAlternate(newValue)

if self.title == "Open Recent" {
let openRecentMenu = NSMenu(title: "Open Recent")
openRecentMenu.perform(NSSelectorFromString("_setMenuName:"), with: "NSRecentDocumentsMenu")
self.submenu = openRecentMenu
NSDocumentController.shared.value(forKey: "_installOpenRecentMenus")
self.submenu = FileCommands.recentProjectsMenu.makeMenu()
}

if self.title == "OpenWindowAction" || self.title.isEmpty {
Expand Down
Loading

0 comments on commit adf6fa7

Please sign in to comment.