Skip to content

Commit 2a3cee2

Browse files
LPFchanclaude
andcommitted
TextEdit menu audit + WYSIWYG viewer + UX polish
File menu additions (from TextEdit audit): - Open Recent submenu wired to NSDocumentController.recentDocumentURLs - Duplicate (Cmd+Opt+S) — in-memory copy of current document - Rename… — moves file on disk, updates proxy icon - Move To… — relocates file via save panel - Revert to Saved — confirms, then reloads from disk - Export as PDF… — renders preview HTML → PDFExporter via WKWebView WYSIWYG viewer mode: - Preview/viewer is now a directly editable contentEditable surface - Click anywhere in the formatted document and start typing - Paste and Match Style enforced automatically in JS: all pasted content strips RTF/HTML formatting to plain text on arrival - Viewer edits sync back to document.rawText (debounced 400ms) - Switching viewer → edit mode extracts innerText before handoff - PreviewNavigationDelegate merged into PreviewView.Coordinator; standalone file removed Paste and Match Style (Cmd+Opt+Shift+V): - In edit mode: routes to NSTextView.pasteAsPlainText (native) - In viewer mode: JS paste interceptor handles it automatically UX: - Default window width reduced to 660pt (≈60% of 1100) - No untitled document created on first launch — shows welcome screen - Tab bar hidden when only one document is open - Tab bar hidden when sidebar is visible (mutual exclusion) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 214645b commit 2a3cee2

11 files changed

Lines changed: 395 additions & 77 deletions

File tree

Markfops.xcodeproj/project.pbxproj

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
1CBFD8563DE8AC10A9D37BB9 /* FindReplaceBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF2BCBDD65A92722203488B3 /* FindReplaceBar.swift */; };
1919
1F56B58BFA0C13FDC10A028B /* PreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 35ABE72586B11782CBEF1196 /* PreviewView.swift */; };
2020
244ECCE7A99431A581667DE0 /* EditMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9DC1A7625D654E0BA585E10 /* EditMode.swift */; };
21-
2D62B3A9CACA83C4C43E5E80 /* PreviewNavigationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC681233B59F888859E58958 /* PreviewNavigationDelegate.swift */; };
2221
3F2BDA9D46E8FF6AC6D6DF9B /* MarkfopsCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2C916E9DBBDC508AC9B40C6 /* MarkfopsCommands.swift */; };
2322
4773F129D7E392E1E516AC2B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14011151494BB8C871B4D646 /* AppDelegate.swift */; };
2423
51151CAB4FFDDC744AD66BFB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = DAD2AA09699D1444F08346FB /* Localizable.strings */; };
@@ -32,6 +31,7 @@
3231
928B97786537075AB7956F9B /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2138F5AC97A090E4B6272FB3 /* SidebarView.swift */; };
3332
95B40AB0A6E715F4F419AB71 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68C64074880ABB02F29BF99B /* SettingsView.swift */; };
3433
991E9BBEAB1CC4AD8C135045 /* HorizontalTabBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 680F15DCD240DD2819574D40 /* HorizontalTabBarView.swift */; };
34+
9F061AB4D3F43EFC7BB45EE6 /* PDFExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F05D215E01EBC0B400953604 /* PDFExporter.swift */; };
3535
A2429F9EC0E81A0422EACD46 /* HeadingNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4625617133DDEF9653F91CA /* HeadingNode.swift */; };
3636
B0E795820CE68F8DE414613B /* MarkdownEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D67A2CE029ADE8669191CD1 /* MarkdownEditorView.swift */; };
3737
E0C3F0B655B41483DB893661 /* EditorContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 82E44B05505F730184B28AED /* EditorContainerView.swift */; };
@@ -83,7 +83,7 @@
8383
CFB5DBBC98ED92CDEC01BA31 /* HeadingParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeadingParserTests.swift; sourceTree = "<group>"; };
8484
DF2BCBDD65A92722203488B3 /* FindReplaceBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindReplaceBar.swift; sourceTree = "<group>"; };
8585
E5A38073AFAC4D127F4B5E82 /* DocumentStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentStore.swift; sourceTree = "<group>"; };
86-
EC681233B59F888859E58958 /* PreviewNavigationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewNavigationDelegate.swift; sourceTree = "<group>"; };
86+
F05D215E01EBC0B400953604 /* PDFExporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PDFExporter.swift; sourceTree = "<group>"; };
8787
F0AF7585BE2B913F939DFAED /* Markfops.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Markfops.app; sourceTree = BUILT_PRODUCTS_DIR; };
8888
/* End PBXFileReference section */
8989

