feat: windows on mouse screen, implements #28
squash of implementation for pull-request #66 and review - adds a new Preference to use the main screen (keyboard focus) or mouse location screen - honors that preference for thumbnail generation and display of all panels - increases the Preferences version - widens PreferencesPanel width and input sizing to allow 3 characters, issue #59 minimal adjustment feat: windows on mouse screen: changes from pull request #66 code review - corrections, improvements & refactoring feat: windows on mouse screen: additional changes for pull request #66 - Screen.preferredScreenOverride: removed - instead currentScreen gets provided or updated now at all required places before triggering UI summon calculations - ThumbnailsPanel.computeThumbnails(): re-creates collectionViewLayout instead of invalidating it - Screen.showCenteredFrontPanel(): useAppleVerticalOffset applies now only if the NSWindow frame is smaller than 2/3 of visibleFrame
This commit is contained in:
1 parent
a184ad5309
commit
6c9304735a
8 files changed
+99
-44
No files matched your search
@@ -2,9 +2,14 @@ import Foundation
|
||||
import Cocoa
|
||||
import Carbon.HIToolbox.Events
|
||||
|
||||
enum ShowOnScreenPreference {
|
||||
case MAIN
|
||||
case MOUSE
|
||||
}
|
||||
|
||||
class Preferences {
|
||||
static var defaults: [String: String] = [
|
||||
"version": "2", // bump this anytime the dictionary is changed
|
||||
"version": "3", // bump this anytime the dictionary is changed
|
||||
"maxScreenUsage": "80",
|
||||
"maxThumbnailsPerRow": "4",
|
||||
"iconSize": "32",
|
||||
@@ -12,11 +17,10 @@ class Preferences {
|
||||
"tabKeyCode": String(kVK_Tab),
|
||||
"metaKey": metaKeyMacro.macros[0].label,
|
||||
"windowDisplayDelay": "0",
|
||||
"theme": themeMacro.macros[0].label
|
||||
"theme": themeMacro.macros[0].label,
|
||||
"showOnScreen": showOnScreenMacro.macros[0].label
|
||||
]
|
||||
static var rawValues = [String: String]()
|
||||
static var thumbnailMaxWidth: CGFloat = 200
|
||||
static var thumbnailMaxHeight: CGFloat = 200
|
||||
static var minimumWindowSize: CGFloat = 200
|
||||
static var fontColor: NSColor = .white
|
||||
static var windowMaterial: NSVisualEffectView.Material = .dark
|
||||
@@ -37,6 +41,7 @@ class Preferences {
|
||||
static var windowDisplayDelay: DispatchTimeInterval?
|
||||
static var windowCornerRadius: CGFloat?
|
||||
static var font: NSFont?
|
||||
static var showOnScreen: ShowOnScreenPreference?
|
||||
static var themeMacro = MacroPreferenceHelper<(CGFloat, CGFloat, CGFloat, NSColor, NSColor)>([
|
||||
MacroPreference(" macOS", (0, 5, 20, .clear, NSColor(red: 0, green: 0, blue: 0, alpha: 0.3))),
|
||||
MacroPreference("❖ Windows 10", (2, 0, 0, .white, .clear))
|
||||
@@ -46,6 +51,10 @@ class Preferences {
|
||||
MacroPreference("⌃ control", ([kVK_Control, kVK_RightControl], .control)),
|
||||
MacroPreference("⌘ command", ([kVK_Command, kVK_RightCommand], .command))
|
||||
])
|
||||
static var showOnScreenMacro = MacroPreferenceHelper<ShowOnScreenPreference>([
|
||||
MacroPreference("with keyboard focus", ShowOnScreenPreference.MAIN),
|
||||
MacroPreference("with mouse pointer", ShowOnScreenPreference.MOUSE),
|
||||
])
|
||||
|
||||
private static let defaultsFile = fileFromPreferencesFolder("alt-tab-macos-defaults.json")
|
||||
private static let userFile = fileFromPreferencesFolder("alt-tab-macos.json")
|
||||
@@ -89,6 +98,9 @@ class Preferences {
|
||||
highlightBackgroundColor = p.preferences.4
|
||||
case "windowDisplayDelay":
|
||||
windowDisplayDelay = DispatchTimeInterval.milliseconds(try Int(value).orThrow())
|
||||
case "showOnScreen":
|
||||
let p = try showOnScreenMacro.labelToMacro[value].orThrow()
|
||||
showOnScreen = p.preferences
|
||||
default:
|
||||
throw "Tried to update an unknown preference: '\(valueName)' = '\(value)'"
|
||||
}
|
||||
|
||||
@@ -2,21 +2,55 @@ 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
|
||||
updateThumbnailMaxSize()
|
||||
// do something
|
||||
}
|
||||
}
|
||||
|
||||
static func updateThumbnailMaxSize() -> Void {
|
||||
if Preferences.thumbnailMaxWidth == 200 && Preferences.thumbnailMaxHeight == 200 {
|
||||
let main = NSScreen.main!.frame
|
||||
Preferences.thumbnailMaxWidth = (NSScreen.main!.frame.size.width * Preferences.maxScreenUsage! - Preferences.windowPadding * 2) / Preferences.maxThumbnailsPerRow! - Preferences.interItemPadding
|
||||
Preferences.thumbnailMaxHeight = Preferences.thumbnailMaxWidth * (main.height / main.width)
|
||||
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 {
|
||||
switch Preferences.showOnScreen! {
|
||||
case .MOUSE:
|
||||
return getScreenWithMouse() ?? NSScreen.main!; // .main as fall-back
|
||||
case .MAIN:
|
||||
return NSScreen.main!;
|
||||
}
|
||||
}
|
||||
|
||||
static func getScreenWithMouse() -> 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
|
||||
|
||||
if useAppleVerticalOffset && panel.frame.height < screen.visibleFrame.height / 1.5 {
|
||||
verticalOffset = CGFloat(screen.visibleFrame.height * 0.175)
|
||||
}
|
||||
|
||||
let centerPosition = NSPoint(x: screen.visibleFrame.midX - panel.frame.width / 2, y: screen.visibleFrame.midY - panel.frame.height / 2 + verticalOffset)
|
||||
panel.setFrameOrigin(centerPosition)
|
||||
panel.makeKeyAndOrderFront(nil)
|
||||
Application.shared.arrangeInFront(nil)
|
||||
}
|
||||
}
|
||||
@@ -33,13 +33,14 @@ class OpenWindow {
|
||||
}
|
||||
}
|
||||
|
||||
func computeDownscaledSize(_ image: NSImage) -> (Int, Int) {
|
||||
func computeDownscaledSize(_ image: NSImage, _ screen: NSScreen) -> (Int, Int) {
|
||||
let imageRatio = image.size.width / image.size.height
|
||||
let thumbnailWidth = Int(floor(Preferences.thumbnailMaxHeight * imageRatio))
|
||||
if thumbnailWidth <= Int(Preferences.thumbnailMaxWidth) {
|
||||
return (thumbnailWidth, Int(Preferences.thumbnailMaxHeight))
|
||||
let thumbnailMaxSize = Screen.calcThumbnailMaxSize(screen)
|
||||
let thumbnailWidth = Int(floor(thumbnailMaxSize.height * imageRatio))
|
||||
if thumbnailWidth <= Int(thumbnailMaxSize.width) {
|
||||
return (thumbnailWidth, Int(thumbnailMaxSize.height))
|
||||
} else {
|
||||
return (Int(Preferences.thumbnailMaxWidth), Int(floor(Preferences.thumbnailMaxWidth / imageRatio)))
|
||||
return (Int(thumbnailMaxSize.width), Int(floor(thumbnailMaxSize.width / imageRatio)))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,12 +27,10 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
|
||||
SystemPermissions.ensureScreenRecordingCheckboxIsChecked()
|
||||
SystemPermissions.ensureAccessibilityCheckboxIsChecked()
|
||||
Preferences.loadFromDiskAndUpdateValues()
|
||||
Screen.updateThumbnailMaxSize()
|
||||
statusItem = StatusItem.make(self)
|
||||
thumbnailsPanel = ThumbnailsPanel(self)
|
||||
thumbnailsPanel = ThumbnailsPanel(self, Screen.getPreferredScreen())
|
||||
preferencesPanel = PreferencesPanel()
|
||||
Keyboard.listenToGlobalEvents(self)
|
||||
Screen.listenToChanges()
|
||||
}
|
||||
|
||||
func showUiOrSelectNext() {
|
||||
@@ -61,13 +59,7 @@ class Application: NSApplication, NSApplicationDelegate, NSWindowDelegate {
|
||||
}
|
||||
|
||||
@objc func showCenteredPreferencesPanel() {
|
||||
showCenteredPanel(preferencesPanel!)
|
||||
}
|
||||
|
||||
func showCenteredPanel(_ panel: NSPanel) {
|
||||
panel.center()
|
||||
panel.makeKeyAndOrderFront(nil)
|
||||
Application.shared.arrangeInFront(nil)
|
||||
Screen.showCenteredFrontPanel(preferencesPanel!, Screen.getPreferredScreen())
|
||||
}
|
||||
|
||||
func computeOpenWindows() {
|
||||
@@ -109,11 +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 {
|
||||
if !workItem.isCancelled { self.thumbnailsPanel!.computeThumbnails() }
|
||||
if !workItem.isCancelled { self.thumbnailsPanel!.computeThumbnails(currentScreen) }
|
||||
if !workItem.isCancelled { self.thumbnailsPanel!.highlightCellAt(step) }
|
||||
if !workItem.isCancelled { self.showCenteredPanel(self.thumbnailsPanel!) }
|
||||
if !workItem.isCancelled { Screen.showCenteredFrontPanel(self.thumbnailsPanel!, currentScreen, true) }
|
||||
}
|
||||
workItems.append(workItem)
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay!, execute: workItem)
|
||||
|
||||
@@ -37,10 +37,10 @@ class Cell: NSCollectionViewItem {
|
||||
|
||||
|
||||
|
||||
func updateWithNewContent(_ element: OpenWindow, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback) {
|
||||
func updateWithNewContent(_ element: OpenWindow, _ mouseDownCallback: @escaping MouseDownCallback, _ mouseMovedCallback: @escaping MouseMovedCallback, _ screen: NSScreen) {
|
||||
openWindow = element
|
||||
thumbnail.image = element.thumbnail
|
||||
let (width, height) = computeDownscaledSize(element.thumbnail)
|
||||
let (width, height) = computeDownscaledSize(element.thumbnail, screen)
|
||||
thumbnail.image!.size = NSSize(width: width, height: height)
|
||||
thumbnail.frame.size = NSSize(width: width, height: height)
|
||||
icon.image = element.icon
|
||||
|
||||
@@ -1,6 +1,17 @@
|
||||
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")
|
||||
}
|
||||
|
||||
override func layoutAttributesForElements(in rect: CGRect) -> [NSCollectionViewLayoutAttributes] {
|
||||
let attributes = super.layoutAttributesForElements(in: rect)
|
||||
if attributes.isEmpty {
|
||||
@@ -14,7 +25,7 @@ class CollectionViewCenterFlowLayout: NSCollectionViewFlowLayout {
|
||||
var widestRow = CGFloat(0)
|
||||
var totalHeight = CGFloat(0)
|
||||
attributes.enumerated().forEach {
|
||||
let isNewRow = abs($1.frame.origin.y - currentRowY) > Preferences.thumbnailMaxHeight
|
||||
let isNewRow = abs($1.frame.origin.y - currentRowY) > Screen.calcThumbnailMaxSize(currentScreen).height
|
||||
if isNewRow {
|
||||
computeOriginXForAllItems(currentRowWidth - minimumInteritemSpacing, previousRowMaxY, currentRow)
|
||||
currentRow.removeAll()
|
||||
|
||||
@@ -6,8 +6,6 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
|
||||
var cellPadding: NSTextView?
|
||||
var cellBorderWidth: NSTextView?
|
||||
var maxThumbnailsPerRow: NSTextView?
|
||||
var thumbnailMaxWidth: NSTextView?
|
||||
var thumbnailMaxHeight: NSTextView?
|
||||
var iconSize: NSTextView?
|
||||
var fontHeight: NSTextView?
|
||||
var interItemPadding: NSTextView?
|
||||
@@ -15,6 +13,7 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
|
||||
var windowDisplayDelay: NSTextView?
|
||||
var metaKey: NSPopUpButton?
|
||||
var theme: NSPopUpButton?
|
||||
var showOnScreen: NSPopUpButton?
|
||||
var inputsMap = [NSTextView: String]()
|
||||
|
||||
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
|
||||
@@ -34,6 +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)
|
||||
]
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
|
||||
let gridView = NSGridView(views: rows)
|
||||
gridView.setContentHuggingPriority(.defaultLow, for: .horizontal)
|
||||
gridView.setContentHuggingPriority(.defaultLow, for: .vertical)
|
||||
gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 360).isActive = true
|
||||
gridView.widthAnchor.constraint(greaterThanOrEqualToConstant: 380).isActive = true
|
||||
gridView.addRow(with: [warningLabel, NSGridCell.emptyContentView])
|
||||
gridView.mergeCells(inHorizontalRange: NSRange(location: 0, length: 2), verticalRange: NSRange(location: gridView.numberOfRows - 1, length: 1))
|
||||
return gridView
|
||||
@@ -61,7 +61,7 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
|
||||
input.delegate = self
|
||||
input.font = Preferences.font
|
||||
input.string = Preferences.rawValues[rawName]!
|
||||
input.widthAnchor.constraint(equalToConstant: 32).isActive = true
|
||||
input.widthAnchor.constraint(equalToConstant: 40).isActive = true
|
||||
self[keyPath: keyPath] = input
|
||||
inputsMap[input] = rawName
|
||||
return [label, input]
|
||||
@@ -86,6 +86,8 @@ class PreferencesPanel: NSPanel, NSTextViewDelegate {
|
||||
try! Preferences.updateAndValidateFromString("theme", popUpButton.titleOfSelectedItem!)
|
||||
case metaKey:
|
||||
try! Preferences.updateAndValidateFromString("metaKey", popUpButton.titleOfSelectedItem!)
|
||||
case showOnScreen:
|
||||
try! Preferences.updateAndValidateFromString("showOnScreen", popUpButton.titleOfSelectedItem!)
|
||||
default:
|
||||
throw "Tried to update an unknown popUpButton: '\(popUpButton)' = '\(popUpButton.titleOfSelectedItem!)'"
|
||||
}
|
||||
|
||||
@@ -5,14 +5,16 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
var collectionView_: NSCollectionView!
|
||||
var application: Application?
|
||||
let cellId = NSUserInterfaceItemIdentifier("Cell")
|
||||
var currentScreen: NSScreen?
|
||||
|
||||
override init(contentRect: NSRect, styleMask style: StyleMask, backing backingStoreType: BackingStoreType, defer flag: Bool) {
|
||||
super.init(contentRect: contentRect, styleMask: style, backing: backingStoreType, defer: flag)
|
||||
}
|
||||
|
||||
convenience init(_ application: Application) {
|
||||
convenience init(_ application: Application, _ currentScreen: NSScreen) {
|
||||
self.init()
|
||||
self.application = application
|
||||
self.currentScreen = currentScreen
|
||||
isFloatingPanel = true
|
||||
animationBehavior = .none
|
||||
hidesOnDeactivate = false
|
||||
@@ -49,29 +51,29 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
}
|
||||
|
||||
func makeLayout() -> CollectionViewCenterFlowLayout {
|
||||
let layout = CollectionViewCenterFlowLayout()
|
||||
layout.estimatedItemSize = NSSize(width: Preferences.thumbnailMaxWidth, height: Preferences.thumbnailMaxHeight)
|
||||
let layout = CollectionViewCenterFlowLayout(currentScreen!)
|
||||
layout.estimatedItemSize = Screen.calcThumbnailMaxSize(currentScreen!)
|
||||
layout.minimumInteritemSpacing = 5
|
||||
layout.minimumLineSpacing = 5
|
||||
return layout
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
|
||||
// debugPrint("collectionView: count items", openWindows.count)
|
||||
// debugPrint("collectionView: count items", application!.openWindows.count)
|
||||
return application!.openWindows.count
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
|
||||
// debugPrint("collectionView: make item", indexPath.item)
|
||||
let item = collectionView.makeItem(withIdentifier: cellId, for: indexPath) as! Cell
|
||||
item.updateWithNewContent(application!.openWindows[indexPath.item], application!.focusSelectedWindow, application!.thumbnailsPanel!.highlightCell)
|
||||
item.updateWithNewContent(application!.openWindows[indexPath.item], application!.focusSelectedWindow, application!.thumbnailsPanel!.highlightCell, currentScreen!)
|
||||
return item
|
||||
}
|
||||
|
||||
func collectionView(_ collectionView: NSCollectionView, layout collectionViewLayout: NSCollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> NSSize {
|
||||
// debugPrint("collectionView: item size")
|
||||
if indexPath.item < application!.openWindows.count {
|
||||
let (width, height) = computeDownscaledSize(application!.openWindows[indexPath.item].thumbnail)
|
||||
let (width, height) = computeDownscaledSize(application!.openWindows[indexPath.item].thumbnail, currentScreen!)
|
||||
return NSSize(width: CGFloat(width) + Preferences.cellPadding * 2, height: CGFloat(height) + max(Preferences.fontHeight!, Preferences.iconSize!) + Preferences.interItemPadding + Preferences.cellPadding * 2)
|
||||
}
|
||||
return .zero
|
||||
@@ -91,10 +93,10 @@ class ThumbnailsPanel: NSPanel, NSCollectionViewDataSource, NSCollectionViewDele
|
||||
}
|
||||
}
|
||||
|
||||
func computeThumbnails() {
|
||||
let maxSize = NSSize(width: NSScreen.main!.frame.width * Preferences.maxScreenUsage!, height: NSScreen.main!.frame.height * Preferences.maxScreenUsage!)
|
||||
collectionView_!.setFrameSize(maxSize)
|
||||
collectionView_!.collectionViewLayout!.invalidateLayout()
|
||||
func computeThumbnails(_ currentScreen: NSScreen) {
|
||||
self.currentScreen = currentScreen
|
||||
collectionView_!.setFrameSize(Screen.calcFrameMaxSize(currentScreen))
|
||||
collectionView_!.collectionViewLayout = makeLayout()
|
||||
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