Skip to content

Commit

Permalink
Changes to Menu Item Spacing
Browse files Browse the repository at this point in the history
- Instead of killing apps we terminate() them gracefully. Only if they do not terminate within 3 second, we forceTerminate() them.
- Disable buttons while restarting apps
- Draw progress circle
- Make Ice settings window always stay on top
  • Loading branch information
stonerl committed Aug 8, 2024
1 parent b6bdc04 commit db12c44
Show file tree
Hide file tree
Showing 3 changed files with 143 additions and 72 deletions.
64 changes: 48 additions & 16 deletions Ice/MenuBarItemSpacing/MenuBarItemSpacingManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class MenuBarItemSpacingManager {
let failedApps: [String]

var errorDescription: String? {
"The following applications failed to relaunch:\n" + failedApps.joined(separator: "\n")
"The following applications failed to quit and were not restarted:\n" + failedApps.joined(separator: "\n")
}

var recoverySuggestion: String? {
Expand All @@ -40,6 +40,9 @@ class MenuBarItemSpacingManager {
/// Does not take effect until ``applyOffset()`` is called.
var offset = 0

/// The wait time before force terminating an app.
private let waitTimeBeforeForceTerminate: UInt64 = 3_000_000_000 // 3 seconds in nanoseconds

/// Runs a command with the given arguments.
private func runCommand(_ command: String, with arguments: [String]) async throws {
let process = Process()
Expand All @@ -65,14 +68,25 @@ class MenuBarItemSpacingManager {
try await runCommand("defaults", with: ["-currentHost", "write", "-globalDomain", key.rawValue, "-int", String(key.defaultValue + offset)])
}

/// Asynchronously quits the given app.
private func quitApp(_ app: NSRunningApplication) async throws {
try await runCommand("kill", with: [String(app.processIdentifier)])
/// Asynchronously signals the given app to quit.
private func signalAppToQuit(_ app: NSRunningApplication) async throws {
Logger.spacing.debug("Signaling application \"\(app.localizedName ?? "<NIL>")\" to quit")
app.terminate()
var cancellable: AnyCancellable?
return try await withCheckedThrowingContinuation { continuation in
let timeoutTask = Task {
try await Task.sleep(nanoseconds: self.waitTimeBeforeForceTerminate)
if !app.isTerminated {
Logger.spacing.debug("Application \"\(app.localizedName ?? "<NIL>")\" did not terminate within \(self.waitTimeBeforeForceTerminate / 1_000_000_000) seconds, attempting to force terminate")
app.forceTerminate()
}
}

cancellable = app.publisher(for: \.isTerminated).sink { isTerminated in
if isTerminated {
timeoutTask.cancel()
cancellable?.cancel()
Logger.spacing.debug("Application \"\(app.localizedName ?? "<NIL>")\" terminated successfully")
continuation.resume()
}
}
Expand All @@ -95,16 +109,20 @@ class MenuBarItemSpacingManager {

/// Asynchronously relaunches the given app.
private func relaunchApp(_ app: NSRunningApplication) async throws {
struct RelaunchError: Error { }
struct RelaunchError: Error {}
guard
let url = app.bundleURL,
let bundleIdentifier = app.bundleIdentifier
else {
throw RelaunchError()
}
try await quitApp(app)
try? await Task.sleep(for: .milliseconds(50))
try await launchApp(at: url, bundleIdentifier: bundleIdentifier)
try await signalAppToQuit(app)
try? await Task.sleep(for: .nanoseconds(waitTimeBeforeForceTerminate))
if app.isTerminated {
try await launchApp(at: url, bundleIdentifier: bundleIdentifier)
} else {
throw RelaunchError()
}
}

/// Applies the current ``offset``.
Expand All @@ -125,30 +143,44 @@ class MenuBarItemSpacingManager {
let pids = Set(items.map { $0.ownerPID })

var failedApps = [String]()
var tasks = [Task<Void, Never>]()

for pid in pids {
guard
let app = NSRunningApplication(processIdentifier: pid),
// ControlCenter handles its own relaunch, so quit it separately
// ControlCenter handles its own relaunch, so skip it
app.bundleIdentifier != "com.apple.controlcenter",
app != .current
else {
continue
}
do {
try await relaunchApp(app)
} catch {
if let name = app.localizedName {
failedApps.append(name)
tasks.append(Task {
do {
try await self.relaunchApp(app)
} catch {
if let name = app.localizedName {
failedApps.append(name)
}
}
}
})
}

// Wait for all tasks to complete
for task in tasks {
await task.value
}

try? await Task.sleep(for: .milliseconds(100))

if let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.controlcenter").first {
do {
try await quitApp(app)
try await signalAppToQuit(app)
try? await Task.sleep(for: .nanoseconds(waitTimeBeforeForceTerminate))
if !app.isTerminated {
if let name = app.localizedName {
failedApps.append(name)
}
}
} catch {
if let name = app.localizedName {
failedApps.append(name)
Expand Down
132 changes: 76 additions & 56 deletions Ice/Settings/SettingsPanes/GeneralSettingsPane.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,55 @@ struct GeneralSettingsPane: View {
@State private var isImportingCustomIceIcon = false
@State private var isPresentingError = false
@State private var presentedError: AnyLocalizedError?
@State private var isApplyingOffset = false
@State private var tempItemSpacingOffset: CGFloat = 0 // Temporary state for the slider

private var manager: GeneralSettingsManager {
appState.settingsManager.generalSettingsManager
}

private var itemSpacingOffset: LocalizedStringKey {
if manager.itemSpacingOffset == -16 {
LocalizedStringKey("none")
} else if manager.itemSpacingOffset == 0 {
LocalizedStringKey("default")
} else if manager.itemSpacingOffset == 16 {
LocalizedStringKey("max")
} else {
LocalizedStringKey(manager.itemSpacingOffset.formatted())
localizedOffsetString(for: manager.itemSpacingOffset)
}

private func localizedOffsetString(for offset: CGFloat) -> LocalizedStringKey {
switch offset {
case -16:
return LocalizedStringKey("none")
case 0:
return LocalizedStringKey("default")
case 16:
return LocalizedStringKey("max")
default:
return LocalizedStringKey(offset.formatted())
}
}

private var rehideInterval: LocalizedStringKey {
let formatted = manager.rehideInterval.formatted()
return if manager.rehideInterval == 1 {
LocalizedStringKey(formatted + " second")
if manager.rehideInterval == 1 {
return LocalizedStringKey(formatted + " second")
} else {
LocalizedStringKey(formatted + " seconds")
return LocalizedStringKey(formatted + " seconds")
}
}

private var hasSliderValueChanged: Bool {
tempItemSpacingOffset != manager.itemSpacingOffset
}

private var isActualValueDifferentFromDefault: Bool {
manager.itemSpacingOffset != 0
}

var body: some View {
Form {
Section {
launchAtLogin
}
Section {
iceIconOptions
}
Section {
useIceBar
}
Section {
showOnClick
showOnHover
showOnScroll
}
Section {
autoRehideOptions
}
Section {
spacingOptions
}
Section { launchAtLogin }
Section { iceIconOptions }
Section { useIceBar }
Section { showOnClick; showOnHover; showOnScroll }
Section { autoRehideOptions }
Section { spacingOptions }
}
.formStyle(.grouped)
.scrollBounceBehavior(.basedOnSize)
Expand Down Expand Up @@ -131,9 +132,7 @@ struct GeneralSettingsPane: View {
do {
let url = try result.get()
if url.startAccessingSecurityScopedResource() {
defer {
url.stopAccessingSecurityScopedResource()
}
defer { url.stopAccessingSecurityScopedResource() }
let data = try Data(contentsOf: url)
manager.iceIcon = ControlItemImageSet(name: .custom, image: .data(data))
}
Expand Down Expand Up @@ -189,47 +188,43 @@ struct GeneralSettingsPane: View {
VStack(alignment: .leading) {
LabeledContent {
CompactSlider(
value: manager.bindings.itemSpacingOffset,
value: $tempItemSpacingOffset,
in: -16...16,
step: 2,
handleVisibility: .hovering(width: 1)
) {
Text(itemSpacingOffset)
Text(localizedOffsetString(for: tempItemSpacingOffset))
.textSelection(.disabled)
}
.compactSliderDisabledHapticFeedback(true)
.disabled(isApplyingOffset)
} label: {
HStack {
Text("Menu bar item spacing")

Spacer()

Button("Apply") {
Task {
do {
try await appState.spacingManager.applyOffset()
} catch {
let alert = NSAlert(error: error)
alert.runModal()
}
}
applyOffset()
}
.help("Apply the current spacing")
.disabled(isApplyingOffset || !hasSliderValueChanged)

Button("Reset", systemImage: "arrow.counterclockwise.circle.fill") {
manager.itemSpacingOffset = 0
Task {
do {
try await appState.spacingManager.applyOffset()
} catch {
let alert = NSAlert(error: error)
alert.runModal()
}
if isApplyingOffset {
ProgressView()
.progressViewStyle(CircularProgressViewStyle())
.scaleEffect(0.5)
.frame(width: 15, height: 15)
} else {
Button {
resetOffsetToDefault()
} label: {
Image(systemName: "arrow.counterclockwise.circle.fill")
}
.buttonStyle(.borderless)
.help("Reset to the default spacing")
.disabled(isApplyingOffset || !isActualValueDifferentFromDefault)
}
.buttonStyle(.borderless)
.labelStyle(.iconOnly)
.help("Reset to the default spacing")
}
}

Expand All @@ -240,6 +235,31 @@ struct GeneralSettingsPane: View {
.font(.subheadline)
.foregroundStyle(.secondary)
}
.onAppear {
tempItemSpacingOffset = manager.itemSpacingOffset
}
}

// Apply offset
private func applyOffset() {
isApplyingOffset = true
manager.itemSpacingOffset = tempItemSpacingOffset
Task {
do {
try await appState.spacingManager.applyOffset()
} catch {
let alert = NSAlert(error: error)
alert.runModal()
}
isApplyingOffset = false
}
}

// Reset offset to default
private func resetOffsetToDefault() {
tempItemSpacingOffset = 0
manager.itemSpacingOffset = tempItemSpacingOffset
applyOffset()
}

@ViewBuilder
Expand Down
19 changes: 19 additions & 0 deletions Ice/Settings/SettingsWindow.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,28 @@ struct SettingsWindow: Scene {
.onAppear(perform: onAppear)
.environmentObject(appState)
.environmentObject(appState.navigationState)
.background(WindowAccessor { window in
window.level = .floating
})
}
.commandsRemoved()
.windowResizability(.contentSize)
.defaultSize(width: 900, height: 625)
}
}

struct WindowAccessor: NSViewRepresentable {
var onReceiveWindow: (NSWindow) -> Void

func makeNSView(context: Context) -> NSView {
let view = NSView()
DispatchQueue.main.async {
if let window = view.window {
self.onReceiveWindow(window)
}
}
return view
}

func updateNSView(_ nsView: NSView, context: Context) {}
}

0 comments on commit db12c44

Please sign in to comment.