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:
Mariusz Bieniek authored and lwouis committed 2019-11-01 23:20:53 +09:00
1 parent a184ad5309
commit 6c9304735a
8 files changed
+99 -44

No files matched your search

+16 -4
View File
@@ -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)'"
}
+40 -6
View File
@@ -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)
}
}
+6 -5
View File
@@ -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)))
}
}
+5 -12
View File
@@ -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)
+2 -2
View File
@@ -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 -4
View File
@@ -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!)'"
}
+12 -10
View File
@@ -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))