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
|
||||
final class AppDelegate: NSObject {
|
||||
|
||||
@AppStorage(AppPreference.confirmsQuit.key)
|
||||
var confirmsQuit = true
|
||||
|
||||
let context: AppContext = .shared
|
||||
// let context: AppContext = .mock(withRegistry: .shared)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.";
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
import Foundation
|
||||
|
||||
public enum AppPreference: String {
|
||||
case confirmsQuit
|
||||
case keepsInMenu
|
||||
|
||||
case locksInBackground
|
||||
|
||||
|
|
Loading…
Reference in New Issue