@@ -135,7 +135,6 @@
135135
children = (
136136
0DCA6FE1E92902863C9859DD /* HTMLTemplate.swift */,
137137
8BABF61C15310FD3F71CDE50 /* MarkdownRenderer.swift */,
138-
EC681233B59F888859E58958 /* PreviewNavigationDelegate.swift */,
139138
);
140139
path = Renderer;
141140
sourceTree = "<group>";
@@ -212,6 +211,7 @@
212211
C1E940C3A47CCAAD33C2FECB /* FileIO */ = {
213212
isa = PBXGroup;
214213
children = (
214+
F05D215E01EBC0B400953604 /* PDFExporter.swift */,
215215
);
216216
path = FileIO;
217217
sourceTree = "<group>";
@@ -350,7 +350,7 @@
350350
0F3DB7DCF1EBB8111808A9C0 /* MarkdownSyntaxHighlighter.swift in Sources */,
351351
012F13E675D8CB2ECB471333 /* MarkfopsApp.swift in Sources */,
352352
3F2BDA9D46E8FF6AC6D6DF9B /* MarkfopsCommands.swift in Sources */,
353-
2D62B3A9CACA83C4C43E5E80 /* PreviewNavigationDelegate.swift in Sources */,
353+
9F061AB4D3F43EFC7BB45EE6 /* PDFExporter.swift in Sources */,
354354
1F56B58BFA0C13FDC10A028B /* PreviewView.swift in Sources */,
355355
95B40AB0A6E715F4F419AB71 /* SettingsView.swift in Sources */,
356356
928B97786537075AB7956F9B /* SidebarView.swift in Sources */,

Markfops.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Markfops/App/MarkfopsApp.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ struct MarkfopsApp: App {
1818
.commands {
1919
MarkfopsCommands()
2020
}
21-
.defaultSize(width: 1100, height: 700)
21+
.defaultSize(width: 660, height: 700)
2222

2323
Settings {
2424
SettingsView()

Markfops/Commands/MarkfopsCommands.swift

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ struct MarkfopsCommands: Commands {
44
@FocusedValue(\.documentStore) private var store
55

66
var body: some Commands {
7-
// Replace default New Window command
7+
// MARK: File menu
88
CommandGroup(replacing: .newItem) {
99
Button("New") {
1010
store?.newDocument()
@@ -22,6 +22,25 @@ struct MarkfopsCommands: Commands {
2222
.keyboardShortcut("o", modifiers: .command)
2323
}
2424

25+
CommandGroup(after: .newItem) {
26+
Menu("Open Recent") {
27+
let recents = NSDocumentController.shared.recentDocumentURLs
28+
if recents.isEmpty {
29+
Text("No Recent Documents").foregroundColor(.secondary)
30+
} else {
31+
ForEach(recents.prefix(10), id: \.self) { url in
32+
Button(url.deletingPathExtension().lastPathComponent) {
33+
store?.open(url: url)
34+
}
35+
}
36+
Divider()
37+
Button("Clear Menu") {
38+
NSDocumentController.shared.clearRecentDocuments(nil)
39+
}
40+
}
41+
}
42+
}
43+
2544
CommandGroup(replacing: .saveItem) {
2645
Button("Save") {
2746
guard let doc = store?.activeDocument else { return }
@@ -35,13 +54,47 @@ struct MarkfopsCommands: Commands {
3554
}
3655
.keyboardShortcut("s", modifiers: [.command, .shift])
3756

57+
Button("Duplicate") {
58+
guard let doc = store?.activeDocument else { return }
59+
store?.duplicate(doc)
60+
}
61+
.keyboardShortcut("s", modifiers: [.command, .option])
62+
63+
Button("Rename…") {
64+
guard let doc = store?.activeDocument else { return }
65+
store?.rename(doc)
66+
}
67+
68+
Button("Move To…") {
69+
guard let doc = store?.activeDocument else { return }
70+
store?.moveTo(doc)
71+
}
72+
73+
Divider()
74+
75+
Button("Revert to Saved") {
76+
guard let doc = store?.activeDocument else { return }
77+
store?.revertToSaved(doc)
78+
}
79+
.disabled(store?.activeDocument?.fileURL == nil || store?.activeDocument?.isDirty == false)
80+
81+
Divider()
82+
83+
Button("Export as PDF…") {
84+
guard let doc = store?.activeDocument else { return }
85+
store?.exportAsPDF(doc)
86+
}
87+
88+
Divider()
89+
3890
Button("Close Tab") {
3991
guard let id = store?.activeID else { return }
4092
store?.close(id: id)
4193
}
4294
.keyboardShortcut("w", modifiers: .command)
4395
}
4496

97+
// MARK: View menu
4598
CommandMenu("View") {
4699
Button("Toggle Edit / Preview") {
47100
guard let doc = store?.activeDocument else { return }
@@ -76,6 +129,7 @@ struct MarkfopsCommands: Commands {
76129
.keyboardShortcut("-", modifiers: .command)
77130
}
78131

132+
// MARK: Format menu
79133
CommandMenu("Format") {
80134
Button("Bold") {
81135
NSApp.sendAction(#selector(NSTextView.wrapBold), to: nil, from: nil)
@@ -91,6 +145,16 @@ struct MarkfopsCommands: Commands {
91145
NSApp.sendAction(#selector(NSTextView.wrapCode), to: nil, from: nil)
92146
}
93147
.keyboardShortcut("k", modifiers: [.command, .option])
148+
149+
Divider()
150+
151+
// Paste and Match Style — strips RTF/rich formatting, inserts plain text.
152+
// In Edit mode: handled natively by NSTextView (pasteAsPlainText:).
153+
// In Preview/WYSIWYG mode: enforced automatically via the JS paste interceptor.
154+
Button("Paste and Match Style") {
155+
NSApp.sendAction(#selector(NSTextView.pasteAsPlainText(_:)), to: nil, from: nil)
156+
}
157+
.keyboardShortcut("v", modifiers: [.command, .option, .shift])
94158
}
95159
}
96160

@@ -108,7 +172,7 @@ struct MarkfopsCommands: Commands {
108172
}
109173
}
110174

111-
// MARK: - NSTextView action selectors
175+
// MARK: - NSTextView formatting selectors
112176

113177
extension NSTextView {
114178
@objc func wrapBold() {

Markfops/FileIO/PDFExporter.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import WebKit
2+
3+
/// Renders an HTML string to a PDF file via an off-screen WKWebView.
4+
enum PDFExporter {
5+
6+
static func export(html: String, to url: URL) {
7+
let webView = WKWebView(frame: NSRect(x: 0, y: 0, width: 800, height: 1))
8+
webView.loadHTMLString(html, baseURL: nil)
9+
10+
// Wait for load then print to PDF
11+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
12+
let printInfo = NSPrintInfo.shared.copy() as! NSPrintInfo
13+
printInfo.jobDisposition = .save
14+
printInfo.dictionary()[NSPrintInfo.AttributeKey("NSPrintJobSavingURL")] = url
15+
16+
let op = webView.printOperation(with: printInfo)
17+
op.showsPrintPanel = false
18+
op.showsProgressPanel = false
19+
op.run()
20+
}
21+
}
22+
}

Markfops/Renderer/PreviewNavigationDelegate.swift

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)