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:
parent
9d6dfe6a76
commit
7f3d897818
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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.";
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public enum AppPreference: String {
|
public enum AppPreference: String {
|
||||||
case confirmsQuit
|
case keepsInMenu
|
||||||
|
|
||||||
case locksInBackground
|
case locksInBackground
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue