Skip to content
GitHub

Typing in the nook

The nook lives in a panel that never activates your app. The app the person was using stays in front, keeps its menu bar, and gets the keyboard back as soon as the person is done typing into the nook. OpenNook handles the parts of that a text input needs, so a TextField in home content or a companion works without extra code.

  • A click gives the nook the keyboard. A click anywhere in the nook makes typing go to it, so keyboard shortcuts in your content work after a click. A click on a text input always does, even after the person has been in another app.
  • Another app takes it back. When the person clicks into another app, the keyboard goes with them and the nook’s text inputs give up focus: @FocusState turns false, and anything tied to it lets go.
  • Collapsing hands it back. When the nook collapses or hides, typing goes back to the app in front, so keys never disappear into a collapsed nook.
  • Editing shortcuts work. Command-X, C, V, A, Z, and Shift-Command-Z come from an app’s Edit menu. A notch app shows no menu bar and often has no main menu, so OpenNook installs a hidden Edit menu at launch when the app has none of its own.

Holding the nook open while a field has focus

Section titled “Holding the nook open while a field has focus”

A person typing may move the pointer off the nook, which would collapse it. Hold it open while the field has focus:

struct ReplyField: View {
@State private var reply = ""
@FocusState private var isFocused: Bool
var body: some View {
TextField("Reply", text: $reply)
.focused($isFocused)
.nookKeepsExpanded(whileFocused: $isFocused)
}
}

The hold lasts while the field has focus and the nook has the keyboard, and ends shortly after either goes - including when the person clicks into another app. If the pointer left the nook during the hold, the nook then closes.

Focus alone is not enough because SwiftUI can focus a field when content first appears, before the person has clicked into the nook; typing then still goes to the app in front. @Environment(\.nookHasKeyboardFocus) tells the two apart, for example to show a caret or a hint only while typing would reach the field.

A field that appears because the person asked for it - a reply field a button reveals - should take typing straight away. Setting its FocusState from onAppear, task, or defaultFocus does not focus a field that has just appeared in the nook, and typing then goes nowhere. Use nookFocusOnAppear(_:), which gives the nook the keyboard and focuses the field once it is in place:

struct ReplyField: View {
@State private var reply = ""
@FocusState private var isFocused: Bool
var body: some View {
TextField("Reply", text: $reply)
.focused($isFocused)
.nookFocusOnAppear($isFocused)
.nookKeepsExpanded(whileFocused: $isFocused)
}
}

Don’t put it on a field that shows whenever the nook opens: hovering the nook would then take the keyboard from the app the person is typing in.

To take the keyboard at any other time - a key handler that should work as soon as the nook opens, say - ask for it from a view with \.nookChromeActions.takeKeyboardFocus().

Outside chrome content, call coordinator.takeNookKeyboardFocus() (it returns false while the nook is hidden) and read coordinator.nookHasKeyboardFocus. Hand the keyboard back yourself - after sending a message, say - with chromeActions.releaseKeyboardFocus() or coordinator.releaseNookKeyboardFocus(), which also ends editing in the focused field.

At the engine level, Nook has the same calls: takeKeyboardFocus(), releaseKeyboardFocus(), and the published hasKeyboardFocus.

By default the show/hide shortcut only shows the nook; typing still goes to the app in front until the person clicks. To type straight away, opt in:

configuration.chromeBehavior.keyboard.shortcutTakesKeyboardFocus = true

When the shortcut opens the nook, the nook then takes the keyboard, and its focused text input takes typing at once. SwiftUI focuses a text input in the nook by itself when the nook gets the keyboard, so a nook with one field needs nothing more; with several, set the one you want through its FocusState. Opening the nook any other way leaves the keyboard alone.

The hidden Edit menu (NookEditMenu) is added only when no item in the app’s main menu already pastes, so a host with its own Edit menu keeps it. Its items have no target, so each shortcut goes to whichever text input has focus. To install your own menu instead, turn it off:

configuration.chromeBehavior.keyboard.installsEditMenu = false

For window-level work OpenNook has no API for, coordinator.nookWindow (or Nook.window) is the panel right now, nil while the nook is hidden. Read it when you need it rather than keeping it: the nook builds a new panel when it shows after being hidden and when it moves to another display. Its accessibility identifier is opennook.panel.

  • Don’t activate the app to type. NSApp.activate pulls your app to the front and takes the menu bar from the person’s app. Taking the keyboard focus is enough. File pickers are the exception, and NookFilePicker already handles them; see File pickers from a module.
  • Keys typed while the nook has the keyboard but no field has focus go nowhere. Focus a field when you take the keyboard, or hand it back.