mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
Fix menu bar UI jumping when copy icon appears
Always reserve space for copy icon, use opacity instead of conditional rendering. This prevents layout shifts when hovering over server addresses. Fixes #247
This commit is contained in:
parent
efa754d692
commit
d9a30e299d
2 changed files with 68 additions and 33 deletions
|
|
@ -125,6 +125,25 @@ final class SystemPermissionManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Force a permission recheck (useful when user manually changes settings)
|
||||||
|
func forcePermissionRecheck() {
|
||||||
|
logger.info("Force permission recheck requested")
|
||||||
|
|
||||||
|
// Clear any cached values
|
||||||
|
permissions[.accessibility] = false
|
||||||
|
permissions[.screenRecording] = false
|
||||||
|
permissions[.appleScript] = false
|
||||||
|
|
||||||
|
// Immediate check
|
||||||
|
Task { @MainActor in
|
||||||
|
await checkAllPermissions()
|
||||||
|
|
||||||
|
// Double-check after a delay to catch any async updates
|
||||||
|
try? await Task.sleep(for: .milliseconds(500))
|
||||||
|
await checkAllPermissions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Show alert explaining why a permission is needed
|
/// Show alert explaining why a permission is needed
|
||||||
func showPermissionAlert(for permission: SystemPermission) {
|
func showPermissionAlert(for permission: SystemPermission) {
|
||||||
let alert = NSAlert()
|
let alert = NSAlert()
|
||||||
|
|
@ -262,26 +281,45 @@ final class SystemPermissionManager {
|
||||||
private func checkAccessibilityPermission() -> Bool {
|
private func checkAccessibilityPermission() -> Bool {
|
||||||
// First check the API
|
// First check the API
|
||||||
let apiResult = AXIsProcessTrusted()
|
let apiResult = AXIsProcessTrusted()
|
||||||
|
logger.debug("AXIsProcessTrusted returned: \(apiResult)")
|
||||||
|
|
||||||
// Then do a direct test - try to get the focused element
|
// More comprehensive test - try to get focused application and its windows
|
||||||
// This will fail if we don't actually have permission
|
// This definitely requires accessibility permission
|
||||||
let systemElement = AXUIElementCreateSystemWide()
|
let systemElement = AXUIElementCreateSystemWide()
|
||||||
var focusedElement: CFTypeRef?
|
var focusedApp: CFTypeRef?
|
||||||
let result = AXUIElementCopyAttributeValue(
|
let appResult = AXUIElementCopyAttributeValue(
|
||||||
systemElement,
|
systemElement,
|
||||||
kAXFocusedUIElementAttribute as CFString,
|
kAXFocusedApplicationAttribute as CFString,
|
||||||
&focusedElement
|
&focusedApp
|
||||||
)
|
)
|
||||||
|
|
||||||
// If we can get the focused element, we truly have permission
|
if appResult == .success, let app = focusedApp {
|
||||||
if result == .success {
|
// Try to get windows from the app - this definitely needs accessibility
|
||||||
logger.debug("Accessibility permission verified through direct test")
|
var windows: CFTypeRef?
|
||||||
|
let windowResult = AXUIElementCopyAttributeValue(
|
||||||
|
app as! AXUIElement,
|
||||||
|
kAXWindowsAttribute as CFString,
|
||||||
|
&windows
|
||||||
|
)
|
||||||
|
|
||||||
|
let hasAccess = windowResult == .success
|
||||||
|
logger.debug("Comprehensive accessibility test result: \(hasAccess), can get windows: \(windows != nil)")
|
||||||
|
|
||||||
|
if hasAccess {
|
||||||
|
logger.debug("Accessibility permission verified through comprehensive test")
|
||||||
return true
|
return true
|
||||||
} else if apiResult {
|
} else if apiResult {
|
||||||
// API says yes but direct test failed - permission might be pending
|
// API says yes but comprehensive test failed - permission not actually working
|
||||||
logger.debug("Accessibility API reports true but direct test failed")
|
logger.debug("Accessibility API reports true but comprehensive test failed")
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Can't even get focused app
|
||||||
|
logger.debug("Cannot get focused application - accessibility permission not granted")
|
||||||
|
if apiResult {
|
||||||
|
logger.debug("API reports true but cannot access UI elements")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -129,8 +129,7 @@ struct ServerAddressRow: View {
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.pointingHandCursor()
|
.pointingHandCursor()
|
||||||
|
|
||||||
// Copy button that appears on hover
|
// Copy button - always present but opacity changes on hover
|
||||||
if isHovered {
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
copyToClipboard()
|
copyToClipboard()
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -141,15 +140,13 @@ struct ServerAddressRow: View {
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
.pointingHandCursor()
|
.pointingHandCursor()
|
||||||
.help(showCopiedFeedback ? "Copied!" : "Copy to clipboard")
|
.help(showCopiedFeedback ? "Copied!" : "Copy to clipboard")
|
||||||
.transition(.scale.combined(with: .opacity))
|
.opacity(isHovered ? 1.0 : 0.0)
|
||||||
}
|
.animation(.easeInOut(duration: 0.15), value: isHovered)
|
||||||
}
|
}
|
||||||
.onHover { hovering in
|
.onHover { hovering in
|
||||||
withAnimation(.easeInOut(duration: 0.15)) {
|
|
||||||
isHovered = hovering
|
isHovered = hovering
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private var computedAddress: String {
|
private var computedAddress: String {
|
||||||
if !address.isEmpty {
|
if !address.isEmpty {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue