Skip to content

Commit

Permalink
feat: show apps with no open window (closes #397)
Browse files Browse the repository at this point in the history
  • Loading branch information
lwouis committed Sep 4, 2020
1 parent dc8b363 commit f0fa02c
Show file tree
Hide file tree
Showing 13 changed files with 126 additions and 35 deletions.
Binary file modified resources/SF-Pro-Text-Regular.otf
Binary file not shown.
3 changes: 3 additions & 0 deletions resources/l10n/Localizable.strings
Expand Up @@ -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:";

Expand Down
3 changes: 3 additions & 0 deletions resources/l10n/en.lproj/Localizable.strings
Expand Up @@ -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:";

Expand Down
2 changes: 1 addition & 1 deletion scripts/subset_font.sh
Expand Up @@ -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="􀥃􀀁􀁎􀁌􀕧􀀸􀀺􀀼􀀾􀁀􀁂􀁄􀑱􀁆􀁈􀁊􀑳􀓵􀓶􀓷􀓸􀓹􀓺􀓻􀓼􀓽􀓾􀓿􀔀􀔁􀔂􀔃􀔄􀔅􀔆􀔇􀔈􀔉􀕬􀀹􀀻􀀽􀀿􀁁􀘘􀁃􀁅􀑲􀁇􀁉􀁋􀑴􀔔􀔕􀔖􀔗􀔘􀔙􀔚􀔛􀔜􀔝􀔞􀔟􀔠􀔡􀔢􀔣􀔤􀔥􀔦􀔧􀔨􀕭"
41 changes: 37 additions & 4 deletions src/logic/Application.swift
Expand Up @@ -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 = [
Expand All @@ -36,17 +37,33 @@ 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()
},
]
}

deinit {
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")
Expand Down Expand Up @@ -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()
}
}
}
Expand All @@ -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() {
Expand Down
10 changes: 5 additions & 5 deletions src/logic/Applications.swift
Expand Up @@ -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() {
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/logic/Preferences.swift
Expand Up @@ -59,6 +59,7 @@ class Preferences {
"maxCellsPerRow": defaultsDependingOnScreenRatio_["maxCellsPerRow"]!,
"shortcutStyle": "0",
"hideAppBadges": "false",
"hideWindowlessApps": "false",
]

// constant values
Expand Down Expand Up @@ -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")) }
Expand Down
39 changes: 30 additions & 9 deletions 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?
Expand All @@ -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?
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -190,3 +210,4 @@ class Window {
return application.runningApplication.localizedName ?? ""
}
}

16 changes: 9 additions & 7 deletions src/logic/Windows.swift
Expand Up @@ -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 {
Expand Down
16 changes: 9 additions & 7 deletions src/logic/events/AccessibilityEvents.swift
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
Expand All @@ -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)
Expand All @@ -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]])
}
Expand All @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions src/ui/main-window/ThumbnailFontIconView.swift
Expand Up @@ -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
Expand Down

0 comments on commit f0fa02c

Please sign in to comment.