diff --git a/src/logic/Window.swift b/src/logic/Window.swift index 974b6445..b8ea8b63 100644 --- a/src/logic/Window.swift +++ b/src/logic/Window.swift @@ -14,6 +14,7 @@ class Window { var axUiElement: AXUIElement var application: Application var axObserver: AXObserver? + var row: Int? static let notifications = [ kAXUIElementDestroyedNotification, diff --git a/src/ui/App.swift b/src/ui/App.swift index fa15e2ef..576110c6 100644 --- a/src/ui/App.swift +++ b/src/ui/App.swift @@ -134,11 +134,15 @@ class App: NSApplication, NSApplicationDelegate { func showUi() { appIsBeingUsed = true appIsBeingUsed = true - DispatchQueue.main.async { () -> () in self.showUiOrCycleSelection(0) } + DispatchQueue.main.async { () -> () in self.showUiOrCycleSelection(.neutral) } } - func cycleSelection(_ step: Int) { - Windows.cycleFocusedWindowIndex(step) + func cycleSelection(_ direction: Direction) { + if direction == .left || direction == .right || direction == .neutral { + Windows.cycleFocusedWindowIndex(direction.step()) + } else { + thumbnailsPanel.thumbnailsView.navigateUpOrDown(direction) + } } func focusSelectedWindow(_ window: Window?) { @@ -183,8 +187,8 @@ class App: NSApplication, NSApplicationDelegate { } } - func showUiOrCycleSelection(_ step: Int) { - debugPrint("showUiOrCycleSelection", step) + func showUiOrCycleSelection(_ direction: Direction) { + debugPrint("showUiOrCycleSelection", direction) if isFirstSummon { debugPrint("showUiOrCycleSelection: isFirstSummon") isFirstSummon = false @@ -197,12 +201,12 @@ class App: NSApplication, NSApplicationDelegate { Windows.refreshWhichWindowsToShowTheUser(screen) if Windows.list.first(where: { $0.shouldShowTheUser }) == nil { hideUi(); return } Windows.updateFocusedWindowIndex(0) - Windows.cycleFocusedWindowIndex(step) + Windows.cycleFocusedWindowIndex(direction.step()) DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + Preferences.windowDisplayDelay) { () -> () in self.rebuildUi() } } else { - cycleSelection(step) + cycleSelection(direction) } } diff --git a/src/ui/main-window/ThumbnailsView.swift b/src/ui/main-window/ThumbnailsView.swift index e238046f..260f8f09 100644 --- a/src/ui/main-window/ThumbnailsView.swift +++ b/src/ui/main-window/ThumbnailsView.swift @@ -3,6 +3,7 @@ import Cocoa class ThumbnailsView: NSVisualEffectView { let scrollView = ScrollView() static var recycledViews = [ThumbnailView]() + var rows = [[ThumbnailView]]() convenience init() { self.init(frame: .zero) @@ -15,6 +16,26 @@ class ThumbnailsView: NSVisualEffectView { (1...100).forEach { _ in ThumbnailsView.recycledViews.append(ThumbnailView()) } } + func nextRow(_ direction: Direction) -> [ThumbnailView] { + let step = direction == .down ? 1 : -1 + let indexAfterStep = (Windows.focusedWindow()!.row! + step) % rows.count + let targetRowIndex = indexAfterStep < 0 ? rows.count + indexAfterStep : indexAfterStep + return rows[targetRowIndex] + } + + func navigateUpOrDown(_ direction: Direction) { + let focusedViewFrame = ThumbnailsView.recycledViews[Windows.focusedWindowIndex].frame + let originCenter = NSMidX(focusedViewFrame) + let targetRow = nextRow(direction) + let leftSide = originCenter < NSMidX(frame) + let iterable = leftSide ? targetRow : targetRow.reversed() + let targetView = iterable.first(where: { + leftSide ? NSMaxX($0.frame) > originCenter : NSMinX($0.frame) < originCenter + }) ?? iterable.last! + let targetIndex = ThumbnailsView.recycledViews.firstIndex(of: targetView)! + Windows.updateFocusedWindowIndex(targetIndex) + } + func updateItems(_ screen: NSScreen) { let widthMax = ThumbnailsPanel.widthMax(screen).rounded() let heightMax = ThumbnailsPanel.heightMax(screen).rounded() @@ -24,6 +45,8 @@ class ThumbnailsView: NSVisualEffectView { var maxX = CGFloat(0) var maxY = height var newViews = [ThumbnailView]() + rows.removeAll() + rows.append([ThumbnailView]()) for (index, window) in Windows.list.enumerated() { guard App.app.appIsBeingUsed else { return } guard window.shouldShowTheUser else { continue } @@ -37,12 +60,15 @@ class ThumbnailsView: NSVisualEffectView { currentX = CGFloat(0) currentY = (currentY + Preferences.interCellPadding + height).rounded(.down) maxY = max(currentY + height, maxY) + rows.append([ThumbnailView]()) } else { maxX = max(currentX + width, maxX) } view.frame.origin = CGPoint(x: currentX, y: currentY) currentX = (currentX + Preferences.interCellPadding + width).rounded(.down) newViews.append(view) + rows[rows.count - 1].append(view) + window.row = rows.count - 1 } scrollView.documentView!.subviews = newViews frame.size = NSSize(width: min(maxX, widthMax) + Preferences.windowPadding * 2, height: min(maxY, heightMax) + Preferences.windowPadding * 2) @@ -126,3 +152,20 @@ class ScrollView: NSScrollView { class FlippedView: NSView { override var isFlipped: Bool { true } } + +enum Direction { + case neutral + case left + case right + case up + case down + + func step() -> Int { + if self == .left { + return -1 + } else if self == .right { + return 1 + } + return 0 + } +} diff --git a/src/ui/preferences-window/tabs/GeneralTab.swift b/src/ui/preferences-window/tabs/GeneralTab.swift index 458e57e8..2a2ee673 100644 --- a/src/ui/preferences-window/tabs/GeneralTab.swift +++ b/src/ui/preferences-window/tabs/GeneralTab.swift @@ -11,10 +11,12 @@ class GeneralTab: NSViewController, PreferencePane { static var shortcutsDependentOnHoldShortcut = [NSControl]() static var shortcutsActionsBlocks = [ "holdShortcut": { App.app.focusTarget() }, - "nextWindowShortcut": { App.app.showUiOrCycleSelection(1) }, - "previousWindowShortcut": { App.app.showUiOrCycleSelection(-1) }, - "→": { App.app.cycleSelection(1) }, - "←": { App.app.cycleSelection(-1) }, + "nextWindowShortcut": { App.app.showUiOrCycleSelection(.right) }, + "previousWindowShortcut": { App.app.showUiOrCycleSelection(.left) }, + "→": { App.app.cycleSelection(.right) }, + "←": { App.app.cycleSelection(.left) }, + "↑": { App.app.cycleSelection(.up) }, + "↓": { App.app.cycleSelection(.down) }, "cancelShortcut": { App.app.hideUi() }, "closeWindowShortcut": { App.app.closeSelectedWindow() }, "minDeminWindowShortcut": { App.app.minDeminSelectedWindow() }, @@ -86,7 +88,7 @@ class GeneralTab: NSViewController, PreferencePane { App.app.isFirstSummon = true } let fn = shortcutsActionsBlocks[controlId]! - let isShortcutAffectingTheUi = ["previousWindowShortcut", "nextWindowShortcut", "←", "→", "holdShortcut"].contains(controlId) + let isShortcutAffectingTheUi = ["previousWindowShortcut", "nextWindowShortcut", "←", "→", "↑", "↓", "holdShortcut"].contains(controlId) if isShortcutAffectingTheUi || isShortcutClosingTheUi { DispatchQueue.main.async { () -> () in fn() } } else { @@ -115,12 +117,11 @@ class GeneralTab: NSViewController, PreferencePane { } @objc static func arrowKeysEnabledCallback(_ sender: NSControl) { + let keys = ["←", "→", "↑", "↓"] if (sender as! NSButton).state == .on { - addShortcut(.down, Shortcut(keyEquivalent: Preferences.holdShortcut + "→")!, "→") - addShortcut(.down, Shortcut(keyEquivalent: Preferences.holdShortcut + "←")!, "←") + keys.forEach { addShortcut(.down, Shortcut(keyEquivalent: Preferences.holdShortcut + $0)!, $0) } } else { - removeShortcutIfExists("→", .down) - removeShortcutIfExists("←", .down) + keys.forEach { removeShortcutIfExists($0, .down) } } }