diff --git a/resources/SF-Pro-Text-Regular.otf b/resources/SF-Pro-Text-Regular.otf index f8a76a75..e6ae7f7b 100644 Binary files a/resources/SF-Pro-Text-Regular.otf and b/resources/SF-Pro-Text-Regular.otf differ diff --git a/resources/l10n/Localizable.strings b/resources/l10n/Localizable.strings index 8e745a4c..fdba6e27 100644 --- a/resources/l10n/Localizable.strings +++ b/resources/l10n/Localizable.strings @@ -130,6 +130,9 @@ /* No comment provided by engineer. */ "Hide app badges:" = "Hide app badges:"; +/* No comment provided by engineer. */ +"Hide apps with no open window:" = "Hide apps with no open window:"; + /* No comment provided by engineer. */ "Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:"; diff --git a/resources/l10n/en.lproj/Localizable.strings b/resources/l10n/en.lproj/Localizable.strings index f5abaee4..8b911e66 100644 --- a/resources/l10n/en.lproj/Localizable.strings +++ b/resources/l10n/en.lproj/Localizable.strings @@ -131,6 +131,9 @@ /*No comment provided by engineer.*/ "Hide app badges:" = "Hide app badges:"; +/*No comment provided by engineer.*/ +"Hide apps with no open window:" = "Hide apps with no open window:"; + /*No comment provided by engineer.*/ "Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:"; diff --git a/scripts/subset_font.sh b/scripts/subset_font.sh index 6fdb5ed0..582c3aa9 100755 --- a/scripts/subset_font.sh +++ b/scripts/subset_font.sh @@ -4,4 +4,4 @@ set -exu "$(pipenv --venv)"/bin/pyftsubset resources/SF-Pro-Text-Regular-Full.otf \ --output-file=resources/SF-Pro-Text-Regular.otf \ - --text="􀀁􀁎􀁌􀕧􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀕬􀀹􀀻􀀽􀀿􀁁􀘘􀁃􀁅􀑲􀁇􀁉􀁋􀑴􀔔􀔕􀔖􀔗􀔘􀔙􀔚􀔛􀔜􀔝􀔞􀔟􀔠􀔡􀔢􀔣􀔤􀔥􀔦􀔧􀔨􀕭" + --text="􀥃􀀁􀁎􀁌􀕧􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀕬􀀹􀀻􀀽􀀿􀁁􀘘􀁃􀁅􀑲􀁇􀁉􀁋􀑴􀔔􀔕􀔖􀔗􀔘􀔙􀔚􀔛􀔜􀔝􀔞􀔟􀔠􀔡􀔢􀔣􀔤􀔥􀔦􀔧􀔨􀕭" diff --git a/src/logic/Application.swift b/src/logic/Application.swift index 39c2b848..50b246d5 100644 --- a/src/logic/Application.swift +++ b/src/logic/Application.swift @@ -10,6 +10,7 @@ class Application: NSObject { var isHidden: Bool! var icon: NSImage? var dockLabel: String? + var pid: pid_t { runningApplication.processIdentifier } static func notifications(_ app: NSRunningApplication) -> [String] { var n = [ @@ -36,8 +37,17 @@ class Application: NSObject { icon = runningApplication.icon addAndObserveWindows() kvObservers = [ - runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() }, - runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() }, + runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in + guard let self = self else { return } + self.addAndObserveWindows() + }, + runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in + guard let self = self else { return } + if self.runningApplication.activationPolicy != .regular { + self.removeWindowslessAppWindow() + } + self.addAndObserveWindows() + }, ] } @@ -45,8 +55,15 @@ class Application: NSObject { debugPrint("Deinit app", runningApplication.bundleIdentifier ?? runningApplication.bundleURL ?? "nil") } + func removeWindowslessAppWindow() { + if let windowlessAppWindow = Windows.list.firstIndex { $0.isWindowlessApp == true && $0.application.pid == pid } { + Windows.list.remove(at: windowlessAppWindow) + App.app.refreshOpenUi() + } + } + func addAndObserveWindows() { - if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited { + if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited && axUiElement == nil { axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier) AXObserverCreate(runningApplication.processIdentifier, axObserverCallback, &axObserver) debugPrint("Adding app", runningApplication.processIdentifier, runningApplication.bundleIdentifier ?? "nil") @@ -75,6 +92,14 @@ class Application: NSObject { DispatchQueue.main.async { [weak self] in guard let self = self else { return } self.addWindows(windows) + self.addWindowslessAppsIfNeeded() + App.app.refreshOpenUi() + } + } else { + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.addWindowslessAppsIfNeeded() + App.app.refreshOpenUi() } } } @@ -92,7 +117,15 @@ class Application: NSObject { if App.app.appIsBeingUsed { Windows.cycleFocusedWindowIndex(windows.count) } - App.app.refreshOpenUi(windows) + } + + func addWindowslessAppsIfNeeded() { + if !Preferences.hideWindowlessApps && + runningApplication.activationPolicy == .regular && + !runningApplication.isTerminated && + (Windows.list.firstIndex { $0.application.pid == pid }) == nil { + Windows.list.insertAndScaleRecycledPool(Window(self), at: Windows.list.count) + } } private func observeEvents() { diff --git a/src/logic/Applications.swift b/src/logic/Applications.swift index 1aebef0c..9b5c4b1c 100644 --- a/src/logic/Applications.swift +++ b/src/logic/Applications.swift @@ -10,7 +10,7 @@ class Applications { guard app.runningApplication.isFinishedLaunching else { continue } app.observeNewWindows(group) } - _ = group.wait(wallTimeout: .now() + .seconds(1)) + _ = group.wait(wallTimeout: .now() + .seconds(2)) } static func initialDiscovery() { @@ -49,13 +49,13 @@ class Applications { static func removeRunningApplications(_ runningApps: [NSRunningApplication]) { var windowsOnTheLeftOfFocusedWindow = 0 for runningApp in runningApps { - Applications.list.removeAll(where: { $0.runningApplication.isEqual(runningApp) }) + Applications.list.removeAll(where: { $0.pid == runningApp.processIdentifier }) Windows.list.enumerated().forEach { (index, window) in - if window.application.runningApplication.isEqual(runningApp) && index < Windows.focusedWindowIndex { + if window.application.pid == runningApp.processIdentifier && index < Windows.focusedWindowIndex { windowsOnTheLeftOfFocusedWindow += 1 } } - Windows.list.removeAll(where: { $0.application.runningApplication.isEqual(runningApp) }) + Windows.list.removeAll(where: { $0.application.pid == runningApp.processIdentifier }) } guard Windows.list.count > 0 else { App.app.hideUi(); return } if windowsOnTheLeftOfFocusedWindow > 0 { @@ -67,7 +67,7 @@ class Applications { static func refreshBadges() { let group = DispatchGroup() retryAxCallUntilTimeout(group) { - if let dockPid = (Applications.list.first { $0.runningApplication.bundleIdentifier == "com.apple.dock" }?.runningApplication.processIdentifier), + if let dockPid = (Applications.list.first { $0.runningApplication.bundleIdentifier == "com.apple.dock" }?.pid), let axList = (try AXUIElementCreateApplication(dockPid).children()?.first { try $0.role() == "AXList" }), let axAppDockItem = (try axList.children()?.filter { try $0.subrole() == "AXApplicationDockItem" && ($0.appIsRunning() ?? false) }) { try Applications.list.forEach { app in diff --git a/src/logic/Preferences.swift b/src/logic/Preferences.swift index 927d07cc..65ef5e34 100644 --- a/src/logic/Preferences.swift +++ b/src/logic/Preferences.swift @@ -59,6 +59,7 @@ class Preferences { "maxCellsPerRow": defaultsDependingOnScreenRatio_["maxCellsPerRow"]!, "shortcutStyle": "0", "hideAppBadges": "false", + "hideWindowlessApps": "false", ] // constant values @@ -98,6 +99,7 @@ class Preferences { static var hideSpaceNumberLabels: Bool { defaults.bool("hideSpaceNumberLabels") } static var hideStatusIcons: Bool { defaults.bool("hideStatusIcons") } static var hideAppBadges: Bool { defaults.bool("hideAppBadges") } + static var hideWindowlessApps: Bool { defaults.bool("hideWindowlessApps") } static var startAtLogin: Bool { defaults.bool("startAtLogin") } static var dontShowBlacklist: [String] { blacklistStringToArray(defaults.string("dontShowBlacklist")) } static var disableShortcutsBlacklist: [String] { blacklistStringToArray(defaults.string("disableShortcutsBlacklist")) } diff --git a/src/logic/Window.swift b/src/logic/Window.swift index 54a20942..448f1f97 100644 --- a/src/logic/Window.swift +++ b/src/logic/Window.swift @@ -1,7 +1,7 @@ import Cocoa class Window { - var cgWindowId: CGWindowID + var cgWindowId = CGWindowID.max var title: String! var thumbnail: NSImage? var thumbnailFullSize: NSSize? @@ -10,13 +10,14 @@ class Window { var isTabbed: Bool = false var isHidden: Bool { get { application.isHidden } } var dockLabel: Int? { get { application.dockLabel.flatMap { Int($0) } } } - var isFullscreen: Bool - var isMinimized: Bool - var isOnAllSpaces: Bool + var isFullscreen = false + var isMinimized = false + var isOnAllSpaces = false + var isWindowlessApp = false var position: CGPoint? - var spaceId: CGSSpaceID - var spaceIndex: SpaceIndex - var axUiElement: AXUIElement + var spaceId = CGSSpaceID.max + var spaceIndex = SpaceIndex.max + var axUiElement: AXUIElement! var application: Application var axObserver: AXObserver? var row: Int? @@ -43,17 +44,27 @@ class Window { self.position = position self.title = bestEffortTitle(axTitle) self.isTabbed = false - refreshThumbnail() + if !Preferences.hideThumbnails { + refreshThumbnail() + } + application.removeWindowslessAppWindow() debugPrint("Adding window", cgWindowId, title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil") observeEvents() } + init(_ application: Application) { + isWindowlessApp = true + self.application = application + self.title = application.runningApplication.localizedName + debugPrint("Adding app-window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil") + } + deinit { debugPrint("Deinit window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil") } private func observeEvents() { - AXObserverCreate(application.runningApplication.processIdentifier, axObserverCallback, &axObserver) + AXObserverCreate(application.pid, axObserverCallback, &axObserver) guard let axObserver = axObserver else { return } for notification in Window.notifications { retryAxCallUntilTimeout { [weak self] in @@ -84,6 +95,7 @@ class Window { } func close() { + if isWindowlessApp { return } BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in guard let self = self else { return } if self.isFullscreen { @@ -96,6 +108,7 @@ class Window { } func minDemin() { + if isWindowlessApp { return } BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in guard let self = self else { return } if self.isFullscreen { @@ -112,6 +125,7 @@ class Window { } func toggleFullscreen() { + if isWindowlessApp { return } BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in guard let self = self else { return } self.axUiElement.setAttribute(kAXFullscreenAttribute, !self.isFullscreen) @@ -140,6 +154,12 @@ class Window { func focus() { if application.runningApplication.processIdentifier == ProcessInfo.processInfo.processIdentifier { App.app.showSecondaryWindow(App.app.window(withWindowNumber: Int(cgWindowId))) + } else if isWindowlessApp { + if let bundleID = application.runningApplication.bundleIdentifier { + NSWorkspace.shared.launchApplication(withBundleIdentifier: bundleID, additionalEventParamDescriptor: nil, launchIdentifier: nil) + } else { + application.runningApplication.activate(options: .activateIgnoringOtherApps) + } } else { // macOS bug: when switching to a System Preferences window in another space, it switches to that space, // but quickly switches back to another window in that space @@ -190,3 +210,4 @@ class Window { return application.runningApplication.localizedName ?? "" } } + diff --git a/src/logic/Windows.swift b/src/logic/Windows.swift index a7921db2..d4636614 100644 --- a/src/logic/Windows.swift +++ b/src/logic/Windows.swift @@ -122,14 +122,16 @@ class Windows { static func refreshIfWindowShouldBeShownToTheUser(_ window: Window, _ screen: NSScreen) { window.shouldShowTheUser = - !(!Preferences.showFullscreenWindows[App.app.shortcutIndex] && window.isFullscreen) && - !(!Preferences.showMinimizedWindows[App.app.shortcutIndex] && window.isMinimized) && - !(!Preferences.showHiddenWindows[App.app.shortcutIndex] && window.isHidden) && + !(window.application.runningApplication.bundleIdentifier.flatMap { Preferences.dontShowBlacklist.contains($0) } ?? false) && !(Preferences.appsToShow[App.app.shortcutIndex] == .active && window.application.runningApplication != NSWorkspace.shared.frontmostApplication) && - !(Preferences.spacesToShow[App.app.shortcutIndex] == .active && window.spaceId != Spaces.currentSpaceId) && - !(Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab && !isOnScreen(window, screen)) && - (Preferences.showTabsAsWindows || !window.isTabbed) && - !(window.application.runningApplication.bundleIdentifier.flatMap { Preferences.dontShowBlacklist.contains($0) } ?? false) + !(!Preferences.showHiddenWindows[App.app.shortcutIndex] && window.isHidden) && + ((!Preferences.hideWindowlessApps && window.isWindowlessApp) || + !window.isWindowlessApp && + !(!Preferences.showFullscreenWindows[App.app.shortcutIndex] && window.isFullscreen) && + !(!Preferences.showMinimizedWindows[App.app.shortcutIndex] && window.isMinimized) && + !(Preferences.spacesToShow[App.app.shortcutIndex] == .active && window.spaceId != Spaces.currentSpaceId) && + !(Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab && !isOnScreen(window, screen)) && + (Preferences.showTabsAsWindows || !window.isTabbed)) } static func isOnScreen(_ window: Window, _ screen: NSScreen) -> Bool { diff --git a/src/logic/events/AccessibilityEvents.swift b/src/logic/events/AccessibilityEvents.swift index 7d9f448c..f8b9a305 100644 --- a/src/logic/events/AccessibilityEvents.swift +++ b/src/logic/events/AccessibilityEvents.swift @@ -28,10 +28,10 @@ func retryAxCallUntilTimeout_(_ group: DispatchGroup?, _ fn: @escaping () throws } func handleEvent(_ type: String, _ element: AXUIElement) throws { - debugPrint("Accessibility event", type, try element.title() ?? "nil") + debugPrint("Accessibility event", type, type != kAXFocusedUIElementChangedNotification ? (try element.title() ?? "nil") : "nil") // events are handled concurrently, thus we check that the app is still running if let pid = try element.pid(), - try (!(pid == ProcessInfo.processInfo.processIdentifier && element.subrole() == "AXUnknown")) { + try (!(type == kAXWindowCreatedNotification && pid == ProcessInfo.processInfo.processIdentifier && element.subrole() == "AXUnknown")) { switch type { case kAXApplicationActivatedNotification: try applicationActivated(element) case kAXApplicationHiddenNotification, @@ -57,7 +57,7 @@ private func focusedUiElementChanged(_ element: AXUIElement, _ pid: pid_t) throw DispatchQueue.main.async { let windows = Windows.list.filter { w in // for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug - let isFromApp = w.application.runningApplication.processIdentifier == pid + let isFromApp = w.application.pid == pid if isFromApp { // this event is the only opportunity we have to check if a window became a tab, or a tab became a window let oldIsTabbed = w.isTabbed @@ -87,12 +87,12 @@ private func applicationActivated(_ element: AXUIElement) throws { private func applicationHiddenOrShown(_ element: AXUIElement, _ pid: pid_t, _ type: String) throws { DispatchQueue.main.async { - if let app = (Applications.list.first { $0.runningApplication.processIdentifier == pid }) { + if let app = (Applications.list.first { $0.pid == pid }) { app.isHidden = type == kAXApplicationHiddenNotification } let windows = Windows.list.filter { // for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug - return $0.application.runningApplication.processIdentifier == pid + return $0.application.pid == pid } App.app.refreshOpenUi(windows) } @@ -112,7 +112,7 @@ private func windowCreated(_ element: AXUIElement, _ pid: pid_t) throws { if Windows.list.firstIndexThatMatches(element, wid) == nil, let runningApp = NSRunningApplication(processIdentifier: pid), element.isActualWindow(runningApp, wid, isOnNormalLevel, axTitle, subrole, role), - let app = (Applications.list.first { $0.runningApplication.processIdentifier == pid }) { + let app = (Applications.list.first { $0.pid == pid }) { let window = Window(element, app, wid, axTitle, isFullscreen, isMinimized, position) Windows.list.insertAndScaleRecycledPool(window, at: 0) Windows.cycleFocusedWindowIndex(1) @@ -139,7 +139,7 @@ private func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) throws { } } else if let runningApp = NSRunningApplication(processIdentifier: pid), element.isActualWindow(runningApp, wid, isOnNormalLevel, axTitle, subrole, role), - let app = (Applications.list.first { $0.runningApplication.processIdentifier == pid }) { + let app = (Applications.list.first { $0.pid == pid }) { Windows.list.insertAndScaleRecycledPool(Window(element, app, wid, axTitle, isFullscreen, isMinimized, position), at: 0) App.app.refreshOpenUi([Windows.list[0]]) } @@ -152,7 +152,9 @@ private func windowDestroyed(_ element: AXUIElement) throws { let wid = try element.cgWindowId() DispatchQueue.main.async { guard let existingIndex = Windows.list.firstIndexThatMatches(element, wid) else { return } + let window = Windows.list[existingIndex] Windows.list.remove(at: existingIndex) + window.application.addWindowslessAppsIfNeeded() guard Windows.list.count > 0 else { App.app.hideUi(); return } Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(existingIndex) App.app.refreshOpenUi() diff --git a/src/ui/main-window/ThumbnailFontIconView.swift b/src/ui/main-window/ThumbnailFontIconView.swift index 029f525f..862553c0 100644 --- a/src/ui/main-window/ThumbnailFontIconView.swift +++ b/src/ui/main-window/ThumbnailFontIconView.swift @@ -11,6 +11,17 @@ enum Symbols: String { case filledCircled = "􀀁" case filledCircledNumber0 = "􀀹" case filledCircledNumber10 = "􀔔" + case newWindow = "􀥃" +} + +class FontIcon: BaseLabel { + convenience init(_ symbol: Symbols, _ color: NSColor = .white) { + self.init(symbol.rawValue) + font = NSFont(name: "SF Pro Text", size: Preferences.fontHeight)! + textColor = color + alignment = .center + shadow = ThumbnailView.makeShadow(.darkGray) + } } // Font icon using SF Symbols from the SF Pro font from Apple diff --git a/src/ui/main-window/ThumbnailView.swift b/src/ui/main-window/ThumbnailView.swift index bb22ce94..6c4ace61 100644 --- a/src/ui/main-window/ThumbnailView.swift +++ b/src/ui/main-window/ThumbnailView.swift @@ -20,6 +20,7 @@ class ThumbnailView: NSStackView { var isHighlighted = false var shouldShowWindowControls = false var isShowingWindowControls = false + var windowlessIcon = FontIcon(.newWindow) convenience init() { self.init(frame: .zero) @@ -44,6 +45,7 @@ class ThumbnailView: NSStackView { setViews([hStackView, thumbnail], in: .leading) addWindowControls() addDockLabelIcon() + thumbnail.addSubview(windowlessIcon, positioned: .above, relativeTo: nil) } private func addDockLabelIcon() { @@ -68,7 +70,7 @@ class ThumbnailView: NSStackView { if let shouldShowWindowControls = shouldShowWindowControls_ { self.shouldShowWindowControls = shouldShowWindowControls } - let shouldShow = shouldShowWindowControls && isHighlighted && !Preferences.hideColoredCircles + let shouldShow = shouldShowWindowControls && isHighlighted && !Preferences.hideColoredCircles && !window_!.isWindowlessApp if isShowingWindowControls != shouldShow { isShowingWindowControls = shouldShow [closeIcon, minimizeIcon, maximizeIcon].forEach { $0.isHidden = !shouldShow } @@ -144,7 +146,18 @@ class ThumbnailView: NSStackView { assignIfDifferent(&frame.size.height, newHeight) let fontIconWidth = CGFloat([fullscreenIcon, minimizedIcon, hiddenIcon, spaceIcon].filter { !$0.isHidden }.count) * (Preferences.fontIconSize + Preferences.intraCellPadding) assignIfDifferent(&label.textContainer!.size.width, frame.width - Preferences.iconSize - Preferences.intraCellPadding * 3 - fontIconWidth) - assignIfDifferent(&subviews.first!.frame.size, frame.size) + assignIfDifferent(&windowlessIcon.isHidden, !element.isWindowlessApp) + if element.isWindowlessApp { + let maxWidth = (ThumbnailView.widthMin(screen) - Preferences.intraCellPadding * 2).rounded() + let maxHeight = ((ThumbnailView.height(screen) - hStackView.fittingSize.height) - Preferences.intraCellPadding * 2).rounded() + // heuristic to determine font size based on bounding box + let fontSize = (min(maxWidth, maxHeight) * 0.6).rounded() + // 1.25 is a heuristic to fit the SF Symbol into the bounding box + windowlessIcon.frame.size = CGSize(width: maxWidth, height: (fontSize * 1.25).rounded()) + windowlessIcon.font = NSFont(name: windowlessIcon.font!.fontName, size: fontSize) + // 2.5 is a heuristic to have a _perceived_ vertical alignment on this particular icon + windowlessIcon.frame.origin = CGPoint(x: (-maxWidth / 2).rounded(), y: (-maxHeight / 2 - windowlessIcon.frame.size.height / 2.5).rounded()) + } self.mouseUpCallback = { () -> Void in App.app.focusSelectedWindow(element) } self.mouseMovedCallback = { () -> Void in Windows.updateFocusedWindowIndex(index) } // force a display to avoid flickering; see https://github.com/lwouis/alt-tab-macos/issues/197 diff --git a/src/ui/preferences-window/tabs/AppearanceTab.swift b/src/ui/preferences-window/tabs/AppearanceTab.swift index aed7b6df..8d8f49d1 100644 --- a/src/ui/preferences-window/tabs/AppearanceTab.swift +++ b/src/ui/preferences-window/tabs/AppearanceTab.swift @@ -20,6 +20,7 @@ class AppearanceTab { LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Show standard tabs as windows:", comment: ""), "showTabsAsWindows"), LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide colored circles on mouse hover:", comment: ""), "hideColoredCircles"), LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide app badges:", comment: ""), "hideAppBadges"), + LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide apps with no open window:", comment: ""), "hideWindowlessApps"), ]) grid.column(at: 0).xPlacement = .trailing grid.rowAlignment = .lastBaseline