mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJAI-363 focus the prompt on the empty editor screen
(cherry picked from commit e7d49b9c5ca4a3e11c510cb4a23721ea257f6e40) Platform-only pick: the original commit also touched plugins/air (AgentPromptInlineEmptyStateProvider.kt, its test, and global-prompt-entry.spec.md); those changes were stripped from this pick. GitOrigin-RevId: bcc87a6acdb72088818a17630605081b8b208ff9
This commit is contained in:
committed by
intellij-monorepo-bot
parent
988bbba960
commit
50a6765b75
+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