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
final class AppDelegate: NSObject {
@AppStorage(AppPreference.confirmsQuit.key)
var confirmsQuit = true
let context: AppContext = .shared
// let context: AppContext = .mock(withRegistry: .shared)

View File

@ -26,20 +26,19 @@
#if os(macOS)
import AppUIMain
import CommonLibrary
import PassepartoutKit
import SwiftUI
extension AppDelegate: NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
configureAppWindow()
hideIfLoginItem()
configure()
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
if confirmsQuit {
return quitConfirmationAlert()
}
return .terminateNow
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
AppWindow.shared.isVisible = false
return !keepsInMenu
}
func application(_ application: NSApplication, open urls: [URL]) {
@ -48,37 +47,23 @@ extension AppDelegate: NSApplicationDelegate {
}
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 {
NSApp.isHidden
}
func configureAppWindow() {
func hideIfLoginItem() {
if isStartedFromLoginItem {
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
public enum Strings {
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 Restricted {
/// The requested feature is unavailable in this build.
@ -663,15 +655,15 @@ public enum Strings {
}
public enum Settings {
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
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
public static let lockInBackground = Strings.tr("Localizable", "views.settings.rows.lock_in_background", fallback: "Lock in background")
public enum LockInBackground {
public static let locksInBackground = Strings.tr("Localizable", "views.settings.rows.locks_in_background", fallback: "Lock in background")
public enum LocksInBackground {
/// 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 {

View File

@ -134,9 +134,9 @@
"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.rows.confirm_quit" = "Ask before quit";
"views.settings.rows.lock_in_background" = "Lock in background";
"views.settings.rows.lock_in_background.message" = "Passepartout is locked";
"views.settings.rows.keeps_in_menu" = "Keep in menu bar";
"views.settings.rows.locks_in_background" = "Lock in background";
"views.settings.rows.locks_in_background.message" = "Passepartout is locked";
"views.settings.rows.erase_icloud" = "Erase iCloud store";
"views.about.title" = "About";
@ -260,9 +260,6 @@
"alerts.iap.restricted.title" = "Restricted";
"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
"errors.app.empty_profile_name" = "Profile name is empty.";

View File

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

View File

@ -50,11 +50,12 @@ public struct AppMenu: View {
public var body: some View {
versionItem
Divider()
dockToggle
showToggle
loginToggle
Divider()
profilesList
Divider()
aboutButton
quitButton
}
}
@ -64,9 +65,9 @@ private extension AppMenu {
Text(BundleConfiguration.mainVersionString)
}
var dockToggle: some View {
Button(model.isVisible ? Strings.Global.hide : Strings.Global.show) {
model.isVisible.toggle()
var showToggle: some View {
Button(Strings.Global.show) {
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 {
Button(Strings.AppMenu.Items.quit(BundleConfiguration.mainDisplayName)) {
NSApp.terminate(self)

View File

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

View File

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

View File

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