* Replaced the NSCollectionView with a custom NSView. Benefit includes simpler code, and control over recycling logic. * In cases where the user has many open windows, replaced the downscaling strategy with a scrollable view * Added a preference: align thumbnails to the left or center
This commit is contained in:
1 parent
2008025302
commit
311beef3cf
19 files changed
+326
-319
No files matched your search
@@ -12,11 +12,11 @@
|
||||
D04BA004884A273D4D2D3EF1 /* HelperExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD91161791D42FEC4A60 /* HelperExtensions.swift */; };
|
||||
D04BA084CD1236EC78D90A01 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BACCBE5F97BE9B6CA645B /* Localizable.strings */; };
|
||||
D04BA100BD0F47828EB649FF /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BAAEC2847830A3991F8D1 /* InfoPlist.strings */; };
|
||||
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */; };
|
||||
D04BA14D93726795A6937832 /* LabelAndControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2526DC6726E0F7ACF7C /* LabelAndControl.swift */; };
|
||||
D04BA15A1B0C4871EA7CB899 /* GeneralTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BACE22DC907F03D193075 /* GeneralTab.swift */; };
|
||||
D04BA1B133D53572D7B312C2 /* CollectionViewItemFontIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA1DF8CAB2FAB7FE9244B /* CollectionViewItemFontIcon.swift */; };
|
||||
D04BA1CEC6B9C8945FEC8740 /* CollectionViewItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA258B56193958D60978A /* CollectionViewItemView.swift */; };
|
||||
D04BA263682C4EA2F85B6CF0 /* QA.md in Sources */ = {isa = PBXBuildFile; fileRef = D04BACAE340252C1E910D3CE /* QA.md */; };
|
||||
D04BA1B133D53572D7B312C2 /* ThumbnailFontIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA1DF8CAB2FAB7FE9244B /* ThumbnailFontIconView.swift */; };
|
||||
D04BA1CEC6B9C8945FEC8740 /* ThumbnailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA258B56193958D60978A /* ThumbnailView.swift */; };
|
||||
D04BA26A691D56031FCCF00C /* Sysctl.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8DB8AA7E5570DAC568A /* Sysctl.swift */; };
|
||||
D04BA276B3241D440F65B149 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = D04BA5C2BB394F1624DD5B45 /* InfoPlist.strings */; };
|
||||
D04BA2A6FF9DDDC5A1A68E36 /* Applications.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA282BB16C1554595A968 /* Applications.swift */; };
|
||||
@@ -30,12 +30,10 @@
|
||||
D04BA5F99B45DC13B9E9DD91 /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA8276B3D3905E80B1739 /* Keyboard.swift */; };
|
||||
D04BA6187A91A847844B6ABB /* Window.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA015A45DE7AFDC9794FE /* Window.swift */; };
|
||||
D04BA691CB6082A3C39CBC89 /* TabViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAE757BB2B605234FBF58 /* TabViewController.swift */; };
|
||||
D04BA69D47B5E60A6AD9CBD9 /* CollectionViewItemTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1297730B191E96E7FE /* CollectionViewItemTitle.swift */; };
|
||||
D04BA6C953494839648107D1 /* CollectionViewItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA2A4A4140AF3E09DA94D /* CollectionViewItem.swift */; };
|
||||
D04BA69D47B5E60A6AD9CBD9 /* ThumbnailTitleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAD1297730B191E96E7FE /* ThumbnailTitleView.swift */; };
|
||||
D04BA737008AA2CD4E230A21 /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA10777505D8A67ABD186 /* Application.swift */; };
|
||||
D04BA73E90EFEF8247A5105D /* CGWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAC34CFD42A7F6F1F01C0 /* CGWindow.swift */; };
|
||||
D04BA76A74267B1346D23687 /* GridView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA6D57A1456C07318B8EA /* GridView.swift */; };
|
||||
D04BA76DDB00FC50D203D62C /* CollectionViewFlowLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BAC2FEF7248B7BF9579E2 /* CollectionViewFlowLayout.swift */; };
|
||||
D04BA775CF3F8D9394A1E256 /* Screen.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA68C2561D9EE4FD851B8 /* Screen.swift */; };
|
||||
D04BA7BE7F3DD24D58ACE942 /* AppearanceTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA64F1F344007EA13BA05 /* AppearanceTab.swift */; };
|
||||
D04BA7F86F1926FBE31F44BF /* BaseLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D04BA53992F116E5E704CAB3 /* BaseLabel.swift */; };
|
||||
@@ -85,15 +83,14 @@
|
||||
D04BA123744B0C27E9F54B05 /* codesign_sparkle_embedded_apps.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = codesign_sparkle_embedded_apps.sh; sourceTree = "<group>"; };
|
||||
D04BA1C3E42AC44CA2C5D3D8 /* app-icon.svg */ = {isa = PBXFileReference; lastKnownFileType = file.svg; path = "app-icon.svg"; sourceTree = "<group>"; };
|
||||
D04BA1D80F4EEF2A91BAD29C /* release.config.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = release.config.js; sourceTree = "<group>"; };
|
||||
D04BA1DF8CAB2FAB7FE9244B /* CollectionViewItemFontIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItemFontIcon.swift; sourceTree = "<group>"; };
|
||||
D04BA1DF8CAB2FAB7FE9244B /* ThumbnailFontIconView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailFontIconView.swift; sourceTree = "<group>"; };
|
||||
D04BA1FC9022590D7AA02486 /* 1 window - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1 window - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BA2526DC6726E0F7ACF7C /* LabelAndControl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LabelAndControl.swift; sourceTree = "<group>"; };
|
||||
D04BA258B56193958D60978A /* CollectionViewItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItemView.swift; sourceTree = "<group>"; };
|
||||
D04BA258B56193958D60978A /* ThumbnailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailView.swift; sourceTree = "<group>"; };
|
||||
D04BA26154AB2A2897E08CAF /* windows-theme.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "windows-theme.jpg"; sourceTree = "<group>"; };
|
||||
D04BA26C75F76C277653C932 /* FeedbackWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackWindow.swift; sourceTree = "<group>"; };
|
||||
D04BA27C87B86C4484A5B15B /* TabViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TabViewItem.swift; sourceTree = "<group>"; };
|
||||
D04BA282BB16C1554595A968 /* Applications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Applications.swift; sourceTree = "<group>"; };
|
||||
D04BA2A4A4140AF3E09DA94D /* CollectionViewItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItem.swift; sourceTree = "<group>"; };
|
||||
D04BA2A4F257F4DCE1421758 /* Podfile.lock */ = {isa = PBXFileReference; lastKnownFileType = file.lock; path = Podfile.lock; sourceTree = "<group>"; };
|
||||
D04BA2C7B51F68651B3C60E2 /* 6 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "6 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BA32F25860B686DFE818A /* 3 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "3 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
@@ -133,6 +130,7 @@
|
||||
D04BA9B93823398A542FF7A0 /* Preferences.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
|
||||
D04BA9EF65B2E7AF9E3ADCA3 /* 2 windows - 1 line.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2 windows - 1 line.jpg"; sourceTree = "<group>"; };
|
||||
D04BAA34E0CB00DED7C04B4F /* 2-rows.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "2-rows.jpg"; sourceTree = "<group>"; };
|
||||
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailsView.swift; sourceTree = "<group>"; };
|
||||
D04BAA9E0539EE620D08F63F /* fr */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; name = fr; path = InfoPlist.strings; sourceTree = "<group>"; };
|
||||
D04BAAB92261FC04854FDDE9 /* App.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
||||
D04BAAF760E3A8A22BDA84D6 /* appcast.xml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = appcast.xml; sourceTree = "<group>"; };
|
||||
@@ -142,7 +140,6 @@
|
||||
D04BABFEC8F9DF41BB7A449E /* import_codesign_certificate_into_keychain.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = import_codesign_certificate_into_keychain.sh; sourceTree = "<group>"; };
|
||||
D04BAC02D60EF22D9CC7D969 /* commitlint.config.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = commitlint.config.js; sourceTree = "<group>"; };
|
||||
D04BAC159731F80FDAF4EA6C /* 1-row.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = "1-row.jpg"; sourceTree = "<group>"; };
|
||||
D04BAC2FEF7248B7BF9579E2 /* CollectionViewFlowLayout.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewFlowLayout.swift; sourceTree = "<group>"; };
|
||||
D04BAC2FF99F629CD4ED20FC /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
D04BAC34CFD42A7F6F1F01C0 /* CGWindow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CGWindow.swift; sourceTree = "<group>"; };
|
||||
D04BAC6AFC7F06D1A567F27A /* replace_environment_variables_in_app.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = replace_environment_variables_in_app.sh; sourceTree = "<group>"; };
|
||||
@@ -152,7 +149,7 @@
|
||||
D04BACD976030676FD0761D5 /* Windows.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Windows.swift; sourceTree = "<group>"; };
|
||||
D04BACE22DC907F03D193075 /* GeneralTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralTab.swift; sourceTree = "<group>"; };
|
||||
D04BACEE8D430B8CAAD8C4CD /* BoldLabel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BoldLabel.swift; sourceTree = "<group>"; };
|
||||
D04BAD1297730B191E96E7FE /* CollectionViewItemTitle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CollectionViewItemTitle.swift; sourceTree = "<group>"; };
|
||||
D04BAD1297730B191E96E7FE /* ThumbnailTitleView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ThumbnailTitleView.swift; sourceTree = "<group>"; };
|
||||
D04BAD241A6928F45355B315 /* es */ = {isa = PBXFileReference; fileEncoding = 2483028224; lastKnownFileType = text.plist.strings; name = es; path = Localizable.strings; sourceTree = "<group>"; };
|
||||
D04BAD40CE2D3A8AAC3819D0 /* .gitignore */ = {isa = PBXFileReference; lastKnownFileType = file.gitignore; path = .gitignore; sourceTree = "<group>"; };
|
||||
D04BAD60C97E609A759E721E /* UpdatesTab.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdatesTab.swift; sourceTree = "<group>"; };
|
||||
@@ -375,12 +372,11 @@
|
||||
D04BA62E30174C336E4080FA /* main-window */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D04BAC2FEF7248B7BF9579E2 /* CollectionViewFlowLayout.swift */,
|
||||
D04BA2A4A4140AF3E09DA94D /* CollectionViewItem.swift */,
|
||||
D04BA653BD073CB58E2CFC93 /* ThumbnailsPanel.swift */,
|
||||
D04BAD1297730B191E96E7FE /* CollectionViewItemTitle.swift */,
|
||||
D04BA1DF8CAB2FAB7FE9244B /* CollectionViewItemFontIcon.swift */,
|
||||
D04BA258B56193958D60978A /* CollectionViewItemView.swift */,
|
||||
D04BAD1297730B191E96E7FE /* ThumbnailTitleView.swift */,
|
||||
D04BA1DF8CAB2FAB7FE9244B /* ThumbnailFontIconView.swift */,
|
||||
D04BA258B56193958D60978A /* ThumbnailView.swift */,
|
||||
D04BAA998119CAA8B70A2B67 /* ThumbnailsView.swift */,
|
||||
);
|
||||
path = "main-window";
|
||||
sourceTree = "<group>";
|
||||
@@ -678,12 +674,10 @@
|
||||
D04BA3CF766857381519B892 /* DispatchQueues.swift in Sources */,
|
||||
D04BA48B00B4211A465C7337 /* DebugProfile.swift in Sources */,
|
||||
D04BAB68B7B8D1B548BC3AD5 /* App.swift in Sources */,
|
||||
D04BA76DDB00FC50D203D62C /* CollectionViewFlowLayout.swift in Sources */,
|
||||
D04BA6C953494839648107D1 /* CollectionViewItem.swift in Sources */,
|
||||
D04BAB048DE698E013577C51 /* ThumbnailsPanel.swift in Sources */,
|
||||
D04BA69D47B5E60A6AD9CBD9 /* CollectionViewItemTitle.swift in Sources */,
|
||||
D04BA1B133D53572D7B312C2 /* CollectionViewItemFontIcon.swift in Sources */,
|
||||
D04BA1CEC6B9C8945FEC8740 /* CollectionViewItemView.swift in Sources */,
|
||||
D04BA69D47B5E60A6AD9CBD9 /* ThumbnailTitleView.swift in Sources */,
|
||||
D04BA1B133D53572D7B312C2 /* ThumbnailFontIconView.swift in Sources */,
|
||||
D04BA1CEC6B9C8945FEC8740 /* ThumbnailView.swift in Sources */,
|
||||
D04BA3BFB0CDF4ED343914B2 /* PreferencesWindow.swift in Sources */,
|
||||
D04BA691CB6082A3C39CBC89 /* TabViewController.swift in Sources */,
|
||||
D04BA14D93726795A6937832 /* LabelAndControl.swift in Sources */,
|
||||
@@ -700,7 +694,7 @@
|
||||
D04BAF25E67A5B31CF7676DB /* TextField.swift in Sources */,
|
||||
D04BAD2A7F2E8BF64EE982E9 /* TextArea.swift in Sources */,
|
||||
D04BA7F86F1926FBE31F44BF /* BaseLabel.swift in Sources */,
|
||||
D04BA263682C4EA2F85B6CF0 /* QA.md in Sources */,
|
||||
D04BA11E56383D082D7BE5A5 /* ThumbnailsView.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -6,7 +6,6 @@ MACOSX_DEPLOYMENT_TARGET = 10.12
|
||||
SWIFT_VERSION = 4.2
|
||||
INFOPLIST_FILE = Info.plist
|
||||
CODE_SIGN_ENTITLEMENTS = alt_tab_macos.entitlements
|
||||
IDEDerivedDataPathOverride = DerivedData
|
||||
FRAMEWORK_SEARCH_PATHS[config=*] = $(inherited) /System/Library/PrivateFrameworks // for SkyLight.framework
|
||||
ENABLE_HARDENED_RUNTIME = YES // for notarization
|
||||
OTHER_CODE_SIGN_FLAGS = --timestamp --deep --options runtime // for notarization
|
||||
|
||||
@@ -74,15 +74,23 @@ extension NSObject {
|
||||
|
||||
extension Array where Element == Window {
|
||||
func firstIndexThatMatches(_ element: AXUIElement) -> Self.Index? {
|
||||
// `CFEqual` is safer than comparing `CGWindowID` because it will succeed even if the window is deallocated
|
||||
// == is safer than comparing `CGWindowID` because it will succeed even if the window is deallocated
|
||||
// by the OS, in which case the `CGWindowID` will be `-1`
|
||||
return firstIndex(where: { CFEqual($0.axUiElement, element) })
|
||||
return firstIndex(where: { $0.axUiElement == element })
|
||||
}
|
||||
|
||||
func firstWindowThatMatches(_ element: AXUIElement) -> Window? {
|
||||
func firstWindowThatMatches(_ element: AXUIElement) -> Element? {
|
||||
guard let index = firstIndexThatMatches(element) else { return nil }
|
||||
return self[index]
|
||||
}
|
||||
|
||||
mutating func insertAndScaleRecycledPool(_ elements: [Element], at i: Int) {
|
||||
insert(contentsOf: elements, at: i)
|
||||
let neededRecycledViews = count - ThumbnailsView.recycledViews.count
|
||||
if neededRecycledViews > 0 {
|
||||
(1...neededRecycledViews).forEach { _ in ThumbnailsView.recycledViews.append(ThumbnailView()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension NSView {
|
||||
@@ -164,3 +172,10 @@ extension NSImage {
|
||||
return img
|
||||
}
|
||||
}
|
||||
|
||||
// only assign if different; useful for performance
|
||||
func assignIfDifferent<T: Equatable>(_ a: UnsafeMutablePointer<T>, _ b: T) {
|
||||
if a.pointee != b {
|
||||
a.pointee = b
|
||||
}
|
||||
}
|
||||
@@ -48,9 +48,9 @@ class Application: NSObject {
|
||||
}
|
||||
|
||||
func observeNewWindows() {
|
||||
let newWindows = axUiElement!.windows()?
|
||||
.filter { $0.isActualWindow(runningApplication) && Windows.list.firstIndexThatMatches($0) == nil } ?? []
|
||||
addWindows(newWindows)
|
||||
if let windows = axUiElement!.windows(), windows.count > 0 {
|
||||
addWindows(windows.filter { $0.isActualWindow(runningApplication) && Windows.list.firstIndexThatMatches($0) == nil })
|
||||
}
|
||||
}
|
||||
|
||||
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
|
||||
@@ -59,8 +59,11 @@ class Application: NSObject {
|
||||
addAndObserveWindows()
|
||||
}
|
||||
|
||||
private func addWindows(_ windows: [AXUIElement]) {
|
||||
Windows.list.insert(contentsOf: windows.map { Window($0, self) }, at: 0)
|
||||
private func addWindows(_ axWindows: [AXUIElement]) {
|
||||
let windows = axWindows.map { Window($0, self) }
|
||||
Windows.list.insertAndScaleRecycledPool(windows, at: 0)
|
||||
windows.forEach { _ in Windows.moveFocusedWindowIndexAfterWindowCreatedInBackground() }
|
||||
(App.shared as! App).refreshOpenUi()
|
||||
}
|
||||
|
||||
private func observeEvents() {
|
||||
@@ -101,11 +104,12 @@ private func eventApplicationActivated(_ app: App, _ element: AXUIElement) {
|
||||
let appFocusedWindow = element.focusedWindow(),
|
||||
let existingIndex = Windows.list.firstIndexThatMatches(appFocusedWindow) else { return }
|
||||
Windows.list.insert(Windows.list.remove(at: existingIndex), at: 0)
|
||||
app.refreshOpenUi()
|
||||
}
|
||||
|
||||
private func eventApplicationHiddenOrShown(_ app: App, _ element: AXUIElement, _ type: String) {
|
||||
for window in Windows.list {
|
||||
guard CFEqual(window.application.axUiElement!, element) else { continue }
|
||||
guard window.application.axUiElement! == element else { continue }
|
||||
window.isHidden = type == kAXApplicationHiddenNotification
|
||||
}
|
||||
app.refreshOpenUi()
|
||||
@@ -116,10 +120,8 @@ private func eventWindowCreated(_ app: App, _ element: AXUIElement, _ applicatio
|
||||
// a window being un-minimized can trigger kAXWindowCreatedNotification
|
||||
guard Windows.list.firstIndexThatMatches(element) == nil else { return }
|
||||
let window = Window(element, application)
|
||||
Windows.list.insert(window, at: 0)
|
||||
Windows.list.insertAndScaleRecycledPool([window], at: 0)
|
||||
Windows.moveFocusedWindowIndexAfterWindowCreatedInBackground()
|
||||
// TODO: find a better way to get thumbnail of the new window
|
||||
window.refreshThumbnail()
|
||||
app.refreshOpenUi()
|
||||
}
|
||||
|
||||
@@ -127,4 +129,5 @@ private func eventFocusedWindowChanged(_ app: App, _ element: AXUIElement) {
|
||||
guard !app.appIsBeingUsed,
|
||||
let existingIndex = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
Windows.list.insert(Windows.list.remove(at: existingIndex), at: 0)
|
||||
app.refreshOpenUi()
|
||||
}
|
||||
@@ -56,11 +56,17 @@ class Applications {
|
||||
static func removeRunningApplications(_ runningApps: [NSRunningApplication]) {
|
||||
for runningApp in runningApps {
|
||||
Applications.list.removeAll(where: { $0.runningApplication.isEqual(runningApp) })
|
||||
var indexesToRemove = [Int]()
|
||||
Windows.list.enumerated().forEach { (index, window) in
|
||||
if window.application.runningApplication.isEqual(runningApp) {
|
||||
indexesToRemove.append(index)
|
||||
}
|
||||
}
|
||||
Windows.list.removeAll(where: { $0.application.runningApplication.isEqual(runningApp) })
|
||||
}
|
||||
guard Windows.list.count > 0 else { (App.shared as! App).hideUi(); return }
|
||||
// TODO: implement of more sophisticated way to decide which thumbnail gets focused on app quit
|
||||
Windows.focusedWindowIndex = 1
|
||||
Windows.updateFocusedWindowIndex(1)
|
||||
(App.shared as! App).refreshOpenUi()
|
||||
}
|
||||
|
||||
|
||||
@@ -27,14 +27,20 @@ func listenToGlobalKeyboardEvents(_ app: App) {
|
||||
}
|
||||
}
|
||||
|
||||
func dispatchWork(_ application: App, _ uiWorkShouldBeDone: Bool, _ fn: @escaping () -> Void) -> Unmanaged<CGEvent>? {
|
||||
application.uiWorkShouldBeDone = uiWorkShouldBeDone
|
||||
func dispatchWork(_ fn: @escaping () -> Void) -> Unmanaged<CGEvent>? {
|
||||
(App.shared as! App).uiWorkShouldBeDone = true
|
||||
DispatchQueue.main.async {
|
||||
fn()
|
||||
}
|
||||
return nil // previously focused app should not receive keys
|
||||
}
|
||||
|
||||
func dispatchWorkCurrentThread(_ fn: @escaping () -> Void) -> Unmanaged<CGEvent>? {
|
||||
(App.shared as! App).uiWorkShouldBeDone = false
|
||||
fn()
|
||||
return nil // previously focused app should not receive keys
|
||||
}
|
||||
|
||||
func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, event_: CGEvent, appPointer: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
|
||||
let app = Unmanaged<App>.fromOpaque(appPointer!).takeUnretainedValue()
|
||||
if type == .keyDown || type == .keyUp || type == .flagsChanged {
|
||||
@@ -46,19 +52,19 @@ func keyboardHandler(proxy: CGEventTapProxy, type: CGEventType, event_: CGEvent,
|
||||
let isLeftArrow = event.keyCode == kVK_LeftArrow
|
||||
let isEscape = event.keyCode == kVK_Escape
|
||||
if type == .keyDown && isEscape && app.appIsBeingUsed {
|
||||
return dispatchWork(app, false, { app.hideUi() })
|
||||
return dispatchWorkCurrentThread { app.hideUi() }
|
||||
} else if isMetaDown && type == .keyDown {
|
||||
if isTab && event.modifierFlags.contains(.shift) {
|
||||
return dispatchWork(app, true, { app.showUiOrCycleSelection(-1) })
|
||||
return dispatchWork { app.showUiOrCycleSelection(-1) }
|
||||
} else if isTab {
|
||||
return dispatchWork(app, true, { app.showUiOrCycleSelection(1) })
|
||||
return dispatchWork { app.showUiOrCycleSelection(1) }
|
||||
} else if isRightArrow && app.appIsBeingUsed {
|
||||
return dispatchWork(app, true, { app.cycleSelection(1) })
|
||||
return dispatchWork { app.cycleSelection(1) }
|
||||
} else if isLeftArrow && app.appIsBeingUsed {
|
||||
return dispatchWork(app, true, { app.cycleSelection(-1) })
|
||||
return dispatchWork { app.cycleSelection(-1) }
|
||||
}
|
||||
} else if isMetaChanged && !isMetaDown {
|
||||
return dispatchWork(app, false, { app.focusTarget() })
|
||||
return dispatchWorkCurrentThread { app.focusTarget() }
|
||||
}
|
||||
}
|
||||
} else if type == .tapDisabledByUserInput || type == .tapDisabledByTimeout {
|
||||
|
||||
@@ -5,11 +5,11 @@ let defaults = UserDefaults.standard
|
||||
|
||||
class Preferences {
|
||||
// default values
|
||||
static var defaultValues: [String : Any] = [
|
||||
static var defaultValues: [String: Any] = [
|
||||
"maxScreenUsage": Float(80),
|
||||
"minCellsPerRow": Float(5),
|
||||
"maxCellsPerRow": Float(10),
|
||||
"minRows": Float(3),
|
||||
"rowsCount": Float(3),
|
||||
"iconSize": Float(32),
|
||||
"fontHeight": Float(15),
|
||||
"tabKeyCode": kVK_Tab,
|
||||
@@ -17,6 +17,7 @@ class Preferences {
|
||||
"metaKey": MacroPreferences.metaKeyList.keys.first!,
|
||||
"theme": MacroPreferences.themeList.keys.first!,
|
||||
"showOnScreen": MacroPreferences.showOnScreenList.keys.first!,
|
||||
"alignThumbnails": MacroPreferences.alignThumbnailsList.keys.first!,
|
||||
"hideSpaceNumberLabels": false,
|
||||
"startAtLogin": true,
|
||||
]
|
||||
@@ -34,7 +35,7 @@ class Preferences {
|
||||
static var maxScreenUsage: CGFloat { CGFloat(defaults.float(forKey: "maxScreenUsage") / 100) }
|
||||
static var minCellsPerRow: CGFloat { CGFloat(defaults.float(forKey: "minCellsPerRow")) }
|
||||
static var maxCellsPerRow: CGFloat { CGFloat(defaults.float(forKey: "maxCellsPerRow")) }
|
||||
static var minRows: CGFloat { CGFloat(defaults.float(forKey: "minRows")) }
|
||||
static var rowsCount: CGFloat { CGFloat(defaults.float(forKey: "rowsCount")) }
|
||||
static var iconSize: CGFloat { CGFloat(defaults.float(forKey: "iconSize")) }
|
||||
static var fontHeight: CGFloat { CGFloat(defaults.float(forKey: "fontHeight")) }
|
||||
static var tabKeyCode: UInt16 { UInt16(defaults.integer(forKey: "tabKeyCode")) }
|
||||
@@ -46,6 +47,7 @@ class Preferences {
|
||||
static var theme: Theme { MacroPreferences.themeList[defaults.string(forKey: "theme")!]! }
|
||||
static var metaKey: MetaKey { MacroPreferences.metaKeyList[defaults.string(forKey: "metaKey")!]! }
|
||||
static var showOnScreen: ShowOnScreenPreference { MacroPreferences.showOnScreenList[defaults.string(forKey: "showOnScreen")!]! }
|
||||
static var alignThumbnails: AlignThumbnailsPreference { MacroPreferences.alignThumbnailsList[defaults.string(forKey: "alignThumbnails")!]! }
|
||||
|
||||
// derived values
|
||||
static var cellBorderWidth: CGFloat { theme.cellBorderWidth }
|
||||
@@ -100,6 +102,11 @@ enum ShowOnScreenPreference {
|
||||
case mouse
|
||||
}
|
||||
|
||||
enum AlignThumbnailsPreference {
|
||||
case left
|
||||
case center
|
||||
}
|
||||
|
||||
// macros are collection of values derived from a single key
|
||||
// we don't want to store every value in UserDefaults as the user could change them and contradict the macro
|
||||
class MacroPreferences {
|
||||
@@ -116,4 +123,8 @@ class MacroPreferences {
|
||||
"Main screen": ShowOnScreenPreference.main,
|
||||
"Screen including mouse": ShowOnScreenPreference.mouse,
|
||||
]
|
||||
static let alignThumbnailsList = [
|
||||
"Center": AlignThumbnailsPreference.center,
|
||||
"Left": AlignThumbnailsPreference.left,
|
||||
]
|
||||
}
|
||||
+13
-9
@@ -1,6 +1,7 @@
|
||||
import Cocoa
|
||||
|
||||
class Window {
|
||||
// weak var itemView: CollectionViewItemView?
|
||||
var cgWindowId: CGWindowID
|
||||
var title: String
|
||||
var thumbnail: NSImage?
|
||||
@@ -19,6 +20,7 @@ class Window {
|
||||
kAXTitleChangedNotification,
|
||||
kAXWindowMiniaturizedNotification,
|
||||
kAXWindowDeminiaturizedNotification,
|
||||
kAXWindowResizedNotification,
|
||||
]
|
||||
|
||||
static func stopSubscriptionRetries(_ notification: String, _ cgWindowId: CGWindowID) {
|
||||
@@ -57,8 +59,7 @@ class Window {
|
||||
}
|
||||
|
||||
func refreshThumbnail() {
|
||||
guard (App.shared as! App).appIsBeingUsed,
|
||||
let cgImage = cgWindowId.screenshot() else { return }
|
||||
guard let cgImage = cgWindowId.screenshot() else { return }
|
||||
thumbnail = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height))
|
||||
}
|
||||
|
||||
@@ -121,6 +122,7 @@ private func axObserverCallback(observer: AXObserver, element: AXUIElement, noti
|
||||
case kAXUIElementDestroyedNotification: eventWindowDestroyed(app, element)
|
||||
case kAXWindowMiniaturizedNotification, kAXWindowDeminiaturizedNotification: eventWindowMiniaturizedOrDeminiaturized(app, element, type)
|
||||
case kAXTitleChangedNotification: eventWindowTitleChanged(app, element)
|
||||
case kAXWindowResizedNotification: eventWindowResized(app, element)
|
||||
default: return
|
||||
}
|
||||
}
|
||||
@@ -134,20 +136,22 @@ private func eventWindowDestroyed(_ app: App, _ element: AXUIElement) {
|
||||
}
|
||||
|
||||
private func eventWindowMiniaturizedOrDeminiaturized(_ app: App, _ element: AXUIElement, _ type: String) {
|
||||
guard let window = Windows.list.firstWindowThatMatches(element) else { return }
|
||||
guard let index = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
let window = Windows.list[index]
|
||||
window.isMinimized = type == kAXWindowMiniaturizedNotification
|
||||
// TODO: find a better way to get thumbnail of the new window (when AltTab is triggered min/demin animation)
|
||||
window.refreshThumbnail()
|
||||
app.refreshOpenUi()
|
||||
}
|
||||
|
||||
private func eventWindowTitleChanged(_ app: App, _ element: AXUIElement) {
|
||||
guard let window = Windows.list.firstWindowThatMatches(element),
|
||||
let newTitle = window.axUiElement.title(),
|
||||
guard let index = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
let window = Windows.list[index]
|
||||
guard let newTitle = window.axUiElement.title(),
|
||||
newTitle != window.title else { return }
|
||||
window.title = newTitle
|
||||
window.refreshThumbnail()
|
||||
app.refreshOpenUi()
|
||||
}
|
||||
|
||||
|
||||
private func eventWindowResized(_ app: App, _ element: AXUIElement) {
|
||||
guard let index = Windows.list.firstIndexThatMatches(element) else { return }
|
||||
app.refreshOpenUi()
|
||||
}
|
||||
+12
-4
@@ -3,26 +3,34 @@ import Cocoa
|
||||
class Windows {
|
||||
// order in the array is important: most-recently-used elements are first
|
||||
static var list = [Window]()
|
||||
static var previousFocusedWindowIndex = Array<Window>.Index(0)
|
||||
static var focusedWindowIndex = Array<Window>.Index(0)
|
||||
static var windowsInSubscriptionRetryLoop = [String]()
|
||||
|
||||
static func updateFocusedWindowIndex(_ newValue: Array<Window>.Index) {
|
||||
previousFocusedWindowIndex = focusedWindowIndex
|
||||
focusedWindowIndex = newValue
|
||||
let focusedView = ThumbnailsView.recycledViews[focusedWindowIndex]
|
||||
ThumbnailsPanel.highlightCell(ThumbnailsView.recycledViews[previousFocusedWindowIndex], focusedView)
|
||||
(App.shared as! App).thumbnailsPanel!.thumbnailsView.scrollView.contentView.scrollToVisible(focusedView.frame)
|
||||
}
|
||||
|
||||
static func focusedWindow() -> Window? {
|
||||
return list.count > focusedWindowIndex ? list[focusedWindowIndex] : nil
|
||||
}
|
||||
|
||||
static func cycleFocusedWindowIndex(_ step: Array<Window>.Index) {
|
||||
focusedWindowIndex = focusedWindowIndex + step < 0 ? list.count - 1 : (focusedWindowIndex + step) % list.count
|
||||
updateFocusedWindowIndex(focusedWindowIndex + step < 0 ? list.count - 1 : (focusedWindowIndex + step) % list.count)
|
||||
}
|
||||
|
||||
static func moveFocusedWindowIndexAfterWindowDestroyedInBackground(_ destroyedWindowIndex: Array<Window>.Index) {
|
||||
if focusedWindowIndex <= destroyedWindowIndex {
|
||||
focusedWindowIndex -= 1
|
||||
return
|
||||
updateFocusedWindowIndex(max(focusedWindowIndex - 1, 0))
|
||||
}
|
||||
}
|
||||
|
||||
static func moveFocusedWindowIndexAfterWindowCreatedInBackground() {
|
||||
focusedWindowIndex += 1
|
||||
updateFocusedWindowIndex(focusedWindowIndex + 1)
|
||||
}
|
||||
|
||||
static func updateSpaces() {
|
||||
|
||||
+39
-33
@@ -42,6 +42,8 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
Keyboard.listenToGlobalEvents(self)
|
||||
preferencesWindow = PreferencesWindow()
|
||||
UpdatesTab.observeUserDefaults()
|
||||
// UserDefaults.standard.set(false, forKey: "NSConstraintBasedLayoutVisualizeMutuallyExclusiveConstraints")
|
||||
// TODO: add warm up code for faster first launch
|
||||
}
|
||||
|
||||
// keyboard shortcuts are broken without a menu. We generated the default menu from XCode and load it
|
||||
@@ -67,7 +69,6 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
func focusTarget() {
|
||||
debugPrint("focusTarget")
|
||||
if appIsBeingUsed {
|
||||
debugPrint("focusTarget: appIsBeingUsed")
|
||||
let window = Windows.focusedWindow()
|
||||
focusSelectedWindow(window)
|
||||
}
|
||||
@@ -95,13 +96,35 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
|
||||
@objc
|
||||
func showUi() {
|
||||
uiWorkShouldBeDone = true
|
||||
showUiOrCycleSelection(0)
|
||||
_ = dispatchWork { self.showUiOrCycleSelection(0) }
|
||||
}
|
||||
|
||||
func cycleSelection(_ step: Int) {
|
||||
Windows.cycleFocusedWindowIndex(step)
|
||||
thumbnailsPanel!.highlightCell()
|
||||
}
|
||||
|
||||
func focusSelectedWindow(_ window: Window?) {
|
||||
hideUi()
|
||||
guard !CGWindow.isMissionControlActive() else { return }
|
||||
window?.focus()
|
||||
}
|
||||
|
||||
func reopenUi() {
|
||||
// TODO: retest this
|
||||
thumbnailsPanel!.orderOut(nil)
|
||||
Windows.refreshAllThumbnails()
|
||||
refreshOpenUi()
|
||||
thumbnailsPanel!.show()
|
||||
}
|
||||
|
||||
func refreshOpenUi() {
|
||||
guard appIsBeingUsed else { return }
|
||||
let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
|
||||
guard uiWorkShouldBeDone else { return }
|
||||
thumbnailsPanel!.thumbnailsView.updateItems(currentScreen)
|
||||
thumbnailsPanel!.setFrame(thumbnailsPanel!.thumbnailsView.frame, display: false)
|
||||
guard uiWorkShouldBeDone else { return }
|
||||
Screen.repositionPanel(thumbnailsPanel!, currentScreen, .appleCentered)
|
||||
}
|
||||
|
||||
func showUiOrCycleSelection(_ step: Int) {
|
||||
@@ -119,39 +142,22 @@ class App: NSApplication, NSApplicationDelegate {
|
||||
Spaces.updateIsSingleSpace()
|
||||
// TODO: find a way to update space index when windows are moved to another space, instead of on every trigger
|
||||
Windows.updateSpaces()
|
||||
// TODO: find a way to update thumbnails by listening to content change, instead of every trigger. Or better, switch to video
|
||||
Windows.refreshAllThumbnails()
|
||||
Windows.focusedWindowIndex = 0
|
||||
Windows.updateFocusedWindowIndex(0)
|
||||
Windows.cycleFocusedWindowIndex(step)
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay, execute: { [weak self] in
|
||||
guard let self = self else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay) {
|
||||
guard self.uiWorkShouldBeDone else { return }
|
||||
Windows.refreshAllThumbnails()
|
||||
guard self.uiWorkShouldBeDone else { return }
|
||||
self.refreshOpenUi()
|
||||
if self.uiWorkShouldBeDone { self.thumbnailsPanel?.show() }
|
||||
})
|
||||
guard self.uiWorkShouldBeDone else { return }
|
||||
self.thumbnailsPanel!.show()
|
||||
// DispatchQueue.main.async {
|
||||
// guard self.uiWorkShouldBeDone else { return }
|
||||
// self.refreshThumbnails()
|
||||
// }
|
||||
}
|
||||
} else {
|
||||
debugPrint("showUiOrCycleSelection: !isFirstSummon")
|
||||
cycleSelection(step)
|
||||
}
|
||||
}
|
||||
|
||||
func reopenUi() {
|
||||
thumbnailsPanel!.orderOut(nil)
|
||||
Windows.refreshAllThumbnails()
|
||||
refreshOpenUi()
|
||||
thumbnailsPanel!.show()
|
||||
}
|
||||
|
||||
func refreshOpenUi() {
|
||||
guard appIsBeingUsed else { return }
|
||||
let currentScreen = Screen.preferred() // fix screen between steps since it could change (e.g. mouse moved to another screen)
|
||||
if uiWorkShouldBeDone { thumbnailsPanel!.refreshCollectionView(currentScreen, uiWorkShouldBeDone); debugPrint("refreshCollectionView") }
|
||||
if uiWorkShouldBeDone { thumbnailsPanel!.highlightCell(); debugPrint("highlightCellAt") }
|
||||
if uiWorkShouldBeDone { Screen.repositionPanel(thumbnailsPanel!, currentScreen, .appleCentered); debugPrint("repositionPanel") }
|
||||
}
|
||||
|
||||
func focusSelectedWindow(_ window: Window?) {
|
||||
hideUi()
|
||||
guard !CGWindow.isMissionControlActive() else { return }
|
||||
window?.focus()
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import Cocoa
|
||||
|
||||
class CollectionViewFlowLayout: NSCollectionViewFlowLayout {
|
||||
var currentScreen: NSScreen?
|
||||
var widestRow: CGFloat?
|
||||
var totalHeight: CGFloat?
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [NSCollectionViewLayoutAttributes] {
|
||||
let attributes_ = super.layoutAttributesForElements(in: rect)
|
||||
guard !attributes_.isEmpty else { return attributes_ }
|
||||
let attributes = NSArray(array: attributes_, copyItems: true) as! [NSCollectionViewLayoutAttributes]
|
||||
var currentRow: [NSCollectionViewLayoutAttributes] = []
|
||||
var currentRowY = CGFloat(0)
|
||||
var currentRowWidth = CGFloat(0)
|
||||
var previousRowMaxHeight = CGFloat(0)
|
||||
var currentRowMaxHeight = CGFloat(0)
|
||||
var widestRow = CGFloat(0)
|
||||
var totalHeight = CGFloat(0)
|
||||
for (index, attribute) in attributes.enumerated() {
|
||||
let isNewRow = abs(attribute.frame.origin.y - currentRowY) > CollectionViewItemView.height(currentScreen!)
|
||||
if isNewRow {
|
||||
currentRowWidth -= Preferences.interCellPadding
|
||||
widestRow = max(widestRow, currentRowWidth)
|
||||
setCenteredPositionForPreviousRowCells(currentRowWidth, previousRowMaxHeight, currentRow)
|
||||
currentRow.removeAll()
|
||||
currentRowY = attribute.frame.origin.y
|
||||
currentRowWidth = 0
|
||||
previousRowMaxHeight += currentRowMaxHeight + Preferences.interCellPadding
|
||||
currentRowMaxHeight = 0
|
||||
}
|
||||
currentRow.append(attribute)
|
||||
currentRowWidth += attribute.frame.size.width + Preferences.interCellPadding
|
||||
currentRowMaxHeight = max(currentRowMaxHeight, attribute.frame.size.height)
|
||||
if index == attributes.count - 1 {
|
||||
currentRowWidth -= Preferences.interCellPadding
|
||||
widestRow = max(widestRow, currentRowWidth)
|
||||
totalHeight = previousRowMaxHeight + currentRowMaxHeight
|
||||
setCenteredPositionForPreviousRowCells(currentRowWidth, previousRowMaxHeight, currentRow)
|
||||
}
|
||||
}
|
||||
shiftCenteredElementToTheLeft(attributes, widestRow, totalHeight)
|
||||
self.widestRow = widestRow
|
||||
self.totalHeight = totalHeight
|
||||
return attributes
|
||||
}
|
||||
|
||||
private func shiftCenteredElementToTheLeft(_ attributes: [NSCollectionViewLayoutAttributes], _ widestRow: CGFloat, _ totalHeight: CGFloat) {
|
||||
let horizontalMargin = ((collectionView!.frame.size.width - widestRow) / 2).rounded()
|
||||
for attribute in attributes {
|
||||
attribute.frame.origin.x -= horizontalMargin
|
||||
}
|
||||
}
|
||||
|
||||
private func setCenteredPositionForPreviousRowCells(_ currentRowWidth: CGFloat, _ previousRowMaxHeight: CGFloat, _ currentRow: [NSCollectionViewLayoutAttributes]) {
|
||||
var marginLeft = (collectionView!.frame.size.width - currentRowWidth) / 2
|
||||
for attribute in currentRow {
|
||||
attribute.frame.origin.x = marginLeft
|
||||
attribute.frame.origin.y = previousRowMaxHeight
|
||||
marginLeft += attribute.frame.size.width + Preferences.interCellPadding
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import Cocoa
|
||||
import WebKit
|
||||
|
||||
class CollectionViewItem: NSCollectionViewItem {
|
||||
var view_: CollectionViewItemView { view as! CollectionViewItemView }
|
||||
|
||||
override func loadView() {
|
||||
view = CollectionViewItemView()
|
||||
view.wantsLayer = true
|
||||
}
|
||||
|
||||
override var isSelected: Bool {
|
||||
didSet {
|
||||
view.layer!.backgroundColor = isSelected ? Preferences.highlightBackgroundColor.cgColor : .clear
|
||||
view.layer!.borderColor = isSelected ? Preferences.highlightBorderColor.cgColor : .clear
|
||||
}
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -2,7 +2,7 @@ import Cocoa
|
||||
|
||||
// Font icon using SF Symbols from the SF Pro font from Apple
|
||||
// see https://developer.apple.com/design/human-interface-guidelines/sf-symbols/overview/
|
||||
class FontIcon: CellTitle {
|
||||
class ThumbnailFontIconView: ThumbnailTitleView {
|
||||
static let sfSymbolCircledMinusSign = ""
|
||||
static let sfSymbolCircledDotSign = ""
|
||||
static let sfSymbolCircledNumber0 = ""
|
||||
@@ -22,19 +22,19 @@ class FontIcon: CellTitle {
|
||||
// number should be in the interval [0-50]
|
||||
func setNumber(_ number: UInt32) {
|
||||
let (baseCharacter, offset) = baseCharacterAndOffset(number)
|
||||
string = String(UnicodeScalar(baseCharacter.unicodeScalars.first!.value + offset)!)
|
||||
assignIfDifferent(&string, String(UnicodeScalar(baseCharacter.unicodeScalars.first!.value + offset)!))
|
||||
}
|
||||
|
||||
func setStar() {
|
||||
string = FontIcon.sfSymbolCircledStart
|
||||
assignIfDifferent(&string, ThumbnailFontIconView.sfSymbolCircledStart)
|
||||
}
|
||||
|
||||
private func baseCharacterAndOffset(_ number: UInt32) -> (String, UInt32) {
|
||||
if number <= 9 {
|
||||
// numbers alternate between empty and full circles; we skip the full circles
|
||||
return (FontIcon.sfSymbolCircledNumber0, number * UInt32(2))
|
||||
return (ThumbnailFontIconView.sfSymbolCircledNumber0, number * UInt32(2))
|
||||
} else {
|
||||
return (FontIcon.sfSymbolCircledNumber10, number - 10)
|
||||
return (ThumbnailFontIconView.sfSymbolCircledNumber10, number - 10)
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
import Cocoa
|
||||
|
||||
class CellTitle: BaseLabel {
|
||||
class ThumbnailTitleView: BaseLabel {
|
||||
var magicOffset = CGFloat(0)
|
||||
|
||||
convenience init(_ size: CGFloat, _ magicOffset: CGFloat = 0) {
|
||||
@@ -14,7 +14,7 @@ class CellTitle: BaseLabel {
|
||||
self.init(NSRect.zero, textContainer)
|
||||
self.magicOffset = magicOffset
|
||||
textColor = Preferences.fontColor
|
||||
shadow = CollectionViewItemView.makeShadow(.darkGray)
|
||||
shadow = ThumbnailView.makeShadow(.darkGray)
|
||||
defaultParagraphStyle = makeParagraphStyle(size)
|
||||
heightAnchor.constraint(equalToConstant: size + magicOffset).isActive = true
|
||||
}
|
||||
+58
-54
@@ -1,13 +1,13 @@
|
||||
import Cocoa
|
||||
|
||||
class CollectionViewItemView: NSStackView {
|
||||
class ThumbnailView: NSStackView {
|
||||
var window_: Window?
|
||||
var thumbnail = NSImageView()
|
||||
var appIcon = NSImageView()
|
||||
var label = CellTitle(Preferences.fontHeight)
|
||||
var minimizedIcon = FontIcon(FontIcon.sfSymbolCircledMinusSign, Preferences.fontIconSize, .white)
|
||||
var hiddenIcon = FontIcon(FontIcon.sfSymbolCircledDotSign, Preferences.fontIconSize, .white)
|
||||
var spaceIcon = FontIcon(FontIcon.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
|
||||
var label = ThumbnailTitleView(Preferences.fontHeight)
|
||||
var minimizedIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledMinusSign, Preferences.fontIconSize, .white)
|
||||
var hiddenIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledDotSign, Preferences.fontIconSize, .white)
|
||||
var spaceIcon = ThumbnailFontIconView(ThumbnailFontIconView.sfSymbolCircledNumber0, Preferences.fontIconSize, .white)
|
||||
var mouseDownCallback: MouseDownCallback!
|
||||
var mouseMovedCallback: MouseMovedCallback!
|
||||
var dragAndDropTimer: Timer?
|
||||
@@ -16,12 +16,58 @@ class CollectionViewItemView: NSStackView {
|
||||
self.init(frame: .zero)
|
||||
let hStackView = makeHStackView()
|
||||
setupView(hStackView)
|
||||
let shadow = CollectionViewItemView.makeShadow(.gray)
|
||||
let shadow = ThumbnailView.makeShadow(.gray)
|
||||
thumbnail.shadow = shadow
|
||||
appIcon.shadow = shadow
|
||||
observeDragAndDrop()
|
||||
}
|
||||
|
||||
func updateRecycledCellWithNewContent(_ element: Window, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ newHeight: CGFloat,_ screen: NSScreen) {
|
||||
window_ = element
|
||||
// element.itemView = self
|
||||
if thumbnail.image != element.thumbnail {
|
||||
thumbnail.image = element.thumbnail
|
||||
let (thumbnailWidth, thumbnailHeight) = ThumbnailView.thumbnailSize(element.thumbnail, screen)
|
||||
let thumbnailSize = NSSize(width: thumbnailWidth.rounded(), height: thumbnailHeight.rounded())
|
||||
thumbnail.image?.size = thumbnailSize
|
||||
thumbnail.frame.size = thumbnailSize
|
||||
}
|
||||
if appIcon.image != element.icon {
|
||||
appIcon.image = element.icon
|
||||
let appIconSize = NSSize(width: Preferences.iconSize, height: Preferences.iconSize)
|
||||
appIcon.image?.size = appIconSize
|
||||
appIcon.frame.size = appIconSize
|
||||
}
|
||||
if label.string != element.title {
|
||||
label.string = element.title
|
||||
// workaround: setting string on NSTextView change the font (most likely a Cocoa bug)
|
||||
label.font = Preferences.font
|
||||
}
|
||||
assignIfDifferent(&hiddenIcon.isHidden, !window_!.isHidden)
|
||||
assignIfDifferent(&minimizedIcon.isHidden, !window_!.isMinimized)
|
||||
assignIfDifferent(&spaceIcon.isHidden, element.spaceIndex == nil || Spaces.isSingleSpace || Preferences.hideSpaceNumberLabels)
|
||||
if !spaceIcon.isHidden {
|
||||
if element.isOnAllSpaces {
|
||||
spaceIcon.setStar()
|
||||
} else {
|
||||
spaceIcon.setNumber(UInt32(element.spaceIndex!))
|
||||
}
|
||||
}
|
||||
assignIfDifferent(&frame.size.width, thumbnail.frame.size.width + Preferences.intraCellPadding * 2)
|
||||
assignIfDifferent(&frame.size.height, newHeight)
|
||||
let fontIconWidth = CGFloat([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)
|
||||
self.mouseDownCallback = mouseDownCallback
|
||||
self.mouseMovedCallback = mouseMovedCallback
|
||||
if trackingAreas.count == 0 {
|
||||
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil))
|
||||
} else if trackingAreas.count > 0 && trackingAreas[0].rect != bounds {
|
||||
removeTrackingArea(trackingAreas[0])
|
||||
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil))
|
||||
}
|
||||
}
|
||||
|
||||
private func observeDragAndDrop() {
|
||||
// NSImageView instances are registered to drag-and-drop by default
|
||||
thumbnail.unregisterDraggedTypes()
|
||||
@@ -63,41 +109,6 @@ class CollectionViewItemView: NSStackView {
|
||||
mouseDownCallback()
|
||||
}
|
||||
|
||||
func updateRecycledCellWithNewContent(_ element: Window, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) {
|
||||
window_ = element
|
||||
thumbnail.image = element.thumbnail
|
||||
let (thumbnailWidth, thumbnailHeight) = CollectionViewItemView.thumbnailSize(element.thumbnail, screen)
|
||||
let thumbnailSize = NSSize(width: thumbnailWidth.rounded(), height: thumbnailHeight.rounded())
|
||||
thumbnail.image?.size = thumbnailSize
|
||||
thumbnail.frame.size = thumbnailSize
|
||||
appIcon.image = element.icon
|
||||
let appIconSize = NSSize(width: Preferences.iconSize, height: Preferences.iconSize)
|
||||
appIcon.image?.size = appIconSize
|
||||
appIcon.frame.size = appIconSize
|
||||
label.string = element.title
|
||||
// workaround: setting string on NSTextView change the font (most likely a Cocoa bug)
|
||||
label.font = Preferences.font
|
||||
hiddenIcon.isHidden = !window_!.isHidden
|
||||
minimizedIcon.isHidden = !window_!.isMinimized
|
||||
spaceIcon.isHidden = element.spaceIndex == nil || Spaces.isSingleSpace || Preferences.hideSpaceNumberLabels
|
||||
if !spaceIcon.isHidden {
|
||||
if element.isOnAllSpaces {
|
||||
spaceIcon.setStar()
|
||||
} else {
|
||||
spaceIcon.setNumber(UInt32(element.spaceIndex!))
|
||||
}
|
||||
}
|
||||
let fontIconWidth = CGFloat([minimizedIcon, hiddenIcon, spaceIcon].filter { !$0.isHidden }.count) * (Preferences.fontIconSize + Preferences.intraCellPadding)
|
||||
label.textContainer!.size.width = frame.width - Preferences.iconSize - Preferences.intraCellPadding * 3 - fontIconWidth
|
||||
subviews.first!.frame.size = frame.size
|
||||
self.mouseDownCallback = mouseDownCallback
|
||||
self.mouseMovedCallback = mouseMovedCallback
|
||||
if trackingAreas.count > 0 {
|
||||
removeTrackingArea(trackingAreas[0])
|
||||
}
|
||||
addTrackingArea(NSTrackingArea(rect: bounds, options: [.mouseMoved, .activeAlways], owner: self, userInfo: nil))
|
||||
}
|
||||
|
||||
static func makeShadow(_ color: NSColor) -> NSShadow {
|
||||
let shadow = NSShadow()
|
||||
shadow.shadowColor = color
|
||||
@@ -106,33 +117,26 @@ class CollectionViewItemView: NSStackView {
|
||||
return shadow
|
||||
}
|
||||
|
||||
static func downscaleFactor() -> CGFloat {
|
||||
let nCellsBeforePotentialOverflow = Preferences.minRows * Preferences.minCellsPerRow
|
||||
guard CGFloat(Windows.list.count) > nCellsBeforePotentialOverflow else { return 1 }
|
||||
// TODO: replace this buggy heuristic with a correct implementation of downscaling
|
||||
return nCellsBeforePotentialOverflow / (nCellsBeforePotentialOverflow + (sqrt(CGFloat(Windows.list.count) - nCellsBeforePotentialOverflow) * 2))
|
||||
}
|
||||
|
||||
static func widthMax(_ screen: NSScreen) -> CGFloat {
|
||||
return (ThumbnailsPanel.widthMax(screen) / Preferences.minCellsPerRow - Preferences.interCellPadding) * CollectionViewItemView.downscaleFactor()
|
||||
return ThumbnailsPanel.widthMax(screen) / Preferences.minCellsPerRow - Preferences.interCellPadding
|
||||
}
|
||||
|
||||
static func widthMin(_ screen: NSScreen) -> CGFloat {
|
||||
return (ThumbnailsPanel.widthMax(screen) / Preferences.maxCellsPerRow - Preferences.interCellPadding) * CollectionViewItemView.downscaleFactor()
|
||||
return ThumbnailsPanel.widthMax(screen) / Preferences.maxCellsPerRow - Preferences.interCellPadding
|
||||
}
|
||||
|
||||
static func height(_ screen: NSScreen) -> CGFloat {
|
||||
return (ThumbnailsPanel.heightMax(screen) / Preferences.minRows - Preferences.interCellPadding) * CollectionViewItemView.downscaleFactor()
|
||||
return ThumbnailsPanel.heightMax(screen) / Preferences.rowsCount - Preferences.interCellPadding
|
||||
}
|
||||
|
||||
static func width(_ image: NSImage?, _ screen: NSScreen) -> CGFloat {
|
||||
return max(thumbnailSize(image, screen).0 + Preferences.intraCellPadding * 2, CollectionViewItemView.widthMin(screen))
|
||||
return max(thumbnailSize(image, screen).0 + Preferences.intraCellPadding * 2, ThumbnailView.widthMin(screen))
|
||||
}
|
||||
|
||||
static func thumbnailSize(_ image: NSImage?, _ screen: NSScreen) -> (CGFloat, CGFloat) {
|
||||
guard let image = image else { return (0, 0) }
|
||||
let thumbnailHeightMax = CollectionViewItemView.height(screen) - Preferences.intraCellPadding * 3 - Preferences.iconSize
|
||||
let thumbnailWidthMax = CollectionViewItemView.widthMax(screen) - Preferences.intraCellPadding * 2
|
||||
let thumbnailHeightMax = ThumbnailView.height(screen) - Preferences.intraCellPadding * 3 - Preferences.iconSize
|
||||
let thumbnailWidthMax = ThumbnailView.widthMax(screen) - Preferences.intraCellPadding * 2
|
||||
let thumbnailHeight = min(image.size.height, thumbnailHeightMax)
|
||||
let thumbnailWidth = min(image.size.width, thumbnailWidthMax)
|
||||
let imageRatio = image.size.width / image.size.height
|
||||
@@ -1,15 +1,12 @@
|
||||
import Cocoa
|
||||
|
||||
class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDelegate, NSCollectionViewDelegateFlowLayout {
|
||||
var backgroundView: NSVisualEffectView!
|
||||
var collectionView: NSCollectionView!
|
||||
var app: App?
|
||||
class ThumbnailsPanel: NSPanel {
|
||||
var thumbnailsView = ThumbnailsView()
|
||||
var currentScreen: NSScreen?
|
||||
static let cellId = NSUserInterfaceItemIdentifier("Cell")
|
||||
|
||||
convenience init(_ app: App) {
|
||||
self.init()
|
||||
self.app = app
|
||||
self.init(contentRect: .zero, styleMask: .utilityWindow, backing: .buffered, defer: true)
|
||||
isFloatingPanel = true
|
||||
animationBehavior = .none
|
||||
hidesOnDeactivate = false
|
||||
@@ -17,10 +14,7 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
titleVisibility = .hidden
|
||||
styleMask.remove(.titled)
|
||||
backgroundColor = .clear
|
||||
makeCollectionView()
|
||||
backgroundView = ThumbnailsPanel.makeBackgroundView()
|
||||
backgroundView.addSubview(collectionView)
|
||||
contentView!.addSubview(backgroundView)
|
||||
contentView!.addSubview(thumbnailsView)
|
||||
// 2nd highest level possible; this allows the app to go on top of context menus
|
||||
// highest level is .screenSaver but makes drag and drop on top the main window impossible
|
||||
level = .popUpMenu
|
||||
@@ -32,79 +26,11 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
static func makeBackgroundView() -> NSVisualEffectView {
|
||||
let backgroundView = NSVisualEffectView()
|
||||
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backgroundView.material = Preferences.windowMaterial
|
||||
backgroundView.state = .active
|
||||
backgroundView.wantsLayer = true
|
||||
backgroundView.layer!.cornerRadius = Preferences.windowCornerRadius
|
||||
return backgroundView
|
||||
}
|
||||
|
||||
func makeCollectionView() {
|
||||
collectionView = NSCollectionView()
|
||||
collectionView.dataSource = self
|
||||
collectionView.delegate = self
|
||||
collectionView.collectionViewLayout = makeLayout()
|
||||
collectionView.backgroundColors = [.clear]
|
||||
collectionView.isSelectable = true
|
||||
collectionView.allowsMultipleSelection = false
|
||||
collectionView.register(CollectionViewItem.self, forItemWithIdentifier: ThumbnailsPanel.cellId)
|
||||
}
|
||||
|
||||
private func makeLayout() -> CollectionViewFlowLayout {
|
||||
let layout = CollectionViewFlowLayout()
|
||||
layout.minimumInteritemSpacing = Preferences.interCellPadding
|
||||
layout.minimumLineSpacing = Preferences.interCellPadding
|
||||
return layout
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
return Windows.list.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
|
||||
let item = collectionView.makeItem(withIdentifier: ThumbnailsPanel.cellId, for: indexPath) as! CollectionViewItem
|
||||
item.view_.updateRecycledCellWithNewContent(Windows.list[indexPath.item],
|
||||
{ self.app!.focusSelectedWindow(item.view_.window_) },
|
||||
{ self.app!.thumbnailsPanel!.highlightCell(item) },
|
||||
currentScreen!)
|
||||
return item
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
|
||||
guard indexPath.item < Windows.list.count else { return .zero }
|
||||
return NSSize(width: CollectionViewItemView.width(Windows.list[indexPath.item].thumbnail, currentScreen!).rounded(), height: CollectionViewItemView.height(currentScreen!).rounded())
|
||||
}
|
||||
|
||||
func highlightCell() {
|
||||
collectionView.deselectAll(nil)
|
||||
collectionView.selectItems(at: [IndexPath(item: Windows.focusedWindowIndex, section: 0)], scrollPosition: .top)
|
||||
}
|
||||
|
||||
func highlightCell(_ cell: CollectionViewItem) {
|
||||
let newIndex = collectionView.indexPath(for: cell)!
|
||||
if Windows.focusedWindowIndex != newIndex.item {
|
||||
collectionView.selectItems(at: [newIndex], scrollPosition: .top)
|
||||
collectionView.deselectItems(at: [IndexPath(item: Windows.focusedWindowIndex, section: 0)])
|
||||
Windows.focusedWindowIndex = newIndex.item
|
||||
}
|
||||
}
|
||||
|
||||
func refreshCollectionView(_ screen: NSScreen, _ uiWorkShouldBeDone: Bool) {
|
||||
if uiWorkShouldBeDone { self.currentScreen = screen }
|
||||
let layout = collectionView.collectionViewLayout as! CollectionViewFlowLayout
|
||||
if uiWorkShouldBeDone { layout.currentScreen = screen }
|
||||
if uiWorkShouldBeDone { layout.invalidateLayout() }
|
||||
if uiWorkShouldBeDone { collectionView.setFrameSize(NSSize(width: ThumbnailsPanel.widthMax(screen).rounded(), height: ThumbnailsPanel.heightMax(screen).rounded())) }
|
||||
if uiWorkShouldBeDone { collectionView.reloadData() }
|
||||
if uiWorkShouldBeDone { collectionView.layoutSubtreeIfNeeded() }
|
||||
if uiWorkShouldBeDone { collectionView.setFrameSize(NSSize(width: layout.widestRow!, height: layout.totalHeight!)) }
|
||||
let windowSize = NSSize(width: layout.widestRow! + Preferences.windowPadding * 2, height: layout.totalHeight! + Preferences.windowPadding * 2)
|
||||
if uiWorkShouldBeDone { setContentSize(windowSize) }
|
||||
if uiWorkShouldBeDone { backgroundView!.setFrameSize(windowSize) }
|
||||
if uiWorkShouldBeDone { collectionView.setFrameOrigin(NSPoint(x: Preferences.windowPadding, y: Preferences.windowPadding)) }
|
||||
static func highlightCell(_ previousView: NSView, _ newView: NSView) {
|
||||
previousView.layer!.backgroundColor = .clear
|
||||
previousView.layer!.borderColor = .clear
|
||||
newView.layer!.backgroundColor = Preferences.highlightBackgroundColor.cgColor
|
||||
newView.layer!.borderColor = Preferences.highlightBorderColor.cgColor
|
||||
}
|
||||
|
||||
static func widthMax(_ screen: NSScreen) -> CGFloat {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
import Cocoa
|
||||
|
||||
class ThumbnailsView: NSVisualEffectView {
|
||||
let scrollView = ScrollView()
|
||||
static var recycledViews = [ThumbnailView]()
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
material = Preferences.windowMaterial
|
||||
state = .active
|
||||
wantsLayer = true
|
||||
layer!.cornerRadius = Preferences.windowCornerRadius
|
||||
addSubview(scrollView)
|
||||
// TODO: think about this optimization more
|
||||
(1...100).forEach { _ in ThumbnailsView.recycledViews.append(ThumbnailView()) }
|
||||
}
|
||||
|
||||
func updateItems(_ screen: NSScreen) {
|
||||
let widthMax = ThumbnailsPanel.widthMax(screen).rounded()
|
||||
let heightMax = ThumbnailsPanel.heightMax(screen).rounded()
|
||||
let height = ThumbnailView.height(screen).rounded(.down)
|
||||
var currentX = CGFloat(0)
|
||||
var currentY = CGFloat(0)
|
||||
var maxX = CGFloat(0)
|
||||
var maxY = height
|
||||
var newViews = [ThumbnailView]()
|
||||
for (index, window) in Windows.list.enumerated() {
|
||||
let view = ThumbnailsView.recycledViews[index]
|
||||
view.updateRecycledCellWithNewContent(window,
|
||||
{ (App.shared as! App).focusSelectedWindow(window) },
|
||||
{ Windows.updateFocusedWindowIndex(index) },
|
||||
height, screen)
|
||||
let width = view.frame.size.width
|
||||
if (currentX + Preferences.interCellPadding + width).rounded(.down) > widthMax {
|
||||
currentX = CGFloat(0)
|
||||
currentY = (currentY + Preferences.interCellPadding + height).rounded(.down)
|
||||
maxY = max(currentY + height, maxY)
|
||||
} else {
|
||||
maxX = max(currentX + width, maxX)
|
||||
}
|
||||
view.frame.origin = CGPoint(x: currentX, y: currentY)
|
||||
currentX = (currentX + Preferences.interCellPadding + width).rounded(.down)
|
||||
newViews.append(view)
|
||||
}
|
||||
scrollView.documentView!.subviews = newViews
|
||||
frame.size = NSSize(width: min(maxX, widthMax) + Preferences.windowPadding * 2, height: min(maxY, heightMax) + Preferences.windowPadding * 2)
|
||||
scrollView.frame.size = NSSize(width: min(maxX, widthMax), height: min(maxY, heightMax))
|
||||
scrollView.frame.origin = CGPoint(x: Preferences.windowPadding, y: Preferences.windowPadding)
|
||||
scrollView.contentView.frame.size = scrollView.frame.size
|
||||
scrollView.documentView!.frame.size = NSSize(width: maxX, height: maxY)
|
||||
if Preferences.alignThumbnails == .center {
|
||||
centerRows(maxX)
|
||||
}
|
||||
}
|
||||
|
||||
func centerRows(_ maxX: CGFloat) {
|
||||
var rowStartIndex = 0
|
||||
var rowWidth = CGFloat(0)
|
||||
var rowY = CGFloat(0)
|
||||
for (index, window) in Windows.list.enumerated() {
|
||||
let view = ThumbnailsView.recycledViews[index]
|
||||
if view.frame.origin.y == rowY {
|
||||
rowWidth += Preferences.interCellPadding + view.frame.size.width
|
||||
} else {
|
||||
if rowStartIndex == 0 {
|
||||
rowWidth -= Preferences.interCellPadding // first row has 1 extra padding
|
||||
}
|
||||
shiftRow(maxX, rowWidth, rowStartIndex, index)
|
||||
rowStartIndex = index
|
||||
rowWidth = view.frame.size.width
|
||||
rowY = view.frame.origin.y
|
||||
}
|
||||
}
|
||||
shiftRow(maxX, rowWidth, rowStartIndex, Windows.list.count)
|
||||
}
|
||||
|
||||
private func shiftRow(_ maxX: CGFloat, _ rowWidth: CGFloat, _ rowStartIndex: Int, _ index: Int) {
|
||||
let offset = ((maxX - rowWidth) / 2).rounded()
|
||||
if offset > 0 {
|
||||
(rowStartIndex..<index).forEach {
|
||||
ThumbnailsView.recycledViews[$0].frame.origin.x += offset
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollView: NSScrollView {
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
documentView = FlippedView(frame: .zero)
|
||||
drawsBackground = false
|
||||
hasVerticalScroller = true
|
||||
}
|
||||
|
||||
override func tile() {
|
||||
super.tile()
|
||||
// draw the scroller on top of the content
|
||||
contentView.frame = bounds
|
||||
}
|
||||
}
|
||||
|
||||
class FlippedView: NSView {
|
||||
override var isFlipped: Bool { true }
|
||||
}
|
||||
@@ -10,10 +10,11 @@ class AppearanceTab {
|
||||
private static func makeView() -> NSGridView {
|
||||
let view = GridView.make([
|
||||
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Theme", comment: ""), "theme", MacroPreferences.themeList.values.map { $0.label }),
|
||||
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Align windows", comment: ""), "alignThumbnails", Array(MacroPreferences.alignThumbnailsList.keys)),
|
||||
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Max size on screen", comment: ""), "maxScreenUsage", 10, 100, 10, true, "%"),
|
||||
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Min windows per row", comment: ""), "minCellsPerRow", 1, 20, 20, true),
|
||||
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Max windows per row", comment: ""), "maxCellsPerRow", 1, 40, 20, true),
|
||||
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Min rows of windows", comment: ""), "minRows", 1, 20, 20, true),
|
||||
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Rows of windows", comment: ""), "rowsCount", 1, 20, 20, true),
|
||||
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Window app icon size", comment: ""), "iconSize", 0, 64, 11, false, "px"),
|
||||
LabelAndControl.makeLabelWithSlider(NSLocalizedString("Window title font size", comment: ""), "fontHeight", 0, 64, 11, false, "px"),
|
||||
LabelAndControl.makeLabelWithDropdown(NSLocalizedString("Show on", comment: ""), "showOnScreen", Array(MacroPreferences.showOnScreenList.keys)),
|
||||
|
||||
@@ -50,7 +50,7 @@ class GeneralTab {
|
||||
LSSharedFileListInsertItemURL(loginItems, kLSSharedFileListItemBeforeFirst.takeRetainedValue(), nil, nil, App.url, nil, nil)
|
||||
} else {
|
||||
loginItemsSnapshot.forEach {
|
||||
if CFEqual(LSSharedFileListItemCopyResolvedURL($0, 0, nil).takeRetainedValue(), App.url) {
|
||||
if LSSharedFileListItemCopyResolvedURL($0, 0, nil).takeRetainedValue() == App.url {
|
||||
LSSharedFileListItemRemove(loginItems, $0)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user