feat: show apps with no open window (closes #397)
This commit is contained in:
1 parent
dc8b363e57
commit
f0fa02c4a2
13 files changed
+126
-35
No files matched your search
Binary file not shown.
@@ -130,6 +130,9 @@
|
|||||||
/* No comment provided by engineer. */
|
/* No comment provided by engineer. */
|
||||||
"Hide app badges:" = "Hide app badges:";
|
"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. */
|
/* No comment provided by engineer. */
|
||||||
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
|
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
|
||||||
|
|
||||||
|
|||||||
@@ -131,6 +131,9 @@
|
|||||||
/*No comment provided by engineer.*/
|
/*No comment provided by engineer.*/
|
||||||
"Hide app badges:" = "Hide app badges:";
|
"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.*/
|
/*No comment provided by engineer.*/
|
||||||
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
|
"Hide colored circles on mouse hover:" = "Hide colored circles on mouse hover:";
|
||||||
|
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ set -exu
|
|||||||
|
|
||||||
"$(pipenv --venv)"/bin/pyftsubset resources/SF-Pro-Text-Regular-Full.otf \
|
"$(pipenv --venv)"/bin/pyftsubset resources/SF-Pro-Text-Regular-Full.otf \
|
||||||
--output-file=resources/SF-Pro-Text-Regular.otf \
|
--output-file=resources/SF-Pro-Text-Regular.otf \
|
||||||
--text=""
|
--text=""
|
||||||
@@ -10,6 +10,7 @@ class Application: NSObject {
|
|||||||
var isHidden: Bool!
|
var isHidden: Bool!
|
||||||
var icon: NSImage?
|
var icon: NSImage?
|
||||||
var dockLabel: String?
|
var dockLabel: String?
|
||||||
|
var pid: pid_t { runningApplication.processIdentifier }
|
||||||
|
|
||||||
static func notifications(_ app: NSRunningApplication) -> [String] {
|
static func notifications(_ app: NSRunningApplication) -> [String] {
|
||||||
var n = [
|
var n = [
|
||||||
@@ -36,8 +37,17 @@ class Application: NSObject {
|
|||||||
icon = runningApplication.icon
|
icon = runningApplication.icon
|
||||||
addAndObserveWindows()
|
addAndObserveWindows()
|
||||||
kvObservers = [
|
kvObservers = [
|
||||||
runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() },
|
runningApplication.observe(\.isFinishedLaunching, options: [.new]) { [weak self] _, _ in
|
||||||
runningApplication.observe(\.activationPolicy, options: [.new]) { [weak self] _, _ in self?.addAndObserveWindows() },
|
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")
|
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() {
|
func addAndObserveWindows() {
|
||||||
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited {
|
if runningApplication.isFinishedLaunching && runningApplication.activationPolicy != .prohibited && axUiElement == nil {
|
||||||
axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier)
|
axUiElement = AXUIElementCreateApplication(runningApplication.processIdentifier)
|
||||||
AXObserverCreate(runningApplication.processIdentifier, axObserverCallback, &axObserver)
|
AXObserverCreate(runningApplication.processIdentifier, axObserverCallback, &axObserver)
|
||||||
debugPrint("Adding app", runningApplication.processIdentifier, runningApplication.bundleIdentifier ?? "nil")
|
debugPrint("Adding app", runningApplication.processIdentifier, runningApplication.bundleIdentifier ?? "nil")
|
||||||
@@ -75,6 +92,14 @@ class Application: NSObject {
|
|||||||
DispatchQueue.main.async { [weak self] in
|
DispatchQueue.main.async { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.addWindows(windows)
|
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 {
|
if App.app.appIsBeingUsed {
|
||||||
Windows.cycleFocusedWindowIndex(windows.count)
|
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() {
|
private func observeEvents() {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class Applications {
|
|||||||
guard app.runningApplication.isFinishedLaunching else { continue }
|
guard app.runningApplication.isFinishedLaunching else { continue }
|
||||||
app.observeNewWindows(group)
|
app.observeNewWindows(group)
|
||||||
}
|
}
|
||||||
_ = group.wait(wallTimeout: .now() + .seconds(1))
|
_ = group.wait(wallTimeout: .now() + .seconds(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
static func initialDiscovery() {
|
static func initialDiscovery() {
|
||||||
@@ -49,13 +49,13 @@ class Applications {
|
|||||||
static func removeRunningApplications(_ runningApps: [NSRunningApplication]) {
|
static func removeRunningApplications(_ runningApps: [NSRunningApplication]) {
|
||||||
var windowsOnTheLeftOfFocusedWindow = 0
|
var windowsOnTheLeftOfFocusedWindow = 0
|
||||||
for runningApp in runningApps {
|
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
|
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
|
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 }
|
guard Windows.list.count > 0 else { App.app.hideUi(); return }
|
||||||
if windowsOnTheLeftOfFocusedWindow > 0 {
|
if windowsOnTheLeftOfFocusedWindow > 0 {
|
||||||
@@ -67,7 +67,7 @@ class Applications {
|
|||||||
static func refreshBadges() {
|
static func refreshBadges() {
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
retryAxCallUntilTimeout(group) {
|
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 axList = (try AXUIElementCreateApplication(dockPid).children()?.first { try $0.role() == "AXList" }),
|
||||||
let axAppDockItem = (try axList.children()?.filter { try $0.subrole() == "AXApplicationDockItem" && ($0.appIsRunning() ?? false) }) {
|
let axAppDockItem = (try axList.children()?.filter { try $0.subrole() == "AXApplicationDockItem" && ($0.appIsRunning() ?? false) }) {
|
||||||
try Applications.list.forEach { app in
|
try Applications.list.forEach { app in
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class Preferences {
|
|||||||
"maxCellsPerRow": defaultsDependingOnScreenRatio_["maxCellsPerRow"]!,
|
"maxCellsPerRow": defaultsDependingOnScreenRatio_["maxCellsPerRow"]!,
|
||||||
"shortcutStyle": "0",
|
"shortcutStyle": "0",
|
||||||
"hideAppBadges": "false",
|
"hideAppBadges": "false",
|
||||||
|
"hideWindowlessApps": "false",
|
||||||
]
|
]
|
||||||
|
|
||||||
// constant values
|
// constant values
|
||||||
@@ -98,6 +99,7 @@ class Preferences {
|
|||||||
static var hideSpaceNumberLabels: Bool { defaults.bool("hideSpaceNumberLabels") }
|
static var hideSpaceNumberLabels: Bool { defaults.bool("hideSpaceNumberLabels") }
|
||||||
static var hideStatusIcons: Bool { defaults.bool("hideStatusIcons") }
|
static var hideStatusIcons: Bool { defaults.bool("hideStatusIcons") }
|
||||||
static var hideAppBadges: Bool { defaults.bool("hideAppBadges") }
|
static var hideAppBadges: Bool { defaults.bool("hideAppBadges") }
|
||||||
|
static var hideWindowlessApps: Bool { defaults.bool("hideWindowlessApps") }
|
||||||
static var startAtLogin: Bool { defaults.bool("startAtLogin") }
|
static var startAtLogin: Bool { defaults.bool("startAtLogin") }
|
||||||
static var dontShowBlacklist: [String] { blacklistStringToArray(defaults.string("dontShowBlacklist")) }
|
static var dontShowBlacklist: [String] { blacklistStringToArray(defaults.string("dontShowBlacklist")) }
|
||||||
static var disableShortcutsBlacklist: [String] { blacklistStringToArray(defaults.string("disableShortcutsBlacklist")) }
|
static var disableShortcutsBlacklist: [String] { blacklistStringToArray(defaults.string("disableShortcutsBlacklist")) }
|
||||||
|
|||||||
+30
-9
@@ -1,7 +1,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
|
|
||||||
class Window {
|
class Window {
|
||||||
var cgWindowId: CGWindowID
|
var cgWindowId = CGWindowID.max
|
||||||
var title: String!
|
var title: String!
|
||||||
var thumbnail: NSImage?
|
var thumbnail: NSImage?
|
||||||
var thumbnailFullSize: NSSize?
|
var thumbnailFullSize: NSSize?
|
||||||
@@ -10,13 +10,14 @@ class Window {
|
|||||||
var isTabbed: Bool = false
|
var isTabbed: Bool = false
|
||||||
var isHidden: Bool { get { application.isHidden } }
|
var isHidden: Bool { get { application.isHidden } }
|
||||||
var dockLabel: Int? { get { application.dockLabel.flatMap { Int($0) } } }
|
var dockLabel: Int? { get { application.dockLabel.flatMap { Int($0) } } }
|
||||||
var isFullscreen: Bool
|
var isFullscreen = false
|
||||||
var isMinimized: Bool
|
var isMinimized = false
|
||||||
var isOnAllSpaces: Bool
|
var isOnAllSpaces = false
|
||||||
|
var isWindowlessApp = false
|
||||||
var position: CGPoint?
|
var position: CGPoint?
|
||||||
var spaceId: CGSSpaceID
|
var spaceId = CGSSpaceID.max
|
||||||
var spaceIndex: SpaceIndex
|
var spaceIndex = SpaceIndex.max
|
||||||
var axUiElement: AXUIElement
|
var axUiElement: AXUIElement!
|
||||||
var application: Application
|
var application: Application
|
||||||
var axObserver: AXObserver?
|
var axObserver: AXObserver?
|
||||||
var row: Int?
|
var row: Int?
|
||||||
@@ -43,17 +44,27 @@ class Window {
|
|||||||
self.position = position
|
self.position = position
|
||||||
self.title = bestEffortTitle(axTitle)
|
self.title = bestEffortTitle(axTitle)
|
||||||
self.isTabbed = false
|
self.isTabbed = false
|
||||||
refreshThumbnail()
|
if !Preferences.hideThumbnails {
|
||||||
|
refreshThumbnail()
|
||||||
|
}
|
||||||
|
application.removeWindowslessAppWindow()
|
||||||
debugPrint("Adding window", cgWindowId, title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil")
|
debugPrint("Adding window", cgWindowId, title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil")
|
||||||
observeEvents()
|
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 {
|
deinit {
|
||||||
debugPrint("Deinit window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil")
|
debugPrint("Deinit window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func observeEvents() {
|
private func observeEvents() {
|
||||||
AXObserverCreate(application.runningApplication.processIdentifier, axObserverCallback, &axObserver)
|
AXObserverCreate(application.pid, axObserverCallback, &axObserver)
|
||||||
guard let axObserver = axObserver else { return }
|
guard let axObserver = axObserver else { return }
|
||||||
for notification in Window.notifications {
|
for notification in Window.notifications {
|
||||||
retryAxCallUntilTimeout { [weak self] in
|
retryAxCallUntilTimeout { [weak self] in
|
||||||
@@ -84,6 +95,7 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func close() {
|
func close() {
|
||||||
|
if isWindowlessApp { return }
|
||||||
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
|
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if self.isFullscreen {
|
if self.isFullscreen {
|
||||||
@@ -96,6 +108,7 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func minDemin() {
|
func minDemin() {
|
||||||
|
if isWindowlessApp { return }
|
||||||
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
|
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
if self.isFullscreen {
|
if self.isFullscreen {
|
||||||
@@ -112,6 +125,7 @@ class Window {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toggleFullscreen() {
|
func toggleFullscreen() {
|
||||||
|
if isWindowlessApp { return }
|
||||||
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
|
BackgroundWork.accessibilityCommandsQueue.asyncWithCap { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
self.axUiElement.setAttribute(kAXFullscreenAttribute, !self.isFullscreen)
|
self.axUiElement.setAttribute(kAXFullscreenAttribute, !self.isFullscreen)
|
||||||
@@ -140,6 +154,12 @@ class Window {
|
|||||||
func focus() {
|
func focus() {
|
||||||
if application.runningApplication.processIdentifier == ProcessInfo.processInfo.processIdentifier {
|
if application.runningApplication.processIdentifier == ProcessInfo.processInfo.processIdentifier {
|
||||||
App.app.showSecondaryWindow(App.app.window(withWindowNumber: Int(cgWindowId)))
|
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 {
|
} else {
|
||||||
// macOS bug: when switching to a System Preferences window in another space, it switches to that space,
|
// 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
|
// but quickly switches back to another window in that space
|
||||||
@@ -190,3 +210,4 @@ class Window {
|
|||||||
return application.runningApplication.localizedName ?? ""
|
return application.runningApplication.localizedName ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -122,14 +122,16 @@ class Windows {
|
|||||||
|
|
||||||
static func refreshIfWindowShouldBeShownToTheUser(_ window: Window, _ screen: NSScreen) {
|
static func refreshIfWindowShouldBeShownToTheUser(_ window: Window, _ screen: NSScreen) {
|
||||||
window.shouldShowTheUser =
|
window.shouldShowTheUser =
|
||||||
!(!Preferences.showFullscreenWindows[App.app.shortcutIndex] && window.isFullscreen) &&
|
!(window.application.runningApplication.bundleIdentifier.flatMap { Preferences.dontShowBlacklist.contains($0) } ?? false) &&
|
||||||
!(!Preferences.showMinimizedWindows[App.app.shortcutIndex] && window.isMinimized) &&
|
|
||||||
!(!Preferences.showHiddenWindows[App.app.shortcutIndex] && window.isHidden) &&
|
|
||||||
!(Preferences.appsToShow[App.app.shortcutIndex] == .active && window.application.runningApplication != NSWorkspace.shared.frontmostApplication) &&
|
!(Preferences.appsToShow[App.app.shortcutIndex] == .active && window.application.runningApplication != NSWorkspace.shared.frontmostApplication) &&
|
||||||
!(Preferences.spacesToShow[App.app.shortcutIndex] == .active && window.spaceId != Spaces.currentSpaceId) &&
|
!(!Preferences.showHiddenWindows[App.app.shortcutIndex] && window.isHidden) &&
|
||||||
!(Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab && !isOnScreen(window, screen)) &&
|
((!Preferences.hideWindowlessApps && window.isWindowlessApp) ||
|
||||||
(Preferences.showTabsAsWindows || !window.isTabbed) &&
|
!window.isWindowlessApp &&
|
||||||
!(window.application.runningApplication.bundleIdentifier.flatMap { Preferences.dontShowBlacklist.contains($0) } ?? false)
|
!(!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 {
|
static func isOnScreen(_ window: Window, _ screen: NSScreen) -> Bool {
|
||||||
|
|||||||
@@ -28,10 +28,10 @@ func retryAxCallUntilTimeout_(_ group: DispatchGroup?, _ fn: @escaping () throws
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleEvent(_ type: String, _ element: AXUIElement) 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
|
// events are handled concurrently, thus we check that the app is still running
|
||||||
if let pid = try element.pid(),
|
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 {
|
switch type {
|
||||||
case kAXApplicationActivatedNotification: try applicationActivated(element)
|
case kAXApplicationActivatedNotification: try applicationActivated(element)
|
||||||
case kAXApplicationHiddenNotification,
|
case kAXApplicationHiddenNotification,
|
||||||
@@ -57,7 +57,7 @@ private func focusedUiElementChanged(_ element: AXUIElement, _ pid: pid_t) throw
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let windows = Windows.list.filter { w in
|
let windows = Windows.list.filter { w in
|
||||||
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
|
// 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 {
|
if isFromApp {
|
||||||
// this event is the only opportunity we have to check if a window became a tab, or a tab became a window
|
// 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
|
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 {
|
private func applicationHiddenOrShown(_ element: AXUIElement, _ pid: pid_t, _ type: String) throws {
|
||||||
DispatchQueue.main.async {
|
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
|
app.isHidden = type == kAXApplicationHiddenNotification
|
||||||
}
|
}
|
||||||
let windows = Windows.list.filter {
|
let windows = Windows.list.filter {
|
||||||
// for AXUIElement of apps, CFEqual or == don't work; looks like a Cocoa bug
|
// 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)
|
App.app.refreshOpenUi(windows)
|
||||||
}
|
}
|
||||||
@@ -112,7 +112,7 @@ private func windowCreated(_ element: AXUIElement, _ pid: pid_t) throws {
|
|||||||
if Windows.list.firstIndexThatMatches(element, wid) == nil,
|
if Windows.list.firstIndexThatMatches(element, wid) == nil,
|
||||||
let runningApp = NSRunningApplication(processIdentifier: pid),
|
let runningApp = NSRunningApplication(processIdentifier: pid),
|
||||||
element.isActualWindow(runningApp, wid, isOnNormalLevel, axTitle, subrole, role),
|
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)
|
let window = Window(element, app, wid, axTitle, isFullscreen, isMinimized, position)
|
||||||
Windows.list.insertAndScaleRecycledPool(window, at: 0)
|
Windows.list.insertAndScaleRecycledPool(window, at: 0)
|
||||||
Windows.cycleFocusedWindowIndex(1)
|
Windows.cycleFocusedWindowIndex(1)
|
||||||
@@ -139,7 +139,7 @@ private func focusedWindowChanged(_ element: AXUIElement, _ pid: pid_t) throws {
|
|||||||
}
|
}
|
||||||
} else if let runningApp = NSRunningApplication(processIdentifier: pid),
|
} else if let runningApp = NSRunningApplication(processIdentifier: pid),
|
||||||
element.isActualWindow(runningApp, wid, isOnNormalLevel, axTitle, subrole, role),
|
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)
|
Windows.list.insertAndScaleRecycledPool(Window(element, app, wid, axTitle, isFullscreen, isMinimized, position), at: 0)
|
||||||
App.app.refreshOpenUi([Windows.list[0]])
|
App.app.refreshOpenUi([Windows.list[0]])
|
||||||
}
|
}
|
||||||
@@ -152,7 +152,9 @@ private func windowDestroyed(_ element: AXUIElement) throws {
|
|||||||
let wid = try element.cgWindowId()
|
let wid = try element.cgWindowId()
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
guard let existingIndex = Windows.list.firstIndexThatMatches(element, wid) else { return }
|
guard let existingIndex = Windows.list.firstIndexThatMatches(element, wid) else { return }
|
||||||
|
let window = Windows.list[existingIndex]
|
||||||
Windows.list.remove(at: existingIndex)
|
Windows.list.remove(at: existingIndex)
|
||||||
|
window.application.addWindowslessAppsIfNeeded()
|
||||||
guard Windows.list.count > 0 else { App.app.hideUi(); return }
|
guard Windows.list.count > 0 else { App.app.hideUi(); return }
|
||||||
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(existingIndex)
|
Windows.moveFocusedWindowIndexAfterWindowDestroyedInBackground(existingIndex)
|
||||||
App.app.refreshOpenUi()
|
App.app.refreshOpenUi()
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ enum Symbols: String {
|
|||||||
case filledCircled = ""
|
case filledCircled = ""
|
||||||
case filledCircledNumber0 = ""
|
case filledCircledNumber0 = ""
|
||||||
case filledCircledNumber10 = ""
|
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
|
// Font icon using SF Symbols from the SF Pro font from Apple
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class ThumbnailView: NSStackView {
|
|||||||
var isHighlighted = false
|
var isHighlighted = false
|
||||||
var shouldShowWindowControls = false
|
var shouldShowWindowControls = false
|
||||||
var isShowingWindowControls = false
|
var isShowingWindowControls = false
|
||||||
|
var windowlessIcon = FontIcon(.newWindow)
|
||||||
|
|
||||||
convenience init() {
|
convenience init() {
|
||||||
self.init(frame: .zero)
|
self.init(frame: .zero)
|
||||||
@@ -44,6 +45,7 @@ class ThumbnailView: NSStackView {
|
|||||||
setViews([hStackView, thumbnail], in: .leading)
|
setViews([hStackView, thumbnail], in: .leading)
|
||||||
addWindowControls()
|
addWindowControls()
|
||||||
addDockLabelIcon()
|
addDockLabelIcon()
|
||||||
|
thumbnail.addSubview(windowlessIcon, positioned: .above, relativeTo: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addDockLabelIcon() {
|
private func addDockLabelIcon() {
|
||||||
@@ -68,7 +70,7 @@ class ThumbnailView: NSStackView {
|
|||||||
if let shouldShowWindowControls = shouldShowWindowControls_ {
|
if let shouldShowWindowControls = shouldShowWindowControls_ {
|
||||||
self.shouldShowWindowControls = shouldShowWindowControls
|
self.shouldShowWindowControls = shouldShowWindowControls
|
||||||
}
|
}
|
||||||
let shouldShow = shouldShowWindowControls && isHighlighted && !Preferences.hideColoredCircles
|
let shouldShow = shouldShowWindowControls && isHighlighted && !Preferences.hideColoredCircles && !window_!.isWindowlessApp
|
||||||
if isShowingWindowControls != shouldShow {
|
if isShowingWindowControls != shouldShow {
|
||||||
isShowingWindowControls = shouldShow
|
isShowingWindowControls = shouldShow
|
||||||
[closeIcon, minimizeIcon, maximizeIcon].forEach { $0.isHidden = !shouldShow }
|
[closeIcon, minimizeIcon, maximizeIcon].forEach { $0.isHidden = !shouldShow }
|
||||||
@@ -144,7 +146,18 @@ class ThumbnailView: NSStackView {
|
|||||||
assignIfDifferent(&frame.size.height, newHeight)
|
assignIfDifferent(&frame.size.height, newHeight)
|
||||||
let fontIconWidth = CGFloat([fullscreenIcon, minimizedIcon, hiddenIcon, spaceIcon].filter { !$0.isHidden }.count) * (Preferences.fontIconSize + Preferences.intraCellPadding)
|
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(&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.mouseUpCallback = { () -> Void in App.app.focusSelectedWindow(element) }
|
||||||
self.mouseMovedCallback = { () -> Void in Windows.updateFocusedWindowIndex(index) }
|
self.mouseMovedCallback = { () -> Void in Windows.updateFocusedWindowIndex(index) }
|
||||||
// force a display to avoid flickering; see https://github.com/lwouis/alt-tab-macos/issues/197
|
// force a display to avoid flickering; see https://github.com/lwouis/alt-tab-macos/issues/197
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ class AppearanceTab {
|
|||||||
LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Show standard tabs as windows:", comment: ""), "showTabsAsWindows"),
|
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 colored circles on mouse hover:", comment: ""), "hideColoredCircles"),
|
||||||
LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Hide app badges:", comment: ""), "hideAppBadges"),
|
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.column(at: 0).xPlacement = .trailing
|
||||||
grid.rowAlignment = .lastBaseline
|
grid.rowAlignment = .lastBaseline
|
||||||
|
|||||||
Reference in new issue
Block a user