Add optional single-click focus for inactive panes (#1796)
* Add failing first-click pane focus tests * Enable optional first-click focus for inactive panes * Address PR review follow-ups --------- Co-authored-by: Lawrence Chen <lawrencecchen@users.noreply.github.com>
This commit is contained in:
parent
5cab7c4a7b
commit
7cbac356fc
7 changed files with 221 additions and 1 deletions
|
|
@ -97,6 +97,7 @@
|
|||
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */; };
|
||||
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */; };
|
||||
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */; };
|
||||
F1C1AA21B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1C1AA20B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift */; };
|
||||
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */; };
|
||||
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008380 /* BrowserFindJavaScriptTests.swift */; };
|
||||
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5008382 /* CommandPaletteSearchEngineTests.swift */; };
|
||||
|
|
@ -262,6 +263,7 @@
|
|||
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceContentViewVisibilityTests.swift; sourceTree = "<group>"; };
|
||||
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocketControlPasswordStoreTests.swift; sourceTree = "<group>"; };
|
||||
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GhosttyEnsureFocusWindowActivationTests.swift; sourceTree = "<group>"; };
|
||||
F1C1AA20B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InactivePaneFirstClickFocusTests.swift; sourceTree = "<group>"; };
|
||||
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspaceStressProfileTests.swift; sourceTree = "<group>"; };
|
||||
A5008380 /* BrowserFindJavaScriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowserFindJavaScriptTests.swift; sourceTree = "<group>"; };
|
||||
A5008382 /* CommandPaletteSearchEngineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommandPaletteSearchEngineTests.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -516,6 +518,7 @@
|
|||
F7000001A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift */,
|
||||
F8000001A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift */,
|
||||
F9000001A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift */,
|
||||
F1C1AA20B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift */,
|
||||
FA000001A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift */,
|
||||
A5008380 /* BrowserFindJavaScriptTests.swift */,
|
||||
A5008382 /* CommandPaletteSearchEngineTests.swift */,
|
||||
|
|
@ -775,6 +778,7 @@
|
|||
F7000000A1B2C3D4E5F60718 /* WorkspaceContentViewVisibilityTests.swift in Sources */,
|
||||
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */,
|
||||
F9000000A1B2C3D4E5F60718 /* GhosttyEnsureFocusWindowActivationTests.swift in Sources */,
|
||||
F1C1AA21B7E84D10A1C10001 /* InactivePaneFirstClickFocusTests.swift in Sources */,
|
||||
FA000000A1B2C3D4E5F60718 /* WorkspaceStressProfileTests.swift in Sources */,
|
||||
A5008381 /* BrowserFindJavaScriptTests.swift in Sources */,
|
||||
A5008383 /* CommandPaletteSearchEngineTests.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -43335,6 +43335,57 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings.app.paneFirstClickFocus": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Focus Pane on First Click"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "最初のクリックでペインにフォーカス"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.paneFirstClickFocus.subtitleOff": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "When cmux is inactive, the first click only activates the window. Click again to focus the pane."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmuxが非アクティブのとき、最初のクリックはウインドウをアクティブにするだけです。ペインにフォーカスするにはもう一度クリックします。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.paneFirstClickFocus.subtitleOn": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "When cmux is inactive, clicking a pane activates the window and focuses that pane in one click."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "cmuxが非アクティブのとき、ペインをクリックすると1回でウインドウをアクティブにしてそのペインへフォーカスします。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.openSidebarPRLinks": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
|
|||
|
|
@ -4639,6 +4639,10 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
_ = inputContext.perform(updateSelectionSelector)
|
||||
}
|
||||
|
||||
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
||||
PaneFirstClickFocusSettings.isEnabled()
|
||||
}
|
||||
|
||||
override var acceptsFirstResponder: Bool { true }
|
||||
|
||||
override func becomeFirstResponder() -> Bool {
|
||||
|
|
|
|||
|
|
@ -67,6 +67,10 @@ final class CmuxWebView: WKWebView {
|
|||
}
|
||||
var debugPointerFocusAllowanceDepth: Int { pointerFocusAllowanceDepth }
|
||||
|
||||
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
||||
PaneFirstClickFocusSettings.isEnabled()
|
||||
}
|
||||
|
||||
override func becomeFirstResponder() -> Bool {
|
||||
guard allowsFirstResponderAcquisitionEffective else {
|
||||
#if DEBUG
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ private struct MarkdownPointerObserver: NSViewRepresentable {
|
|||
final class MarkdownPanelPointerObserverView: NSView {
|
||||
var onPointerDown: (() -> Void)?
|
||||
private var eventMonitor: Any?
|
||||
private weak var forwardedMouseTarget: NSView?
|
||||
|
||||
override var mouseDownCanMoveWindow: Bool { false }
|
||||
|
||||
|
|
@ -326,7 +327,29 @@ final class MarkdownPanelPointerObserverView: NSView {
|
|||
}
|
||||
|
||||
override func hitTest(_ point: NSPoint) -> NSView? {
|
||||
nil
|
||||
guard PaneFirstClickFocusSettings.isEnabled(),
|
||||
window?.isKeyWindow != true,
|
||||
bounds.contains(point) else { return nil }
|
||||
return self
|
||||
}
|
||||
|
||||
override func acceptsFirstMouse(for event: NSEvent?) -> Bool {
|
||||
PaneFirstClickFocusSettings.isEnabled()
|
||||
}
|
||||
|
||||
override func mouseDown(with event: NSEvent) {
|
||||
onPointerDown?()
|
||||
forwardedMouseTarget = forwardedTarget(for: event)
|
||||
forwardedMouseTarget?.mouseDown(with: event)
|
||||
}
|
||||
|
||||
override func mouseDragged(with event: NSEvent) {
|
||||
forwardedMouseTarget?.mouseDragged(with: event)
|
||||
}
|
||||
|
||||
override func mouseUp(with event: NSEvent) {
|
||||
forwardedMouseTarget?.mouseUp(with: event)
|
||||
forwardedMouseTarget = nil
|
||||
}
|
||||
|
||||
func shouldHandle(_ event: NSEvent) -> Bool {
|
||||
|
|
@ -334,6 +357,9 @@ final class MarkdownPanelPointerObserverView: NSView {
|
|||
let window,
|
||||
event.window === window,
|
||||
!isHiddenOrHasHiddenAncestor else { return false }
|
||||
if PaneFirstClickFocusSettings.isEnabled(), window.isKeyWindow != true {
|
||||
return false
|
||||
}
|
||||
let point = convert(event.locationInWindow, from: nil)
|
||||
return bounds.contains(point)
|
||||
}
|
||||
|
|
@ -352,4 +378,24 @@ final class MarkdownPanelPointerObserverView: NSView {
|
|||
self?.handleEventIfNeeded(event) ?? event
|
||||
}
|
||||
}
|
||||
|
||||
private func forwardedTarget(for event: NSEvent) -> NSView? {
|
||||
guard let window else {
|
||||
#if DEBUG
|
||||
NSLog("MarkdownPanelPointerObserverView.forwardedTarget skipped, window=0 contentView=0")
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
guard let contentView = window.contentView else {
|
||||
#if DEBUG
|
||||
NSLog("MarkdownPanelPointerObserverView.forwardedTarget skipped, window=1 contentView=0")
|
||||
#endif
|
||||
return nil
|
||||
}
|
||||
isHidden = true
|
||||
defer { isHidden = false }
|
||||
let point = contentView.convert(event.locationInWindow, from: nil)
|
||||
let target = contentView.hitTest(point)
|
||||
return target === self ? nil : target
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,15 @@ enum WorkspaceButtonFadeSettings {
|
|||
}
|
||||
}
|
||||
|
||||
enum PaneFirstClickFocusSettings {
|
||||
static let enabledKey = "paneFirstClickFocus.enabled"
|
||||
static let defaultEnabled = false
|
||||
|
||||
static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
|
||||
defaults.object(forKey: enabledKey) as? Bool ?? defaultEnabled
|
||||
}
|
||||
}
|
||||
|
||||
enum UITestLaunchManifest {
|
||||
static let argumentName = "-cmuxUITestLaunchManifest"
|
||||
|
||||
|
|
@ -3809,6 +3818,8 @@ struct SettingsView: View {
|
|||
@AppStorage(WorkspacePlacementSettings.placementKey) private var newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||
@AppStorage(LastSurfaceCloseShortcutSettings.key)
|
||||
private var closeWorkspaceOnLastSurfaceShortcut = LastSurfaceCloseShortcutSettings.defaultValue
|
||||
@AppStorage(PaneFirstClickFocusSettings.enabledKey)
|
||||
private var paneFirstClickFocusEnabled = PaneFirstClickFocusSettings.defaultEnabled
|
||||
@AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||
@AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey)
|
||||
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
|
||||
|
|
@ -3902,6 +3913,19 @@ struct SettingsView: View {
|
|||
)
|
||||
}
|
||||
|
||||
private var paneFirstClickFocusSubtitle: String {
|
||||
if paneFirstClickFocusEnabled {
|
||||
return String(
|
||||
localized: "settings.app.paneFirstClickFocus.subtitleOn",
|
||||
defaultValue: "When cmux is inactive, clicking a pane activates the window and focuses that pane in one click."
|
||||
)
|
||||
}
|
||||
return String(
|
||||
localized: "settings.app.paneFirstClickFocus.subtitleOff",
|
||||
defaultValue: "When cmux is inactive, the first click only activates the window. Click again to focus the pane."
|
||||
)
|
||||
}
|
||||
|
||||
private var selectedSidebarActiveTabIndicatorStyle: SidebarActiveTabIndicatorStyle {
|
||||
SidebarActiveTabIndicatorSettings.resolvedStyle(rawValue: sidebarActiveTabIndicatorStyle)
|
||||
}
|
||||
|
|
@ -4371,6 +4395,20 @@ struct SettingsView: View {
|
|||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.app.paneFirstClickFocus", defaultValue: "Focus Pane on First Click"),
|
||||
subtitle: paneFirstClickFocusSubtitle
|
||||
) {
|
||||
Toggle("", isOn: $paneFirstClickFocusEnabled)
|
||||
.labelsHidden()
|
||||
.controlSize(.small)
|
||||
.accessibilityLabel(
|
||||
String(localized: "settings.app.paneFirstClickFocus", defaultValue: "Focus Pane on First Click")
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.app.reorderOnNotification", defaultValue: "Reorder on Notification"),
|
||||
subtitle: String(localized: "settings.app.reorderOnNotification.subtitle", defaultValue: "Move workspaces to the top when they receive a notification. Disable for stable shortcut positions.")
|
||||
|
|
@ -5530,6 +5568,7 @@ struct SettingsView: View {
|
|||
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyTitlebarControlsVisibilityModeKey)
|
||||
defaults.removeObject(forKey: WorkspaceButtonFadeSettings.legacyPaneTabBarControlsVisibilityModeKey)
|
||||
closeWorkspaceOnLastSurfaceShortcut = LastSurfaceCloseShortcutSettings.defaultValue
|
||||
paneFirstClickFocusEnabled = PaneFirstClickFocusSettings.defaultEnabled
|
||||
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||
sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
|
||||
sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
|
||||
|
|
|
|||
72
cmuxTests/InactivePaneFirstClickFocusTests.swift
Normal file
72
cmuxTests/InactivePaneFirstClickFocusTests.swift
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import XCTest
|
||||
import AppKit
|
||||
import WebKit
|
||||
|
||||
#if canImport(cmux_DEV)
|
||||
@testable import cmux_DEV
|
||||
#elseif canImport(cmux)
|
||||
@testable import cmux
|
||||
#endif
|
||||
|
||||
@MainActor
|
||||
final class InactivePaneFirstClickFocusTests: XCTestCase {
|
||||
private let settingsKey = "paneFirstClickFocus.enabled"
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
UserDefaults.standard.removeObject(forKey: settingsKey)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
UserDefaults.standard.removeObject(forKey: settingsKey)
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
func testTerminalViewAcceptsFirstMouseWhenSettingEnabled() {
|
||||
UserDefaults.standard.set(true, forKey: settingsKey)
|
||||
|
||||
let view = GhosttyNSView(frame: .zero)
|
||||
|
||||
XCTAssertTrue(view.acceptsFirstMouse(for: nil))
|
||||
}
|
||||
|
||||
func testTerminalViewRejectsFirstMouseWhenSettingDisabled() {
|
||||
UserDefaults.standard.set(false, forKey: settingsKey)
|
||||
|
||||
let view = GhosttyNSView(frame: .zero)
|
||||
|
||||
XCTAssertFalse(view.acceptsFirstMouse(for: nil))
|
||||
}
|
||||
|
||||
func testBrowserViewAcceptsFirstMouseWhenSettingEnabled() {
|
||||
UserDefaults.standard.set(true, forKey: settingsKey)
|
||||
|
||||
let view = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
|
||||
|
||||
XCTAssertTrue(view.acceptsFirstMouse(for: nil))
|
||||
}
|
||||
|
||||
func testBrowserViewRejectsFirstMouseWhenSettingDisabled() {
|
||||
UserDefaults.standard.set(false, forKey: settingsKey)
|
||||
|
||||
let view = CmuxWebView(frame: .zero, configuration: WKWebViewConfiguration())
|
||||
|
||||
XCTAssertFalse(view.acceptsFirstMouse(for: nil))
|
||||
}
|
||||
|
||||
func testMarkdownPointerObserverAcceptsFirstMouseWhenSettingEnabled() {
|
||||
UserDefaults.standard.set(true, forKey: settingsKey)
|
||||
|
||||
let view = MarkdownPanelPointerObserverView(frame: .zero)
|
||||
|
||||
XCTAssertTrue(view.acceptsFirstMouse(for: nil))
|
||||
}
|
||||
|
||||
func testMarkdownPointerObserverRejectsFirstMouseWhenSettingDisabled() {
|
||||
UserDefaults.standard.set(false, forKey: settingsKey)
|
||||
|
||||
let view = MarkdownPanelPointerObserverView(frame: .zero)
|
||||
|
||||
XCTAssertFalse(view.acceptsFirstMouse(for: nil))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue