Skip to content

Commit

Permalink
Improve window activation and management (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
lfroms authored Sep 23, 2024
1 parent d9b5578 commit 7a79f72
Show file tree
Hide file tree
Showing 13 changed files with 146 additions and 126 deletions.
8 changes: 4 additions & 4 deletions Tophat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
80629BFA293981A80077960E /* PinnedApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80629BF9293981A80077960E /* PinnedApplication.swift */; };
80629BFC293981B10077960E /* CodableAppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80629BFB293981B10077960E /* CodableAppStorage.swift */; };
80629C22293A8D270077960E /* ProvisioningProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80629C21293A8D270077960E /* ProvisioningProfile.swift */; };
8079E3562C850FE0000CB5B3 /* View+ShowDockIconWhenOpen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8079E3552C850FE0000CB5B3 /* View+ShowDockIconWhenOpen.swift */; };
807D7B0F29835762007942B4 /* TophatFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = 807D7B0E29835762007942B4 /* TophatFoundation */; };
807D7B132983576C007942B4 /* TophatCtl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 807D7B102983576C007942B4 /* TophatCtl.swift */; };
807D7B1629835795007942B4 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = 807D7B1529835795007942B4 /* ArgumentParser */; };
Expand All @@ -72,7 +73,6 @@
8090E201294FA29E003106B9 /* FluidMenuBarExtra in Frameworks */ = {isa = PBXBuildFile; productRef = 8090E200294FA29E003106B9 /* FluidMenuBarExtra */; };
8090E2032950C1CC003106B9 /* CollapsibleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8090E2022950C1CC003106B9 /* CollapsibleSection.swift */; };
8090E2132950E01E003106B9 /* DeviceList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8090E2122950E01E003106B9 /* DeviceList.swift */; };
8090E250296772F7003106B9 /* View+OnTrackingHover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8090E24F296772F7003106B9 /* View+OnTrackingHover.swift */; };
8090E25A2967741F003106B9 /* TaskProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8090E2562967741F003106B9 /* TaskProgress.swift */; };
8090E25B2967741F003106B9 /* TaskState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8090E2572967741F003106B9 /* TaskState.swift */; };
8090E25C2967741F003106B9 /* TaskStatusReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8090E2582967741F003106B9 /* TaskStatusReporter.swift */; };
Expand Down Expand Up @@ -257,13 +257,13 @@
80629BF9293981A80077960E /* PinnedApplication.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PinnedApplication.swift; sourceTree = "<group>"; };
80629BFB293981B10077960E /* CodableAppStorage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableAppStorage.swift; sourceTree = "<group>"; };
80629C21293A8D270077960E /* ProvisioningProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvisioningProfile.swift; sourceTree = "<group>"; };
8079E3552C850FE0000CB5B3 /* View+ShowDockIconWhenOpen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+ShowDockIconWhenOpen.swift"; sourceTree = "<group>"; };
807D7B0729835756007942B4 /* tophatctl */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = tophatctl; sourceTree = BUILT_PRODUCTS_DIR; };
807D7B102983576C007942B4 /* TophatCtl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TophatCtl.swift; sourceTree = "<group>"; };
807D7B122983576C007942B4 /* FastInstall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastInstall.swift; sourceTree = "<group>"; };
8086AE3628F9E8680069217E /* TophatModules */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TophatModules; sourceTree = "<group>"; };
8090E2022950C1CC003106B9 /* CollapsibleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsibleSection.swift; sourceTree = "<group>"; };
8090E2122950E01E003106B9 /* DeviceList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceList.swift; sourceTree = "<group>"; };
8090E24F296772F7003106B9 /* View+OnTrackingHover.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "View+OnTrackingHover.swift"; sourceTree = "<group>"; };
8090E2562967741F003106B9 /* TaskProgress.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskProgress.swift; sourceTree = "<group>"; };
8090E2572967741F003106B9 /* TaskState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskState.swift; sourceTree = "<group>"; };
8090E2582967741F003106B9 /* TaskStatusReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TaskStatusReporter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -459,7 +459,7 @@
80EB5D4E296F658E0011DE5F /* LaunchRequestBuilderError+LocalizedError.swift */,
80B7BAD629762D8900267C3C /* NSApplication+ShowSettingsWindow.swift */,
80629BF72939819F0077960E /* String+IsValidURL.swift */,
8090E24F296772F7003106B9 /* View+OnTrackingHover.swift */,
8079E3552C850FE0000CB5B3 /* View+ShowDockIconWhenOpen.swift */,
);
path = Extensions;
sourceTree = "<group>";
Expand Down Expand Up @@ -920,7 +920,6 @@
80A91A162981BA2D00D8A8B9 /* LaunchFromURLPanel.swift in Sources */,
8090E2032950C1CC003106B9 /* CollapsibleSection.swift in Sources */,
80EB5D5F2970A7940011DE5F /* PinnedApplicationState.swift in Sources */,
8090E250296772F7003106B9 /* View+OnTrackingHover.swift in Sources */,
80EB5D5D2970A25D0011DE5F /* UpdateIconTask.swift in Sources */,
80518F632984A64F00FB8803 /* OnboardingWindow.swift in Sources */,
80EB5D55297095890011DE5F /* TaskStatusMetadata.swift in Sources */,
Expand Down Expand Up @@ -981,6 +980,7 @@
809C8575297B0FA9004CE6A2 /* ShowingAdvancedOptionsViewModifier.swift in Sources */,
80301620292874B70016F25E /* ArtifactUnpacker.swift in Sources */,
80EB5D4F296F658E0011DE5F /* LaunchRequestBuilderError+LocalizedError.swift in Sources */,
8079E3562C850FE0000CB5B3 /* View+ShowDockIconWhenOpen.swift in Sources */,
80A66D6D2981BC9900ECBCB6 /* ErrorNotifier.swift in Sources */,
80FF03EF29087473008509E0 /* InstallCoordinator.swift in Sources */,
B6AA44DD296F78670017321C /* GeneralTab.swift in Sources */,
Expand Down
87 changes: 0 additions & 87 deletions Tophat/Extensions/View+OnTrackingHover.swift

