Add DirectoryDocument (#278)

This commit is contained in:
1024jp 2024-05-27 23:55:42 +09:00
parent 9cf39fe4a0
commit 309cfd98d8
17 changed files with 1209 additions and 19 deletions

View File

@ -399,6 +399,8 @@
2A78BFBD1D1B376000A583D2 /* ServicesProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A78BFBB1D1B376000A583D2 /* ServicesProvider.swift */; };
2A7C92FC29FD64A8008343C8 /* DefaultKey+FontType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7C92FB29FD64A8008343C8 /* DefaultKey+FontType.swift */; };
2A7C92FD29FD64A8008343C8 /* DefaultKey+FontType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7C92FB29FD64A8008343C8 /* DefaultKey+FontType.swift */; };
2A7DFA4A2BE96C06001D5BDD /* DirectoryDocument+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7DFA492BE96C06001D5BDD /* DirectoryDocument+Actions.swift */; };
2A7DFA4B2BE96C06001D5BDD /* DirectoryDocument+Actions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7DFA492BE96C06001D5BDD /* DirectoryDocument+Actions.swift */; };
2A7F4DFF2871F46D0029CE66 /* PrintPanelAccessory.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A7F4E022871F46D0029CE66 /* PrintPanelAccessory.storyboard */; };
2A7F4E002871F46D0029CE66 /* PrintPanelAccessory.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2A7F4E022871F46D0029CE66 /* PrintPanelAccessory.storyboard */; };
2A7FEF0B2B90B05C0042BEFF /* FilterField.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2A7FEF0A2B90B05C0042BEFF /* FilterField.xcstrings */; };
@ -513,6 +515,12 @@
2AA6E0C82B75AC4900E536F8 /* SyntaxEditor.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 2AA6E0B82B744FF300E536F8 /* SyntaxEditor.xcstrings */; };
2AA704CE2987878B008CBCB5 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA704CD2987878B008CBCB5 /* Node.swift */; };
2AA704CF2987878B008CBCB5 /* Node.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA704CD2987878B008CBCB5 /* Node.swift */; };
2AA71A3B2BE25C3F0084EC0A /* DirectoryDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A3A2BE25C3F0084EC0A /* DirectoryDocument.swift */; };
2AA71A3C2BE25C3F0084EC0A /* DirectoryDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A3A2BE25C3F0084EC0A /* DirectoryDocument.swift */; };
2AA71A3E2BE25FB30084EC0A /* FileBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A3D2BE25FB30084EC0A /* FileBrowserView.swift */; };
2AA71A3F2BE25FB30084EC0A /* FileBrowserView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A3D2BE25FB30084EC0A /* FileBrowserView.swift */; };
2AA71A442BE278880084EC0A /* FileNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A432BE278880084EC0A /* FileNode.swift */; };
2AA71A452BE278880084EC0A /* FileNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A432BE278880084EC0A /* FileNode.swift */; };
2AA71A532BE366520084EC0A /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A522BE366520084EC0A /* Observation.swift */; };
2AA71A542BE366520084EC0A /* Observation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA71A522BE366520084EC0A /* Observation.swift */; };
2AA749C31D3C263300850802 /* DocumentWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AA749C21D3C263300850802 /* DocumentWindowController.swift */; };
@ -1004,6 +1012,7 @@
2A78BFBB1D1B376000A583D2 /* ServicesProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ServicesProvider.swift; sourceTree = "<group>"; };
2A7C92FB29FD64A8008343C8 /* DefaultKey+FontType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DefaultKey+FontType.swift"; sourceTree = "<group>"; };
2A7E06E52C1A711B00E5396D /* EditorKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = EditorKit; sourceTree = "<group>"; };
2A7DFA492BE96C06001D5BDD /* DirectoryDocument+Actions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DirectoryDocument+Actions.swift"; sourceTree = "<group>"; };
2A7F4E012871F46D0029CE66 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/PrintPanelAccessory.storyboard; sourceTree = "<group>"; };
2A7FEF0A2B90B05C0042BEFF /* FilterField.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = FilterField.xcstrings; sourceTree = "<group>"; };
2A80BE8C27FFA61700D2F7FF /* LineEndingScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineEndingScanner.swift; sourceTree = "<group>"; };
@ -1064,6 +1073,9 @@
2AA6E0B82B744FF300E536F8 /* SyntaxEditor.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = SyntaxEditor.xcstrings; sourceTree = "<group>"; };
2AA6E0BE2B74728700E536F8 /* mul */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; name = mul; path = mul.lproj/FindPanelFieldView.xcstrings; sourceTree = "<group>"; };
2AA704CD2987878B008CBCB5 /* Node.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Node.swift; sourceTree = "<group>"; };
2AA71A3A2BE25C3F0084EC0A /* DirectoryDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectoryDocument.swift; sourceTree = "<group>"; };
2AA71A3D2BE25FB30084EC0A /* FileBrowserView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileBrowserView.swift; sourceTree = "<group>"; };
2AA71A432BE278880084EC0A /* FileNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileNode.swift; sourceTree = "<group>"; };
2AA71A522BE366520084EC0A /* Observation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Observation.swift; sourceTree = "<group>"; };
2AA749C21D3C263300850802 /* DocumentWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DocumentWindowController.swift; sourceTree = "<group>"; };
2AA761341D45634400031AAF /* String+Counting.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Counting.swift"; sourceTree = "<group>"; };
@ -1711,7 +1723,10 @@
isa = PBXGroup;
children = (
2AD616CB1D3E583D0016EFB6 /* DocumentController.swift */,
2AA71A3A2BE25C3F0084EC0A /* DirectoryDocument.swift */,
2A7DFA492BE96C06001D5BDD /* DirectoryDocument+Actions.swift */,
2A1679E51D3CE07100E8261D /* Document.swift */,
2AA71A432BE278880084EC0A /* FileNode.swift */,
);
name = Document;
sourceTree = "<group>";
@ -1721,6 +1736,7 @@
children = (
2AA749C21D3C263300850802 /* DocumentWindowController.swift */,
2A17A3121D2D16F1001DD717 /* WindowContentViewController.swift */,
2AA71A3D2BE25FB30084EC0A /* FileBrowserView.swift */,
2A2184121D0426E800522EF5 /* Window */,
2ADD36941CFCAD4200F3175D /* Inspector */,
2A2184221D043D7E00522EF5 /* Content View */,
@ -2685,6 +2701,8 @@
2AFB5AE91D597ABB003895A7 /* DefaultSettings+Encodings.swift in Sources */,
2AA4EE3D28D55CE80014B045 /* DelegateContext.swift in Sources */,
2ACFE5881D2037800005233A /* DetachablePopoverViewController.swift in Sources */,
2AA71A3B2BE25C3F0084EC0A /* DirectoryDocument.swift in Sources */,
2A7DFA4A2BE96C06001D5BDD /* DirectoryDocument+Actions.swift in Sources */,
2AC52BDC1D48CC0E007D6371 /* DispatchQueue.swift in Sources */,
2A1679E71D3CE07100E8261D /* Document.swift in Sources */,
2AF0C12E1D3DABD000B6FCB6 /* Document+ScriptingSupport.swift in Sources */,
@ -2717,7 +2735,9 @@
2A5DCE8A1D18FFDB00D5D74C /* EncodingListView.swift in Sources */,
2A4257A81D22E0660086DAAD /* EncodingManager.swift in Sources */,
2A50AA63204D513500D10A10 /* FileAttributes.swift in Sources */,
2AA71A3E2BE25FB30084EC0A /* FileBrowserView.swift in Sources */,
2A4682B31D2F6B580005410E /* FileDropItem.swift in Sources */,
2AA71A442BE278880084EC0A /* FileNode.swift in Sources */,
2A0A602B27ABD74500725B70 /* FilterField.swift in Sources */,
2A5D13461D1FE66300D38E6A /* FindPanelButtonView.swift in Sources */,
2ACFE58C1D20730B0005233A /* FindPanelContentViewController.swift in Sources */,
@ -3001,6 +3021,8 @@
2AFB5AE81D597ABB003895A7 /* DefaultSettings+Encodings.swift in Sources */,
2AA4EE3E28D55CE80014B045 /* DelegateContext.swift in Sources */,
2ACFE5871D2037800005233A /* DetachablePopoverViewController.swift in Sources */,
2AA71A3C2BE25C3F0084EC0A /* DirectoryDocument.swift in Sources */,
2A7DFA4B2BE96C06001D5BDD /* DirectoryDocument+Actions.swift in Sources */,
2AC52BDB1D48CC0E007D6371 /* DispatchQueue.swift in Sources */,
2A1679E61D3CE07100E8261D /* Document.swift in Sources */,
2AF0C12D1D3DABD000B6FCB6 /* Document+ScriptingSupport.swift in Sources */,
@ -3033,7 +3055,9 @@
2AA106B02470F05F00979CB7 /* EncodingListView.swift in Sources */,
2A4257A71D22E0660086DAAD /* EncodingManager.swift in Sources */,
2A50AA62204D513500D10A10 /* FileAttributes.swift in Sources */,
2AA71A3F2BE25FB30084EC0A /* FileBrowserView.swift in Sources */,
2A4682B21D2F6B580005410E /* FileDropItem.swift in Sources */,
2AA71A452BE278880084EC0A /* FileNode.swift in Sources */,
2A0A602C27ABD74500725B70 /* FilterField.swift in Sources */,
2A5D13451D1FE66300D38E6A /* FindPanelButtonView.swift in Sources */,
2ACFE58B1D20730B0005233A /* FindPanelContentViewController.swift in Sources */,

View File

@ -70,6 +70,20 @@
<key>NSUbiquitousDocumentUserActivityType</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).editing</string>
</dict>
<dict>
<key>CFBundleTypeName</key>
<string>Directory</string>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.directory</string>
</array>
<key>NSDocumentClass</key>
<string>$(PRODUCT_MODULE_NAME).DirectoryDocument</string>
</dict>
</array>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>

