Split fade buttons from titlebar visibility
This commit is contained in:
parent
f883299152
commit
70ec1a0915
4 changed files with 224 additions and 54 deletions
|
|
@ -43658,13 +43658,13 @@
|
|||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Hide the workspace title bar and show sidebar or pane actions only on hover."
|
||||
"value": "Hide the folder and active title above pane tabs."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ワークスペースのタイトルバーを隠し、サイドバーやペインタブの操作はホバー時のみ表示します。"
|
||||
"value": "ペインタブの上にあるフォルダ名と現在のタイトルを隠します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -43686,6 +43686,57 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"settings.app.fadeButtons": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Fade Buttons"
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "ボタンをフェード表示"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.fadeButtons.subtitleOff": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Keep action buttons always visible."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "操作ボタンを常に表示します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.fadeButtons.subtitleOn": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
"en": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "Show action buttons only on hover."
|
||||
}
|
||||
},
|
||||
"ja": {
|
||||
"stringUnit": {
|
||||
"state": "translated",
|
||||
"value": "操作ボタンはホバー時のみ表示します。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.app.showPullRequests": {
|
||||
"extractionState": "manual",
|
||||
"localizations": {
|
||||
|
|
|
|||
|
|
@ -257,10 +257,14 @@ struct TitlebarControlsView: View {
|
|||
let onToggleNotifications: () -> Void
|
||||
let onNewTab: () -> Void
|
||||
@AppStorage("titlebarControlsStyle") private var styleRawValue = TitlebarControlsStyle.classic.rawValue
|
||||
@AppStorage(WorkspaceButtonFadeSettings.modeKey)
|
||||
private var workspaceButtonsFadeMode = WorkspaceButtonFadeSettings.defaultMode.rawValue
|
||||
@AppStorage(ShortcutHintDebugSettings.titlebarHintXKey) private var titlebarShortcutHintXOffset = ShortcutHintDebugSettings.defaultTitlebarHintX
|
||||
@AppStorage(ShortcutHintDebugSettings.titlebarHintYKey) private var titlebarShortcutHintYOffset = ShortcutHintDebugSettings.defaultTitlebarHintY
|
||||
@AppStorage(ShortcutHintDebugSettings.alwaysShowHintsKey) private var alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||
@State private var shortcutRefreshTick = 0
|
||||
@State private var isHoveringControls = false
|
||||
@State private var isNotificationsPopoverShown = false
|
||||
@StateObject private var modifierKeyMonitor = TitlebarShortcutHintModifierMonitor()
|
||||
private let titlebarHintRightSafetyShift: CGFloat = 10
|
||||
private let titlebarHintBaseXShift: CGFloat = -10
|
||||
|
|
@ -295,6 +299,17 @@ struct TitlebarControlsView: View {
|
|||
alwaysShowShortcutHints || modifierKeyMonitor.isModifierPressed
|
||||
}
|
||||
|
||||
private var fadeButtonsEnabled: Bool {
|
||||
WorkspaceButtonFadeSettings.mode(for: workspaceButtonsFadeMode) == .enabled
|
||||
}
|
||||
|
||||
private var shouldShowControls: Bool {
|
||||
if !fadeButtonsEnabled {
|
||||
return true
|
||||
}
|
||||
return isHoveringControls || isNotificationsPopoverShown || shouldShowTitlebarShortcutHints
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Force the `.safeHelp(...)` tooltips to re-evaluate when shortcuts are changed in settings.
|
||||
// (The titlebar controls don't otherwise re-render on UserDefaults changes.)
|
||||
|
|
@ -304,15 +319,28 @@ struct TitlebarControlsView: View {
|
|||
controlsGroup(config: config)
|
||||
.padding(.leading, 4)
|
||||
.padding(.trailing, titlebarHintTrailingInset)
|
||||
.contentShape(Rectangle())
|
||||
.opacity(shouldShowControls ? 1 : 0)
|
||||
.allowsHitTesting(shouldShowControls)
|
||||
.animation(.easeInOut(duration: 0.14), value: shouldShowControls)
|
||||
.background(
|
||||
WindowAccessor { window in
|
||||
modifierKeyMonitor.setHostWindow(window)
|
||||
}
|
||||
.frame(width: 0, height: 0)
|
||||
)
|
||||
.onHover { hovering in
|
||||
isHoveringControls = hovering
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification)) { _ in
|
||||
shortcutRefreshTick &+= 1
|
||||
}
|
||||
.onAppear {
|
||||
isNotificationsPopoverShown = AppDelegate.shared?.isNotificationsPopoverShown() ?? false
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .cmuxNotificationsPopoverVisibilityDidChange)) { notification in
|
||||
isNotificationsPopoverShown = (notification.userInfo?[NotificationsPopoverVisibilityUserInfoKey.isShown] as? Bool) ?? false
|
||||
}
|
||||
.onAppear {
|
||||
modifierKeyMonitor.start()
|
||||
}
|
||||
|
|
@ -527,48 +555,24 @@ struct TitlebarControlsView: View {
|
|||
struct HiddenTitlebarSidebarControlsView: View {
|
||||
@ObservedObject var notificationStore: TerminalNotificationStore
|
||||
@StateObject private var viewModel = TitlebarControlsViewModel()
|
||||
@State private var isHoveringControls = false
|
||||
@State private var isNotificationsPopoverShown = false
|
||||
|
||||
private let hostWidth: CGFloat = 124
|
||||
private let hostHeight: CGFloat = 28
|
||||
|
||||
private var shouldShowControls: Bool {
|
||||
isHoveringControls || isNotificationsPopoverShown
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .leading) {
|
||||
Color.clear
|
||||
.frame(width: hostWidth, height: hostHeight)
|
||||
|
||||
TitlebarControlsView(
|
||||
notificationStore: notificationStore,
|
||||
viewModel: viewModel,
|
||||
onToggleSidebar: { _ = AppDelegate.shared?.sidebarState?.toggle() },
|
||||
onToggleNotifications: { [viewModel] in
|
||||
AppDelegate.shared?.toggleNotificationsPopover(
|
||||
animated: true,
|
||||
anchorView: viewModel.notificationsAnchorView
|
||||
)
|
||||
},
|
||||
onNewTab: { _ = AppDelegate.shared?.tabManager?.addTab() }
|
||||
)
|
||||
.opacity(shouldShowControls ? 1 : 0)
|
||||
.allowsHitTesting(shouldShowControls)
|
||||
.animation(.easeInOut(duration: 0.12), value: shouldShowControls)
|
||||
}
|
||||
TitlebarControlsView(
|
||||
notificationStore: notificationStore,
|
||||
viewModel: viewModel,
|
||||
onToggleSidebar: { _ = AppDelegate.shared?.sidebarState?.toggle() },
|
||||
onToggleNotifications: { [viewModel] in
|
||||
AppDelegate.shared?.toggleNotificationsPopover(
|
||||
animated: true,
|
||||
anchorView: viewModel.notificationsAnchorView
|
||||
)
|
||||
},
|
||||
onNewTab: { _ = AppDelegate.shared?.tabManager?.addTab() }
|
||||
)
|
||||
.frame(width: hostWidth, height: hostHeight, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onHover { hovering in
|
||||
isHoveringControls = hovering
|
||||
}
|
||||
.onAppear {
|
||||
isNotificationsPopoverShown = AppDelegate.shared?.isNotificationsPopoverShown() ?? false
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .cmuxNotificationsPopoverVisibilityDidChange)) { notification in
|
||||
isNotificationsPopoverShown = (notification.userInfo?[NotificationsPopoverVisibilityUserInfoKey.isShown] as? Bool) ?? false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -16,6 +16,54 @@ enum WorkspaceTitlebarSettings {
|
|||
}
|
||||
}
|
||||
|
||||
enum WorkspaceButtonFadeSettings {
|
||||
static let modeKey = "workspaceButtonsFadeMode"
|
||||
static let legacyTitlebarControlsVisibilityModeKey = "titlebarControlsVisibilityMode"
|
||||
static let legacyPaneTabBarControlsVisibilityModeKey = "paneTabBarControlsVisibilityMode"
|
||||
|
||||
enum Mode: String {
|
||||
case enabled
|
||||
case disabled
|
||||
}
|
||||
|
||||
static let defaultMode: Mode = .disabled
|
||||
|
||||
static func mode(for rawValue: String?) -> Mode {
|
||||
Mode(rawValue: rawValue ?? "") ?? defaultMode
|
||||
}
|
||||
|
||||
static func isEnabled(defaults: UserDefaults = .standard) -> Bool {
|
||||
mode(for: defaults.string(forKey: modeKey)) == .enabled
|
||||
}
|
||||
|
||||
static func initializeStoredModeIfNeeded(defaults: UserDefaults = .standard) {
|
||||
guard defaults.string(forKey: modeKey) == nil else { return }
|
||||
|
||||
if let migratedMode = migratedLegacyMode(defaults: defaults) {
|
||||
defaults.set(migratedMode.rawValue, forKey: modeKey)
|
||||
return
|
||||
}
|
||||
|
||||
let initialMode: Mode = WorkspaceTitlebarSettings.isVisible(defaults: defaults) ? .disabled : .enabled
|
||||
defaults.set(initialMode.rawValue, forKey: modeKey)
|
||||
}
|
||||
|
||||
private static func migratedLegacyMode(defaults: UserDefaults) -> Mode? {
|
||||
let legacyValues = [
|
||||
defaults.string(forKey: legacyTitlebarControlsVisibilityModeKey),
|
||||
defaults.string(forKey: legacyPaneTabBarControlsVisibilityModeKey),
|
||||
]
|
||||
|
||||
if legacyValues.contains(where: { $0 == "onHover" || $0 == "hover" || $0 == "enabled" }) {
|
||||
return .enabled
|
||||
}
|
||||
if legacyValues.contains(where: { $0 == "always" || $0 == "disabled" }) {
|
||||
return .disabled
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct cmuxApp: App {
|
||||
@StateObject private var tabManager: TabManager
|
||||
|
|
@ -66,6 +114,7 @@ struct cmuxApp: App {
|
|||
_tabManager = StateObject(wrappedValue: TabManager())
|
||||
// Migrate legacy and old-format socket mode values to the new enum.
|
||||
let defaults = UserDefaults.standard
|
||||
WorkspaceButtonFadeSettings.initializeStoredModeIfNeeded(defaults: defaults)
|
||||
if let stored = defaults.string(forKey: SocketControlSettings.appStorageKey) {
|
||||
let migrated = SocketControlSettings.migrateMode(stored)
|
||||
if migrated.rawValue != stored {
|
||||
|
|
@ -1994,6 +2043,7 @@ private struct AcknowledgmentsView: View {
|
|||
|
||||
final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
||||
static let shared = SettingsWindowController()
|
||||
private var pendingFocusRestoreWorkItems: [DispatchWorkItem] = []
|
||||
|
||||
private init() {
|
||||
let window = NSWindow(
|
||||
|
|
@ -2036,6 +2086,37 @@ final class SettingsWindowController: NSWindowController, NSWindowDelegate {
|
|||
dlog("settings.window.show completed isVisible=\(window.isVisible ? 1 : 0) isKey=\(window.isKeyWindow ? 1 : 0)")
|
||||
#endif
|
||||
}
|
||||
|
||||
func preserveFocusAfterPreferenceMutation() {
|
||||
guard let window, window.isVisible, window.isKeyWindow else { return }
|
||||
cancelPendingFocusRestore()
|
||||
|
||||
let delays: [TimeInterval] = [0, 0.05, 0.12, 0.24, 0.4]
|
||||
for delay in delays {
|
||||
let workItem = DispatchWorkItem { [weak window] in
|
||||
guard let window, window.isVisible else { return }
|
||||
guard !window.isKeyWindow else { return }
|
||||
NSApp.activate(ignoringOtherApps: true)
|
||||
window.orderFrontRegardless()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
pendingFocusRestoreWorkItems.append(workItem)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
func windowWillClose(_ notification: Notification) {
|
||||
cancelPendingFocusRestore()
|
||||
}
|
||||
|
||||
func windowDidBecomeKey(_ notification: Notification) {
|
||||
cancelPendingFocusRestore()
|
||||
}
|
||||
|
||||
private func cancelPendingFocusRestore() {
|
||||
pendingFocusRestoreWorkItems.forEach { $0.cancel() }
|
||||
pendingFocusRestoreWorkItems.removeAll()
|
||||
}
|
||||
}
|
||||
|
||||
enum SettingsNavigationTarget: String {
|
||||
|
|
@ -3099,6 +3180,8 @@ struct SettingsView: View {
|
|||
@AppStorage(AppIconSettings.modeKey) private var appIconMode = AppIconSettings.defaultMode.rawValue
|
||||
@AppStorage(WorkspaceTitlebarSettings.showTitlebarKey)
|
||||
private var showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar
|
||||
@AppStorage(WorkspaceButtonFadeSettings.modeKey)
|
||||
private var workspaceButtonsFadeMode = WorkspaceButtonFadeSettings.defaultMode.rawValue
|
||||
@AppStorage(SocketControlSettings.appStorageKey) private var socketControlMode = SocketControlSettings.defaultMode.rawValue
|
||||
@AppStorage(ClaudeCodeIntegrationSettings.hooksEnabledKey)
|
||||
private var claudeCodeHooksEnabled = ClaudeCodeIntegrationSettings.defaultHooksEnabled
|
||||
|
|
@ -3190,7 +3273,24 @@ struct SettingsView: View {
|
|||
}
|
||||
return String(
|
||||
localized: "settings.app.showWorkspaceTitlebar.subtitleOff",
|
||||
defaultValue: "Hide the workspace title bar and show sidebar or pane actions only on hover."
|
||||
defaultValue: "Hide the folder and active title above pane tabs."
|
||||
)
|
||||
}
|
||||
|
||||
private var fadeButtonsEnabled: Bool {
|
||||
WorkspaceButtonFadeSettings.mode(for: workspaceButtonsFadeMode) == .enabled
|
||||
}
|
||||
|
||||
private var workspaceButtonFadeSubtitle: String {
|
||||
if fadeButtonsEnabled {
|
||||
return String(
|
||||
localized: "settings.app.fadeButtons.subtitleOn",
|
||||
defaultValue: "Show action buttons only on hover."
|
||||
)
|
||||
}
|
||||
return String(
|
||||
localized: "settings.app.fadeButtons.subtitleOff",
|
||||
defaultValue: "Keep action buttons always visible."
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3246,7 +3346,19 @@ struct SettingsView: View {
|
|||
get: { showWorkspaceTitlebar },
|
||||
set: { newValue in
|
||||
showWorkspaceTitlebar = newValue
|
||||
reassertSettingsWindowFocusIfNeeded()
|
||||
SettingsWindowController.shared.preserveFocusAfterPreferenceMutation()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private var fadeButtonsBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { fadeButtonsEnabled },
|
||||
set: { newValue in
|
||||
workspaceButtonsFadeMode = newValue
|
||||
? WorkspaceButtonFadeSettings.Mode.enabled.rawValue
|
||||
: WorkspaceButtonFadeSettings.Mode.disabled.rawValue
|
||||
SettingsWindowController.shared.preserveFocusAfterPreferenceMutation()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -3611,6 +3723,21 @@ struct SettingsView: View {
|
|||
|
||||
SettingsCardDivider()
|
||||
|
||||
SettingsCardRow(
|
||||
String(localized: "settings.app.fadeButtons", defaultValue: "Fade Buttons"),
|
||||
subtitle: workspaceButtonFadeSubtitle
|
||||
) {
|
||||
Toggle("", isOn: fadeButtonsBinding)
|
||||
.labelsHidden()
|
||||
.controlSize(.small)
|
||||
.accessibilityIdentifier("SettingsFadeButtonsToggle")
|
||||
.accessibilityLabel(
|
||||
String(localized: "settings.app.fadeButtons", defaultValue: "Fade Buttons")
|
||||
)
|
||||
}
|
||||
|
||||
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.")
|
||||
|
|
@ -4636,19 +4763,6 @@ struct SettingsView: View {
|
|||
NSApplication.shared.terminate(nil)
|
||||
}
|
||||
|
||||
private func reassertSettingsWindowFocusIfNeeded() {
|
||||
DispatchQueue.main.async {
|
||||
guard let window = SettingsWindowController.shared.window, window.isVisible else { return }
|
||||
window.orderFrontRegardless()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
DispatchQueue.main.async {
|
||||
guard window.isVisible else { return }
|
||||
window.orderFrontRegardless()
|
||||
window.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func resetAllSettings() {
|
||||
isResettingSettings = true
|
||||
appLanguage = LanguageSettings.defaultLanguage.rawValue
|
||||
|
|
@ -4689,6 +4803,7 @@ struct SettingsView: View {
|
|||
alwaysShowShortcutHints = ShortcutHintDebugSettings.defaultAlwaysShowHints
|
||||
newWorkspacePlacement = WorkspacePlacementSettings.defaultPlacement.rawValue
|
||||
showWorkspaceTitlebar = WorkspaceTitlebarSettings.defaultShowTitlebar
|
||||
workspaceButtonsFadeMode = WorkspaceButtonFadeSettings.defaultMode.rawValue
|
||||
workspaceAutoReorder = WorkspaceAutoReorderSettings.defaultValue
|
||||
sidebarHideAllDetails = SidebarWorkspaceDetailSettings.defaultHideAllDetails
|
||||
sidebarShowNotificationMessage = SidebarWorkspaceDetailSettings.defaultShowNotificationMessage
|
||||
|
|
|
|||
2
vendor/bonsplit
vendored
2
vendor/bonsplit
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit 743de85dfbdd138a7b07c2b91fc9e60c88fa2cf5
|
||||
Subproject commit 1ae8ee43d6813e6ce7ee67611afa1af42c0762af
|
||||
Loading…
Add table
Add a link
Reference in a new issue