mirror of
https://github.com/coteditor/CotEditor.git
synced 2024-09-11 19:27:30 +03:00
Add DirectoryDocument (#278)
This commit is contained in:
parent
9cf39fe4a0
commit
309cfd98d8
@ -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 */,
|
||||
|
@ -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>
|
||||
|
@ -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 Sepeti’ne 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 un’altra 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" : {
|
||||
|
@ -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" : {
|
||||
|
@ -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")
|
||||
|
@ -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,
|
||||
|
150
CotEditor/Sources/DirectoryDocument+Actions.swift
Normal file
150
CotEditor/Sources/DirectoryDocument+Actions.swift
Normal 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)
|
||||
}
|
||||
}
|
265
CotEditor/Sources/DirectoryDocument.swift
Normal file
265
CotEditor/Sources/DirectoryDocument.swift
Normal 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.")
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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]
|
||||
}
|
||||
|
160
CotEditor/Sources/FileBrowserView.swift
Normal file
160
CotEditor/Sources/FileBrowserView.swift
Normal 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)
|
||||
}
|
101
CotEditor/Sources/FileNode.swift
Normal file
101
CotEditor/Sources/FileNode.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
@ -212,7 +212,7 @@ private extension NSTextView {
|
||||
|
||||
var documentName: String? {
|
||||
|
||||
self.window?.windowController?.document?.displayName
|
||||
(self.window?.windowController as? DocumentWindowController)?.fileDocument?.displayName
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user