fix toolbar persistence, menu pruning, and tabbed window frame handling
This commit is contained in:
1 parent
1abc51eb25
commit
baac7e71a8
5 files changed
+123
-22
No files matched your search
@@ -0,0 +1,17 @@
|
||||
# Think different
|
||||
|
||||
## Here's to the crazy ones.
|
||||
|
||||
1. The misfits
|
||||
2. The rebels
|
||||
3. The troublemakers
|
||||
|
||||
The round pegs in the square holes. The ones who see things differently. They're not fond of rules.
|
||||
And they have no respect for the status quo. You can quote them, disagree with them, glorify or vilify them. About the only thing you can't do is ignore them. Because they change things.
|
||||
|
||||
***They push the human race forward***.
|
||||
|
||||
And while some may see them as the crazy ones, we see genius. Because the people who are crazy enough to think they can change the world, are the ones who do.
|
||||
|
||||
[Think different](http://www.apple.com)
|
||||
|
||||
@@ -342,24 +342,34 @@ final class DocumentToolbarController: NSObject, ObservableObject, NSToolbarDele
|
||||
self.setActiveThemeName = setActiveThemeName
|
||||
self.showMissingEditor = showMissingEditor
|
||||
|
||||
if window.toolbar?.identifier != .documentToolbar || window.toolbar?.delegate !== self {
|
||||
Self.removeLegacySavedToolbarConfigurations()
|
||||
if window.toolbar?.identifier != .documentToolbar {
|
||||
let toolbar = NSToolbar(identifier: .documentToolbar)
|
||||
toolbar.delegate = self
|
||||
toolbar.allowsUserCustomization = true
|
||||
toolbar.autosavesConfiguration = true
|
||||
toolbar.displayMode = NSToolbar.DisplayMode.default
|
||||
window.toolbar = toolbar
|
||||
} else if window.toolbar?.delegate !== self {
|
||||
window.toolbar?.delegate = self
|
||||
}
|
||||
|
||||
updateVisibleItems()
|
||||
}
|
||||
|
||||
private static func removeLegacySavedToolbarConfigurations() {
|
||||
let defaults = UserDefaults.standard
|
||||
for identifier in legacyToolbarIdentifiers {
|
||||
defaults.removeObject(forKey: "NSToolbar Configuration \(identifier)")
|
||||
}
|
||||
}
|
||||
|
||||
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
return [.toc, .tocDepth, .markdownSource, .zoom, .theme, .externalEditor, .space]
|
||||
return [.toc, .tocDepth, .markdownSource, .zoom, .theme, .externalEditor, .fixedSpace]
|
||||
}
|
||||
|
||||
func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
return [.toc, .tocDepth, .space, .markdownSource, .zoom, .theme, .externalEditor]
|
||||
return [.toc, .tocDepth, .fixedSpace, .markdownSource, .zoom, .theme, .externalEditor]
|
||||
}
|
||||
|
||||
func toolbarSelectableItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
|
||||
@@ -391,11 +401,25 @@ final class DocumentToolbarController: NSObject, ObservableObject, NSToolbarDele
|
||||
return makeThemeItem()
|
||||
case .externalEditor:
|
||||
return makeExternalEditorItem()
|
||||
case .fixedSpace:
|
||||
return makeFixedSpaceItem()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func makeFixedSpaceItem() -> NSToolbarItem {
|
||||
let item = NSToolbarItem(itemIdentifier: .fixedSpace)
|
||||
item.label = "Space"
|
||||
item.paletteLabel = "Space"
|
||||
let spacer = NSView()
|
||||
spacer.translatesAutoresizingMaskIntoConstraints = false
|
||||
spacer.widthAnchor.constraint(equalToConstant: 16).isActive = true
|
||||
spacer.heightAnchor.constraint(equalToConstant: 1).isActive = true
|
||||
item.view = spacer
|
||||
return item
|
||||
}
|
||||
|
||||
private func makeTOCItem() -> NSToolbarItem {
|
||||
let item = NSToolbarItem(itemIdentifier: .toc)
|
||||
item.label = "TOC"
|
||||
@@ -730,16 +754,22 @@ final class DocumentToolbarController: NSObject, ObservableObject, NSToolbarDele
|
||||
case .toc:
|
||||
item.image = NSImage(systemSymbolName: tocVisible ? "list.bullet.circle.fill" : "list.bullet.circle", accessibilityDescription: "TOC")
|
||||
item.toolTip = tocVisible ? "Hide Table of Contents" : "Show Table of Contents"
|
||||
item.target = self
|
||||
item.action = #selector(toggleTOC)
|
||||
item.menuFormRepresentation = toolbarMenuItem(title: "TOC", action: #selector(toggleTOC), state: tocVisible ? .on : .off)
|
||||
case .tocDepth:
|
||||
if let popup = item.view as? NSPopUpButton {
|
||||
popup.selectItem(at: max(0, min(5, tocDepth - 1)))
|
||||
popup.isEnabled = tocVisible
|
||||
popup.target = self
|
||||
popup.action = #selector(tocDepthChanged(_:))
|
||||
}
|
||||
item.menuFormRepresentation = tocDepthMenuItem()
|
||||
case .markdownSource:
|
||||
item.image = NSImage(systemSymbolName: rawVisible ? "doc.plaintext.fill" : "doc.plaintext", accessibilityDescription: "Markdown Source")
|
||||
item.toolTip = rawVisible ? "Hide Markdown Source" : "Show Markdown Source"
|
||||
item.target = self
|
||||
item.action = #selector(toggleMarkdownSource)
|
||||
item.menuFormRepresentation = toolbarMenuItem(title: "Markdown Source", action: #selector(toggleMarkdownSource), state: rawVisible ? .on : .off)
|
||||
case .zoom:
|
||||
item.view = zoomControlView()
|
||||
@@ -751,6 +781,8 @@ final class DocumentToolbarController: NSObject, ObservableObject, NSToolbarDele
|
||||
popup.addItem(withTitle: theme.name)
|
||||
}
|
||||
popup.selectItem(withTitle: activeThemeName)
|
||||
popup.target = self
|
||||
popup.action = #selector(themeChanged(_:))
|
||||
}
|
||||
item.menuFormRepresentation = themeMenuItem()
|
||||
case .externalEditor:
|
||||
@@ -778,12 +810,20 @@ private extension NSToolbarItem.Identifier {
|
||||
static let zoom = NSToolbarItem.Identifier("zoom")
|
||||
static let theme = NSToolbarItem.Identifier("theme")
|
||||
static let externalEditor = NSToolbarItem.Identifier("external-editor")
|
||||
static let fixedSpace = NSToolbarItem.Identifier("document-fixed-space")
|
||||
}
|
||||
|
||||
private extension NSToolbar.Identifier {
|
||||
static let documentToolbar = NSToolbar.Identifier("document-toolbar-v3")
|
||||
static let documentToolbar = NSToolbar.Identifier("document-toolbar-v5")
|
||||
}
|
||||
|
||||
private let legacyToolbarIdentifiers = [
|
||||
"document-toolbar",
|
||||
"document-toolbar-v2",
|
||||
"document-toolbar-v3",
|
||||
"document-toolbar-v4"
|
||||
]
|
||||
|
||||
private final class ZoomToolbarControlView: NSView {
|
||||
private let control: NSSegmentedControl
|
||||
|
||||
|
||||
@@ -25,25 +25,42 @@ enum FileMenuPruner {
|
||||
}
|
||||
}
|
||||
|
||||
final class FileMenuPrunerApplicationDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
|
||||
final class FileMenuPrunerApplicationDelegate: NSObject, NSApplicationDelegate {
|
||||
private var menuObserver: NSObjectProtocol?
|
||||
|
||||
func applicationDidFinishLaunching(_ notification: Notification) {
|
||||
installMenuObserver()
|
||||
DispatchQueue.main.async {
|
||||
self.configureFileMenu()
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillUpdate(_ notification: Notification) {
|
||||
configureFileMenu()
|
||||
deinit {
|
||||
if let menuObserver {
|
||||
NotificationCenter.default.removeObserver(menuObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func menuWillOpen(_ menu: NSMenu) {
|
||||
FileMenuPruner.removeEditingItems(from: menu)
|
||||
ExternalEditorFileMenuController.shared.update(in: menu)
|
||||
private func installMenuObserver() {
|
||||
guard menuObserver == nil else { return }
|
||||
menuObserver = NotificationCenter.default.addObserver(
|
||||
forName: NSMenu.didBeginTrackingNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { notification in
|
||||
guard let menu = notification.object as? NSMenu,
|
||||
menu === FileMenuPruner.fileMenu(in: NSApp.mainMenu)
|
||||
else { return }
|
||||
self.updateFileMenu(menu)
|
||||
}
|
||||
}
|
||||
|
||||
private func configureFileMenu() {
|
||||
guard let fileMenu = FileMenuPruner.fileMenu(in: NSApp.mainMenu) else { return }
|
||||
fileMenu.delegate = self
|
||||
updateFileMenu(fileMenu)
|
||||
}
|
||||
|
||||
private func updateFileMenu(_ fileMenu: NSMenu) {
|
||||
FileMenuPruner.removeEditingItems(from: fileMenu)
|
||||
ExternalEditorFileMenuController.shared.update(in: fileMenu)
|
||||
}
|
||||
|
||||
@@ -169,6 +169,10 @@ struct DocumentWindowConfigurator: NSViewRepresentable {
|
||||
return NSSize(width: width, height: 800)
|
||||
}
|
||||
|
||||
static func shouldApplyDocumentFrame(isTabbedWindow: Bool) -> Bool {
|
||||
!isTabbedWindow
|
||||
}
|
||||
|
||||
static func autosaveName(for fileURL: URL) -> String {
|
||||
var allowedCharacters = CharacterSet.alphanumerics
|
||||
allowedCharacters.insert(charactersIn: "-_.")
|
||||
@@ -200,15 +204,21 @@ struct DocumentWindowConfigurator: NSViewRepresentable {
|
||||
let autosaveName = Self.autosaveName(for: fileURL)
|
||||
guard context.coordinator.needsConfiguration(for: window, autosaveName: autosaveName) else { return }
|
||||
|
||||
let restoredFrame = window.setFrameUsingName(autosaveName)
|
||||
if !restoredFrame {
|
||||
window.setContentSize(Self.defaultDocumentWindowSize(tocVisible: defaultTocVisible, rawVisible: defaultRawVisible))
|
||||
window.center()
|
||||
if Self.shouldApplyDocumentFrame(isTabbedWindow: Self.isTabbedWindow(window)) {
|
||||
let restoredFrame = window.setFrameUsingName(autosaveName)
|
||||
if !restoredFrame {
|
||||
window.setContentSize(Self.defaultDocumentWindowSize(tocVisible: defaultTocVisible, rawVisible: defaultRawVisible))
|
||||
window.center()
|
||||
}
|
||||
}
|
||||
window.setFrameAutosaveName(autosaveName)
|
||||
context.coordinator.configureSavingFrame(for: window, autosaveName: autosaveName)
|
||||
}
|
||||
|
||||
private static func isTabbedWindow(_ window: NSWindow) -> Bool {
|
||||
window.tabbedWindows?.contains { $0 !== window } == true
|
||||
}
|
||||
|
||||
class Coordinator {
|
||||
private weak var configuredWindow: NSWindow?
|
||||
private var configuredAutosaveName: String?
|
||||
|
||||
@@ -79,7 +79,12 @@ final class TemplateScrollbarTests: XCTestCase {
|
||||
XCTAssertTrue(appSource.contains("FileMenuPrunerApplicationDelegate"))
|
||||
|
||||
let prunerSource = try loadSourceFile("markdownViewr/FileMenuPruner.swift")
|
||||
XCTAssertTrue(prunerSource.contains("func menuWillOpen"))
|
||||
XCTAssertTrue(prunerSource.contains("NSMenu.didBeginTrackingNotification"))
|
||||
XCTAssertFalse(prunerSource.contains("NSMenuDelegate"))
|
||||
XCTAssertFalse(prunerSource.contains(".delegate = self"))
|
||||
XCTAssertFalse(prunerSource.contains("func menuNeedsUpdate"))
|
||||
XCTAssertFalse(prunerSource.contains("func applicationWillUpdate"))
|
||||
XCTAssertFalse(prunerSource.contains("func menuWillOpen"))
|
||||
}
|
||||
|
||||
func testExternalEditorCommandIsSeparatedBelowOpenRecent() throws {
|
||||
@@ -265,6 +270,11 @@ final class TemplateScrollbarTests: XCTestCase {
|
||||
XCTAssertEqual(DocumentWindowConfigurator.defaultDocumentWindowSize(tocVisible: true, rawVisible: true), NSSize(width: 1500, height: 800))
|
||||
}
|
||||
|
||||
func testDocumentWindowFrameConfigurationSkipsTabbedWindows() {
|
||||
XCTAssertTrue(DocumentWindowConfigurator.shouldApplyDocumentFrame(isTabbedWindow: false))
|
||||
XCTAssertFalse(DocumentWindowConfigurator.shouldApplyDocumentFrame(isTabbedWindow: true))
|
||||
}
|
||||
|
||||
func testInitialSourceWidthUsesHalfAvailableWindowWidth() {
|
||||
XCTAssertEqual(
|
||||
DocumentViewLayout.initialRawWidth(windowWidth: 1200, tocVisible: false, tocWidth: 300),
|
||||
@@ -384,19 +394,26 @@ final class TemplateScrollbarTests: XCTestCase {
|
||||
XCTAssertTrue(contentSource.contains("toolbar.allowsUserCustomization = true"))
|
||||
XCTAssertTrue(contentSource.contains("toolbar.autosavesConfiguration = true"))
|
||||
XCTAssertTrue(contentSource.contains("toolbar.displayMode = NSToolbar.DisplayMode.default"))
|
||||
XCTAssertTrue(contentSource.contains(#"NSToolbar.Identifier("document-toolbar-v3")"#))
|
||||
XCTAssertFalse(contentSource.contains(#"NSToolbar.Identifier("document-toolbar-v2")"#))
|
||||
XCTAssertTrue(contentSource.contains(#"NSToolbar.Identifier("document-toolbar-v5")"#))
|
||||
XCTAssertTrue(contentSource.contains("removeLegacySavedToolbarConfigurations()"))
|
||||
XCTAssertTrue(contentSource.contains(#""NSToolbar Configuration \(identifier)""#))
|
||||
XCTAssertTrue(contentSource.contains(#""document-toolbar-v4""#))
|
||||
XCTAssertFalse(contentSource.contains(#"NSToolbar.Identifier("document-toolbar-v4")"#))
|
||||
XCTAssertTrue(contentSource.contains("} else if window.toolbar?.delegate !== self {\n window.toolbar?.delegate = self\n }"))
|
||||
XCTAssertTrue(contentSource.contains("item.target = self"))
|
||||
XCTAssertTrue(contentSource.contains("toolbarAllowedItemIdentifiers"))
|
||||
XCTAssertTrue(contentSource.contains("toolbarDefaultItemIdentifiers"))
|
||||
}
|
||||
|
||||
func testAppKitToolbarCustomizationIncludesRepeatableSpaceItems() throws {
|
||||
func testDocumentToolbarIncludesFixedSpaceItem() throws {
|
||||
let contentSource = try loadSourceFile("markdownViewr/ContentView.swift")
|
||||
|
||||
XCTAssertTrue(contentSource.contains(".space"))
|
||||
XCTAssertFalse(contentSource.contains(".space"))
|
||||
XCTAssertFalse(contentSource.contains(".flexibleSpace"))
|
||||
XCTAssertTrue(contentSource.contains("return [.toc, .tocDepth, .markdownSource, .zoom, .theme, .externalEditor, .space]"))
|
||||
XCTAssertTrue(contentSource.contains("return [.toc, .tocDepth, .space, .markdownSource, .zoom, .theme, .externalEditor]"))
|
||||
XCTAssertTrue(contentSource.contains("return [.toc, .tocDepth, .markdownSource, .zoom, .theme, .externalEditor, .fixedSpace]"))
|
||||
XCTAssertTrue(contentSource.contains("return [.toc, .tocDepth, .fixedSpace, .markdownSource, .zoom, .theme, .externalEditor]"))
|
||||
XCTAssertTrue(contentSource.contains("case .fixedSpace:\n return makeFixedSpaceItem()"))
|
||||
XCTAssertTrue(contentSource.contains(#"NSToolbarItem.Identifier("document-fixed-space")"#))
|
||||
}
|
||||
|
||||
private func loadTemplate() throws -> String {
|
||||
|
||||
Reference in new issue
Block a user