View File

@ -2290,6 +2290,83 @@
}
}
},
"Keep Folders on Top" : {
"comment" : "menu item label",
"localizations" : {
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Nejprve zobrazovat složky"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ordner oben anzeigen"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Keep Folders on Top"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mantener las carpetas en la parte superior"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Laisser les dossiers en haut"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tieni le cartelle in alto"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "フォルダを常に先頭に表示"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Zorg dat mappen bovenaan staan"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Manter pastas na parte superior"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Klasörleri en üstte tut"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "将文件夹保持在顶部"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "將檔案夾保留在最上方"
}
}
}
},
"Line" : {
"comment" : "label in document inspector\ntable column header",
"localizations" : {
@ -3137,6 +3214,83 @@
}
}
},
"Move to Trash" : {
"comment" : "menu item label",
"localizations" : {
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Přesunout do koše"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "In den Papierkorb bewegen"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Move To Bin"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Trasladar a la papelera"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Placer dans la corbeille"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Sposta nel Cestino"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ゴミ箱に入れる"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Verplaats naar prullenmand"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mover para o Lixo"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Çöp Sepetine Taşı"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "移到废纸篓"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "丟到垃圾桶"
}
}
}
},
"Navigation Bar" : {
"comment" : "accessibility label",
"localizations" : {
@ -3467,6 +3621,106 @@
}
}
},
"Open in Separate Window" : {
"comment" : "menu item label",
"localizations" : {
"cs" : {
"stringUnit" : {
"state" : "translated",
"value" : "Otevřít v samostatném okně"
}
},
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "In eigenem Fenster öffnen"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Open in Separate Window"
}
},
"es" : {
"stringUnit" : {
"state" : "translated",
"value" : "Abrir en una ventana diferente"
}
},
"fr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ouvrir dans une fenêtre distincte"
}
},
"it" : {
"stringUnit" : {
"state" : "translated",
"value" : "Apri in unaltra finestra"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "別のウインドウで開く"
}
},
"nl" : {
"stringUnit" : {
"state" : "translated",
"value" : "Open in afzonderlijk venster"
}
},
"pt" : {
"stringUnit" : {
"state" : "translated",
"value" : "Abrir em uma Janela Separada"
}
},
"tr" : {
"stringUnit" : {
"state" : "translated",
"value" : "Ayrı Pencerede Aç"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "在单独窗口中打开"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "在獨立視窗中打開"
}
}
}
},
"Open with External Editor" : {
"comment" : "menu item label",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Mit externem Editor öffenen"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Open with External Editor"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "外部エディタで開く"
}
}
}
},
"Outline" : {
"comment" : "inspector pane title\nsection title in inspector",
"localizations" : {
@ -3897,7 +4151,54 @@
}
}
},
"Show Filename Extensions" : {
"comment" : "menu item label (Check how Apple translates the term “filename extension.”)",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Dateinamensuffixe einblenden"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Filename Extensions"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ファイル名拡張子を表示"
}
}
}
},
"Show Hidden Files" : {
"comment" : "menu item label",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Versteckte Dateien einblenden"
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "Show Hidden Files"
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "不可視ファイルを表示"
}
}
}
},
"Show in Finder" : {
"comment" : "menu item label",
"localizations" : {
"cs" : {
"stringUnit" : {

View File

@ -558,6 +558,65 @@
}
}
},
"DirectoryDocumentError.alreadyOpen.description" : {
"extractionState" : "extracted_with_value",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Die Datei „%@“ ist bereits in einem anderen Fenster geöffnet."
}
},
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "The file “%@” is already open in a different window."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "The file “%@” is already open in a different window."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "ファイル“%@”はすでに別のウインドウで開かれています。"
}
}
}
},
"DirectoryDocumentError.alreadyOpen.recoverySuggestion" : {
"comment" : "“it” is the file in description.",
"extractionState" : "extracted_with_value",
"localizations" : {
"de" : {
"stringUnit" : {
"state" : "translated",
"value" : "Um sie in diesem Fenster zu öffnen, schließ zuerst das bestehende Fenster."
}
},
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "To open it in this window, close the existing window first."
}
},
"en-GB" : {
"stringUnit" : {
"state" : "translated",
"value" : "To open it in this window, close the existing window first."
}
},
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "このウインドウで開くには、先にすでにあるウインドウを閉じてください。"
}
}
}
},
"DocumentOpeningError.binaryFile.description" : {
"extractionState" : "extracted_with_value",
"localizations" : {

View File

@ -153,6 +153,11 @@ extension DefaultKeys {
static let countTreatsConsecutiveWhitespaceAsSingle = DefaultKey<Bool>("countOptionTreatsConsecutiveWhitespaceAsSingle")
static let countEncoding = DefaultKey<Int>("countOptionEncoding")
// file browser
static let fileBrowserKeepsFoldersOnTop = DefaultKey<Bool>("fileBrowserKeepsFoldersOnTop")
static let fileBrowserShowsHiddenFiles = DefaultKey<Bool>("fileBrowserShowsHiddenFiles")
static let fileBrowserShowsFilenameExtensions = DefaultKey<Bool>("fileBrowserShowsFilenameExtensions")
// settings that are not in the Settings window
static let pinsThemeAppearance = DefaultKey<Bool>("pinsThemeAppearance")
static let lastSettingsPaneIdentifier = DefaultKey<String?>("lastPreferencesPaneIdentifier")

View File

@ -160,6 +160,11 @@ struct DefaultSettings {
.countTreatsConsecutiveWhitespaceAsSingle: false,
.countEncoding: String.Encoding.utf8.rawValue,
// file browser
.fileBrowserKeepsFoldersOnTop: true,
.fileBrowserShowsHiddenFiles: false,
.fileBrowserShowsFilenameExtensions: true,
// settings not in the Settings window
.pinsThemeAppearance: false,
.colorCodeType: 1,

View File

@ -0,0 +1,150 @@
//
// DirectoryDocument+Actions.swift
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2024-05-07.
//
// ---------------------------------------------------------------------------
//
// © 2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AppKit
// -> Pass all possible actions manually since NSDocument has no next responder (2024-05, macOS 14)
extension DirectoryDocument {
override func validateMenuItem(_ menuItem: NSMenuItem) -> Bool {
switch menuItem.action {
case #selector(changeEncoding),
#selector(changeLineEnding),
#selector(changeSyntax):
return self.currentDocument?.validateMenuItem(menuItem) ?? false
default:
return super.validateMenuItem(menuItem)
}
}
override func validateUserInterfaceItem(_ item: any NSValidatedUserInterfaceItem) -> Bool {
switch item.action {
case #selector(saveAs):
// prevent file document from being moved out of the folder
return false
case #selector(save(_:)),
#selector(saveTo(_:)),
#selector(duplicate(_:)),
#selector(revertToSaved),
#selector(browseVersions),
#selector(lock(_:)),
#selector(unlock(_:)),
#selector(runPageLayout),
#selector(printDocument),
#selector(shareDocument):
return self.currentDocument?.validateUserInterfaceItem(item) ?? false
default:
return super.validateUserInterfaceItem(item)
}
}
override func save(_ sender: Any?) {
self.currentDocument?.save(sender)
}
override func saveTo(_ sender: Any?) {
self.currentDocument?.saveTo(sender)
}
override func duplicate(_ sender: Any?) {
self.currentDocument?.duplicate(sender)
}
override func revertToSaved(_ sender: Any?) {
self.currentDocument?.revertToSaved(sender)
}
override func browseVersions(_ sender: Any?) {
self.currentDocument?.browseVersions(sender)
}
override func lock(_ sender: Any?) {
assertionFailure()
self.currentDocument?.lock(sender)
}
override func unlock(_ sender: Any?) {
assertionFailure()
self.currentDocument?.unlock(sender)
}
override func runPageLayout(_ sender: Any?) {
self.currentDocument?.runPageLayout(sender)
}
override func printDocument(_ sender: Any?) {
self.currentDocument?.printDocument(sender)
}
// MARK: Document actions
@objc func changeEncoding(_ sender: NSMenuItem) {
self.currentDocument?.changeEncoding(sender)
}
@objc func changeLineEnding(_ sender: NSMenuItem) {
self.currentDocument?.changeLineEnding(sender)
}
@objc func changeSyntax(_ sender: NSMenuItem) {
self.currentDocument?.changeSyntax(sender)
}
@objc func shareDocument(_ sender: NSMenuItem) {
self.currentDocument?.shareDocument(sender)
}
}

View File

@ -0,0 +1,265 @@
//
// DirectoryDocument.swift
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2024-05-01.
//
// ---------------------------------------------------------------------------
//
// © 2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import AppKit
import Observation
import UniformTypeIdentifiers
@Observable final class DirectoryDocument: NSDocument {
// MARK: Public Properties
private(set) var fileNode: FileNode?
private(set) weak var currentDocument: Document?
// MARK: Private Properties
private var documents: [Document] = []
private var windowController: DocumentWindowController? { self.windowControllers.first as? DocumentWindowController }
// MARK: Document Methods
override class var autosavesInPlace: Bool {
true // for moving location from the proxy icon
}
override class func canConcurrentlyReadDocuments(ofType typeName: String) -> Bool {
true
}
override func makeWindowControllers() {
let document = Document()
self.documents.append(document)
NSDocumentController.shared.addDocument(document)
let windowController = DocumentWindowController(document: document, directoryDocument: self)
self.addWindowController(windowController)
document.windowController = windowController
}
override nonisolated func read(from url: URL, ofType typeName: String) throws {
let fileWrapper = try FileWrapper(url: url)
let node = FileNode(fileWrapper: fileWrapper, fileURL: url)
Task { @MainActor in
self.fileNode = node
self.windowController?.synchronizeWindowTitleWithDocumentName()
}
}
override func data(ofType typeName: String) throws -> Data {
fatalError("\(self.className) is readonly")
}
override func move(to url: URL) async throws {
try await super.move(to: url)
// remake node tree
self.revert()
}
override func shouldCloseWindowController(_ windowController: NSWindowController, delegate: Any?, shouldClose shouldCloseSelector: Selector?, contextInfo: UnsafeMutableRawPointer?) {
Task {
// save unsaved changes in the file documents before closing
var canClose = true
for document in self.documents where document.isDocumentEdited {
// ask to the user one-by-one
guard await document.canClose() else {
canClose = false
break
}
}
DelegateContext(delegate: delegate, selector: shouldCloseSelector, contextInfo: contextInfo).perform(from: self, flag: canClose)
}
}
override func close() {
super.close()
for document in self.documents {
document.close()
}
}
// MARK: File Presenter Methods
override nonisolated func presentedItemDidChange() {
// called also when:
// - subitem moved (presentedSubitem(at:didMoveTo:))
// - new subitem added (presentedSubitemDidAppear(at:))
super.presentedItemDidChange()
// remake node tree
Task { @MainActor in
self.revert()
}
}
override nonisolated func presentedItemDidMove(to newURL: URL) {
super.presentedItemDidMove(to: newURL)
// remake fileURLs with the new location
Task { @MainActor in
self.revert(fileURL: newURL)
}
}
// MARK: Public Methods
/// Opens a document as a member.
///
/// - Parameter fileURL: The file URL of the document to open.
/// - Returns: Return `true` if the document of the given file did successfully open.
@discardableResult func openDocument(at fileURL: URL) async -> Bool {
assert(!fileURL.hasDirectoryPath)
guard fileURL != self.currentDocument?.fileURL else { return true } // already open
// existing document
if let document = NSDocumentController.shared.document(for: fileURL) as? Document {
if self.documents.contains(document) {
self.changeFrontmostDocument(to: document)
return true
} else {
return await withCheckedContinuation { continuation in
self.presentErrorAsSheet(DirectoryDocumentError.alreadyOpen(fileURL)) { _ in
document.showWindows()
continuation.resume(returning: false)
}
}
}
}
let contentType = try? fileURL.resourceValues(forKeys: [.contentTypeKey]).contentType
// ignore (possibly) unsupported files
guard contentType?.conforms(to: .text) == true || fileURL.pathExtension.isEmpty,
fileURL.lastPathComponent != ".DS_Store"
else { return true }
// make document
let document: NSDocument
do {
document = try NSDocumentController.shared.makeDocument(withContentsOf: fileURL, ofType: (contentType ?? .data).identifier)
} catch {
self.presentErrorAsSheet(error)
return false
}
guard let document = document as? Document else { return false }
self.documents.append(document)
NSDocumentController.shared.addDocument(document)
self.changeFrontmostDocument(to: document)
return true
}
// MARK: Private Methods
/// Changes the frontmost document.
///
/// - Parameter document: The document to bring frontmost.
private func changeFrontmostDocument(to document: Document) {
assert(self.documents.contains(document))
// remove window controller from current document
self.windowController?.fileDocument?.windowController = nil
document.windowController = self.windowController
self.windowController?.fileDocument = document
self.currentDocument = document
document.makeWindowControllers()
// clean-up
self.disposeUnusedDocuments()
}
/// Disposes unused documents.
private func disposeUnusedDocuments() {
// -> postpone closing opened document if edited
for document in self.documents where !document.isDocumentEdited && document != self.currentDocument {
document.close()
self.documents.removeFirst(document)
}
}
}
private enum DirectoryDocumentError: LocalizedError {
case alreadyOpen(URL)
var errorDescription: String? {
switch self {
case .alreadyOpen(let fileURL):
String(localized: "DirectoryDocumentError.alreadyOpen.description", defaultValue: "The file “\(fileURL.lastPathComponent)” is already open in a different window.")
}
}
var recoverySuggestion: String? {
switch self {
case .alreadyOpen:
String(localized: "DirectoryDocumentError.alreadyOpen.recoverySuggestion", defaultValue: "To open it in this window, close the existing window first.", comment: "“it” is the file in description.")
}
}
}

View File

@ -62,6 +62,8 @@ extension Document: EditorSource {
var isTransient = false // untitled & empty document that was created automatically
nonisolated(unsafe) var isVerticalText = false
weak var windowController: DocumentWindowController?
// MARK: Readonly Properties
@ -261,9 +263,13 @@ extension Document: EditorSource {
override func makeWindowControllers() {
if self.windowControllers.isEmpty { // -> A transient document already has one.
// -> The window controller already exists either when:
// - The window of a transient document was reused.
// - The document is a member of a DirectoryDocument.
if self.windowController == nil {
let windowController = DocumentWindowController(document: self)
self.addWindowController(windowController)
self.windowController = windowController
// avoid showing "edited" indicator in the close button when the content is empty
if !Self.autosavesInPlace {
@ -281,6 +287,20 @@ extension Document: EditorSource {
}
override func showWindows() {
super.showWindows()
self.windowController?.showWindow(nil)
}
override var windowForSheet: NSWindow? {
super.windowForSheet ?? self.windowController?.window
}
override nonisolated func fileNameExtension(forType typeName: String, saveOperation: NSDocument.SaveOperationType) -> String? {
if !self.isDraft, let pathExtension = self.fileURL?.pathExtension {
@ -838,7 +858,7 @@ extension Document: EditorSource {
/// The view controller represents document.
var viewController: DocumentViewController? {
(self.windowControllers.first?.contentViewController as? WindowContentViewController)?.documentViewController
(self.windowController?.contentViewController as? WindowContentViewController)?.documentViewController
}
@ -1319,7 +1339,7 @@ extension Document: EditorSource {
/// Shows the warning inspector in the document window.
private func showWarningInspector() {
(self.windowControllers.first?.contentViewController as? WindowContentViewController)?.showInspector(pane: .warnings)
(self.windowController?.contentViewController as? WindowContentViewController)?.showInspector(pane: .warnings)
}
}

View File

@ -64,7 +64,8 @@ final class DocumentController: NSDocumentController {
// observe the frontmost syntax change
self.mainWindowObserver = NSApp.publisher(for: \.mainWindow)
.map { $0?.windowController?.document as? Document }
.map { $0?.windowController as? DocumentWindowController }
.map { $0?.fileDocument }
.sink { [unowned self] in
self.currentSyntaxName = $0?.syntaxParser.name
self.syntaxObserver = $0?.didChangeSyntax
@ -84,6 +85,11 @@ final class DocumentController: NSDocumentController {
override func openDocument(withContentsOf url: URL, display displayDocument: Bool) async throws -> (NSDocument, Bool) {
// do nothing for DirectoryDocument
if url.hasDirectoryPath {
return try await super.openDocument(withContentsOf: url, display: displayDocument)
}
// obtain transient document if exists
let transientDocument: Document? = self.transientDocumentLock.withLock { [unowned self] in
guard
@ -223,7 +229,7 @@ final class DocumentController: NSDocumentController {
switch item.action {
case #selector(newDocumentAsTab):
return self.currentDocument != nil
return self.currentDocument is Document
default:
break
}

View File

@ -43,10 +43,23 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
}
weak var fileDocument: Document? {
didSet {
if let fileDocument {
self.updateDocument(fileDocument)
}
}
}
// MARK: Private Properties
private static let windowFrameName = NSWindow.FrameAutosaveName("Document")
private var directoryDocument: DirectoryDocument?
private var hasDirectoryBrowser: Bool { self.directoryDocument != nil }
private lazy var editedIndicator: NSView = NSHostingView(rootView: Circle()
.fill(.tertiary)
.frame(width: 4, height: 4)
@ -67,12 +80,16 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
// MARK: Lifecycle
required init(document: Document) {
required init(document: Document, directoryDocument: DirectoryDocument? = nil) {
let window = DocumentWindow(contentViewController: WindowContentViewController(document: document))
let window = DocumentWindow(contentViewController: WindowContentViewController(document: document, directoryDocument: directoryDocument))
window.styleMask.update(with: .fullSizeContentView)
window.setFrameAutosaveName(Self.windowFrameName)
if directoryDocument != nil {
window.tabbingMode = .disallowed
}
// set window size
let width = UserDefaults.standard[.windowWidth] ?? 0
let height = UserDefaults.standard[.windowHeight] ?? 0
@ -82,12 +99,16 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
window.setFrame(.init(origin: window.frame.origin, size: frameSize), display: false)
}
self.directoryDocument = directoryDocument
super.init(window: window)
self.updateDocument(document)
window.delegate = self
// setup toolbar
let toolbar = NSToolbar(identifier: .document)
let toolbar = NSToolbar(identifier: self.hasDirectoryBrowser ? .directoryDocument : .document)
toolbar.displayMode = .iconOnly
toolbar.allowsUserCustomization = true
toolbar.autosavesConfiguration = true
@ -154,6 +175,17 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
}
override func synchronizeWindowTitleWithDocumentName() {
super.synchronizeWindowTitleWithDocumentName()
if let directoryURL = self.directoryDocument?.fileURL, let fileDocument = self.fileDocument {
// display current document title as window subtitle
self.window?.subtitle = fileDocument.fileURL?.path(relativeTo: directoryURL) ?? fileDocument.displayName
}
}
// MARK: Window Delegate
@ -202,6 +234,8 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
viewController.document = document
}
self.synchronizeWindowTitleWithDocumentName()
// observe document's syntax change for toolbar
self.selectSyntaxPopUpItem(with: document.syntaxParser.name)
self.documentSyntaxObserver = document.didChangeSyntax
@ -253,7 +287,7 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
return item
}
if let syntaxName = (self.document as? Document)?.syntaxParser.name {
if let syntaxName = self.fileDocument?.syntaxParser.name {
self.selectSyntaxPopUpItem(with: syntaxName)
}
}
@ -304,6 +338,7 @@ final class DocumentWindowController: NSWindowController, NSWindowDelegate {
private extension NSToolbar.Identifier {
static let document = Self("Document")
static let directoryDocument = Self("DirectoryDocument")
}
@ -351,18 +386,28 @@ private extension NSToolbarItem.Identifier {
extension DocumentWindowController: NSToolbarDelegate {
private var directoryIdentifiers: [NSToolbarItem.Identifier] {
self.hasDirectoryBrowser ? [
.toggleSidebar,
.sidebarTrackingSeparator,
] : []
}
func toolbarImmovableItemIdentifiers(_ toolbar: NSToolbar) -> Set<NSToolbarItem.Identifier> {
[
.flexibleSpace,
Set(self.directoryIdentifiers).union([
.inspectorTrackingSeparator,
]
.flexibleSpace,
.inspector,
])
}
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[
self.directoryIdentifiers + [
.syntax,
.inspectorTrackingSeparator,
.flexibleSpace,
@ -373,7 +418,7 @@ extension DocumentWindowController: NSToolbarDelegate {
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[
self.directoryIdentifiers + [
.syntax,
.inspector,
.textSize,
@ -756,7 +801,7 @@ extension DocumentWindowController: NSSharingServicePickerToolbarItemDelegate {
public func items(for pickerToolbarItem: NSSharingServicePickerToolbarItem) -> [Any] {
guard let document = self.document else { return [] }
guard let document = self.fileDocument else { return [] }
return [document]
}

View File

@ -0,0 +1,160 @@
//
// FileBrowserView.swift
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2024-05-01.
//
// ---------------------------------------------------------------------------
//
// © 2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import SwiftUI
import AppKit
import UniformTypeIdentifiers
import AudioToolbox
import Defaults
struct FileBrowserView: View {
@State var document: DirectoryDocument
@AppStorage(.fileBrowserKeepsFoldersOnTop) private var keepsFoldersOnTop
@AppStorage(.fileBrowserShowsHiddenFiles) private var showsHiddenFiles
@AppStorage(.fileBrowserShowsFilenameExtensions) private var showsFilenameExtensions
@State private var selection: FileNode.ID?
@State private var error: (any Error)?
var body: some View {
VStack(spacing: 0) {
let fileNodes = (self.document.fileNode?.children ?? [])
.recursivelyFilter { self.showsHiddenFiles || !$0.isHidden }
.sorted(keepsFoldersOnTop: self.keepsFoldersOnTop)
List(fileNodes, children: \.children, selection: $selection) { node in
NodeView(node: node)
}
.listStyle(.sidebar)
.contextMenu(forSelectionType: FileNode.ID.self) { ids in
let node = ids.first.flatMap { self.document.fileNode?.node(with: $0, keyPath: \.id) }
self.contextMenu(node: node)
}
}
.onChange(of: self.selection) { (oldValue, _) in
guard let node = self.selectedNode, !node.isDirectory else { return }
Task {
guard await self.document.openDocument(at: node.fileURL) else {
self.selection = oldValue
return
}
}
}
.onChange(of: self.document.currentDocument) { (_, newValue) in
guard
let fileURL = newValue?.fileURL,
let node = self.document.fileNode?.node(with: fileURL, keyPath: \.fileURL)
else { return }
self.selection = node.id
}
.alert(error: $error)
}
// MARK: Private Methods
private var selectedNode: FileNode? {
self.selection.flatMap { self.document.fileNode?.node(with: $0, keyPath: \.id) }
}
@ViewBuilder private func contextMenu(node: FileNode?) -> some View {
if let node {
Button(String(localized: "Show in Finder", table: "Document", comment: "menu item label")) {
NSWorkspace.shared.activateFileViewerSelecting([node.fileURL])
}
Divider()
if !node.isDirectory {
Button(String(localized: "Open with External Editor", table: "Document", comment: "menu item label")) {
NSWorkspace.shared.open(node.fileURL)
}
}
if !node.isDirectory, NSDocumentController.shared.document(for: node.fileURL) == nil {
Button(String(localized: "Open in Separate Window", table: "Document", comment: "menu item label")) {
NSDocumentController.shared.openDocument(withContentsOf: node.fileURL, display: true) { (_, _, error) in
self.error = error
}
}
}
Divider()
Button(String(localized: "Move to Trash", table: "Document", comment: "menu item label")) {
do {
try FileManager.default.trashItem(at: node.fileURL, resultingItemURL: nil)
AudioServicesPlaySystemSound(.moveToTrash)
} catch {
self.error = error
}
}
Divider()
}
Toggle(String(localized: "Show Filename Extensions", table: "Document", comment: "menu item label (Check how Apple translates the term “filename extension.”)"), isOn: $showsFilenameExtensions)
Toggle(String(localized: "Show Hidden Files", table: "Document", comment: "menu item label"), isOn: $showsHiddenFiles)
Toggle(String(localized: "Keep Folders on Top", table: "Document", comment: "menu item label"), isOn: $keepsFoldersOnTop)
}
}
private struct NodeView: View {
var node: FileNode
@AppStorage(.fileBrowserShowsFilenameExtensions) private var showsFilenameExtensions
var body: some View {
Label {
Text(self.showsFilenameExtensions ? self.node.name : self.node.name.deletingPathExtension)
} icon: {
Image(systemName: self.node.isDirectory ? "folder" : "doc")
}
.lineLimit(1)
.opacity(self.node.isHidden ? 0.5 : 1)
}
}
// MARK: - Preview
#Preview(traits: .fixedLayout(width: 200, height: 400)) {
FileBrowserView(document: DirectoryDocument())
.listStyle(.sidebar)
}

View File

@ -0,0 +1,101 @@
//
// FileNode.swift
//
// CotEditor
// https://coteditor.com
//
// Created by 1024jp on 2024-05-01.
//
// ---------------------------------------------------------------------------
//
// © 2024 1024jp
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct FileNode: Equatable, Sendable {
var name: String
var paths: [String]
var children: [FileNode]?
var isDirectory: Bool
var fileURL: URL
var isHidden: Bool { self.name.starts(with: ".") }
var directoryURL: URL { self.isDirectory ? self.fileURL : self.fileURL.deletingLastPathComponent() }
}
extension FileNode: Identifiable {
var id: [String] { self.paths + [self.name] }
}
extension FileNode {
init?(fileWrapper: FileWrapper, paths: [String] = [], fileURL: URL) {
guard let filename = fileWrapper.filename else { return nil }
self.name = filename
self.paths = paths
self.isDirectory = fileWrapper.isDirectory
self.fileURL = fileURL
if fileWrapper.isDirectory {
self.children = fileWrapper.fileWrappers?
.compactMap { FileNode(fileWrapper: $0.value, paths: paths + [filename], fileURL: fileURL.appending(component: $0.key)) }
}
}
/// Returns a file node with the given ID in the receiver's tree if exists.
///
/// - Parameter id: The identifier of the node to find.
/// - Returns: A found file node.
func node<Value: Equatable>(with value: Value, keyPath: KeyPath<FileNode, Value>) -> FileNode? {
(self[keyPath: keyPath] == value) ? self : self.children?
.lazy
.compactMap { $0.node(with: value, keyPath: keyPath) }
.first
}
}
extension [FileNode] {
func sorted(keepsFoldersOnTop: Bool) -> [FileNode] {
self.map {
var tree = $0
tree.children = tree.children?.sorted(keepsFoldersOnTop: keepsFoldersOnTop)
return tree
}
.sorted(\.name, options: [.caseInsensitive, .localized])
.sorted { keepsFoldersOnTop ? ($0.isDirectory && !$1.isDirectory) : false }
}
func recursivelyFilter(_ isIncluded: (FileNode) -> Bool) -> [FileNode] {
self.filter(isIncluded).map {
var tree = $0
tree.children = tree.children?.recursivelyFilter(isIncluded)
return tree
}
}
}

View File

@ -212,7 +212,7 @@ private extension NSTextView {
var documentName: String? {
self.window?.windowController?.document?.displayName
(self.window?.windowController as? DocumentWindowController)?.fileDocument?.displayName
}
}

View File

@ -241,7 +241,7 @@ class LayoutManager: NSLayoutManager, InvisibleDrawing, ValidationIgnorable, Lin
switch invisible {
case .newLine:
let textView = self.firstTextView
return MainActor.assumeIsolated { (textView?.window?.windowController?.document as? Document)?.lineEndingScanner.isInvalidLineEnding(at: characterIndex) == true }
return MainActor.assumeIsolated { (textView?.window?.windowController as? DocumentWindowController)?.fileDocument?.lineEndingScanner.isInvalidLineEnding(at: characterIndex) == true }
default:
return false
}

View File

@ -81,6 +81,32 @@ extension NSDocument {
}
// MARK: Close
extension NSDocument {
/// Asks the user for the handling unsaved changes and waits for the answer.
///
/// - Returns: Returns `false` when the user cancels the operation, otherwise `true`.
final func canClose() async -> Bool {
await withCheckedContinuation { continuation in
self.canClose(withDelegate: self,
shouldClose: #selector(self.document(_:shouldClose:contextInfo:)),
contextInfo: bridgeWrapped(continuation))
}
}
@objc private final func document(_ document: NSDocument, shouldClose: Bool, contextInfo: UnsafeRawPointer) {
let continuation: CheckedContinuation<Bool, Never> = bridgeUnwrapped(contextInfo)
continuation.resume(returning: shouldClose)
}
}
// MARK: Error Handling
extension NSDocument {

View File

@ -24,12 +24,14 @@
//
import AppKit
import SwiftUI
final class WindowContentViewController: NSSplitViewController {
// MARK: Public Properties
var document: Document { didSet { self.updateDocument() } }
var directoryDocument: DirectoryDocument?
var documentViewController: DocumentViewController? { self.contentViewController.documentViewController }
@ -45,9 +47,10 @@ final class WindowContentViewController: NSSplitViewController {
// MARK: Split View Controller Methods
init(document: Document) {
init(document: Document, directoryDocument: DirectoryDocument?) {
self.document = document
self.directoryDocument = directoryDocument
super.init(nibName: nil, bundle: nil)
}
@ -74,10 +77,16 @@ final class WindowContentViewController: NSSplitViewController {
super.viewDidLoad()
// -> Need to set *both* identifier and autosaveName to make autosaving work.
let autosaveName = "WindowContentSplitView"
let autosaveName = (self.directoryDocument == nil) ? "WindowContentSplitView" : "DirectoryWindowContentSplitView"
self.splitView.identifier = NSUserInterfaceItemIdentifier(autosaveName)
self.splitView.autosaveName = autosaveName
if let directoryDocument {
let rootView = FileBrowserView(document: directoryDocument)
let sidebarViewItem = NSSplitViewItem(sidebarWithViewController: NSHostingController(rootView: rootView))
self.addSplitViewItem(sidebarViewItem)
}
let contentViewController = ContentViewController(document: self.document)
self.contentViewItem = NSSplitViewItem(viewController: contentViewController)
self.addSplitViewItem(self.contentViewItem)