Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update Recently Opened Menu #1919

Merged
merged 8 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading