mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-10-01 19:02:13 +07:00
IJAI-363 focus the prompt on the empty editor screen
The editor area's empty state had no place in the focus path: nothing focused a mounted component, and the area's default focus component was null without a composite, so a project that restored no editors left the caret nowhere and the inline composer had to be clicked before it could be typed in. Give that empty state the focus contract an editor has. A provider declares that its empty state is the area's focus target and names the component to focus; the platform focuses it where it would have focused an editor - project open that restored none, and an area whose last tab was just closed - and offers it as the area's default focus component, so Focus Editor and a tool window returning focus reach it. Focus is claimed rather than taken: a tool window the user is working in keeps it, as does anything outside the area's own window, and an editor that takes the area over cancels a pending request. Because a claiming empty state takes that focus, project open opens the Project tool window without focusing it. AIR's inline composer claims it and names its editable prompt text, which is what puts the caret in the input; its own focus plumbing goes away, since only the platform knows whether a presentation is one the user's focus was headed for. The Project view test opens the tool window's options menu right after closing the area's last editor, which used to leave focus in that tool window: the platform focused the most recently active one. Now that the editor area's empty state claims that focus, the tool window is no longer active there, and the New UI keeps a header toolbar out of the component tree while its tool window is neither hovered nor active - so the options button was not merely faded, it was gone, and the driver timed out looking for it. Focus the tree first, the way the test's own second options step already did: a click in blank row space hovers and activates the tool window without moving the selection the surrounding steps depend on. Both steps share one helper now. The spec's user-experience note still said the empty state must not take focus, which is the contract this test encoded and the opposite of what the same spec now states as a requirement; it says what ships instead. (cherry picked from commit 45cb31cec092ea0dbf0e87a779c706f387af8f84) GitOrigin-RevId: 5ec589cbeb739d6c2cc90aa7ba1e77812a5b4270
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1e8a99b307
commit
adb4e6c9d4
+181
-1
@@ -11,7 +11,11 @@ import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.diagnostic.debug
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.extensions.PluginDescriptor
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.toolWindow.InternalDecoratorImpl
|
||||
import com.intellij.ui.ComponentUtil
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineName
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -33,6 +37,7 @@ import java.awt.Container
|
||||
import java.awt.Dimension
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import java.awt.KeyboardFocusManager
|
||||
import java.awt.LayoutManager2
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import javax.swing.Box
|
||||
@@ -103,6 +108,16 @@ internal class EditorEmptyStateComponentController(
|
||||
private var presentationGateTimeout: Duration = PRESENTATION_GATE_TIMEOUT
|
||||
private var creationGate: (suspend () -> Unit)? = null
|
||||
|
||||
/**
|
||||
* A pending request to focus the empty state, made where an editor would have been focused — project open finding no editors to
|
||||
* restore, or the user closing the area's last tab.
|
||||
*
|
||||
* One-shot, and outlives creation rather than the area: the component of an area the user just emptied is mounted a creation delay
|
||||
* later, so the request is remembered until there is something to focus, and dropped as soon as the area stops being empty.
|
||||
*/
|
||||
private var focusRequest: EmptyStateFocusRequest? = null
|
||||
private var focusRequesterForTests: ((JComponent) -> Unit)? = null
|
||||
|
||||
init {
|
||||
coroutineScope.coroutineContext.job.invokeOnCompletion {
|
||||
disposeComponentsOnEdt()
|
||||
@@ -113,6 +128,56 @@ internal class EditorEmptyStateComponentController(
|
||||
|
||||
fun isVisible(): Boolean = componentHost != null
|
||||
|
||||
/**
|
||||
* Whether the empty state of this area is a focus target: a mounted component whose provider claims focus, or — before anything is
|
||||
* mounted — an available provider of the kind that would be presented.
|
||||
*
|
||||
* Answered before the component exists as well as after, because project open asks it while the empty state is still being prepared.
|
||||
* Deliberately not gated on whether rich components are enabled yet: project open asks this while they are still off and enables them
|
||||
* on its way out, so gating would answer for the moment of the question rather than for the moment of the mount.
|
||||
*/
|
||||
fun claimsFocus(): Boolean {
|
||||
val entries = componentEntries
|
||||
if (entries.isNotEmpty()) {
|
||||
return entries.any { claimsFocus(it.provider, it.pluginDescriptor) }
|
||||
}
|
||||
return getProvidersToCreate().any { claimsFocus(it.provider, it.pluginDescriptor) }
|
||||
}
|
||||
|
||||
/** The component to focus inside the mounted empty state, or `null` when nothing is mounted or nothing claims focus. */
|
||||
fun preferredFocusedComponent(): JComponent? {
|
||||
return componentEntries.firstNotNullOfOrNull { entry ->
|
||||
if (claimsFocus(entry.provider, entry.pluginDescriptor)) preferredFocusedComponent(entry) else null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a request made through [requestFocusWhenPresented] has not settled yet, so that whoever else would focus something on an
|
||||
* empty editor area can stand down for it — the tool window manager focusing a tool window by default when the last editor closes.
|
||||
*/
|
||||
fun isFocusRequestPending(): Boolean = focusRequest != null
|
||||
|
||||
/**
|
||||
* Focuses the empty state where an editor would have been focused, once there is something to focus.
|
||||
*
|
||||
* The component of an area the user just emptied is mounted a creation delay later, and the one prepared during project open is
|
||||
* mounted when the presentation gate opens, so this is a request rather than a focus call. It is honoured at most once, and only
|
||||
* while the area stays empty.
|
||||
*
|
||||
* @param onFocusUnclaimed run on the EDT when this claim leaves the focus it asked for unclaimed — nothing was built for an area that
|
||||
* is still empty, or what was built named no component to focus. Whoever stood down for the claim focuses its own target from here;
|
||||
* it is not run when someone else holds the focus instead, an editor that took the area over or a tool window the user is working in.
|
||||
*/
|
||||
fun requestFocusWhenPresented(onFocusUnclaimed: (() -> Unit)?) {
|
||||
val request = EmptyStateFocusRequest(onFocusUnclaimed)
|
||||
if (componentHost != null) {
|
||||
focusRequest = null
|
||||
honourFocusRequest(request)
|
||||
return
|
||||
}
|
||||
focusRequest = request
|
||||
}
|
||||
|
||||
fun suppressRichComponents() {
|
||||
if (!richComponentsEnabled && componentHost == null && creationJob == null) {
|
||||
return
|
||||
@@ -147,6 +212,85 @@ internal class EditorEmptyStateComponentController(
|
||||
}
|
||||
}
|
||||
|
||||
private fun claimsFocus(provider: EditorEmptyStateComponentProvider, pluginDescriptor: PluginDescriptor): Boolean {
|
||||
return try {
|
||||
provider.claimsFocus(splitters)
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
LOG.error(PluginException("Cannot check editor empty state focus claim using $provider", e, pluginDescriptor.pluginId))
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun preferredFocusedComponent(entry: EditorEmptyStateComponentEntry): JComponent? {
|
||||
return try {
|
||||
entry.provider.getPreferredFocusedComponent(entry.component)
|
||||
}
|
||||
catch (e: Throwable) {
|
||||
LOG.error(PluginException(
|
||||
"Cannot get editor empty state preferred focused component using ${entry.provider}", e, entry.pluginDescriptor.pluginId,
|
||||
))
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Settles a request: the empty state takes the focus it claimed, or hands the claim back to whoever stood down for it. */
|
||||
private fun honourFocusRequest(request: EmptyStateFocusRequest) {
|
||||
if (focusClaimedComponent() == FocusOutcome.UNCLAIMED) {
|
||||
request.onFocusUnclaimed?.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops a pending request that will not be honoured because nothing was built for an area that is still empty, and hands the claim
|
||||
* back. Nothing was mounted, so there is no component and no other focus owner: whoever stood down focuses its own target instead.
|
||||
*/
|
||||
private fun abandonFocusRequest() {
|
||||
val request = focusRequest ?: return
|
||||
focusRequest = null
|
||||
LOG.debug { "Editor empty state claimed focus but nothing was presented to take it" }
|
||||
request.onFocusUnclaimed?.invoke()
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the mounted empty state, unless the user is working somewhere this must not take focus from.
|
||||
*
|
||||
* Requested through [IdeFocusManager] rather than by [JComponent.requestFocus], for the same reason [focusEditorOnComposite] does:
|
||||
* this runs while the frame may still be settling its own focus.
|
||||
*/
|
||||
private fun focusClaimedComponent(): FocusOutcome {
|
||||
val target = preferredFocusedComponent()
|
||||
if (target == null) {
|
||||
// a claim was made for this area before anything was built, and what was built claims nothing or names nothing — see [claimsFocus]
|
||||
LOG.debug { "Editor empty state claimed focus but named no component to focus" }
|
||||
return FocusOutcome.UNCLAIMED
|
||||
}
|
||||
if (!canTakeFocus()) {
|
||||
return FocusOutcome.HELD_ELSEWHERE
|
||||
}
|
||||
val requester = focusRequesterForTests
|
||||
if (requester != null) {
|
||||
requester(target)
|
||||
return FocusOutcome.TAKEN
|
||||
}
|
||||
val project = splitters.manager.project
|
||||
IdeFocusManager.getInstance(project).requestFocusInProject(target, project)
|
||||
return FocusOutcome.TAKEN
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the focus this empty state claims is focus it may actually take.
|
||||
*
|
||||
* A tool window the user is working in keeps its focus — closing the area's last tab from there is not a request to leave it — and so
|
||||
* does anything outside this area's own window, a dialog or another frame among them. What is left is focus the empty state inherits:
|
||||
* an editor that has just gone, the frame that has not given focus to anything yet, or nothing at all.
|
||||
*/
|
||||
private fun canTakeFocus(): Boolean {
|
||||
val focusOwner = KeyboardFocusManager.getCurrentKeyboardFocusManager().focusOwner ?: return true
|
||||
return UIUtil.getParentOfType(InternalDecoratorImpl::class.java, focusOwner) == null &&
|
||||
ComponentUtil.getWindow(focusOwner) === ComponentUtil.getWindow(splitters)
|
||||
}
|
||||
|
||||
fun update() {
|
||||
if (showEmptyState()) {
|
||||
showComponents()
|
||||
@@ -171,6 +315,9 @@ internal class EditorEmptyStateComponentController(
|
||||
|
||||
fun disposeComponents() {
|
||||
cancelCreation()
|
||||
// The empty state a pending request was made for is gone; a later one comes with its own request. Dropped silently: this area stops
|
||||
// being empty when an editor takes it over, and that editor is focused by whoever opened it.
|
||||
focusRequest = null
|
||||
val host = componentHost ?: return
|
||||
// uninstalling fires `removeNotify` on a provider's component, which may release an editor — see [mount]
|
||||
WriteIntentReadAction.run {
|
||||
@@ -210,6 +357,11 @@ internal class EditorEmptyStateComponentController(
|
||||
creationGate = gate
|
||||
}
|
||||
|
||||
/** @param requester `null` restores the real [IdeFocusManager] request, which a headless test cannot observe. */
|
||||
fun setFocusRequesterForTests(requester: ((JComponent) -> Unit)?) {
|
||||
focusRequesterForTests = requester
|
||||
}
|
||||
|
||||
private fun showComponents() {
|
||||
if (componentHost != null || creationJob != null) {
|
||||
return
|
||||
@@ -285,6 +437,12 @@ internal class EditorEmptyStateComponentController(
|
||||
if (!mounted && richComponentsEnabled && showEmptyState()) {
|
||||
splitters.repaint()
|
||||
}
|
||||
// A creation of this generation that mounted nothing is the last word on what this area shows, so a claim on its focus is
|
||||
// over too — unless the area stopped being empty, where the editor that took it over owns that focus instead, or something
|
||||
// was mounted after all by the creation this one gave way to.
|
||||
if (!mounted && componentHost == null && showEmptyState()) {
|
||||
abandonFocusRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,7 +547,7 @@ internal class EditorEmptyStateComponentController(
|
||||
null
|
||||
}
|
||||
if (component != null) {
|
||||
entries.add(EditorEmptyStateComponentEntry(provider, component, kind))
|
||||
entries.add(EditorEmptyStateComponentEntry(provider, component, kind, pluginDescriptor))
|
||||
}
|
||||
}
|
||||
return entries
|
||||
@@ -420,6 +578,12 @@ internal class EditorEmptyStateComponentController(
|
||||
splitters.revalidate()
|
||||
splitters.repaint()
|
||||
}
|
||||
// outside the lock: the component is in the hierarchy and showing by now, which is what focusing it needs
|
||||
val request = focusRequest
|
||||
if (request != null) {
|
||||
focusRequest = null
|
||||
honourFocusRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -474,6 +638,21 @@ internal class EditorEmptyStateUiBuildTime : AbstractCoroutineContextElement(Key
|
||||
get() = nanos.get().nanoseconds
|
||||
}
|
||||
|
||||
/** A request to focus the empty state once it is presented, and what to do when the focus it claimed ends up unclaimed. */
|
||||
private class EmptyStateFocusRequest(@JvmField val onFocusUnclaimed: (() -> Unit)?)
|
||||
|
||||
/**
|
||||
* What became of the focus an empty state claimed.
|
||||
*
|
||||
* [HELD_ELSEWHERE] is told apart from [UNCLAIMED] because only the latter leaves the frame without a focus owner: a claim that was
|
||||
* refused because the user is working somewhere else needs no one to step in, while a claim that found nothing to focus does.
|
||||
*/
|
||||
private enum class FocusOutcome {
|
||||
TAKEN,
|
||||
HELD_ELSEWHERE,
|
||||
UNCLAIMED,
|
||||
}
|
||||
|
||||
private data class EditorEmptyStateProviderEntry(
|
||||
val provider: EditorEmptyStateComponentProvider,
|
||||
val pluginDescriptor: PluginDescriptor,
|
||||
@@ -484,6 +663,7 @@ private data class EditorEmptyStateComponentEntry(
|
||||
val provider: EditorEmptyStateComponentProvider,
|
||||
val component: JComponent,
|
||||
val kind: EditorEmptyStateComponentProvider.Kind,
|
||||
val pluginDescriptor: PluginDescriptor,
|
||||
)
|
||||
|
||||
internal class EditorsSplittersLayout : LayoutManager2 {
|
||||
|
||||
+23
@@ -35,6 +35,29 @@ interface EditorEmptyStateComponentProvider {
|
||||
fun disposeComponent(component: JComponent) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this provider's empty state is the focus target of an editor area that shows it — the counterpart, for an area with no
|
||||
* editor in it, of the composite the platform focuses when it opens one.
|
||||
*
|
||||
* Asked synchronously and before the component exists, because project open decides what to focus while the empty state is still
|
||||
* being prepared. A provider that claims focus is focused where an editor would have been: after project open finds no editors to
|
||||
* restore, and after the user closes the area's last tab. It is also offered as the area's default focus component, so
|
||||
* <kbd>Esc</kbd> and Focus Editor reach it. `false` — the default — keeps an empty state out of the focus path entirely.
|
||||
*
|
||||
* Claiming focus is not the same as taking it: focus is never moved out of a tool window the user is working in.
|
||||
*/
|
||||
fun claimsFocus(splitters: EditorsSplitters): Boolean = false
|
||||
|
||||
/**
|
||||
* The component to focus inside [component], which this provider created — the editable field of a composer rather than its host
|
||||
* panel, say. Consulted only for a provider that returns `true` from [claimsFocus]; `null` means there is nothing to focus yet.
|
||||
*
|
||||
* `null` is also the default, rather than [component] itself: a host panel is usually not focusable, so returning it would answer a
|
||||
* claim with a focus request that quietly does nothing, where `null` tells the platform that the claim it made on this area's focus
|
||||
* cannot be kept and lets whoever stood down for it focus instead.
|
||||
*/
|
||||
fun getPreferredFocusedComponent(component: JComponent): JComponent? = null
|
||||
|
||||
enum class Kind {
|
||||
RICH,
|
||||
FALLBACK,
|
||||
|
||||
+68
-1
@@ -60,6 +60,7 @@ import com.intellij.openapi.vfs.VirtualFileWithoutContent
|
||||
import com.intellij.openapi.wm.FocusWatcher
|
||||
import com.intellij.openapi.wm.IdeFocusManager
|
||||
import com.intellij.openapi.wm.IdeFrame
|
||||
import com.intellij.openapi.wm.ToolWindowManager
|
||||
import com.intellij.openapi.wm.ex.IdeFocusTraversalPolicy
|
||||
import com.intellij.openapi.wm.ex.IdeFrameEx
|
||||
import com.intellij.openapi.wm.ex.WelcomeScreenTabService
|
||||
@@ -70,6 +71,7 @@ import com.intellij.openapi.wm.impl.FrameTitleBuilder
|
||||
import com.intellij.openapi.wm.impl.IdeBackgroundUtil
|
||||
import com.intellij.openapi.wm.impl.IdeFrameImpl
|
||||
import com.intellij.openapi.wm.impl.ProjectFrameHelper
|
||||
import com.intellij.openapi.wm.impl.ToolWindowManagerImpl
|
||||
import com.intellij.platform.diagnostic.telemetry.impl.span
|
||||
import com.intellij.platform.fileEditor.FileEntry
|
||||
import com.intellij.platform.fileEditor.parseFileEntry
|
||||
@@ -178,7 +180,10 @@ open class EditorsSplitters internal constructor(
|
||||
const val SPLITTER_KEY: @NonNls String = "EditorsSplitters"
|
||||
|
||||
fun findDefaultComponentInSplitters(project: Project?): JComponent? {
|
||||
return getSplittersToFocus(project)?.currentCompositeFlow?.value?.preferredFocusedComponent
|
||||
val splitters = getSplittersToFocus(project) ?: return null
|
||||
// an editor area with no editor in it still has a default component when its empty state claims focus — that is what Focus Editor
|
||||
// and a tool window returning focus reach there
|
||||
return splitters.currentCompositeFlow.value?.preferredFocusedComponent ?: splitters.emptyStatePreferredFocusedComponent()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@@ -284,6 +289,45 @@ open class EditorsSplitters internal constructor(
|
||||
emptyStateComponentController.enableRichComponents()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this area's empty state takes the focus an editor would have taken, so that whoever else focuses something on an empty
|
||||
* editor area — project open activating the Project tool window — can leave it to the empty state instead.
|
||||
*/
|
||||
@Internal
|
||||
@RequiresEdt
|
||||
fun emptyStateClaimsFocus(): Boolean = emptyStateComponentController.claimsFocus()
|
||||
|
||||
/** The default focus component of an area with no editor in it; `null` unless a mounted empty state claims focus. */
|
||||
@Internal
|
||||
@RequiresEdt
|
||||
fun emptyStatePreferredFocusedComponent(): JComponent? = emptyStateComponentController.preferredFocusedComponent()
|
||||
|
||||
/**
|
||||
* Asks this area's empty state to take focus once it is presented — where the platform would have focused an editor: project open
|
||||
* that restored none, and an area whose last tab the user just closed.
|
||||
*
|
||||
* @param onFocusUnclaimed run on the EDT when the claim leaves that focus unclaimed, so that whoever stood down for it can focus its
|
||||
* own target after all; see [EditorEmptyStateComponentController.requestFocusWhenPresented].
|
||||
*/
|
||||
@Internal
|
||||
@RequiresEdt
|
||||
fun requestEmptyStateFocusWhenPresented(onFocusUnclaimed: (() -> Unit)? = null) {
|
||||
emptyStateComponentController.requestFocusWhenPresented(onFocusUnclaimed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this area's empty state has been asked to take focus and has not settled that claim yet, so that whoever else would focus
|
||||
* something on an empty editor area can leave it to the empty state — see [requestEmptyStateFocusWhenPresented].
|
||||
*/
|
||||
@Internal
|
||||
@RequiresEdt
|
||||
fun isEmptyStateFocusRequestPending(): Boolean = emptyStateComponentController.isFocusRequestPending()
|
||||
|
||||
@TestOnly
|
||||
internal fun setEmptyStateComponentFocusRequesterForTests(requester: ((JComponent) -> Unit)?) {
|
||||
emptyStateComponentController.setFocusRequesterForTests(requester)
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds back presentation of the empty state while project open may still open editors of its own — the welcome tab, a README, a
|
||||
* file named on the command line, What's New, a file a project wizard generated.
|
||||
@@ -847,6 +891,12 @@ open class EditorsSplitters internal constructor(
|
||||
|
||||
internal open fun afterFileClosed(file: VirtualFile) {
|
||||
cancelEmptyStateComponentCreation()
|
||||
if (showEmptyText()) {
|
||||
// The closed editor's focus is inherited by what replaces it, and on an area with nothing left that is its empty state; the
|
||||
// request is dropped again if this turns out to be a close that another editor takes over (a preview replacement, say).
|
||||
// The tool window manager stands down for this claim, so a claim that finds nothing to focus hands that focus back to it.
|
||||
requestEmptyStateFocusWhenPresented(onFocusUnclaimed = { focusToolWindowByDefaultOnEmptiedArea(manager) })
|
||||
}
|
||||
updateEmptyStateComponent()
|
||||
}
|
||||
|
||||
@@ -1750,6 +1800,23 @@ private fun getSplitCount(component: JComponent): Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses what [ToolWindowManagerImpl] focuses when the last editor is closed: the most recently active visible tool window.
|
||||
*
|
||||
* It stands down while a claim on the focus of the emptied area is pending, because that focus is the empty state's a creation delay
|
||||
* later. This is the other half of standing down — the claim found nothing to focus, so the focus goes where it would have gone.
|
||||
*/
|
||||
@RequiresEdt
|
||||
private fun focusToolWindowByDefaultOnEmptiedArea(manager: FileEditorManagerImpl) {
|
||||
val project = manager.project
|
||||
// `hasOpenFiles` is the condition the tool window manager stood down under, so this hands back exactly the focus it gave up: an area
|
||||
// this one shares an editor with — a docked editor window — is one it never stood down for
|
||||
if (project.isDisposed || manager.hasOpenFiles()) {
|
||||
return
|
||||
}
|
||||
(ToolWindowManager.getInstance(project) as? ToolWindowManagerImpl)?.focusToolWindowByDefault()
|
||||
}
|
||||
|
||||
private fun getSplittersToFocus(suggestedProject: Project?): EditorsSplitters? {
|
||||
var project = suggestedProject
|
||||
var activeWindow = WindowManagerEx.getInstanceEx().mostRecentFocusedWindow
|
||||
|
||||
+51
-4
@@ -54,6 +54,7 @@ import com.intellij.openapi.util.SystemInfoRt
|
||||
import com.intellij.openapi.util.registry.RegistryManager
|
||||
import com.intellij.openapi.vfs.VirtualFile
|
||||
import com.intellij.openapi.wm.ToolWindow
|
||||
import com.intellij.openapi.wm.ToolWindowId
|
||||
import com.intellij.openapi.wm.ToolWindowManager
|
||||
import com.intellij.openapi.wm.WindowManager
|
||||
import com.intellij.openapi.wm.ex.ProjectFrameCapabilitiesService
|
||||
@@ -83,6 +84,7 @@ import com.intellij.toolWindow.computeToolWindowBeans
|
||||
import com.intellij.ui.ScreenUtil
|
||||
import com.intellij.util.PlatformUtils
|
||||
import com.intellij.util.TimeoutUtil
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.messages.SimpleMessageBusConnection
|
||||
import com.intellij.util.ui.accessibility.ScreenReader
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -609,8 +611,29 @@ private suspend fun postOpenEditors(
|
||||
// check after `initDockableContentFactory` - editor in a docked window
|
||||
if (!fileEditorManager.hasOpenFiles()) {
|
||||
stopOpenFilesActivity(project)
|
||||
// An editor area whose empty state claims focus is where the caret belongs on a project that restored no editors, the same way it
|
||||
// would belong in a restored editor. The request is made here, where "project open restored nothing" is known, and honoured when
|
||||
// the empty state is presented — the hold released below. An editor opened after this point (the README) drops it again.
|
||||
// Not on a remote dev host, which does not focus anything of its own on project open either — see `openProjectViewIfNeeded`.
|
||||
val emptyStateClaimKept = AtomicBoolean(true)
|
||||
val emptyStateTakesFocus = !AppMode.isRemoteDevHost() &&
|
||||
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
|
||||
val splitters = fileEditorManager.mainSplitters
|
||||
splitters.emptyStateClaimsFocus().also { takesFocus ->
|
||||
if (takesFocus) {
|
||||
// The Project view below is opened without focus for this claim, which is a promise made before
|
||||
// there is a component to keep it with. Where it cannot be kept — a provider that builds nothing,
|
||||
// an empty state that is never presented — the Project view is focused after all, either here or
|
||||
// at the point it is opened, whichever comes second.
|
||||
splitters.requestEmptyStateFocusWhenPresented(onFocusUnclaimed = {
|
||||
emptyStateClaimKept.set(false)
|
||||
focusProjectViewIfOpened(project)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!isNotificationSilentMode(project)) {
|
||||
openProjectViewIfNeeded(project, toolWindowInitJob)
|
||||
openProjectViewIfNeeded(project, toolWindowInitJob, focusProjectView = { !emptyStateTakesFocus || !emptyStateClaimKept.get() })
|
||||
findAndOpenReadmeIfNeeded(project)
|
||||
}
|
||||
FUSProjectHotStartUpMeasurer.reportNoMoreEditorsOnStartup(System.nanoTime())
|
||||
@@ -788,7 +811,12 @@ private fun installMaximizeListener(frame: IdeFrameImpl) {
|
||||
})
|
||||
}
|
||||
|
||||
private suspend fun openProjectViewIfNeeded(project: Project, toolWindowInitJob: Job) {
|
||||
/**
|
||||
* @param focusProjectView `false` when something else on this project's editor area takes focus instead — an empty state that claims
|
||||
* it — so the Project view is opened without focus rather than focused and then focused away from. Asked at the moment the Project view
|
||||
* is activated rather than in advance, because a claim on the editor area's focus can be given up before that moment.
|
||||
*/
|
||||
private suspend fun openProjectViewIfNeeded(project: Project, toolWindowInitJob: Job, focusProjectView: () -> Boolean) {
|
||||
if (!serviceAsync<RegistryManager>().`is`("ide.open.project.view.on.startup")) {
|
||||
return
|
||||
}
|
||||
@@ -799,17 +827,36 @@ private suspend fun openProjectViewIfNeeded(project: Project, toolWindowInitJob:
|
||||
val toolWindowManager = project.serviceAsync<ToolWindowManager>()
|
||||
withContext(Dispatchers.ui(CoroutineSupport.UiDispatcherKind.STRICT)) {
|
||||
if (toolWindowManager.activeToolWindowId == null) {
|
||||
val toolWindow = toolWindowManager.getToolWindow("Project")
|
||||
val toolWindow = toolWindowManager.getToolWindow(ToolWindowId.PROJECT_VIEW)
|
||||
if (toolWindow != null) {
|
||||
// maybe readAction
|
||||
withContext(Dispatchers.EDT) {
|
||||
toolWindow.activate(null, !AppMode.isRemoteDevHost())
|
||||
toolWindow.activate(null, focusProjectView() && !AppMode.isRemoteDevHost())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the Project view that [openProjectViewIfNeeded] opened without focus, because the editor area's empty state claimed that focus
|
||||
* and then found nothing to take it with.
|
||||
*
|
||||
* Does nothing when the Project view is not showing: it was never opened — notification silent mode, the registry key off — and there is
|
||||
* nothing here to focus. Where that is only because it has not been opened *yet*, the claim is already known to be given up by the time
|
||||
* it is, and it is opened focused instead.
|
||||
*/
|
||||
@RequiresEdt
|
||||
private fun focusProjectViewIfOpened(project: Project) {
|
||||
if (project.isDisposed) {
|
||||
return
|
||||
}
|
||||
val toolWindow = ToolWindowManager.getInstance(project).getToolWindow(ToolWindowId.PROJECT_VIEW) ?: return
|
||||
if (toolWindow.isVisible) {
|
||||
toolWindow.activate(null, true)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun findAndOpenReadmeIfNeeded(project: Project) {
|
||||
if (!AdvancedSettings.getBoolean("ide.open.readme.md.on.startup")) {
|
||||
return
|
||||
|
||||
@@ -26,12 +26,14 @@ import com.intellij.openapi.application.asContextElement
|
||||
import com.intellij.openapi.application.writeIntentReadAction
|
||||
import com.intellij.openapi.components.ComponentManagerEx
|
||||
import com.intellij.openapi.components.service
|
||||
import com.intellij.openapi.components.serviceIfCreated
|
||||
import com.intellij.openapi.diagnostic.debug
|
||||
import com.intellij.openapi.diagnostic.logger
|
||||
import com.intellij.openapi.extensions.PluginDescriptor
|
||||
import com.intellij.openapi.fileEditor.FileEditorManager
|
||||
import com.intellij.openapi.fileEditor.FileEditorManagerListener
|
||||
import com.intellij.openapi.fileEditor.impl.EditorsSplitters
|
||||
import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl
|
||||
import com.intellij.openapi.options.advanced.AdvancedSettings
|
||||
import com.intellij.openapi.progress.ProcessCanceledException
|
||||
import com.intellij.openapi.project.Project
|
||||
@@ -118,6 +120,20 @@ import javax.swing.JRootPane
|
||||
private val LOG = logger<ToolWindowManagerImpl>()
|
||||
private val performShowInSeparateTask = System.getProperty("idea.toolwindow.show.separate.task", "false").toBoolean()
|
||||
|
||||
/**
|
||||
* Whether the editor area has claimed the focus of the editor that was just closed, because its empty state is that area's focus target.
|
||||
*
|
||||
* The claimed component is mounted a creation delay later, so focusing a tool window by default here would take focus the empty state is
|
||||
* about to be given — and take it visibly, only to lose it again. A claim that ends up with nothing to focus calls
|
||||
* [ToolWindowManagerImpl.focusToolWindowByDefault] itself, so standing down cannot leave the frame without a focus owner.
|
||||
*/
|
||||
@RequiresEdt
|
||||
private fun emptyEditorAreaClaimsClosedEditorFocus(project: Project): Boolean {
|
||||
val fileEditorManager = project.serviceIfCreated<FileEditorManager>() as? FileEditorManagerImpl ?: return false
|
||||
// the init job is awaited because `splitters` reaches `mainSplitters`, which exists only once that job has completed
|
||||
return fileEditorManager.initJob.isCompleted && fileEditorManager.splitters.isEmptyStateFocusRequestPending()
|
||||
}
|
||||
|
||||
private typealias Mutation = ((WindowInfoImpl) -> Unit)
|
||||
|
||||
@ApiStatus.Internal
|
||||
@@ -437,7 +453,7 @@ open class ToolWindowManagerImpl @NonInjectable @TestOnly internal constructor(
|
||||
coroutineScope.launch(Dispatchers.EDT) {
|
||||
@Suppress("DEPRECATION")
|
||||
focusManager.doWhenFocusSettlesDown(ExpirableRunnable.forProject(project) {
|
||||
if (!FileEditorManager.getInstance(project).hasOpenFiles()) {
|
||||
if (!FileEditorManager.getInstance(project).hasOpenFiles() && !emptyEditorAreaClaimsClosedEditorFocus(project)) {
|
||||
focusToolWindowByDefault()
|
||||
}
|
||||
})
|
||||
@@ -1889,7 +1905,14 @@ open class ToolWindowManagerImpl @NonInjectable @TestOnly internal constructor(
|
||||
fireStateChanged(ToolWindowManagerEventType.MovedOrResized, toolWindow)
|
||||
}
|
||||
|
||||
private fun focusToolWindowByDefault() {
|
||||
/**
|
||||
* Focuses the most recently active visible tool window, where the editor area has nothing to hand focus to.
|
||||
*
|
||||
* Also called from the editor area itself, for a claim on the focus of an emptied area that could not be kept — see
|
||||
* [EditorsSplitters.requestEmptyStateFocusWhenPresented].
|
||||
*/
|
||||
@RequiresEdt
|
||||
internal fun focusToolWindowByDefault() {
|
||||
var toFocus: ToolWindowEntry? = null
|
||||
for (each in activeStack.stack) {
|
||||
if (each.readOnlyWindowInfo.isVisible) {
|
||||
|
||||
+238
@@ -48,6 +48,7 @@ import org.junit.jupiter.api.Test
|
||||
import java.awt.event.InputEvent
|
||||
import java.awt.event.KeyEvent
|
||||
import java.nio.file.Files
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.swing.JComponent
|
||||
@@ -98,6 +99,7 @@ internal class EditorEmptyTextPainterTest {
|
||||
originalShortcuts.forEach { (actionId, shortcuts) -> resetShortcuts(actionId, shortcuts) }
|
||||
val splitters = manager.mainSplitters
|
||||
splitters.setEmptyStateComponentCreationGateForTests(null)
|
||||
splitters.setEmptyStateComponentFocusRequesterForTests(null)
|
||||
// a creation left waiting out an inflated delay must not survive into the next test
|
||||
splitters.suppressRichEmptyStateComponents()
|
||||
splitters.setEmptyStateComponentCreationDelayForTests(null)
|
||||
@@ -823,6 +825,192 @@ internal class EditorEmptyTextPainterTest {
|
||||
assertThat(disposedComponents).hasValue(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aClaimingEmptyStateIsFocusedWhenItIsPresented(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProvider(disposable)
|
||||
manager.closeAllFiles()
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
splitters.setEmptyStateComponentCreationDelayForTests(NEVER_ELAPSING_CREATION_DELAY)
|
||||
splitters.beginStartupEmptyStatePresentationHold()
|
||||
|
||||
// the request is made where project open makes it: before anything is built, and honoured only once the empty state is presented
|
||||
splitters.requestEmptyStateFocusWhenPresented()
|
||||
splitters.finishStartupEditorRestore()
|
||||
dispatchEventsFor(100.milliseconds)
|
||||
|
||||
assertThat(focusRequests).isEmpty()
|
||||
|
||||
releaseStartupHoldFromProjectOpensHop(splitters)
|
||||
waitForEmptyStateComponent(splitters, "The claimed empty state was not presented")
|
||||
|
||||
assertThat(focusRequests).containsExactly(findFocusTargetComponent(splitters))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anEmptyStateThatDoesNotClaimFocusIsNotFocused(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerComponentProvider(disposable)
|
||||
manager.closeAllFiles()
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
|
||||
assertThat(splitters.emptyStateClaimsFocus()).isFalse()
|
||||
|
||||
splitters.requestEmptyStateFocusWhenPresented()
|
||||
enableRichEmptyStateComponentsWithoutDelay(splitters)
|
||||
waitForEmptyStateComponent(splitters, "The empty state was not presented")
|
||||
|
||||
assertThat(focusRequests).isEmpty()
|
||||
assertThat(splitters.emptyStatePreferredFocusedComponent()).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aClaimingEmptyStateClaimsFocusBeforeItIsBuilt(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProvider(disposable)
|
||||
manager.closeAllFiles()
|
||||
|
||||
// project open asks this while the empty state is still being prepared, so the answer must not depend on a mounted component
|
||||
assertThat(splitters.emptyStateClaimsFocus()).isTrue()
|
||||
assertThat(splitters.emptyStatePreferredFocusedComponent()).isNull()
|
||||
|
||||
enableRichEmptyStateComponentsWithoutDelay(splitters)
|
||||
waitForEmptyStateComponent(splitters, "The claimed empty state was not presented")
|
||||
|
||||
assertThat(splitters.emptyStateClaimsFocus()).isTrue()
|
||||
assertThat(splitters.emptyStatePreferredFocusedComponent()).isSameAs(findFocusTargetComponent(splitters))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aFocusRequestIsDroppedWhenAnEditorTakesTheAreaOver(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProvider(disposable)
|
||||
manager.closeAllFiles()
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
splitters.setEmptyStateComponentCreationDelayForTests(NEVER_ELAPSING_CREATION_DELAY)
|
||||
splitters.requestEmptyStateFocusWhenPresented()
|
||||
splitters.enableRichEmptyStateComponents()
|
||||
|
||||
val file = LightVirtualFile("empty-state-focus-drop.txt", "content")
|
||||
manager.openFile(file, false)
|
||||
waitForEmptyStateComponentCreation(splitters)
|
||||
|
||||
// the editor that took the area over owns the focus the request was made for
|
||||
assertThat(findEmptyStateComponent(splitters)).isNull()
|
||||
assertThat(focusRequests).isEmpty()
|
||||
|
||||
manager.closeFile(file)
|
||||
enableRichEmptyStateComponentsWithoutDelay(splitters)
|
||||
waitForEmptyStateComponent(splitters, "The claimed empty state was not presented after the editor was closed")
|
||||
|
||||
// and closing that editor is a request of its own rather than the dropped one coming back
|
||||
assertThat(focusRequests).containsExactly(findFocusTargetComponent(splitters))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun closingTheLastEditorFocusesAClaimingEmptyState(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProvider(disposable)
|
||||
manager.closeAllFiles()
|
||||
enableRichEmptyStateComponentsWithoutDelay(splitters)
|
||||
waitForEmptyStateComponent(splitters, "The claimed empty state was not presented")
|
||||
|
||||
val file = LightVirtualFile("empty-state-last-tab.txt", "content")
|
||||
manager.openFile(file, false)
|
||||
waitForNoEmptyStateComponent(splitters)
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
|
||||
manager.closeFile(file)
|
||||
waitForEmptyStateComponent(splitters, "The claimed empty state did not come back after the last editor was closed")
|
||||
|
||||
// the focus of the editor the user just closed is inherited by the empty state that replaces it
|
||||
assertThat(focusRequests).containsExactly(findFocusTargetComponent(splitters))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aClaimIsPendingUntilTheEmptyStateTakesTheFocus(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProvider(disposable)
|
||||
manager.closeAllFiles()
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
val handedBack = AtomicInteger()
|
||||
splitters.setEmptyStateComponentCreationDelayForTests(NEVER_ELAPSING_CREATION_DELAY)
|
||||
splitters.beginStartupEmptyStatePresentationHold()
|
||||
|
||||
splitters.requestEmptyStateFocusWhenPresented(onFocusUnclaimed = { handedBack.incrementAndGet() })
|
||||
splitters.finishStartupEditorRestore()
|
||||
dispatchEventsFor(100.milliseconds)
|
||||
|
||||
// what the tool window manager stands down for while the claimed component is still being prepared
|
||||
assertThat(splitters.isEmptyStateFocusRequestPending()).isTrue()
|
||||
|
||||
releaseStartupHoldFromProjectOpensHop(splitters)
|
||||
waitForEmptyStateComponent(splitters, "The claimed empty state was not presented")
|
||||
|
||||
assertThat(focusRequests).containsExactly(findFocusTargetComponent(splitters))
|
||||
assertThat(splitters.isEmptyStateFocusRequestPending()).isFalse()
|
||||
assertThat(handedBack).hasValue(0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aClaimThatIsNeverPresentedHandsTheFocusBack(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProviderThatBuildsNothing(disposable)
|
||||
manager.closeAllFiles()
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
val handedBack = AtomicInteger()
|
||||
|
||||
// the claim is made on an available provider, before it is known that the provider will build nothing
|
||||
assertThat(splitters.emptyStateClaimsFocus()).isTrue()
|
||||
|
||||
splitters.requestEmptyStateFocusWhenPresented(onFocusUnclaimed = { handedBack.incrementAndGet() })
|
||||
enableRichEmptyStateComponentsWithoutDelay(splitters)
|
||||
waitForEmptyStateComponentCreation(splitters)
|
||||
|
||||
// nothing was presented for the area to focus, so whoever stood down for the claim gets it back
|
||||
assertThat(focusRequests).isEmpty()
|
||||
assertThat(handedBack).hasValue(1)
|
||||
assertThat(splitters.isEmptyStateFocusRequestPending()).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun aClaimingEmptyStateThatNamesNoComponentHandsTheFocusBack(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProviderWithoutFocusTarget(disposable)
|
||||
manager.closeAllFiles()
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
val handedBack = AtomicInteger()
|
||||
|
||||
splitters.requestEmptyStateFocusWhenPresented(onFocusUnclaimed = { handedBack.incrementAndGet() })
|
||||
enableRichEmptyStateComponentsWithoutDelay(splitters)
|
||||
waitForEmptyStateComponent(splitters, "The claimed empty state was not presented")
|
||||
|
||||
assertThat(focusRequests).isEmpty()
|
||||
assertThat(handedBack).hasValue(1)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun anEditorTakingTheAreaOverDoesNotHandTheFocusBack(@TestDisposable disposable: Disposable) {
|
||||
val splitters = manager.mainSplitters
|
||||
registerFocusClaimingComponentProvider(disposable)
|
||||
manager.closeAllFiles()
|
||||
val focusRequests = recordFocusRequests(splitters)
|
||||
val handedBack = AtomicInteger()
|
||||
splitters.setEmptyStateComponentCreationDelayForTests(NEVER_ELAPSING_CREATION_DELAY)
|
||||
splitters.requestEmptyStateFocusWhenPresented(onFocusUnclaimed = { handedBack.incrementAndGet() })
|
||||
splitters.enableRichEmptyStateComponents()
|
||||
|
||||
// the editor that took the area over owns the focus the claim was made for — the README on a project's first open
|
||||
val file = LightVirtualFile("empty-state-focus-handback.txt", "content")
|
||||
manager.openFile(file, false)
|
||||
waitForEmptyStateComponentCreation(splitters)
|
||||
dispatchEventsFor(100.milliseconds)
|
||||
|
||||
assertThat(focusRequests).isEmpty()
|
||||
assertThat(handedBack).hasValue(0)
|
||||
assertThat(splitters.isEmptyStateFocusRequestPending()).isFalse()
|
||||
}
|
||||
|
||||
private fun registerDefaultEmptyTextProvider(disposable: Disposable) {
|
||||
ExtensionTestUtil.maskExtensions(EditorEmptyTextProvider.EP_NAME, listOf(DefaultEditorEmptyTextProvider()), disposable)
|
||||
}
|
||||
@@ -874,6 +1062,44 @@ internal class EditorEmptyTextPainterTest {
|
||||
}, disposable)
|
||||
}
|
||||
|
||||
/** A provider whose empty state is the focus target of the area it is shown in, and whose focus target is inside its component. */
|
||||
private fun registerFocusClaimingComponentProvider(disposable: Disposable) {
|
||||
ExtensionTestUtil.maskExtensions(EditorEmptyStateComponentProvider.EP_NAME, listOf(object : EditorEmptyStateComponentProvider {
|
||||
override suspend fun createComponent(splitters: EditorsSplitters): JComponent = withContext(Dispatchers.EDT) {
|
||||
JPanel().apply {
|
||||
name = EMPTY_STATE_COMPONENT_NAME
|
||||
add(JPanel().apply { name = FOCUS_TARGET_COMPONENT_NAME })
|
||||
}
|
||||
}
|
||||
|
||||
override fun claimsFocus(splitters: EditorsSplitters): Boolean = true
|
||||
|
||||
override fun getPreferredFocusedComponent(component: JComponent): JComponent? {
|
||||
return UIUtil.uiTraverser(component).find { it is JComponent && it.name == FOCUS_TARGET_COMPONENT_NAME } as? JComponent
|
||||
}
|
||||
}), disposable)
|
||||
}
|
||||
|
||||
/** A provider that claims the area's focus and then builds nothing, so the claim it made cannot be kept. */
|
||||
private fun registerFocusClaimingComponentProviderThatBuildsNothing(disposable: Disposable) {
|
||||
ExtensionTestUtil.maskExtensions(EditorEmptyStateComponentProvider.EP_NAME, listOf(object : EditorEmptyStateComponentProvider {
|
||||
override suspend fun createComponent(splitters: EditorsSplitters): JComponent? = null
|
||||
|
||||
override fun claimsFocus(splitters: EditorsSplitters): Boolean = true
|
||||
}), disposable)
|
||||
}
|
||||
|
||||
/** A provider that claims the area's focus and presents a component that names nothing to focus inside it. */
|
||||
private fun registerFocusClaimingComponentProviderWithoutFocusTarget(disposable: Disposable) {
|
||||
ExtensionTestUtil.maskExtensions(EditorEmptyStateComponentProvider.EP_NAME, listOf(object : EditorEmptyStateComponentProvider {
|
||||
override suspend fun createComponent(splitters: EditorsSplitters): JComponent = withContext(Dispatchers.EDT) {
|
||||
JPanel().apply { name = EMPTY_STATE_COMPONENT_NAME }
|
||||
}
|
||||
|
||||
override fun claimsFocus(splitters: EditorsSplitters): Boolean = true
|
||||
}), disposable)
|
||||
}
|
||||
|
||||
private fun registerNullAndFallbackComponentProviders(disposable: Disposable) {
|
||||
ExtensionTestUtil.maskExtensions(EditorEmptyStateComponentProvider.EP_NAME, buildList {
|
||||
add(object : EditorEmptyStateComponentProvider {
|
||||
@@ -901,6 +1127,17 @@ internal class EditorEmptyTextPainterTest {
|
||||
return UIUtil.uiTraverser(splitters).find { it is JComponent && it.name == EMPTY_STATE_COMPONENT_NAME } as? JComponent
|
||||
}
|
||||
|
||||
private fun findFocusTargetComponent(splitters: EditorsSplitters): JComponent {
|
||||
return checkNotNull(UIUtil.uiTraverser(splitters).find { it is JComponent && it.name == FOCUS_TARGET_COMPONENT_NAME } as? JComponent)
|
||||
}
|
||||
|
||||
/** Records what the empty state asks to focus, which is all a headless test can observe of a focus request. */
|
||||
private fun recordFocusRequests(splitters: EditorsSplitters): List<JComponent> {
|
||||
val requests = CopyOnWriteArrayList<JComponent>()
|
||||
splitters.setEmptyStateComponentFocusRequesterForTests { requests.add(it) }
|
||||
return requests
|
||||
}
|
||||
|
||||
private fun findEmptyTextComponent(splitters: EditorsSplitters): JComponent? {
|
||||
return UIUtil.uiTraverser(splitters).find { it is JComponent && it.name == EDITOR_EMPTY_TEXT_COMPONENT_NAME } as? JComponent
|
||||
}
|
||||
@@ -1061,6 +1298,7 @@ internal class EditorEmptyTextPainterTest {
|
||||
const val PROVIDER_ACTION_ID: String = "EditorEmptyTextPainterTest.ProviderAction"
|
||||
const val PROVIDER_ACTION_TEXT: String = "Provider Action"
|
||||
const val EMPTY_STATE_COMPONENT_NAME: String = "EditorEmptyTextPainterTest.EmptyStateComponent"
|
||||
const val FOCUS_TARGET_COMPONENT_NAME: String = "EditorEmptyTextPainterTest.FocusTarget"
|
||||
|
||||
/** Long enough that a test which reaches the delay fails on its own timeout rather than passing slowly. */
|
||||
val NEVER_ELAPSING_CREATION_DELAY: Duration = 10.minutes
|
||||
|
||||
Reference in New Issue
Block a user