From 8573a7d1261fb26ba6d8a18a3dab95fdf4ff49e9 Mon Sep 17 00:00:00 2001 From: Hans Hoffmeister Date: Fri, 18 Sep 2026 01:03:45 +0000 Subject: [PATCH 1/6] Add a "Save and Open" toolbar button Saves the current document (same save pipeline as File > Save, so it respects the existing save options/progress reporting) and then opens the resulting file in its default application once the save completes. Matches the style of the existing toolbar buttons (ButtonToolbarItem, SF Symbol with a legacy template-image fallback). Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ETy3zxWgGdZPVzjN3vqbbC --- Classes/Document.swift | 15 +++++++++++++++ Classes/DocumentToolbarDelegate.swift | 13 +++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Classes/Document.swift b/Classes/Document.swift index 7c1ac110..f0a010ae 100644 --- a/Classes/Document.swift +++ b/Classes/Document.swift @@ -110,6 +110,17 @@ final class Document: NSDocument { save(self) } + /// Whether the next completed save should open the resulting file in + /// its default application (see saveAndOpen(_:) below). Consumed and + /// reset to false as soon as that save finishes, so a plain save + /// triggered afterwards (Cmd-S, etc.) doesn't also open the file. + private var openAfterSave: Bool = false + + @IBAction func saveAndOpen(_ sender: Any) { + openAfterSave = true + save(self) + } + override func canAsynchronouslyWrite(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType) -> Bool { return true } override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, completionHandler: @escaping (Error?) -> Void) { @@ -125,6 +136,10 @@ final class Document: NSDocument { let reloadedFile = try MP42File(url: url) self.mp4 = reloadedFile docController?.reloadData() + if self.openAfterSave { + self.openAfterSave = false + NSWorkspace.shared.open(url) + } completionHandler(error) } catch { completionHandler(error) diff --git a/Classes/DocumentToolbarDelegate.swift b/Classes/DocumentToolbarDelegate.swift index b8dc630a..be5168e5 100644 --- a/Classes/DocumentToolbarDelegate.swift +++ b/Classes/DocumentToolbarDelegate.swift @@ -14,6 +14,7 @@ private extension NSToolbarItem.Identifier { static let searchMetadata: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "SearchMetadata") static let sendToQueue: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "SendToQueue") static let showQueue: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "ShowQueue") + static let saveAndOpen: NSToolbarItem.Identifier = NSToolbarItem.Identifier(rawValue: "SaveAndOpen") } class DocumentToolbarDelegate: NSObject, NSToolbarDelegate { @@ -143,17 +144,25 @@ class DocumentToolbarDelegate: NSObject, NSToolbarDelegate { symbolName: "photo.stack", target: nil, action: #selector(AppDelegate.showQueueWindow(_:))) + } else if itemIdentifier == .saveAndOpen { + return ButtonToolbarItem(itemIdentifier: itemIdentifier, + label: NSLocalizedString("Save and Open", comment: "Toolbar"), + toolTip: NSLocalizedString("Save the document and open the file", comment: "Toolbar"), + image: "NSShareTemplate", + symbolName: "arrow.up.forward.app", + target: target, + action: #selector(Document.saveAndOpen(_:))) } return nil } @MainActor func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.importTracks, .searchMetadata, .space, .actions, .space, .deleteTracks, .flexibleSpace, .sendToQueue, .showQueue,] + return [.importTracks, .searchMetadata, .space, .actions, .space, .deleteTracks, .flexibleSpace, .sendToQueue, .saveAndOpen, .showQueue,] } @MainActor func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - return [.importTracks, .deleteTracks, .actions, .searchMetadata, .sendToQueue, .showQueue, .flexibleSpace, .space] + return [.importTracks, .deleteTracks, .actions, .searchMetadata, .sendToQueue, .saveAndOpen, .showQueue, .flexibleSpace, .space] } } From 2d76a21275cb5ab599e3ef2d8eccd1480718a475 Mon Sep 17 00:00:00 2001 From: Hans Hoffmeister Date: Fri, 18 Sep 2026 02:07:19 +0000 Subject: [PATCH 2/6] Add a GitHub Actions build check Compile-only verification (no signing, no tests) on a macOS runner: checks out the repo with submodules, then xcodebuilds the Subler target. Runs on every push and PR to any branch, so a branch shows red/green on GitHub before anyone has to open Xcode. This is fork/CI infrastructure, not app code -- merged into every branch that should get build coverage, but deliberately kept off of main so main stays a byte-for-byte mirror of upstream for clean rebasing. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ETy3zxWgGdZPVzjN3vqbbC --- .github/workflows/build.yml | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/build.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..3d45b785 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,33 @@ +name: Build + +# Compile-only verification, no signing/notarization/tests. Runs on every +# push and PR so a branch (main, a PR branch, a personal-build branch, +# whatever) shows red/green on GitHub before anyone has to open Xcode. +on: + push: + branches: ["**"] + pull_request: + branches: ["**"] + workflow_dispatch: + +jobs: + build: + runs-on: macos-14 + + steps: + - name: Check out repo (with submodules) + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Show Xcode version + run: xcodebuild -version + + - name: Build Subler + run: | + xcodebuild \ + -project Subler.xcodeproj \ + -target Subler \ + -configuration Debug \ + CODE_SIGNING_ALLOWED=NO \ + build From 71ad32077133117975c681f7f1a0ff272b151cc9 Mon Sep 17 00:00:00 2001 From: Hans Hoffmeister Date: Sat, 19 Sep 2026 22:37:50 +0000 Subject: [PATCH 3/6] Rename "Save and Open" toolbar button to "Open and Save" Also bump the document window's toolbar identifier (SublerDocumentToolbar -> SublerDocumentToolbar2). The button was already wired into toolbarDefaultItemIdentifiers() when it was added, but with autosavesConfiguration on, NSToolbar only consults the delegate's defaults the first time it sees a given identifier -- an install with a toolbar layout already saved from before this button existed just keeps replaying that old saved layout forever, so the button never actually showed up for it. Bumping the identifier makes everyone pick up the current default set again. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ETy3zxWgGdZPVzjN3vqbbC --- Classes/DocumentToolbarDelegate.swift | 2 +- Classes/DocumentWindowController.swift | 10 +++++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/Classes/DocumentToolbarDelegate.swift b/Classes/DocumentToolbarDelegate.swift index be5168e5..ae79a2be 100644 --- a/Classes/DocumentToolbarDelegate.swift +++ b/Classes/DocumentToolbarDelegate.swift @@ -146,7 +146,7 @@ class DocumentToolbarDelegate: NSObject, NSToolbarDelegate { action: #selector(AppDelegate.showQueueWindow(_:))) } else if itemIdentifier == .saveAndOpen { return ButtonToolbarItem(itemIdentifier: itemIdentifier, - label: NSLocalizedString("Save and Open", comment: "Toolbar"), + label: NSLocalizedString("Open and Save", comment: "Toolbar"), toolTip: NSLocalizedString("Save the document and open the file", comment: "Toolbar"), image: "NSShareTemplate", symbolName: "arrow.up.forward.app", diff --git a/Classes/DocumentWindowController.swift b/Classes/DocumentWindowController.swift index f3b0e651..359223b0 100644 --- a/Classes/DocumentWindowController.swift +++ b/Classes/DocumentWindowController.swift @@ -46,7 +46,15 @@ final class DocumentWindowController: NSWindowController, TracksViewControllerDe toolbarDelegate.target = self - let toolbar = NSToolbar(identifier: "SublerDocumentToolbar") + // Identifier bumped (was "SublerDocumentToolbar") so that anyone + // with a toolbar layout already saved from before "Open and + // Save" existed gets the current toolbarDefaultItemIdentifiers() + // set again -- with autosavesConfiguration on, NSToolbar only + // ever falls back to the delegate's defaults for a layout it + // hasn't seen before, otherwise it keeps replaying whatever was + // saved under this exact identifier indefinitely, new default + // items and all. + let toolbar = NSToolbar(identifier: "SublerDocumentToolbar2") toolbar.delegate = toolbarDelegate toolbar.allowsUserCustomization = true toolbar.autosavesConfiguration = true From e097992ad5aebb890894193498b28f8b8f575c04 Mon Sep 17 00:00:00 2001 From: Hans Hoffmeister Date: Sun, 20 Sep 2026 17:14:16 +0000 Subject: [PATCH 4/6] Fix "Open and Save": open the next file, not the saved one externally The toolbar button's underlying saveAndOpen(_:) was implemented as a different feature than the one it was meant for: it saved the current document, then opened the just-saved file in whatever app macOS has registered for its type (e.g. QuickTime) via NSWorkspace.shared.open(url). That's not what "open another file without going to the File menu" means -- the actual goal (the app already opens with a file-open prompt on an untitled launch, via AppDelegate.applicationShouldOpenUntitledFile) is to let the user pick the *next* file to open once they're done with the current one. Once the save completes, this now calls NSDocumentController.shared.openDocument(self) instead -- the same Open panel path used at launch -- so finishing one file and moving to the next never requires a trip to File > Open. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ETy3zxWgGdZPVzjN3vqbbC --- Classes/Document.swift | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/Classes/Document.swift b/Classes/Document.swift index f0a010ae..0840205b 100644 --- a/Classes/Document.swift +++ b/Classes/Document.swift @@ -110,12 +110,21 @@ final class Document: NSDocument { save(self) } - /// Whether the next completed save should open the resulting file in - /// its default application (see saveAndOpen(_:) below). Consumed and - /// reset to false as soon as that save finishes, so a plain save - /// triggered afterwards (Cmd-S, etc.) doesn't also open the file. + /// Whether the next completed save should prompt to open another file + /// once it finishes (see saveAndOpen(_:) below). Consumed and reset to + /// false as soon as that save finishes, so a plain save triggered + /// afterwards (Cmd-S, etc.) doesn't also bring up the Open panel. private var openAfterSave: Bool = false + /// Saves the current document, then -- once that finishes -- brings up + /// the same Open panel Subler shows an untitled launch with + /// (AppDelegate.applicationShouldOpenUntitledFile(_:) / + /// NSDocumentController.openDocument(_:)), so picking up the next file + /// in a batch never needs a trip to File > Open. This previously + /// opened the just-saved file in its default external application + /// instead (e.g. QuickTime) -- a different, unrelated feature that + /// didn't serve the actual goal here: an easy way to move on to + /// another file once the current one is done. @IBAction func saveAndOpen(_ sender: Any) { openAfterSave = true save(self) @@ -138,7 +147,7 @@ final class Document: NSDocument { docController?.reloadData() if self.openAfterSave { self.openAfterSave = false - NSWorkspace.shared.open(url) + NSDocumentController.shared.openDocument(self) } completionHandler(error) } catch { From 666d1b96fac9d980fa6c65a19252ccce4bb9c080 Mon Sep 17 00:00:00 2001 From: Hans Hoffmeister Date: Sun, 20 Sep 2026 17:24:44 +0000 Subject: [PATCH 5/6] Fix "Open and Save" toolbar button always being disabled ButtonToolbarItem.validate() asks the target's validateUserInterfaceItem(_:) whether to enable itself, but Document.validateUserInterfaceItem(_:)'s switch had no case at all for saveAndOpen(_:) (the button's action) -- every call fell through to the default `return false`, so the button was permanently disabled regardless of whether the document actually had unsaved changes. Added a case matching save(_:)'s own condition (enabled exactly when isDocumentEdited is true), since "Open and Save" should be available under the same circumstances as a plain Save. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ETy3zxWgGdZPVzjN3vqbbC --- Classes/Document.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Classes/Document.swift b/Classes/Document.swift index 0840205b..5e53292d 100644 --- a/Classes/Document.swift +++ b/Classes/Document.swift @@ -284,6 +284,8 @@ final class Document: NSDocument { return true case #selector(save(_:)) where isDocumentEdited == true: return true + case #selector(saveAndOpen(_:)) where isDocumentEdited == true: + return true case #selector(saveAs(_:)), #selector(sendToQueue(_:)): return true From 7f6bfb1e0970d2dfa334925f437df2e4f218f192 Mon Sep 17 00:00:00 2001 From: Hans Hoffmeister Date: Sun, 20 Sep 2026 18:02:00 +0000 Subject: [PATCH 6/6] Fix "Open and Save" toolbar button not actually doing anything on click The toolbar item's target is DocumentWindowController (not Document), and NSApplication.sendAction only invokes an action directly on an explicit target if that target itself responds to the selector -- it does not fall back to walking the responder chain the way toolbar/menu validation does (that uses target(forAction:to:from:), which does walk the chain). DocumentWindowController had no saveAndOpen(_:) of its own, so clicking the button silently did nothing even though it correctly showed itself as enabled. Add a forwarding saveAndOpen(_:) that calls doc.saveAndOpen(_:), matching the existing sendToQueue(_:) forwarding pattern right above it. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01ETy3zxWgGdZPVzjN3vqbbC --- Classes/DocumentWindowController.swift | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Classes/DocumentWindowController.swift b/Classes/DocumentWindowController.swift index 359223b0..792df3d1 100644 --- a/Classes/DocumentWindowController.swift +++ b/Classes/DocumentWindowController.swift @@ -307,6 +307,18 @@ final class DocumentWindowController: NSWindowController, TracksViewControllerDe doc.sendToQueue(self) } + /// The "Open and Save" toolbar item's target is this window controller + /// (see DocumentToolbarDelegate), not the document, following the same + /// pattern as sendToQueue(_:) above -- so this forwarding method has to + /// exist here under the same selector name for the click to actually + /// reach Document.saveAndOpen(_:). Without it, NSApplication.sendAction + /// silently does nothing when the toolbar item's explicit target + /// doesn't respond to the action, even though menu/toolbar validation + /// (which walks the responder chain) still reports the item as enabled. + @IBAction func saveAndOpen(_ sender: Any) { + doc.saveAndOpen(self) + } + @IBAction func sendToExternalApp(_ sender: Any) { if let fileURL = doc.fileURL { _ = sendToFileExternalApp(fileURL: fileURL)