feat: windows on mouse screen, implements #28
squash of pull-request corrections and improvements refactor: better mimic apple centering Also use enum instead of bool to self-document the interface docs: clarify the show-on-screen preference wording Discussion on this SO (https://stackoverflow.com/a/864432/2249756) seem to indicate that .main is not the screen with the menubar, however in my tests, it is always that window. I think macOS changed the behavior of .main without updating the documentation refactor: shorter method names as swift promotes + removed debug code fix: all methods in screen should use visibleFrame not frame refactor: (hopefully) made code intent clearer fix: panel and collection layout now use dynamic screen variable fix: fix vertical origin from 4c44100f196
This commit is contained in:
1 parent
6c9304735a
commit
b841ec7a57
9 files changed
+43
-65
No files matched your search
@@ -52,8 +52,8 @@ class Preferences {
|
||||
MacroPreference("⌘ command", ([kVK_Command, kVK_RightCommand], .command))
|
||||
])
|
||||
static var showOnScreenMacro = MacroPreferenceHelper<ShowOnScreenPreference>([
|
||||
MacroPreference("with keyboard focus", ShowOnScreenPreference.MAIN),
|
||||
MacroPreference("with mouse pointer", ShowOnScreenPreference.MOUSE),
|
||||
MacroPreference("Main screen", ShowOnScreenPreference.MAIN),
|
||||
MacroPreference("Screen including mouse", ShowOnScreenPreference.MOUSE),
|
||||
])
|
||||
|
||||
private static let defaultsFile = fileFromPreferencesFolder("alt-tab-macos-defaults.json")
|
||||
|
||||
@@ -2,55 +2,44 @@ import Foundation
|
||||
import Cocoa
|
||||
|
||||
class Screen {
|
||||
// currently not in use (but kept for reference & future use), earlier registered by Application.applicationDidFinishLaunching()
|
||||
static func listenToChanges() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: NSApplication.didChangeScreenParametersNotification,
|
||||
object: NSApplication.shared,
|
||||
queue: OperationQueue.main
|
||||
) { notification -> Void in
|
||||
// do something
|
||||
}
|
||||
}
|
||||
|
||||
static func calcThumbnailMaxSize(_ screen: NSScreen) -> NSSize {
|
||||
let width: CGFloat = (screen.frame.size.width * Preferences.maxScreenUsage! - Preferences.windowPadding * 2) / Preferences.maxThumbnailsPerRow! - Preferences.interItemPadding
|
||||
let height: CGFloat = width * (screen.frame.height / screen.frame.width)
|
||||
return NSSize(width: width, height: height)
|
||||
}
|
||||
|
||||
static func calcFrameMaxSize(_ screen: NSScreen) -> NSSize {
|
||||
return NSSize(width: screen.frame.width * Preferences.maxScreenUsage!, height: screen.frame.height * Preferences.maxScreenUsage!)
|
||||
}
|
||||
|
||||
// TODO: currently unknown and unhandled error use-case possible: NSScreen.main being nil
|
||||
static func getPreferredScreen() -> NSScreen {
|
||||
static func preferredScreen() -> NSScreen {
|
||||
switch Preferences.showOnScreen! {
|
||||
case .MOUSE:
|
||||
return getScreenWithMouse() ?? NSScreen.main!; // .main as fall-back
|
||||
return screenWithMouse() ?? NSScreen.main!; // .main as fall-back
|
||||
case .MAIN:
|
||||
return NSScreen.main!;
|
||||
}
|
||||
}
|
||||
|
||||
static func getScreenWithMouse() -> NSScreen? {
|
||||
private static func screenWithMouse() -> NSScreen? {
|
||||
return NSScreen.screens.first { NSMouseInRect(NSEvent.mouseLocation, $0.frame, false) }
|
||||
}
|
||||
|
||||
/*
|
||||
usage notes:
|
||||
- useAppleVerticalOffset: applies a vertical offset (higher positioning) attempting to approximate the NSView.center() results but only if we are not dealing with screen filling frames (height)
|
||||
*/
|
||||
static func showCenteredFrontPanel(_ panel: NSPanel, _ screen: NSScreen, _ useAppleVerticalOffset: Bool = true) {
|
||||
var verticalOffset: CGFloat = 0
|
||||
static func thumbnailMaxSize(_ screen: NSScreen) -> NSSize {
|
||||
let frame = screen.visibleFrame
|
||||
let width = (frame.width * Preferences.maxScreenUsage! - Preferences.windowPadding * 2) / Preferences.maxThumbnailsPerRow! - Preferences.interItemPadding
|
||||
let height = width * (frame.height / frame.width)
|
||||
return NSSize(width: width, height: height)
|
||||
}
|
||||
|
||||
if useAppleVerticalOffset && panel.frame.height < screen.visibleFrame.height / 1.5 {
|
||||
verticalOffset = CGFloat(screen.visibleFrame.height * 0.175)
|
||||
}
|
||||
static func thumbnailPanelMaxSize(_ screen: NSScreen) -> NSSize {
|
||||
let frame = screen.visibleFrame
|
||||
return NSSize(width: frame.width * Preferences.maxScreenUsage!, height: frame.height * Preferences.maxScreenUsage!)
|
||||
}
|
||||
|
||||
let centerPosition = NSPoint(x: screen.visibleFrame.midX - panel.frame.width / 2, y: screen.visibleFrame.midY - panel.frame.height / 2 + verticalOffset)
|
||||
panel.setFrameOrigin(centerPosition)
|
||||
static func showPanel(_ panel: NSPanel, _ screen: NSScreen, _ alignment: VerticalAlignment) {
|
||||
let screenFrame = screen.visibleFrame
|
||||
let panelFrame = panel.frame
|
||||
let x = screenFrame.minX + max(screenFrame.width - panelFrame.width, 0) * 0.5
|
||||
let y = screenFrame.minY + max(screenFrame.height - panelFrame.height, 0) * alignment.rawValue
|
||||
panel.setFrameOrigin(NSPoint(x: x, y: y))
|
||||
panel.makeKeyAndOrderFront(nil)
|
||||
Application.shared.arrangeInFront(nil)
|
||||
}
|
||||
}
|
||||
|
||||
enum VerticalAlignment: CGFloat {
|
||||
case centered = 0.5
|
||||
// vertically centered but with an upward offset, similar to a book title; mimics NSView.center()
|
||||
case appleCentered = 0.75
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class OpenWindow {
|
||||
|
||||
func computeDownscaledSize(_ image: NSImage, _ screen: NSScreen) -> (Int, Int) {
|
||||
let imageRatio = image.size.width / image.size.height
|
||||
let thumbnailMaxSize = Screen.calcThumbnailMaxSize(screen)
|
||||
let thumbnailMaxSize = Screen.thumbnailMaxSize(screen)
|
||||
let thumbnailWidth = Int(floor(thumbnailMaxSize.height * imageRatio))
|
||||
if thumbnailWidth <= Int(thumbnailMaxSize.width) {
|
||||
return (thumbnailWidth, Int(thumbnailMaxSize.height))
|
||||
|
||||
@@ -28,7 +28,7 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
|
||||
SystemPermissions.ensureAccessibilityCheckboxIsChecked()
|
||||
Preferences.loadFromDiskAndUpdateValues()
|
||||
statusItem = StatusItem.make(self)
|
||||
thumbnailsPanel = ThumbnailsPanel(self, Screen.getPreferredScreen())
|
||||
thumbnailsPanel = ThumbnailsPanel(self)
|
||||
preferencesPanel = PreferencesPanel()
|
||||
Keyboard.listenToGlobalEvents(self)
|
||||
}
|
||||
@@ -58,8 +58,8 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
@objc func showCenteredPreferencesPanel() {
|
||||
Screen.showCenteredFrontPanel(preferencesPanel!, Screen.getPreferredScreen())
|
||||
@objc func showPreferencesPanel() {
|
||||
Screen.showPanel(preferencesPanel!, Screen.preferredScreen(), .appleCentered)
|
||||
}
|
||||
|
||||
func computeOpenWindows() {
|
||||
@@ -101,12 +101,12 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
|
||||
return
|
||||
}
|
||||
selectedOpenWindow = cellWithStep(step)
|
||||
let currentScreen = Screen.getPreferredScreen() // we want all computations and renderings to use the same screen for this summon (in case mouse movements switching screens etc.)
|
||||
var workItem: DispatchWorkItem!
|
||||
workItem = DispatchWorkItem {
|
||||
let currentScreen = Screen.preferredScreen() // fix screen between steps since it could change (e.g. mouse moved to another screen)
|
||||
if !workItem.isCancelled { self.thumbnailsPanel!.computeThumbnails(currentScreen) }
|
||||
if !workItem.isCancelled { self.thumbnailsPanel!.highlightCellAt(step) }
|
||||
if !workItem.isCancelled { Screen.showCenteredFrontPanel(self.thumbnailsPanel!, currentScreen, true) }
|
||||
if !workItem.isCancelled { Screen.showPanel(self.thumbnailsPanel!, currentScreen, .appleCentered) }
|
||||
}
|
||||
workItems.append(workItem)
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay!, execute: workItem)
|
||||
|
||||
@@ -35,8 +35,6 @@ class Cell: NSCollectionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
func updateWithNewContent(_ element: OpenWindow, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) {
|
||||
openWindow = element
|
||||
thumbnail.image = element.thumbnail
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import Cocoa
|
||||
|
||||
class CollectionViewCenterFlowLayout: NSCollectionViewFlowLayout {
|
||||
let currentScreen: NSScreen
|
||||
|
||||
init(_ screen: NSScreen) {
|
||||
currentScreen = screen
|
||||
super.init()
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
var currentScreen: NSScreen?
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [NSCollectionViewLayoutAttributes] {
|
||||
let attributes = super.layoutAttributesForElements(in: rect)
|
||||
@@ -25,7 +16,7 @@ class CollectionViewCenterFlowLayout: NSCollectionViewFlowLayout {
|
||||
var widestRow = CGFloat(0)
|
||||
var totalHeight = CGFloat(0)
|
||||
attributes.enumerated().forEach {
|
||||
let isNewRow = abs($1.frame.origin.y - currentRowY) > Screen.calcThumbnailMaxSize(currentScreen).height
|
||||
let isNewRow = abs($1.frame.origin.y - currentRowY) > Screen.thumbnailMaxSize(currentScreen!).height
|
||||
if isNewRow {
|
||||
computeOriginXForAllItems(currentRowWidth - minimumInteritemSpacing, previousRowMaxY, currentRow)
|
||||
currentRow.removeAll()
|
||||
|
||||
@@ -33,7 +33,7 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
|
||||
makeLabelWithInput(\PreferencesPanel.iconSize, "Apps icon size (px)", "iconSize"),
|
||||
makeLabelWithInput(\PreferencesPanel.fontHeight, "Font size (px)", "fontHeight"),
|
||||
makeLabelWithInput(\PreferencesPanel.windowDisplayDelay, "Window apparition delay (ms)", "windowDisplayDelay"),
|
||||
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on Screen", "showOnScreen", Preferences.showOnScreenMacro.labels)
|
||||
makeLabelWithDropdown(\PreferencesPanel.showOnScreen, "Show on", "showOnScreen", Preferences.showOnScreenMacro.labels)
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ class StatusItem {
|
||||
keyEquivalent: "")
|
||||
item.menu!.addItem(
|
||||
withTitle: "Preferences…",
|
||||
action: #selector(application.showCenteredPreferencesPanel),
|
||||
action: #selector(application.showPreferencesPanel),
|
||||
keyEquivalent: ",")
|
||||
item.menu!.addItem(
|
||||
withTitle: "Quit \(ProcessInfo.processInfo.processName)",
|
||||
|
||||
@@ -11,10 +11,9 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
|
||||
}
|
||||
|
||||
convenience init(_ application: Application, _ currentScreen: NSScreen) {
|
||||
convenience init(_ application: Application) {
|
||||
self.init()
|
||||
self.application = application
|
||||
self.currentScreen = currentScreen
|
||||
isFloatingPanel = true
|
||||
animationBehavior = .none
|
||||
hidesOnDeactivate = false
|
||||
@@ -51,8 +50,8 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
}
|
||||
|
||||
func makeLayout() -> CollectionViewCenterFlowLayout {
|
||||
let layout = CollectionViewCenterFlowLayout(currentScreen!)
|
||||
layout.estimatedItemSize = Screen.calcThumbnailMaxSize(currentScreen!)
|
||||
let layout = CollectionViewCenterFlowLayout()
|
||||
layout.estimatedItemSize = NSSize(width: 200, height: 200)
|
||||
layout.minimumInteritemSpacing = 5
|
||||
layout.minimumLineSpacing = 5
|
||||
return layout
|
||||
@@ -95,8 +94,9 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
|
||||
func computeThumbnails(_ currentScreen: NSScreen) {
|
||||
self.currentScreen = currentScreen
|
||||
collectionView_!.setFrameSize(Screen.calcFrameMaxSize(currentScreen))
|
||||
collectionView_!.collectionViewLayout = makeLayout()
|
||||
(collectionView_.collectionViewLayout as! CollectionViewCenterFlowLayout).currentScreen = currentScreen
|
||||
collectionView_!.setFrameSize(Screen.thumbnailPanelMaxSize(currentScreen))
|
||||
collectionView_!.collectionViewLayout!.invalidateLayout()
|
||||
collectionView_!.reloadData()
|
||||
collectionView_!.layoutSubtreeIfNeeded()
|
||||
setContentSize(NSSize(width: collectionView_!.frame.size.width + Preferences.windowPadding * 2, height: collectionView_!.frame.size.height + Preferences.windowPadding * 2))
|
||||
|
||||
Reference in new issue
Block a user