Rename items in folder browser

This commit is contained in:
1024jp 2024-05-20 20:16:18 +09:00
parent c2d1b6bd9a
commit 383275a784
2 changed files with 108 additions and 4 deletions

View File

@ -252,6 +252,62 @@ import OSLog
}
/// Renames the file at the given `fileURL` with a new name.
///
/// - Parameters:
/// - fileURL: The file URL at the file to rename.
/// - name: The new file name.
func renameItem(at fileURL: URL, with name: String) throws {
// validate new name
guard !name.isEmpty else {
throw InvalidNameError.empty
}
guard !name.contains("/") else {
throw InvalidNameError.invalidCharacter("/")
}
guard !name.contains(":") else {
throw InvalidNameError.invalidCharacter(":")
}
let newURL = fileURL.deletingLastPathComponent().appending(component: name)
do {
try self.moveItem(from: fileURL, to: newURL)
} catch {
if (error as? CocoaError)?.errorCode == CocoaError.fileWriteFileExists.rawValue {
throw InvalidNameError.duplicated(name: name)
} else {
throw error
}
}
}
/// Move the file to a new destination inside the directory.
///
/// - Parameters:
/// - sourceURL: The current file URL.
/// - destinationURL: The destination.
func moveItem(from sourceURL: URL, to destinationURL: URL) throws {
var coordinationError: NSError?
var movingError: (any Error)?
let coordinator = NSFileCoordinator(filePresenter: self)
coordinator.coordinate(writingItemAt: sourceURL, options: .forMoving, writingItemAt: destinationURL, options: .forMoving, error: &coordinationError) { (newSourceURL, newDestinationURL) in
do {
try FileManager.default.moveItem(at: newSourceURL, to: newDestinationURL)
} catch {
movingError = error
}
}
if let error = coordinationError ?? movingError {
throw error
}
}
/// Properly moves the item to the trash.
///
/// - Parameters:
@ -325,6 +381,7 @@ private enum DirectoryDocumentError: LocalizedError {
switch self {
case .alreadyOpen(let fileURL):
String(localized: "DirectoryDocumentError.alreadyOpen.description", defaultValue: "The file “\(fileURL.lastPathComponent)” is already open in a different window.")
}
}

View File

@ -50,7 +50,15 @@ struct FileBrowserView: View {
.sorted(keepsFoldersOnTop: self.keepsFoldersOnTop)
List(fileNodes, children: \.children, selection: $selection) { node in
NodeView(node: node)
NodeView(node: node) { name in
do {
try self.document.renameItem(at: node.fileURL, with: name)
} catch {
self.error = error
return false
}
return true
}
}
.listStyle(.sidebar)
.contextMenu(forSelectionType: FileNode.ID.self) { ids in
@ -134,19 +142,58 @@ struct FileBrowserView: View {
private struct NodeView: View {
var node: FileNode
var onEdit: (String) -> Bool
@AppStorage(.fileBrowserShowsFilenameExtensions) private var showsFilenameExtensions
@FocusState var isFocused: Bool
@State private var name: String
init(node: FileNode, onEdit: @escaping (String) -> Bool) {
self.node = node
self.onEdit = onEdit
self._name = State(initialValue: node.name)
self.resetName()
}
var body: some View {
Label {
Text(self.showsFilenameExtensions ? self.node.name : self.node.name.deletingPathExtension)
TextField(text: $name, label: EmptyView.init)
.focused($isFocused)
} icon: {
Image(systemName: self.node.isDirectory ? "folder" : "doc")
}
.lineLimit(1)
.opacity(self.node.isHidden ? 0.5 : 1)
.opacity((self.node.isHidden && !self.isFocused) ? 0.5 : 1)
.onChange(of: self.showsFilenameExtensions) {
self.resetName()
}
.onChange(of: self.isFocused) { (_, newValue) in
if newValue {
// enter focus
self.name = self.node.name
} else {
// exit focus
guard
self.name != self.node.name,
self.onEdit(self.name)
else {
self.resetName()
return
}
}
}
}
private func resetName() {
self.name = self.showsFilenameExtensions ? self.node.name : self.node.name.deletingPathExtension
}
}