feat: separate mouse hover from keyboard selection
closes #2078, closes #1617
This commit is contained in:
1 parent
c386a0c439
commit
3fb9a195f8
6 files changed
+75
-52
No files matched your search
@@ -149,11 +149,8 @@ class Preferences {
|
||||
static var menubarIcon: MenubarIconPreference { defaults.macroPref("menubarIcon", MenubarIconPreference.allCases) }
|
||||
|
||||
// derived values
|
||||
static var cellBorderWidth: CGFloat { theme.themeParameters.cellBorderWidth }
|
||||
static var cellCornerRadius: CGFloat { theme.themeParameters.cellCornerRadius }
|
||||
static var windowCornerRadius: CGFloat { theme.themeParameters.windowCornerRadius }
|
||||
static var highlightBorderColor: NSColor { theme.themeParameters.highlightBorderColor }
|
||||
static var highlightBackgroundColor: NSColor { theme.themeParameters.highlightBackgroundColor }
|
||||
static var font: NSFont { NSFont.systemFont(ofSize: fontHeight) }
|
||||
|
||||
static func initialize() {
|
||||
@@ -429,11 +426,8 @@ protocol MacroPreference {
|
||||
|
||||
struct ThemeParameters {
|
||||
let label: String
|
||||
let cellBorderWidth: CGFloat
|
||||
let cellCornerRadius: CGFloat
|
||||
let windowCornerRadius: CGFloat
|
||||
let highlightBorderColor: NSColor
|
||||
let highlightBackgroundColor: NSColor
|
||||
}
|
||||
|
||||
typealias LocalizedString = String
|
||||
@@ -570,8 +564,8 @@ enum ThemePreference: String, CaseIterable, MacroPreference {
|
||||
|
||||
var themeParameters: ThemeParameters {
|
||||
switch self {
|
||||
case .macOs: return ThemeParameters(label: localizedString, cellBorderWidth: 0, cellCornerRadius: 10, windowCornerRadius: 23, highlightBorderColor: .clear, highlightBackgroundColor: NSColor(red: 0, green: 0, blue: 0, alpha: 0.4))
|
||||
case .windows10: return ThemeParameters(label: localizedString, cellBorderWidth: 2, cellCornerRadius: 0, windowCornerRadius: 0, highlightBorderColor: .white, highlightBackgroundColor: .clear)
|
||||
case .macOs: return ThemeParameters(label: localizedString, cellCornerRadius: 10, windowCornerRadius: 23)
|
||||
case .windows10: return ThemeParameters(label: localizedString, cellCornerRadius: 0, windowCornerRadius: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+33
-10
@@ -3,6 +3,7 @@ import Cocoa
|
||||
class Windows {
|
||||
static var list = [Window]()
|
||||
static var focusedWindowIndex = Int(0)
|
||||
static var hoveredWindowIndex: Int?
|
||||
// the first few thumbnails are the most commonly looked at; we pay special attention to them
|
||||
static let criticalFirstThumbnails = 3
|
||||
|
||||
@@ -24,14 +25,23 @@ class Windows {
|
||||
}
|
||||
}
|
||||
|
||||
static func setInitialFocusedWindowIndex() {
|
||||
static func setInitialFocusedAndHoveredWindowIndex() {
|
||||
let oldIndex = focusedWindowIndex
|
||||
focusedWindowIndex = 0
|
||||
ThumbnailsView.highlight(oldIndex)
|
||||
if let oldIndex = hoveredWindowIndex {
|
||||
hoveredWindowIndex = nil
|
||||
ThumbnailsView.highlight(oldIndex)
|
||||
}
|
||||
if let app = Applications.find(NSWorkspace.shared.frontmostApplication?.processIdentifier),
|
||||
app.focusedWindow == nil,
|
||||
let lastFocusedWindowIndex = getLastFocusedWindowIndex() {
|
||||
updateFocusedWindowIndex(lastFocusedWindowIndex)
|
||||
} else {
|
||||
updateFocusedWindowIndex(0)
|
||||
cycleFocusedWindowIndex(1)
|
||||
if focusedWindowIndex == 0 {
|
||||
updateFocusedWindowIndex(0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,21 +99,34 @@ class Windows {
|
||||
return nil
|
||||
}
|
||||
|
||||
static func updateFocusedWindowIndex(_ newIndex: Int) {
|
||||
ThumbnailsView.recycledViews[focusedWindowIndex].highlight(false)
|
||||
focusedWindowIndex = newIndex
|
||||
let focusedView = ThumbnailsView.recycledViews[focusedWindowIndex]
|
||||
focusedView.highlight(true)
|
||||
static func updateFocusedWindowIndex(_ newIndex: Int, _ fromMouse: Bool = false) {
|
||||
if (fromMouse && newIndex == hoveredWindowIndex) || (!fromMouse && newIndex == focusedWindowIndex) {
|
||||
return
|
||||
}
|
||||
if fromMouse {
|
||||
let oldIndex = hoveredWindowIndex
|
||||
hoveredWindowIndex = newIndex
|
||||
if let oldIndex = oldIndex {
|
||||
ThumbnailsView.highlight(oldIndex)
|
||||
}
|
||||
} else {
|
||||
let oldIndex = focusedWindowIndex
|
||||
focusedWindowIndex = newIndex
|
||||
ThumbnailsView.highlight(oldIndex)
|
||||
}
|
||||
let index = fromMouse ? hoveredWindowIndex! : focusedWindowIndex
|
||||
ThumbnailsView.highlight(index)
|
||||
let focusedView = ThumbnailsView.recycledViews[index]
|
||||
App.app.thumbnailsPanel.thumbnailsView.scrollView.contentView.scrollToVisible(focusedView.frame)
|
||||
voiceOverFocusedWindow()
|
||||
voiceOverWindow(index)
|
||||
}
|
||||
|
||||
static func voiceOverFocusedWindow() {
|
||||
static func voiceOverWindow(_ windowIndex: Int = focusedWindowIndex) {
|
||||
guard App.app.appIsBeingUsed && App.app.thumbnailsPanel.isKeyWindow else { return }
|
||||
// it seems that sometimes makeFirstResponder is called before the view is visible
|
||||
// and it creates a delay in showing the main window; calling it with some delay seems to work around this
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(10)) {
|
||||
let window = ThumbnailsView.recycledViews[focusedWindowIndex]
|
||||
let window = ThumbnailsView.recycledViews[windowIndex]
|
||||
if window.window_ != nil && window.window != nil {
|
||||
App.app.thumbnailsPanel.makeFirstResponder(window)
|
||||
}
|
||||
|
||||
+2
-2
@@ -238,7 +238,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
|
||||
thumbnailsPanel.display()
|
||||
guard appIsBeingUsed else { return }
|
||||
currentScreen.repositionPanel(thumbnailsPanel, .appleCentered)
|
||||
Windows.voiceOverFocusedWindow() // at this point ThumbnailViews are assigned to the window, and ready
|
||||
Windows.voiceOverWindow() // at this point ThumbnailViews are assigned to the window, and ready
|
||||
}
|
||||
|
||||
private func refreshSpecificWindows(_ windowsToUpdate: [Window]?, _ currentScreen: NSScreen) -> ()? {
|
||||
@@ -272,7 +272,7 @@ class App: AppCenterApplication, NSApplicationDelegate {
|
||||
Windows.refreshWhichWindowsToShowTheUser(screen)
|
||||
Windows.reorderList()
|
||||
if (!Windows.list.contains { $0.shouldShowTheUser }) { hideUi(); return }
|
||||
Windows.setInitialFocusedWindowIndex()
|
||||
Windows.setInitialFocusedAndHoveredWindowIndex()
|
||||
delayedDisplayScheduled += 1
|
||||
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay) { () -> () in
|
||||
if self.delayedDisplayScheduled == 1 {
|
||||
|
||||
@@ -52,7 +52,7 @@ class CustomRecorderControl: RecorderControl, RecorderControlDelegate {
|
||||
let isArrowKeys = ["←", "→", "↑", "↓"].contains(shortcutAlreadyAssigned.id)
|
||||
let existing = ControlsTab.shortcutControls[shortcutAlreadyAssigned.id]
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .critical
|
||||
alert.alertStyle = .warning
|
||||
alert.messageText = NSLocalizedString("Conflicting shortcut", comment: "")
|
||||
alert.informativeText = String(format: NSLocalizedString("Shortcut already assigned to another action: %@", comment: ""), (isArrowKeys ? "Arrow keys" : existing!.1).replacingOccurrences(of: " ", with: "\u{00A0}"))
|
||||
if !id.starts(with: "holdShortcut") {
|
||||
|
||||
@@ -3,6 +3,7 @@ import Cocoa
|
||||
class ThumbnailView: NSStackView {
|
||||
static let windowsControlSize = CGFloat(16)
|
||||
static let windowsControlSpacing = CGFloat(8)
|
||||
static let highlightBackgroundColor = NSColor(red: 0, green: 0, blue: 0, alpha: 0.4)
|
||||
var window_: Window?
|
||||
var thumbnail = NSImageView()
|
||||
var appIcon = NSImageView()
|
||||
@@ -20,10 +21,11 @@ class ThumbnailView: NSStackView {
|
||||
var mouseUpCallback: (() -> Void)!
|
||||
var mouseMovedCallback: (() -> Void)!
|
||||
var dragAndDropTimer: Timer?
|
||||
var isHighlighted = false
|
||||
var indexInRecycledViews: Int!
|
||||
var shouldShowWindowControls = false
|
||||
var isShowingWindowControls = false
|
||||
var windowlessIcon = FontIcon(.newWindow, NSLocalizedString("App is running but has no open window", comment: ""))
|
||||
var frameInset = Preferences.intraCellPadding
|
||||
|
||||
// for VoiceOver cursor
|
||||
override var canBecomeKeyView: Bool { true }
|
||||
@@ -42,7 +44,7 @@ class ThumbnailView: NSStackView {
|
||||
layer!.backgroundColor = .clear
|
||||
layer!.borderColor = .clear
|
||||
layer!.cornerRadius = Preferences.cellCornerRadius
|
||||
layer!.borderWidth = Preferences.cellBorderWidth
|
||||
layer!.borderWidth = CGFloat(2)
|
||||
edgeInsets = NSEdgeInsets(top: Preferences.intraCellPadding, left: Preferences.intraCellPadding, bottom: Preferences.intraCellPadding, right: Preferences.intraCellPadding)
|
||||
orientation = .vertical
|
||||
let shadow = ThumbnailView.makeShadow(.gray)
|
||||
@@ -95,22 +97,19 @@ class ThumbnailView: NSStackView {
|
||||
}
|
||||
}
|
||||
|
||||
func highlight(_ highlight: Bool) {
|
||||
if isHighlighted != highlight {
|
||||
isHighlighted = highlight
|
||||
if frame != NSRect.zero {
|
||||
highlightOrNot()
|
||||
}
|
||||
func drawHighlight(_ i: Int) {
|
||||
let isFocused = indexInRecycledViews == Windows.focusedWindowIndex
|
||||
let isHovered = indexInRecycledViews == Windows.hoveredWindowIndex
|
||||
layer!.backgroundColor = (Preferences.theme == .macOs && isFocused) || (Preferences.theme == .windows10 && isHovered)
|
||||
? ThumbnailView.highlightBackgroundColor.cgColor : .clear
|
||||
layer!.borderColor = (Preferences.theme == .macOs && isHovered) || (Preferences.theme == .windows10 && isFocused)
|
||||
? NSColor.controlColor.cgColor : .clear
|
||||
let newFrameInset = (isFocused) ? -Preferences.intraCellPadding : Preferences.intraCellPadding
|
||||
if newFrameInset != frameInset {
|
||||
frameInset = newFrameInset
|
||||
frame = frame.insetBy(dx: frameInset, dy: frameInset)
|
||||
}
|
||||
showOrHideWindowControls(false)
|
||||
}
|
||||
|
||||
func highlightOrNot() {
|
||||
layer!.backgroundColor = isHighlighted ? Preferences.highlightBackgroundColor.cgColor : .clear
|
||||
layer!.borderColor = isHighlighted ? Preferences.highlightBorderColor.cgColor : .clear
|
||||
let frameInset: CGFloat = Preferences.intraCellPadding * (isHighlighted ? -1 : 1)
|
||||
frame = frame.insetBy(dx: frameInset, dy: frameInset)
|
||||
let edgeInsets_: CGFloat = Preferences.intraCellPadding * (isHighlighted ? 2 : 1)
|
||||
let edgeInsets_: CGFloat = Preferences.intraCellPadding * (isFocused ? 2 : 1)
|
||||
edgeInsets.top = edgeInsets_
|
||||
edgeInsets.right = edgeInsets_
|
||||
edgeInsets.bottom = edgeInsets_
|
||||
@@ -119,6 +118,7 @@ class ThumbnailView: NSStackView {
|
||||
|
||||
func updateRecycledCellWithNewContent(_ element: Window, _ index: Int, _ newHeight: CGFloat, _ screen: NSScreen) {
|
||||
window_ = element
|
||||
frameInset = Preferences.intraCellPadding
|
||||
assignIfDifferent(&thumbnail.isHidden, Preferences.hideThumbnails || element.isWindowlessApp)
|
||||
if !thumbnail.isHidden {
|
||||
thumbnail.image = element.thumbnail
|
||||
@@ -186,7 +186,7 @@ class ThumbnailView: NSStackView {
|
||||
windowlessIcon.needsDisplay = true
|
||||
}
|
||||
self.mouseUpCallback = { () -> Void in App.app.focusSelectedWindow(element) }
|
||||
self.mouseMovedCallback = { () -> Void in Windows.updateFocusedWindowIndex(index) }
|
||||
self.mouseMovedCallback = { () -> Void in Windows.updateFocusedWindowIndex(index, true) }
|
||||
[quitIcon, closeIcon, minimizeIcon, maximizeIcon].forEach { $0.window_ = element }
|
||||
showOrHideWindowControls(false)
|
||||
// force a display to avoid flickering; see https://github.com/lwouis/alt-tab-macos/issues/197
|
||||
@@ -208,7 +208,7 @@ class ThumbnailView: NSStackView {
|
||||
}
|
||||
dockLabelIcon.setFrameOrigin(NSPoint(
|
||||
x: appIcon.frame.maxX - (dockLabelIcon.fittingSize.width / 2) - (appIcon.frame.width / 7),
|
||||
y: appIcon.frame.maxY - (dockLabelIcon.fittingSize.height / 2) - (appIcon.frame.height / 5)))
|
||||
y: appIcon.frame.maxY - (dockLabelIcon.fittingSize.height / 2) - (appIcon.frame.height / 5)))
|
||||
view.setAccessibilityLabel(getAccessibilityTextForBadge(dockLabel))
|
||||
return true
|
||||
}
|
||||
@@ -263,7 +263,7 @@ class ThumbnailView: NSStackView {
|
||||
|
||||
func mouseMoved() {
|
||||
showOrHideWindowControls(true)
|
||||
if Preferences.mouseHoverEnabled && !isHighlighted {
|
||||
if Preferences.mouseHoverEnabled {
|
||||
mouseMovedCallback()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,14 @@ class ThumbnailsView: NSVisualEffectView {
|
||||
(1...100).forEach { _ in ThumbnailsView.recycledViews.append(ThumbnailView()) }
|
||||
}
|
||||
|
||||
static func highlight(_ indexInRecycledViews: Int) {
|
||||
let v = recycledViews[indexInRecycledViews]
|
||||
v.indexInRecycledViews = indexInRecycledViews
|
||||
if v.frame != NSRect.zero {
|
||||
v.drawHighlight(indexInRecycledViews)
|
||||
}
|
||||
}
|
||||
|
||||
/// using layer!.cornerRadius works but the corners are aliased; this custom approach gives smooth rounded corners
|
||||
/// see https://stackoverflow.com/a/29386935/2249756
|
||||
func updateRoundedCorners(_ cornerRadius: CGFloat) {
|
||||
@@ -179,13 +187,7 @@ class ThumbnailsView: NSVisualEffectView {
|
||||
}
|
||||
|
||||
private func highlightStartView() {
|
||||
_ = Windows.list.enumerated().contains { (index, _) in
|
||||
let view = ThumbnailsView.recycledViews[index]
|
||||
if view.isHighlighted {
|
||||
view.highlightOrNot()
|
||||
}
|
||||
return view.isHighlighted
|
||||
}
|
||||
ThumbnailsView.highlight(Windows.focusedWindowIndex)
|
||||
}
|
||||
|
||||
private func shiftRow(_ maxX: CGFloat, _ rowWidth: CGFloat, _ rowStartIndex: Int, _ index: Int) {
|
||||
@@ -251,11 +253,15 @@ class ScrollView: NSScrollView {
|
||||
}
|
||||
|
||||
override func mouseExited(with event: NSEvent) {
|
||||
previousTarget?.showOrHideWindowControls(false)
|
||||
if let oldIndex = Windows.hoveredWindowIndex {
|
||||
Windows.hoveredWindowIndex = nil
|
||||
ThumbnailsView.highlight(oldIndex)
|
||||
ThumbnailsView.recycledViews[oldIndex].showOrHideWindowControls(false)
|
||||
}
|
||||
}
|
||||
|
||||
// holding shift and using the scrolling wheel will generate a horizontal movement
|
||||
// shift can be part of shortcuts so we force shift scrolls to be vertical
|
||||
/// holding shift and using the scrolling wheel will generate a horizontal movement
|
||||
/// shift can be part of shortcuts so we force shift scrolls to be vertical
|
||||
override func scrollWheel(with event: NSEvent) {
|
||||
if event.modifierFlags.contains(.shift) && event.scrollingDeltaY == 0 {
|
||||
let cgEvent = event.cgEvent!
|
||||
@@ -267,7 +273,7 @@ class ScrollView: NSScrollView {
|
||||
}
|
||||
}
|
||||
|
||||
// force overlay style after a change in System Preference > General > Show scroll bars
|
||||
/// force overlay style after a change in System Preference > General > Show scroll bars
|
||||
private func forceOverlayStyle() {
|
||||
NotificationCenter.default.addObserver(forName: NSScroller.preferredScrollerStyleDidChangeNotification, object: nil, queue: nil) { [weak self] _ in
|
||||
self?.scrollerStyle = .overlay
|
||||
|
||||
Reference in new issue
Block a user