This file was deleted.

28 changes: 28 additions & 0 deletions Tophat/Extensions/View+ShowDockIconWhenOpen.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// View+ShowDockIconWhenOpen.swift
// Tophat
//
// Created by Lukas Romsicki on 2024-09-01.
// Copyright © 2024 Shopify. All rights reserved.
//

import SwiftUI

extension View {
func showDockIconWhenOpen() -> some View {
modifier(ShowDockIconWhenOpenViewModifier())
}
}

private struct ShowDockIconWhenOpenViewModifier: ViewModifier {
@Environment(\.controlActiveState) private var controlActiveState

func body(content: Content) -> some View {
content
.onChange(of: controlActiveState) { oldValue, newValue in
if newValue != .inactive, NSApp.activationPolicy() == .accessory {
NSApp.setActivationPolicy(.regular)
}
}
}
}
23 changes: 23 additions & 0 deletions Tophat/TophatApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,15 @@ struct TophatApp: App {
var body: some Scene {
Settings {
SettingsView()
.showDockIconWhenOpen()
.environment(appDelegate.updateController)
.environmentObject(appDelegate.deviceManager)
.environmentObject(appDelegate.pinnedApplicationState)
.environmentObject(appDelegate.utilityPathPreferences)
.environmentObject(appDelegate.launchAtLoginController)
.environmentObject(appDelegate.symbolicLinkManager)
}
.commandsRemoved()
}
}

Expand Down Expand Up @@ -198,6 +200,27 @@ private final class AppDelegate: NSObject, NSApplicationDelegate {
}

private func configureEventSubscriptions() {
Task { @MainActor in
// Companion to the showDockIconWhenOpen() modifier to hide the dock icon when all
// modified windows are closed.
for await _ in NotificationCenter.default.notifications(named: NSWindow.willCloseNotification).compactMap({ _ in }) {
guard NSApp.activationPolicy() != .accessory else {
continue
}

let visibleWindows = NSApp.windows.filter { window in
// _NSOrderOutAnimationProxyWindow appears momentarily while a window is ordering out.
window.isVisible && !window.className.contains("NSOrderOutAnimationProxyWindow")
}

// The application is considered "inactive" when only the NSStatusBarWindow, and the window
// that is about to be closed are visible.
if visibleWindows.count < 2 {
NSApp.setActivationPolicy(.accessory)
}
}
}

Publishers.MergeMany(
self.urlHandler.onLaunchArtifactURL,
self.notificationHandler.onLaunchArtifactURL
Expand Down
3 changes: 2 additions & 1 deletion Tophat/Utilities/ShowOnboardingWindowAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ final class ShowOnboardingWindowAction {
if onboardingWindow == nil {
onboardingWindow = OnboardingWindow {
OnboardingView()
.showDockIconWhenOpen()
.environmentObject(self.symbolicLinkManager)
.environmentObject(self.utilityPathPreferences)
}
}

onboardingWindow?.center()
onboardingWindow?.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: true)
NSRunningApplication.current.activate()
}
}

Expand Down
1 change: 0 additions & 1 deletion Tophat/Views/About/AboutWindowViewModifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ private struct AboutWindowViewModifier<WindowContent: View>: ViewModifier {
}

