mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 19:27:30 +03:00
Rename items in folder browser
This commit is contained in:
parent
c2d1b6bd9a
commit
383275a784
@ -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.")
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user