From 760f8667ca836d2ca3ed9823e798ed3d393fca40 Mon Sep 17 00:00:00 2001 From: atsrus Date: Sun, 4 May 2025 11:16:34 +0100 Subject: [PATCH 1/2] Issue #1061 Recents menu --- .../Welcome/Model/RecentProjectsStore.swift | 84 +++++++++++++++---- .../Utils/RecentProjectsMenu.swift | 71 +++++++++++++--- 2 files changed, 129 insertions(+), 26 deletions(-) diff --git a/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift b/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift index 4f03ef850..c00479739 100644 --- a/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift +++ b/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift @@ -15,18 +15,27 @@ import CoreSpotlight /// 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" + private static let projectsdDefaultsKey = "recentProjectPaths" + private static let fileDefaultsKey = "recentFilePaths" static let didUpdateNotification = Notification.Name("RecentProjectsStore.didUpdate") static func recentProjectPaths() -> [String] { - UserDefaults.standard.array(forKey: defaultsKey) as? [String] ?? [] + UserDefaults.standard.array(forKey: projectsdDefaultsKey) as? [String] ?? [] } static func recentProjectURLs() -> [URL] { - recentProjectPaths().map { URL(filePath: $0) } + return recentProjectPaths().map { URL(filePath: $0) } } - private static func setPaths(_ paths: [String]) { + static func recentFilePaths() -> [String] { + UserDefaults.standard.array(forKey: fileDefaultsKey) as? [String] ?? [] + } + + static func recentFileURLs() -> [URL] { + return recentFilePaths().map { URL(filePath: $0) } + } + + private static func setProjectPaths(_ paths: [String]) { var paths = paths // Remove duplicates var foundPaths = Set() @@ -39,7 +48,25 @@ enum RecentProjectsStore { } // Limit list to to 100 items after de-duplication - UserDefaults.standard.setValue(Array(paths.prefix(100)), forKey: defaultsKey) + UserDefaults.standard.setValue(Array(paths.prefix(100)), forKey: projectsdDefaultsKey) + setDocumentControllerRecents() + donateSearchableItems() + NotificationCenter.default.post(name: Self.didUpdateNotification, object: nil) + } + private static func setFilePaths(_ paths: [String]) { + var paths = paths + // Remove duplicates + var foundPaths = Set() + 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: fileDefaultsKey ) setDocumentControllerRecents() donateSearchableItems() NotificationCenter.default.post(name: Self.didUpdateNotification, object: nil) @@ -50,29 +77,58 @@ enum RecentProjectsStore { /// 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) + var projPaths = recentProjectURLs() + var filePaths = recentFileURLs() + + let urlToString = url.absoluteString + +// if file portion of local URL has "/" at the end then it is a folder , files and folders go in two separate lists + + if urlToString.hasSuffix("/") { + if let containedIndex = projPaths.firstIndex(where: { $0.componentCompare(url) }) { + projPaths.move(fromOffsets: IndexSet(integer: containedIndex), toOffset: 0) + } else { + projPaths.insert(url, at: 0) + } + setProjectPaths(projPaths.map { $0.path(percentEncoded: false) }) } else { - paths.insert(url, at: 0) + if let containedIndex = filePaths.firstIndex(where: { $0.componentCompare(url) }) { + filePaths.move(fromOffsets: IndexSet(integer: containedIndex), toOffset: 0) + } else { + filePaths.insert(url, at: 0) + } + setFilePaths(filePaths.map { $0.path(percentEncoded: false) }) } - setPaths(paths.map { $0.path(percentEncoded: false) }) } - /// Remove all paths in the set. + /// Remove all project 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] { var recentProjectPaths = recentProjectURLs() recentProjectPaths.removeAll(where: { paths.contains($0) }) - setPaths(recentProjectPaths.map { $0.path(percentEncoded: false) }) + setProjectPaths(recentProjectPaths.map { $0.path(percentEncoded: false) }) return recentProjectURLs() } + /// Remove all folder paths in the set. + /// - Parameter paths: The paths to remove. + /// - Returns: The remaining urls in the recent projects list. + + static func removeRecentFiles(_ paths: Set) -> [URL] { + var recentFilePaths = recentFileURLs() + recentFilePaths.removeAll(where: { paths.contains($0) }) + setFilePaths(recentFilePaths.map { $0.path(percentEncoded: false) }) + return recentFileURLs() + } static func clearList() { - setPaths([]) + setProjectPaths([]) + setFilePaths([]) + NotificationCenter.default.post(name: Self.didUpdateNotification, object: nil) } - +// TODO do we need to setdocument controller for Projects AND Files???? +// doesn't seem like it clears in teh finder anyway +// ...more testing whcn Cleairng list required. /// 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) diff --git a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift index 483a6baa2..31af6fd45 100644 --- a/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift +++ b/CodeEdit/Features/WindowCommands/Utils/RecentProjectsMenu.swift @@ -11,9 +11,26 @@ class RecentProjectsMenu: NSObject { func makeMenu() -> NSMenu { let menu = NSMenu(title: NSLocalizedString("Open Recent", comment: "Open Recent menu title")) - let paths = RecentProjectsStore.recentProjectURLs().prefix(10) + projectItems(menu: menu) + menu.addItem(NSMenuItem.separator()) + fileItems(menu: menu) + + menu.addItem(NSMenuItem.separator()) + let clearMenuItem = NSMenuItem( + title: NSLocalizedString("Clear Menu", comment: "Recent project menu clear button"), + action: #selector(clearMenuItemClicked(_:)), + keyEquivalent: "" + ) + clearMenuItem.target = self + menu.addItem(clearMenuItem) + + return menu + } - for projectPath in paths { + private func projectItems( menu: NSMenu) { + let projectPaths = RecentProjectsStore.recentProjectURLs().prefix(10) + + for projectPath in projectPaths { let icon = NSWorkspace.shared.icon(forFile: projectPath.path()) icon.size = NSSize(width: 16, height: 16) let alternateTitle = alternateTitle(for: projectPath) @@ -27,7 +44,7 @@ class RecentProjectsMenu: NSObject { primaryItem.image = icon primaryItem.representedObject = projectPath - let containsDuplicate = paths.contains { url in + let containsDuplicate = projectPaths.contains { url in url != projectPath && url.lastPathComponent == projectPath.lastPathComponent } @@ -51,18 +68,48 @@ class RecentProjectsMenu: NSObject { menu.addItem(primaryItem) menu.addItem(alternateItem) } + } - menu.addItem(NSMenuItem.separator()) + private func fileItems( menu: NSMenu) { + let filePaths = RecentProjectsStore.recentFileURLs().prefix(10) + for filePath in filePaths { + let icon = NSWorkspace.shared.icon(forFile: filePath.path()) + icon.size = NSSize(width: 16, height: 16) + let alternateTitle = alternateTitle(for: filePath) - let clearMenuItem = NSMenuItem( - title: NSLocalizedString("Clear Menu", comment: "Recent project menu clear button"), - action: #selector(clearMenuItemClicked(_:)), - keyEquivalent: "" - ) - clearMenuItem.target = self - menu.addItem(clearMenuItem) + let primaryItem = NSMenuItem( + title: filePath.lastPathComponent, + action: #selector(recentProjectItemClicked(_:)), + keyEquivalent: "" + ) + primaryItem.target = self + primaryItem.image = icon + primaryItem.representedObject = filePath - return menu + let containsDuplicate = filePaths.contains { url in + url != filePath && url.lastPathComponent == filePath.lastPathComponent + } + + // If there's a duplicate, add the path. + if containsDuplicate { + primaryItem.attributedTitle = alternateTitle + } + + let alternateItem = NSMenuItem( + title: "", + action: #selector(recentProjectItemClicked(_:)), + keyEquivalent: "" + ) + alternateItem.attributedTitle = alternateTitle + alternateItem.target = self + alternateItem.image = icon + alternateItem.representedObject = filePath + alternateItem.isAlternate = true + alternateItem.keyEquivalentModifierMask = [.option] + + menu.addItem(primaryItem) + menu.addItem(alternateItem) + } } private func alternateTitle(for projectPath: URL) -> NSAttributedString { From ea5ee7ea67ba3d9fc05b31d17d3088ebe521f161 Mon Sep 17 00:00:00 2001 From: atsrus Date: Mon, 5 May 2025 16:05:58 +0100 Subject: [PATCH 2/2] Tidying comment --- CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift b/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift index c00479739..604c1d226 100644 --- a/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift +++ b/CodeEdit/Features/Welcome/Model/RecentProjectsStore.swift @@ -82,7 +82,7 @@ enum RecentProjectsStore { let urlToString = url.absoluteString -// if file portion of local URL has "/" at the end then it is a folder , files and folders go in two separate lists + // if file portion of local URL has "/" at the end then it is a folder , files and folders go in two separate lists if urlToString.hasSuffix("/") { if let containedIndex = projPaths.firstIndex(where: { $0.componentCompare(url) }) { @@ -113,7 +113,6 @@ enum RecentProjectsStore { /// Remove all folder paths in the set. /// - Parameter paths: The paths to remove. /// - Returns: The remaining urls in the recent projects list. - static func removeRecentFiles(_ paths: Set) -> [URL] { var recentFilePaths = recentFileURLs() recentFilePaths.removeAll(where: { paths.contains($0) }) @@ -126,9 +125,7 @@ enum RecentProjectsStore { setFilePaths([]) NotificationCenter.default.post(name: Self.didUpdateNotification, object: nil) } -// TODO do we need to setdocument controller for Projects AND Files???? -// doesn't seem like it clears in teh finder anyway -// ...more testing whcn Cleairng list required. + /// 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)