From b3e5c7804f557cdc04f3c64edb7e199e51dfc304 Mon Sep 17 00:00:00 2001 From: decodism Date: Thu, 11 Jul 2024 21:40:49 +0200 Subject: [PATCH] feat: limit windows per app --- src/logic/Applications.swift | 2 +- src/logic/DebugProfile.swift | 1 + src/logic/Preferences.swift | 12 +++++++++++ src/logic/Window.swift | 2 ++ src/logic/Windows.swift | 20 +++++++++++++++---- src/ui/App.swift | 8 ++++++-- src/ui/main-window/ThumbnailsView.swift | 4 ++-- .../preferences-window/tabs/ControlsTab.swift | 5 ++++- 8 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/logic/Applications.swift b/src/logic/Applications.swift index 41f88abf2..79ee620e0 100644 --- a/src/logic/Applications.swift +++ b/src/logic/Applications.swift @@ -59,7 +59,7 @@ class Applications { // comparing pid here can fail here, as it can be already nil; we use isEqual here to avoid the issue Applications.list.removeAll { $0.runningApplication.isEqual(runningApp) } Windows.list.enumerated().forEach { (index, window) in - if window.application.runningApplication.isEqual(runningApp) && index < Windows.focusedWindowIndex && window.shouldShowTheUser { + if window.application.runningApplication.isEqual(runningApp) && index < Windows.focusedWindowIndex && window.shouldReallyShowTheUser { windowsOnTheLeftOfFocusedWindow += 1 } } diff --git a/src/logic/DebugProfile.swift b/src/logic/DebugProfile.swift index 680b1df54..94e2cb82f 100644 --- a/src/logic/DebugProfile.swift +++ b/src/logic/DebugProfile.swift @@ -85,6 +85,7 @@ class DebugProfile { ("isTabbed", String(window.isTabbed)), ("isOnAllSpaces", String(window.isOnAllSpaces)), ("shouldShowTheUser", String(window.shouldShowTheUser)), + ("shouldHideTheUser", String(window.shouldHideTheUser)), ("spaceId", String(window.spaceId)), ("spaceIndex", String(window.spaceIndex)), ]) diff --git a/src/logic/Preferences.swift b/src/logic/Preferences.swift index 36a4a5b14..2a9ef2aae 100644 --- a/src/logic/Preferences.swift +++ b/src/logic/Preferences.swift @@ -95,6 +95,16 @@ class Preferences { "hideWindowlessApps": "false", "hideThumbnails": "false", "previewFocusedWindow": "false", + "limitWindowCountPerApp": "false", + "limitWindowCountPerApp2": "false", + "limitWindowCountPerApp3": "false", + "limitWindowCountPerApp4": "false", + "limitWindowCountPerApp5": "false", + "windowCountPerApp": "1", + "windowCountPerApp2": "1", + "windowCountPerApp3": "1", + "windowCountPerApp4": "1", + "windowCountPerApp5": "1", ] // system preferences @@ -141,6 +151,8 @@ class Preferences { static var startAtLogin: Bool { defaults.bool("startAtLogin") } static var blacklist: [BlacklistEntry] { jsonDecode([BlacklistEntry].self, defaults.string("blacklist")) } static var previewFocusedWindow: Bool { defaults.bool("previewFocusedWindow") } + static var limitWindowCountPerApp: [Bool] { ["limitWindowCountPerApp", "limitWindowCountPerApp2", "limitWindowCountPerApp3", "limitWindowCountPerApp4", "limitWindowCountPerApp5"].map { defaults.bool($0) } } + static var windowCountPerApp: [Int] { ["windowCountPerApp", "windowCountPerApp2", "windowCountPerApp3", "windowCountPerApp4", "windowCountPerApp5"].map { defaults.int($0) } } // macro values static var theme: ThemePreference { defaults.macroPref("theme", ThemePreference.allCases) } diff --git a/src/logic/Window.swift b/src/logic/Window.swift index ffbfea380..a4928c2db 100644 --- a/src/logic/Window.swift +++ b/src/logic/Window.swift @@ -10,6 +10,8 @@ class Window { var thumbnailFullSize: NSSize? var icon: NSImage? { get { application.icon } } var shouldShowTheUser = true + var shouldHideTheUser = false + var shouldReallyShowTheUser: Bool { shouldShowTheUser && !shouldHideTheUser } var isTabbed: Bool = false var isHidden: Bool { get { application.isHidden } } var dockLabel: String? { get { application.dockLabel } } diff --git a/src/logic/Windows.swift b/src/logic/Windows.swift index d8f2df348..388191ad2 100644 --- a/src/logic/Windows.swift +++ b/src/logic/Windows.swift @@ -196,7 +196,7 @@ class Windows { let next = (targetIndex + step) % list.count targetIndex = next < 0 ? list.count + next : next iterations += 1 - } while !list[targetIndex].shouldShowTheUser && iterations <= list.count + } while !list[targetIndex].shouldReallyShowTheUser && iterations <= list.count return targetIndex } @@ -208,7 +208,7 @@ class Windows { static func updateFocusedWindowIndex() { if let focusedWindow = focusedWindow() { - if !focusedWindow.shouldShowTheUser { + if !focusedWindow.shouldReallyShowTheUser { cycleFocusedWindowIndex(windowIndexAfterCycling(1) > focusedWindowIndex ? 1 : -1) } else { previewFocusedWindowIfNeeded() @@ -269,7 +269,7 @@ class Windows { static func refreshFirstFewThumbnailsSync() { if Preferences.hideThumbnails { return } - list.filter { $0.shouldShowTheUser } + list.filter { $0.shouldReallyShowTheUser } .prefix(criticalFirstThumbnails) .forEachAsync { window in window.refreshThumbnail() } } @@ -280,7 +280,7 @@ class Windows { BackgroundWork.mainQueueConcurrentWorkQueue.async { if currentIndex < list.count { let window = list[currentIndex] - if window.shouldShowTheUser && !window.isWindowlessApp { + if window.shouldReallyShowTheUser && !window.isWindowlessApp { window.refreshThumbnail() } refreshThumbnailsAsync(screen, currentIndex + 1) @@ -315,6 +315,18 @@ class Windows { !(Preferences.screensToShow[App.app.shortcutIndex] == .showingAltTab && !window.isOnScreen(screen)) && (Preferences.showTabsAsWindows || !window.isTabbed)) } + + static func refreshWhichWindowsToHideTheUser() { + var appOrders = [pid_t: UInt]() + list.forEach { + guard $0.shouldShowTheUser else { + return + } + let appOrder = appOrders[$0.application.pid] ?? 0 + $0.shouldHideTheUser = Preferences.limitWindowCountPerApp[App.app.shortcutIndex] && appOrder >= Preferences.windowCountPerApp[App.app.shortcutIndex] + appOrders[$0.application.pid] = appOrder + 1 + } + } } func sortByAppNameThenWindowTitle(_ w1: Window, _ w2: Window) -> ComparisonResult { diff --git a/src/ui/App.swift b/src/ui/App.swift index 624b99b9e..ae45817b3 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -251,11 +251,14 @@ class App: AppCenterApplication, NSApplicationDelegate { Spaces.refreshAllIdsAndIndexes() guard appIsBeingUsed else { return } refreshSpecificWindows(windowsToUpdate, currentScreen) - if (!Windows.list.contains { $0.shouldShowTheUser }) { hideUi(); return } + if (!Windows.list.contains { $0.shouldReallyShowTheUser }) { hideUi(); return } guard appIsBeingUsed else { return } Windows.reorderList() Windows.updateFocusedWindowIndex() guard appIsBeingUsed else { return } + Windows.refreshWhichWindowsToHideTheUser() + if (!Windows.list.contains { $0.shouldReallyShowTheUser }) { hideUi(); return } + guard appIsBeingUsed else { return } thumbnailsPanel.thumbnailsView.updateItemsAndLayout(currentScreen) guard appIsBeingUsed else { return } thumbnailsPanel.setContentSize(thumbnailsPanel.thumbnailsView.frame.size) @@ -295,7 +298,8 @@ class App: AppCenterApplication, NSApplicationDelegate { self.shortcutIndex = shortcutIndex Windows.refreshWhichWindowsToShowTheUser(screen) Windows.reorderList() - if (!Windows.list.contains { $0.shouldShowTheUser }) { hideUi(); return } + Windows.refreshWhichWindowsToHideTheUser() + if (!Windows.list.contains { $0.shouldReallyShowTheUser }) { hideUi(); return } Windows.setInitialFocusedAndHoveredWindowIndex() delayedDisplayScheduled += 1 DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay) { () -> () in diff --git a/src/ui/main-window/ThumbnailsView.swift b/src/ui/main-window/ThumbnailsView.swift index 2d51a18f9..5fb221464 100644 --- a/src/ui/main-window/ThumbnailsView.swift +++ b/src/ui/main-window/ThumbnailsView.swift @@ -106,7 +106,7 @@ class ThumbnailsView: NSVisualEffectView { rows.append([ThumbnailView]()) for (index, window) in Windows.list.enumerated() { guard App.app.appIsBeingUsed else { return nil } - guard window.shouldShowTheUser else { continue } + guard window.shouldReallyShowTheUser else { continue } let view = ThumbnailsView.recycledViews[index] view.updateRecycledCellWithNewContent(window, index, height, screen) let width = view.frame.size.width @@ -172,7 +172,7 @@ class ThumbnailsView: NSVisualEffectView { var rowY = Preferences.interCellPadding for (index, window) in Windows.list.enumerated() { guard App.app.appIsBeingUsed else { return } - guard window.shouldShowTheUser else { continue } + guard window.shouldReallyShowTheUser else { continue } let view = ThumbnailsView.recycledViews[index] if view.frame.origin.y == rowY { rowWidth += view.frame.size.width + Preferences.interCellPadding diff --git a/src/ui/preferences-window/tabs/ControlsTab.swift b/src/ui/preferences-window/tabs/ControlsTab.swift index 4ef3670c9..e1b300213 100644 --- a/src/ui/preferences-window/tabs/ControlsTab.swift +++ b/src/ui/preferences-window/tabs/ControlsTab.swift @@ -116,6 +116,8 @@ class ControlsTab { let showHiddenWindows = LabelAndControl.makeDropdown(Preferences.indexToName("showHiddenWindows", index), ShowHowPreference.allCases) let showFullscreenWindows = LabelAndControl.makeDropdown(Preferences.indexToName("showFullscreenWindows", index), ShowHowPreference.allCases.filter { $0 != .showAtTheEnd }) let windowOrder = LabelAndControl.makeDropdown(Preferences.indexToName("windowOrder", index), WindowOrderPreference.allCases) + let limitWindowCountPerApp = LabelAndControl.makeLabelWithCheckbox(NSLocalizedString("Limit windows per app:", comment: ""), Preferences.indexToName("limitWindowCountPerApp", index)) + let windowCountPerApp = LabelAndControl.makeLabelWithSlider("", Preferences.indexToName("windowCountPerApp", index), 1, 10, 10, true) let separator = NSBox() separator.boxType = .separator let nextWindowShortcut = LabelAndControl.makeLabelWithRecorder(NSLocalizedString("Select next window", comment: ""), Preferences.indexToName("nextWindowShortcut", index), Preferences.nextWindowShortcut[index], labelPosition: .right) @@ -129,12 +131,13 @@ class ControlsTab { [toShowExplanations3, showHiddenWindows], [toShowExplanations4, showFullscreenWindows], [windowOrderExplanation, windowOrder], + [limitWindowCountPerApp[0], StackView([limitWindowCountPerApp[1], windowCountPerApp[1], windowCountPerApp[2]], .horizontal, false)], [separator], [holdAndPress, StackView(nextWindowShortcut)], shortcutStyle, ], TabView.padding) tab.column(at: 0).xPlacement = .trailing - tab.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: 5, length: 1)) + tab.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: 6, length: 1)) tab.fit() return (holdShortcut, nextWindowShortcut, tab) }