From a25ad22beb8d2b2ec73c54895458af27f41900a4 Mon Sep 17 00:00:00 2001 From: allsochen Date: Mon, 29 Jul 2024 01:09:35 +0800 Subject: [PATCH] feat: support showing applications or windows --- src/logic/Application.swift | 19 +++++++ src/logic/Preferences.swift | 8 +-- src/logic/Window.swift | 49 ++++++++++++++++++- src/logic/Windows.swift | 43 ++++++++++++++++ .../tabs/AppearanceTab.swift | 6 ++- 5 files changed, 117 insertions(+), 8 deletions(-) diff --git a/src/logic/Application.swift b/src/logic/Application.swift index aa47d44a..beda98ec 100644 --- a/src/logic/Application.swift +++ b/src/logic/Application.swift @@ -43,6 +43,25 @@ class Application: NSObject { debugPrint("Deinit app", runningApplication.bundleIdentifier ?? runningApplication.bundleURL ?? "nil") } + override var description: String { + let s = """ + Application(kvObservers: \(kvObservers) + , runningApplication.bundleIdentifier: \(runningApplication.bundleIdentifier) + , runningApplication.title: \(runningApplication.localizedName) + , runningApplication.isActive: \(runningApplication.isActive) + , axUiElement: \(axUiElement) + , axObserver: \(axObserver) + , isReallyFinishedLaunching: \(isReallyFinishedLaunching) + , isHidden: \(isHidden) + , hasBeenActiveOnce: \(hasBeenActiveOnce) + , dockLabel: \(dockLabel) + , pid: \(pid) + , focusedWindow.cgWindowId: \(focusedWindow?.cgWindowId) + , alreadyRequestedToQuit: \(alreadyRequestedToQuit)) + """ + return s.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: " ", with: " ") + } + func removeWindowslessAppWindow() { if let windowlessAppWindow = (Windows.list.firstIndex { $0.isWindowlessApp == true && $0.application.pid == pid }) { Windows.list.remove(at: windowlessAppWindow) diff --git a/src/logic/Preferences.swift b/src/logic/Preferences.swift index 7cf3966e..8cf0a53d 100644 --- a/src/logic/Preferences.swift +++ b/src/logic/Preferences.swift @@ -751,24 +751,24 @@ enum TitleTruncationPreference: String, CaseIterable, MacroPreference { } enum ShowAppsWindowsPreference: String, CaseIterable, MacroPreference { - case apps = "0" + case applications = "0" case windows = "1" var localizedString: LocalizedString { switch self { - case .apps: return NSLocalizedString("Apps", comment: "") + case .applications: return NSLocalizedString("Applications", comment: "") case .windows: return NSLocalizedString("Windows", comment: "") } } } enum ShowAppNamesWindowTitlesPreference: String, CaseIterable, MacroPreference { - case appNames = "0" + case applicationNames = "0" case windowTitles = "1" var localizedString: LocalizedString { switch self { - case .appNames: return NSLocalizedString("App Names", comment: "") + case .applicationNames: return NSLocalizedString("Application Names", comment: "") case .windowTitles: return NSLocalizedString("Window Titles", comment: "") } } diff --git a/src/logic/Window.swift b/src/logic/Window.swift index ffbfea38..ff4bb6d2 100644 --- a/src/logic/Window.swift +++ b/src/logic/Window.swift @@ -1,6 +1,6 @@ import Cocoa -class Window { +class Window: CustomStringConvertible { static var globalCreationCounter = Int.zero var cgWindowId: CGWindowID? var lastFocusOrder = Int.zero @@ -71,6 +71,31 @@ class Window { debugPrint("Deinit window", title ?? "nil", application.runningApplication.bundleIdentifier ?? "nil") } + var description: String { + let s = """ + Window(cgWindowId: \(cgWindowId) + , title: \(title) + , thumbnailFullSize: \(thumbnailFullSize) + , shouldShowTheUser: \(shouldShowTheUser) + , isTabbed: \(isTabbed) + , isFullscreen: \(isFullscreen) + , isMinimized: \(isMinimized) + , isOnAllSpaces: \(isOnAllSpaces) + , isWindowlessApp: \(isWindowlessApp) + , position: \(position) + , size: \(size) + , spaceId: \(spaceId) + , spaceIndex: \(spaceIndex) + , axUiElement: \(axUiElement) + , application: \(application) + , axObserver: \(axObserver) + , row: \(row) + , lastFocusOrder: \(lastFocusOrder) + , creationOrder: \(creationOrder)) + """ + return s.replacingOccurrences(of: "\n", with: "").replacingOccurrences(of: " ", with: " ") + } + /// some apps will not trigger AXApplicationActivated, where we usually update application.focusedWindow /// workaround: we check and possibly do it here func checkIfFocused(_ application: Application, _ wid: CGWindowID) { @@ -278,5 +303,25 @@ class Window { // window-group, instead of picking the focused one return isTabbed ? application.focusedWindow : self } -} + // Determines if this window is the main application window + func isAppMainWindow() -> Bool { + if let element = application.axUiElement { + var mainWindow: AnyObject? + if AXUIElementCopyAttributeValue(element, kAXMainWindowAttribute as CFString, &mainWindow) == .success { + if let mainWin = mainWindow as! AXUIElement? { + do { + let w1 = try mainWin.cgWindowId() + let w2 = try self.axUiElement.cgWindowId() + if w1 == w2 { + return true + } + } catch { + return false + } + } + } + } + return false + } +} diff --git a/src/logic/Windows.swift b/src/logic/Windows.swift index d8f2df34..b7bdcccb 100644 --- a/src/logic/Windows.swift +++ b/src/logic/Windows.swift @@ -295,6 +295,20 @@ class Windows { Windows.list.forEach { (window: Window) in refreshIfWindowShouldBeShownToTheUser(window, screen) } + + if (Preferences.showAppsWindows == .applications && Preferences.appearanceModel != .thumbnails) { + // Group windows by application and select the optimal main window + let windowsGroupedByApp = Dictionary(grouping: Windows.list) { $0.application.pid } + windowsGroupedByApp.forEach { (app, windows) in + if windows.count > 1, let mainWindow = selectMainWindow(from: windows) { + windows.forEach { window in + if window.cgWindowId != mainWindow.cgWindowId { + window.shouldShowTheUser = false + } + } + } + } + } } static func refreshIfWindowShouldBeShownToTheUser(_ window: Window, _ screen: NSScreen) { @@ -315,6 +329,35 @@ class Windows { !(Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab && !window.isOnScreen(screen)) && (Preferences.showTabsAsWindows || !window.isTabbed)) } + + /// Selects the most appropriate main window from a given list of windows. + /// + /// The selection criteria are as follows: + /// 1. Prefer the focused window if it exists. + /// 2. Prefer the main window of the application if the focused window is not found. + /// + /// - Parameter windows: An array of `Window` objects to select from. + /// - Returns: The most appropriate `Window` object based on the selection criteria, or `nil` if the array is empty. + static func selectMainWindow(from windows: [Window]) -> Window? { + let sortedWindows = windows.sorted { (window1, window2) -> Bool in + // Prefer the focus window + if window1.application.focusedWindow?.cgWindowId == window1.cgWindowId { + return true + } else if window2.application.focusedWindow?.cgWindowId == window2.cgWindowId { + return false + } + + // Prefer the main window + if window1.isAppMainWindow() && !window2.isAppMainWindow() { + return true + } else if !window1.isAppMainWindow() && window2.isAppMainWindow() { + return false + } + return true + } + + return sortedWindows.first + } } func sortByAppNameThenWindowTitle(_ w1: Window, _ w2: Window) -> ComparisonResult { diff --git a/src/ui/preferences-window/tabs/AppearanceTab.swift b/src/ui/preferences-window/tabs/AppearanceTab.swift index f39b0ceb..1cbc7fa8 100644 --- a/src/ui/preferences-window/tabs/AppearanceTab.swift +++ b/src/ui/preferences-window/tabs/AppearanceTab.swift @@ -330,11 +330,11 @@ class ModelAdvancedSettingsWindow: NSWindow, NSTabViewDelegate { "alignThumbnails", AlignThumbnailsPreference.allCases) titleTruncation = LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Window title truncation:", comment: ""), "titleTruncation", TitleTruncationPreference.allCases) - showAppsWindows = LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Show:", comment: ""), + showAppsWindows = LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Show running:", comment: ""), "showAppsWindows", ShowAppsWindowsPreference.allCases, extraAction: { _ in self.toggleAppNamesWindowTitles() }) - showAppNamesWindowTitles = LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Show:", comment: ""), + showAppNamesWindowTitles = LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Show titles:", comment: ""), "showAppNamesWindowTitles", ShowAppNamesWindowTitlesPreference.allCases) doneButton = NSButton(title: NSLocalizedString("Done", comment: ""), target: self, action: #selector(onClicked(_:))) @@ -429,6 +429,7 @@ class ModelAdvancedSettingsWindow: NSWindow, NSTabViewDelegate { // view.column(at: 1).width = 150 view.column(at: 0).xPlacement = .trailing view.column(at: 1).xPlacement = .leading + toggleAppNamesWindowTitles() return view } @@ -444,6 +445,7 @@ class ModelAdvancedSettingsWindow: NSWindow, NSTabViewDelegate { // view.column(at: 1).width = 200 view.column(at: 0).xPlacement = .trailing view.column(at: 1).xPlacement = .leading + toggleAppNamesWindowTitles() return view }