Improve macOS window lifecycle (#780)

- Let the user close the window, the app will just remain alive in the
status bar
- Accordingly, replace "Confirm quit" preference with the option to stay
alive in the status bar
- Add "About..." item
This commit is contained in:
Davide 2024-10-30 10:37:45 +01:00 committed by GitHub
parent 9d6dfe6a76
commit 7f3d897818
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 46 additions and 68 deletions

View File

@ -30,10 +30,6 @@ import SwiftUI
@MainActor @MainActor
final class AppDelegate: NSObject { final class AppDelegate: NSObject {
@AppStorage(AppPreference.confirmsQuit.key)
var confirmsQuit = true
let context: AppContext = .shared let context: AppContext = .shared
// let context: AppContext = .mock(withRegistry: .shared) // let context: AppContext = .mock(withRegistry: .shared)

View File

@ -26,20 +26,19 @@
#if os(macOS) #if os(macOS)
import AppUIMain import AppUIMain
import CommonLibrary
import PassepartoutKit import PassepartoutKit
import SwiftUI import SwiftUI
extension AppDelegate: NSApplicationDelegate { extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) { func applicationDidFinishLaunching(_ notification: Notification) {
configureAppWindow() hideIfLoginItem()
configure() configure()
} }
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
if confirmsQuit { AppWindow.shared.isVisible = false
return quitConfirmationAlert() return !keepsInMenu
}
return .terminateNow
} }
func application(_ application: NSApplication, open urls: [URL]) { func application(_ application: NSApplication, open urls: [URL]) {
@ -48,37 +47,23 @@ extension AppDelegate: NSApplicationDelegate {
} }
private extension AppDelegate { private extension AppDelegate {
var keepsInMenu: Bool {
get {
UserDefaults.standard.bool(forKey: AppPreference.keepsInMenu.key)
}
set {
UserDefaults.standard.set(newValue, forKey: AppPreference.keepsInMenu.key)
}
}
var isStartedFromLoginItem: Bool { var isStartedFromLoginItem: Bool {
NSApp.isHidden NSApp.isHidden
} }
func configureAppWindow() { func hideIfLoginItem() {
if isStartedFromLoginItem { if isStartedFromLoginItem {
AppWindow.shared.isVisible = false AppWindow.shared.isVisible = false
} }
AppWindow.shared.removeCloseButton()
}
func quitConfirmationAlert() -> NSApplication.TerminateReply {
let alert = NSAlert()
alert.alertStyle = .warning
alert.messageText = Strings.Alerts.ConfirmQuit.title(BundleConfiguration.mainDisplayName)
alert.informativeText = Strings.Alerts.ConfirmQuit.message
alert.addButton(withTitle: Strings.Global.ok)
alert.addButton(withTitle: Strings.Global.cancel)
alert.addButton(withTitle: Strings.Global.doNotAskAgain)
switch alert.runModal() {
case .alertSecondButtonReturn:
return .terminateCancel
case .alertThirdButtonReturn:
confirmsQuit = false
default:
break
}
return .terminateNow
} }
} }

View File

@ -11,14 +11,6 @@ import Foundation
// swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces // swiftlint:disable nesting type_body_length type_name vertical_whitespace_opening_braces
public enum Strings { public enum Strings {
public enum Alerts { public enum Alerts {
public enum ConfirmQuit {
/// The VPN, if enabled, will still run in the background. Do you want to quit?
public static let message = Strings.tr("Localizable", "alerts.confirm_quit.message", fallback: "The VPN, if enabled, will still run in the background. Do you want to quit?")
/// Quit %@
public static func title(_ p1: Any) -> String {
return Strings.tr("Localizable", "alerts.confirm_quit.title", String(describing: p1), fallback: "Quit %@")
}
}
public enum Iap { public enum Iap {
public enum Restricted { public enum Restricted {
/// The requested feature is unavailable in this build. /// The requested feature is unavailable in this build.
@ -663,15 +655,15 @@ public enum Strings {
} }
public enum Settings { public enum Settings {
public enum Rows { public enum Rows {
/// Ask before quit
public static let confirmQuit = Strings.tr("Localizable", "views.settings.rows.confirm_quit", fallback: "Ask before quit")
/// Erase iCloud store /// Erase iCloud store
public static let eraseIcloud = Strings.tr("Localizable", "views.settings.rows.erase_icloud", fallback: "Erase iCloud store") public static let eraseIcloud = Strings.tr("Localizable", "views.settings.rows.erase_icloud", fallback: "Erase iCloud store")
/// Keep in menu bar
public static let keepsInMenu = Strings.tr("Localizable", "views.settings.rows.keeps_in_menu", fallback: "Keep in menu bar")
/// Lock in background /// Lock in background
public static let lockInBackground = Strings.tr("Localizable", "views.settings.rows.lock_in_background", fallback: "Lock in background") public static let locksInBackground = Strings.tr("Localizable", "views.settings.rows.locks_in_background", fallback: "Lock in background")
public enum LockInBackground { public enum LocksInBackground {
/// Passepartout is locked /// Passepartout is locked
public static let message = Strings.tr("Localizable", "views.settings.rows.lock_in_background.message", fallback: "Passepartout is locked") public static let message = Strings.tr("Localizable", "views.settings.rows.locks_in_background.message", fallback: "Passepartout is locked")
} }
} }
public enum Sections { public enum Sections {

View File

@ -134,9 +134,9 @@
"views.profile.module_list.section.footer" = "Drag modules to rearrange them, as their order determines priority."; "views.profile.module_list.section.footer" = "Drag modules to rearrange them, as their order determines priority.";
"views.settings.sections.icloud.footer" = "To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles."; "views.settings.sections.icloud.footer" = "To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.";
"views.settings.rows.confirm_quit" = "Ask before quit"; "views.settings.rows.keeps_in_menu" = "Keep in menu bar";
"views.settings.rows.lock_in_background" = "Lock in background"; "views.settings.rows.locks_in_background" = "Lock in background";
"views.settings.rows.lock_in_background.message" = "Passepartout is locked"; "views.settings.rows.locks_in_background.message" = "Passepartout is locked";
"views.settings.rows.erase_icloud" = "Erase iCloud store"; "views.settings.rows.erase_icloud" = "Erase iCloud store";
"views.about.title" = "About"; "views.about.title" = "About";
@ -260,9 +260,6 @@
"alerts.iap.restricted.title" = "Restricted"; "alerts.iap.restricted.title" = "Restricted";
"alerts.iap.restricted.message" = "The requested feature is unavailable in this build."; "alerts.iap.restricted.message" = "The requested feature is unavailable in this build.";
"alerts.confirm_quit.title" = "Quit %@";
"alerts.confirm_quit.message" = "The VPN, if enabled, will still run in the background. Do you want to quit?";
// MARK: - Errors // MARK: - Errors
"errors.app.empty_profile_name" = "Profile name is empty."; "errors.app.empty_profile_name" = "Profile name is empty.";

View File

@ -347,7 +347,7 @@ struct ThemeLockScreenModifier: ViewModifier {
do { do {
let isAuthorized = try await context.evaluatePolicy( let isAuthorized = try await context.evaluatePolicy(
policy, policy,
localizedReason: Strings.Views.Settings.Rows.LockInBackground.message localizedReason: Strings.Views.Settings.Rows.LocksInBackground.message
) )
return isAuthorized return isAuthorized
} catch { } catch {

View File

@ -50,11 +50,12 @@ public struct AppMenu: View {
public var body: some View { public var body: some View {
versionItem versionItem
Divider() Divider()
dockToggle showToggle
loginToggle loginToggle
Divider() Divider()
profilesList profilesList
Divider() Divider()
aboutButton
quitButton quitButton
} }
} }
@ -64,9 +65,9 @@ private extension AppMenu {
Text(BundleConfiguration.mainVersionString) Text(BundleConfiguration.mainVersionString)
} }
var dockToggle: some View { var showToggle: some View {
Button(model.isVisible ? Strings.Global.hide : Strings.Global.show) { Button(Strings.Global.show) {
model.isVisible.toggle() model.isVisible = true
} }
} }
@ -103,6 +104,13 @@ private extension AppMenu {
} }
} }
var aboutButton: some View {
Button("\(Strings.Global.about)...") {
NSApp.activate(ignoringOtherApps: true)
NSApp.orderFrontStandardAboutPanel(self)
}
}
var quitButton: some View { var quitButton: some View {
Button(Strings.AppMenu.Items.quit(BundleConfiguration.mainDisplayName)) { Button(Strings.AppMenu.Items.quit(BundleConfiguration.mainDisplayName)) {
NSApp.terminate(self) NSApp.terminate(self)

View File

@ -36,7 +36,7 @@ public final class AppWindow {
NSApp.activationPolicy() == .regular && window.isVisible NSApp.activationPolicy() == .regular && window.isVisible
} }
set { set {
NSApp.setActivationPolicy(newValue ? .regular : .prohibited) NSApp.setActivationPolicy(newValue ? .regular : .accessory)
if newValue { if newValue {
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(self) window.makeKeyAndOrderFront(self)
@ -47,8 +47,8 @@ public final class AppWindow {
private init() { private init() {
} }
public func removeCloseButton() { public func close() {
window.styleMask.remove(.closable) window.close()
} }
} }

View File

@ -31,8 +31,8 @@ import UtilsLibrary
struct SettingsSectionGroup: View { struct SettingsSectionGroup: View {
@AppStorage(AppPreference.confirmsQuit.key) @AppStorage(AppPreference.keepsInMenu.key)
private var confirmsQuit = true private var keepsInMenu = false
@AppStorage(AppPreference.locksInBackground.key) @AppStorage(AppPreference.locksInBackground.key)
private var locksInBackground = false private var locksInBackground = false
@ -48,7 +48,7 @@ struct SettingsSectionGroup: View {
var body: some View { var body: some View {
Group { Group {
#if os(macOS) #if os(macOS)
confirmsQuitToggle keepsInMenuToggle
#endif #endif
#if os(iOS) #if os(iOS)
lockInBackgroundToggle lockInBackgroundToggle
@ -66,12 +66,12 @@ struct SettingsSectionGroup: View {
} }
private extension SettingsSectionGroup { private extension SettingsSectionGroup {
var confirmsQuitToggle: some View { var keepsInMenuToggle: some View {
Toggle(Strings.Views.Settings.Rows.confirmQuit, isOn: $confirmsQuit) Toggle(Strings.Views.Settings.Rows.keepsInMenu, isOn: $keepsInMenu)
} }
var lockInBackgroundToggle: some View { var lockInBackgroundToggle: some View {
Toggle(Strings.Views.Settings.Rows.lockInBackground, isOn: $locksInBackground) Toggle(Strings.Views.Settings.Rows.locksInBackground, isOn: $locksInBackground)
} }
var eraseCloudKitButton: some View { var eraseCloudKitButton: some View {

View File

@ -26,7 +26,7 @@
import Foundation import Foundation
public enum AppPreference: String { public enum AppPreference: String {
case confirmsQuit case keepsInMenu
case locksInBackground case locksInBackground