macOS 状态栏应用程序中 TextField 的请求密钥

发布于 2025-01-11 03:22:33 字数 2490 浏览 3 评论 0原文

我正在编写一个小状态栏应用程序,其中包括文本输入。

该应用程序作为 NSMenuItem 添加到状态栏,其中包含带有 SwiftUI-View 的 NSHostingViewController。状态栏菜单是通过 SwiftUI @main 场景中的 App 委托添加的。我已将 info.plist 中的“应用程序是代理”设置为 true,并且我在 SwiftUI 中有一个空场景作为 @main 。

问题:当我尝试编辑文本时,在状态栏应用程序中,文本字段不可单击且无法接收文本输入。当我添加窗口时,只要窗口位于前台,状态栏应用程序中的文本字段就会按预期工作。

据我了解,这是由于文本字段不在标记为关键窗口的窗口内造成的。是否有任何解决方法可以使文本字段按预期工作而不需要额外的应用程序窗口?

应用程序委托的最小示例:

class AppDelegate: NSObject, NSApplicationDelegate {

  private var statusItem: NSStatusItem?
  func applicationDidFinishLaunching(_ notification: Notification) {
    self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

    if let statusBarButton = statusItem?.button {
        statusBarButton.image = NSImage(systemSymbolName: "6.circle", accessibilityDescription: "")
        statusBarButton.image?.isTemplate = true
    }

    let menuItem = NSMenuItem()
    menuItem.view = createView()

    let menu = NSMenu()
    menu.addItem(menuItem)

    statusItem?.menu = menu
  }


  private func createView() -> NSView {
   let view = HostingView(rootView: ContentView())

   view.frame = NSRect(x: 0, y: 0, width: 520, height: 260)

   return view
  }
}

SwiftUI @main

import SwiftUI
import AppKit

@main
struct MacApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        Settings {
            EmptyView()
        }
    }
}

编辑: 我发现,有两个窗口为我的应用程序注册。一种用于状态栏图标,另一种用于菜单。但是,下面的代码不会导致菜单窗口成为关键窗口。 (尽管它实际上是唯一为 .canBecomeKey 返回 true 的窗口)

.onAppear {
   let window = NSApp.windows.first { $0.canBecomeKey }
   window?.makeKey()
}

第二次编辑:显然,问题不是窗口无法成为关键,而是应用程序未设置当状态栏菜单打开时作为活动应用程序。我想出了这个相当丑陋的黑客,这会导致菜单在另一个应用程序之前处于焦点时关闭并重新打开,但至少按钮和文本字段按预期工作:

func applicationDidFinishLaunching(_ notification: Notification) {
    ...
    // Also let AppDelegate implement NSMenuDelegate
    menu.delegate = self
    ...
}

func menuWillOpen(_ menu: NSMenu) {
        #warning("This is a hack to activate the app when the menu is shown. This will cause the menu to close and re-open when another window was focused before.")
        if !NSApp.isActive {
            NSApp.activate(ignoringOtherApps: true)
            DispatchQueue.main.async {
                self.statusItem?.button?.performClick(nil)
            }
        }
    }

我仍在寻找合适的解决方案。

I am writing a little status bar app, which includes a text input.

The app is added to the status bar as a NSMenuItem which holds a NSHostingViewController with the SwiftUI-View. The status bar menu is added through an App delegate inside SwiftUIs @main scene. I've set "Application is agent" in the info.plist to true and I have an empty scene as @main in SwiftUI.

Problem: When I try to edit text, in the status bar app, the text field is not clickable and does not receive text input. When I add a window, the text field in the status bar app works as expected as long as the window is in foreground.

From my understanding, this is caused by the text field not being inside a window marked as key window. Is there any workaround to make the text field work as expected without the need for an additional app window?

Minimal example for the app delegate:

class AppDelegate: NSObject, NSApplicationDelegate {

  private var statusItem: NSStatusItem?
  func applicationDidFinishLaunching(_ notification: Notification) {
    self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)

    if let statusBarButton = statusItem?.button {
        statusBarButton.image = NSImage(systemSymbolName: "6.circle", accessibilityDescription: "")
        statusBarButton.image?.isTemplate = true
    }

    let menuItem = NSMenuItem()
    menuItem.view = createView()

    let menu = NSMenu()
    menu.addItem(menuItem)

    statusItem?.menu = menu
  }


  private func createView() -> NSView {
   let view = HostingView(rootView: ContentView())

   view.frame = NSRect(x: 0, y: 0, width: 520, height: 260)

   return view
  }
}

SwiftUI @main

import SwiftUI
import AppKit

@main
struct MacApp: App {
    @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    var body: some Scene {
        Settings {
            EmptyView()
        }
    }
}

Edit: I figured out, that there are two windows registered for my app. One for the status bar icon and one for the menu. However, the code below does not result in the menu window to become the key window. (although it is in fact the only window which returns true for .canBecomeKey)

.onAppear {
   let window = NSApp.windows.first { $0.canBecomeKey }
   window?.makeKey()
}

Second Edit: Apparently, the problem is not, that the window is not able to become key, but that the app is not set as the active app when the status bar menu is opened. I came up with this rather ugly hack, which causes the menu to close and re-open when another app was in focus before, but at least buttons and textfields work as expected with this:

func applicationDidFinishLaunching(_ notification: Notification) {
    ...
    // Also let AppDelegate implement NSMenuDelegate
    menu.delegate = self
    ...
}

func menuWillOpen(_ menu: NSMenu) {
        #warning("This is a hack to activate the app when the menu is shown. This will cause the menu to close and re-open when another window was focused before.")
        if !NSApp.isActive {
            NSApp.activate(ignoringOtherApps: true)
            DispatchQueue.main.async {
                self.statusItem?.button?.performClick(nil)
            }
        }
    }

I am still looking for a proper solution.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

魄砕の薆 2025-01-18 03:22:34

子类化 NSApplication (并将 Info.plist 和 Nib 更新为新类)和覆盖 sendEvent 对我来说很有效,无需打开菜单两次(闪烁)。

@interface MyApplication : NSApplication {};
@end

@implementation MyApplication
-(void)sendEvent:(NSEvent*)event
{
  NSEvent* newevp;
  
  if (event.type==NSEventTypeLeftMouseDown && self.active==false) {
    newevp = [event copy];
    [self activateIgnoringOtherApps:YES];
    [NSApp performSelector:@selector(sendEvent:) withObject:newevp afterDelay:0.0001];
  } else {
    [super sendEvent:event];
  };
}
@end

我不保证它是一个“正确”的解决方案。

Subclassing NSApplication (and updating Info.plist and Nib to the new class) and overriding sendEvent works for me without opening the menu twice (flickering).

@interface MyApplication : NSApplication {};
@end

@implementation MyApplication
-(void)sendEvent:(NSEvent*)event
{
  NSEvent* newevp;
  
  if (event.type==NSEventTypeLeftMouseDown && self.active==false) {
    newevp = [event copy];
    [self activateIgnoringOtherApps:YES];
    [NSApp performSelector:@selector(sendEvent:) withObject:newevp afterDelay:0.0001];
  } else {
    [super sendEvent:event];
  };
}
@end

I make no promises about it being a 'proper' solution.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文