Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions GhosttyTabs.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -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>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
51 changes: 51 additions & 0 deletions Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions Sources/GhosttyTerminalView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 4 additions & 0 deletions Sources/Panels/CmuxWebView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
48 changes: 47 additions & 1 deletion Sources/Panels/MarkdownPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }

Expand All @@ -326,14 +327,39 @@ 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 {
guard event.type == .leftMouseDown,
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)
}
Expand All @@ -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
}
}
39 changes: 39 additions & 0 deletions Sources/cmuxApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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
Comment thread
lawrencecchen marked this conversation as resolved.
@AppStorage(WorkspaceAutoReorderSettings.key) private var workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
@AppStorage(SidebarWorkspaceDetailSettings.hideAllDetailsKey)
private var sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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
Expand Down
72 changes: 72 additions & 0 deletions cmuxTests/InactivePaneFirstClickFocusTests.swift
Original file line number Diff line number Diff line change
@@ -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