diff --git a/GhosttyTabsTests/UpdatePillReleaseVisibilityTests.swift b/GhosttyTabsTests/UpdatePillReleaseVisibilityTests.swift index b862e931..4efc8c2b 100644 --- a/GhosttyTabsTests/UpdatePillReleaseVisibilityTests.swift +++ b/GhosttyTabsTests/UpdatePillReleaseVisibilityTests.swift @@ -9,7 +9,6 @@ final class UpdatePillReleaseVisibilityTests: XCTestCase { private let filesToCheck = [ "Sources/Update/UpdateTitlebarAccessory.swift", "Sources/ContentView.swift", - "Sources/WindowToolbarController.swift", ] func testUpdatePillNotGatedBehindDebug() throws { diff --git a/Sources/ContentView.swift b/Sources/ContentView.swift index b9d027bf..45529a73 100644 --- a/Sources/ContentView.swift +++ b/Sources/ContentView.swift @@ -311,10 +311,6 @@ struct ContentView: View { Spacer() - if !sidebarState.isVisible { - UpdatePill(model: updateViewModel) - .padding(.trailing, 8) - } } .frame(height: 28) .padding(.top, 2) diff --git a/Sources/Update/UpdatePill.swift b/Sources/Update/UpdatePill.swift index 4854e4bc..0ff1f606 100644 --- a/Sources/Update/UpdatePill.swift +++ b/Sources/Update/UpdatePill.swift @@ -65,3 +65,16 @@ struct UpdatePill: View { return size.width } } + +/// Menu item that shows "Install Update and Relaunch" when an update is ready. +struct InstallUpdateMenuItem: View { + @ObservedObject var model: UpdateViewModel + + var body: some View { + if model.state.isInstallable { + Button("Install Update and Relaunch") { + model.state.confirm() + } + } + } +} diff --git a/Sources/Update/UpdateTitlebarAccessory.swift b/Sources/Update/UpdateTitlebarAccessory.swift index 2bb41b8e..cd213be8 100644 --- a/Sources/Update/UpdateTitlebarAccessory.swift +++ b/Sources/Update/UpdateTitlebarAccessory.swift @@ -6,15 +6,6 @@ final class NonDraggableHostingView: NSHostingView { override var mouseDownCanMoveWindow: Bool { false } } -private struct TitlebarAccessoryView: View { - @ObservedObject var model: UpdateViewModel - - var body: some View { - UpdatePill(model: model) - .padding(.trailing, 8) - } -} - enum TitlebarControlsStyle: Int, CaseIterable, Identifiable { case classic case compact @@ -867,70 +858,6 @@ private struct NotificationPopoverRow: View { } } -final class UpdateAccessoryViewController: NSTitlebarAccessoryViewController { - private let hostingView: NonDraggableHostingView - private let containerView = NSView() - private var stateCancellable: AnyCancellable? - private var pendingSizeUpdate = false - - init(model: UpdateViewModel) { - hostingView = NonDraggableHostingView(rootView: TitlebarAccessoryView(model: model)) - - super.init(nibName: nil, bundle: nil) - - view = containerView - containerView.translatesAutoresizingMaskIntoConstraints = true - hostingView.translatesAutoresizingMaskIntoConstraints = true - hostingView.autoresizingMask = [.width, .height] - containerView.addSubview(hostingView) - - stateCancellable = model.$state - .receive(on: DispatchQueue.main) - .sink { [weak self] _ in - self?.scheduleSizeUpdate() - } - - scheduleSizeUpdate() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidAppear() { - super.viewDidAppear() - scheduleSizeUpdate() - } - - override func viewDidLayout() { - super.viewDidLayout() - scheduleSizeUpdate() - } - - private func scheduleSizeUpdate() { - guard !pendingSizeUpdate else { return } - pendingSizeUpdate = true - DispatchQueue.main.async { [weak self] in - self?.pendingSizeUpdate = false - self?.updateSize() - } - } - - private func updateSize() { - hostingView.invalidateIntrinsicContentSize() - hostingView.layoutSubtreeIfNeeded() - let pillSize = hostingView.fittingSize - let titlebarHeight = view.window.map { window in - window.frame.height - window.contentLayoutRect.height - } ?? pillSize.height - let containerHeight = max(pillSize.height, titlebarHeight) - let yOffset = max(0, (containerHeight - pillSize.height) / 2.0) - preferredContentSize = NSSize(width: pillSize.width, height: containerHeight) - containerView.frame = NSRect(x: 0, y: 0, width: pillSize.width, height: containerHeight) - hostingView.frame = NSRect(x: 0, y: yOffset, width: pillSize.width, height: pillSize.height) - } -} - final class UpdateTitlebarAccessoryController { private weak var updateViewModel: UpdateViewModel? private var didStart = false diff --git a/Sources/WindowToolbarController.swift b/Sources/WindowToolbarController.swift index a3ac468e..5cc05e12 100644 --- a/Sources/WindowToolbarController.swift +++ b/Sources/WindowToolbarController.swift @@ -5,18 +5,13 @@ import SwiftUI @MainActor final class WindowToolbarController: NSObject, NSToolbarDelegate { private let commandItemIdentifier = NSToolbarItem.Identifier("cmux.focusedCommand") - private let updateItemIdentifier = NSToolbarItem.Identifier("cmux.updatePill") private weak var tabManager: TabManager? - private weak var updateViewModel: UpdateViewModel? private var commandLabels: [ObjectIdentifier: NSTextField] = [:] private var observers: [NSObjectProtocol] = [] - private var updateSizeCancellables: [ObjectIdentifier: AnyCancellable] = [:] - private var updateViewConstraints: [ObjectIdentifier: (width: NSLayoutConstraint, height: NSLayoutConstraint)] = [:] - init(updateViewModel: UpdateViewModel) { - self.updateViewModel = updateViewModel + override init() { super.init() } @@ -24,9 +19,6 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { for observer in observers { NotificationCenter.default.removeObserver(observer) } - for cancellable in updateSizeCancellables.values { - cancellable.cancel() - } } func start(tabManager: TabManager) { @@ -105,11 +97,11 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { // MARK: - NSToolbarDelegate func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - [commandItemIdentifier, .flexibleSpace, updateItemIdentifier] + [commandItemIdentifier, .flexibleSpace] } func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { - [commandItemIdentifier, .flexibleSpace, updateItemIdentifier] + [commandItemIdentifier, .flexibleSpace] } func toolbar(_ toolbar: NSToolbar, itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier, willBeInsertedIntoToolbar flag: Bool) -> NSToolbarItem? { @@ -126,41 +118,8 @@ final class WindowToolbarController: NSObject, NSToolbarDelegate { return item } - if itemIdentifier == updateItemIdentifier, let updateViewModel { - let item = NSToolbarItem(itemIdentifier: itemIdentifier) - let view = NonDraggableHostingView(rootView: UpdatePill(model: updateViewModel)) - let key = ObjectIdentifier(toolbar) - item.view = view - sizeToolbarItem(for: key, hostingView: view) - updateSizeCancellables[key]?.cancel() - updateSizeCancellables[key] = updateViewModel.$state - .receive(on: DispatchQueue.main) - .sink { [weak self, weak view] _ in - guard let self, let view else { return } - self.sizeToolbarItem(for: key, hostingView: view) - } - return item - } return nil } - private func sizeToolbarItem(for key: ObjectIdentifier, hostingView: NSView) { - hostingView.invalidateIntrinsicContentSize() - hostingView.layoutSubtreeIfNeeded() - let size = hostingView.fittingSize - hostingView.setFrameSize(size) - hostingView.setContentHuggingPriority(.required, for: .horizontal) - hostingView.setContentHuggingPriority(.required, for: .vertical) - hostingView.translatesAutoresizingMaskIntoConstraints = false - if let constraints = updateViewConstraints[key] { - constraints.width.constant = size.width - constraints.height.constant = size.height - } else { - let width = hostingView.widthAnchor.constraint(equalToConstant: size.width) - let height = hostingView.heightAnchor.constraint(equalToConstant: size.height) - NSLayoutConstraint.activate([width, height]) - updateViewConstraints[key] = (width: width, height: height) - } - } } diff --git a/Sources/cmuxApp.swift b/Sources/cmuxApp.swift index a33442a7..8c775dce 100644 --- a/Sources/cmuxApp.swift +++ b/Sources/cmuxApp.swift @@ -194,6 +194,7 @@ struct cmuxApp: App { Button("Check for Updates…") { appDelegate.checkForUpdates(nil) } + InstallUpdateMenuItem(model: appDelegate.updateViewModel) } #if DEBUG