private func present() {
NSApp.activate(ignoringOtherApps: true)
window?.center()
window?.makeKeyAndOrderFront(nil)
}
Expand Down
14 changes: 5 additions & 9 deletions Tophat/Views/Generic/CollapsibleSection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,13 @@ struct CollapsibleSection<Content: View>: View {
.rotationEffect(expanded ? .degrees(90) : .zero)
}
}
.buttonStyle(MenuItemButtonStyle())
.buttonStyle(.menuItem(blinks: true))

HStack {
if expanded {
content()
.opacity(expanded ? 1 : 0)
.transition(.move(edge: .top))
}
if expanded {
content()
.transition(.asymmetric(insertion: .identity, removal: .opacity))
}
.frame(maxWidth: .infinity, alignment: .leading)
.clipped()
}
.clipped()
}
}
73 changes: 69 additions & 4 deletions Tophat/Views/Generic/MenuItemButtonStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,75 @@

import SwiftUI

struct MenuItemButtonStyle: ButtonStyle {
struct MenuItemButtonStyle: PrimitiveButtonStyle {
private let blinkDuration = 0.18

@State private var animationTrigger = 0

var activatesApplication = false
var blinks = false

func makeBody(configuration: Configuration) -> some View {
Button {
let trigger = configuration.trigger

Task { @MainActor in
if blinks {
animationTrigger += 1
try? await Task.sleep(for: .seconds(blinkDuration + 0.01), tolerance: .zero)
}

if activatesApplication {
NSRunningApplication.current.activate()
}

trigger()
}
} label: {
configuration.label
}
.buttonStyle(MenuItemButtonStyleInternal(animationTrigger: animationTrigger, blinkDuration: blinkDuration))
}
}

private struct MenuItemButtonStyleInternal: ButtonStyle {
@State private var hovering = false

var animationTrigger: Int
var blinkDuration: TimeInterval

func makeBody(configuration: Configuration) -> some View {
configuration.label
.padding(.vertical, Theme.Size.menuPaddingVertical)
.padding(.horizontal, Theme.Size.menuPaddingHorizontal)
.frame(maxWidth: .infinity, alignment: .leading)
.background(.quaternary.opacity(hovering ? 1 : 0))
.cornerRadius(4)
.onTrackingHover { hovering in
.background {
RoundedRectangle(cornerRadius: 4)
.fill(.quaternary)
.phaseAnimator([hovering ? 1 : 0, 0, 1], trigger: animationTrigger) { view, phase in
view.opacity(phase)
} animation: { _ in
Animation(BlinkAnimation(duration: blinkDuration / 3))
}
}
.onHover { hovering in
self.hovering = hovering
}
.environment(\.buttonPressed, configuration.isPressed)
.environment(\.buttonHovered, hovering)
}
}

extension PrimitiveButtonStyle where Self == MenuItemButtonStyle {
static var menuItem: Self {
menuItem(activatesApplication: false, blinks: false)
}

static func menuItem(activatesApplication: Bool = false, blinks: Bool = false) -> Self {
MenuItemButtonStyle(activatesApplication: activatesApplication, blinks: blinks)
}
}

private struct ButtonPressedKey: EnvironmentKey {
static let defaultValue = false
}
Expand All @@ -45,3 +96,17 @@ extension EnvironmentValues {
set { self[ButtonHoveredKey.self] = newValue }
}
}

private struct BlinkAnimation: CustomAnimation {
var duration: TimeInterval

func animate<V>(value: V, time: TimeInterval, context: inout AnimationContext<V>) -> V? where V: VectorArithmetic {
if time > duration {
return nil
}

let progress = time / duration

return value.scaled(by: progress == 1 ? 1 : 0)
}
}
2 changes: 1 addition & 1 deletion Tophat/Views/Generic/ToggleableRow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,6 @@ struct ToggleableRow<Content: View, IconContent: View>: View {
content()
}
}
.buttonStyle(MenuItemButtonStyle())
.buttonStyle(.menuItem)
}
}
4 changes: 1 addition & 3 deletions Tophat/Views/LaunchFromLocationMenuItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ struct LaunchFromLocationMenuItem: View {

var body: some View {
Button(label, action: didPerformPrimaryAction)
.buttonStyle(MenuItemButtonStyle())
.buttonStyle(.menuItem(activatesApplication: true, blinks: true))
.floatingPanel(isPresented: $launchFromURLPanelPresented) {
LaunchFromURLPanel()
// Panels are created in a new SwiftUI context (NSHostingView) so we need to forward
Expand All @@ -43,8 +43,6 @@ struct LaunchFromLocationMenuItem: View {
panel.prompt = "Launch"
panel.allowedContentTypes = ArtifactFileFormat.contentTypes

NSApp.activate(ignoringOtherApps: true)

guard panel.runModal() == .OK, let url = panel.url else {
return
}
Expand Down
Loading

0 comments on commit 7a79f72

Please sign in to comment.