[rdct] IJPL-171305. Add check for closing tabbed project view in closeWindow API

- Remote Development has a special handlers for the case when all projects are closed to ask a user if they need to shut down a backend process. On macOS when tabbed project view feature is enabled, we need to separate cases when projects are closed via tab or application process close. We need it to keep all opened projects open if a user declines the operation in the confirmation dialog. Updated check in windowClosing() API to support check for tabbed project view;
- Add tests to verify macOS tabbed project view cases.


(cherry picked from commit 826fb0605b47f89ee9c43315deb99b615daa12b7)

IJ-CR-154038

GitOrigin-RevId: 56f092de57486d566d5fb6f76f541c993c09b8f3
This commit is contained in:
Sergei Dubov
2025-02-01 01:42:14 +00:00
committed by intellij-monorepo-bot
parent 4efd15a0ca
commit ce99bf0c1d
3 changed files with 95 additions and 5 deletions
@@ -13,10 +13,15 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.wm.WindowManager
import com.intellij.openapi.wm.impl.welcomeScreen.WelcomeFrame
import com.intellij.ui.mac.MergeAllWindowsAction
import com.intellij.ui.mac.WindowTabsComponent
import com.intellij.util.PlatformUtils
import com.intellij.util.SystemProperties
import com.intellij.util.concurrency.annotations.RequiresEdt
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.annotations.TestOnly
open class CloseProjectWindowHelper {
companion object {
@@ -33,16 +38,44 @@ open class CloseProjectWindowHelper {
protected open val isShowWelcomeScreenFromSettings: Boolean
get() = GeneralSettings.getInstance().isShowWelcomeScreen
@ApiStatus.Internal
protected open fun couldReturnToWelcomeScreen(projects: Array<Project>): Boolean {
return projects.any { project -> couldReturnToWelcomeScreen(project) }
}
@ApiStatus.Internal
protected open fun isMacOsTabbedProjectView(project: Project?): Boolean {
if (!SystemInfo.isMac) {
return false
}
val projectFrame = WindowManager.getInstance().getFrame(project) ?: return false
return MergeAllWindowsAction.isTabbedWindow(projectFrame)
}
@ApiStatus.Internal
protected open fun isCloseTab(project: Project?): Boolean {
project ?: return false
val frame = WindowManager.getInstance().getFrame(project) ?: return false
return frame.rootPane.getClientProperty(WindowTabsComponent.CLOSE_TAB_KEY) == true
}
@RequiresEdt
open fun windowClosing(project: Project?) {
WriteIntentReadAction.run {
val numberOfOpenedProjects = getNumberOfOpenedProjects()
val isMacOsTabbedProjectView = isMacOsTabbedProjectView(project)
// Exit on Linux and Windows if the only opened project frame is closed.
// On macOS behaviour is different - to exit app, quit action should be used, otherwise welcome frame is shown.
// If welcome screen is disabled, behaviour on all OS is the same.
if (numberOfOpenedProjects > 1 ||
// On macOS behavior is different - to exit app, quit action should be used, otherwise welcome frame is shown.
// If the Welcome screen is disabled, behavior on all OS is the same.
if (!isMacOsTabbedProjectView && numberOfOpenedProjects > 1 ||
isMacOsTabbedProjectView && isCloseTab(project) ||
isMacOsTabbedProjectView && couldReturnToWelcomeScreen(projects = WindowManager.getInstance().allProjectFrames.mapNotNull { it.project }.toTypedArray()) ||
serviceIfCreated<LightEditService>()?.project != null ||
(numberOfOpenedProjects == 1 && couldReturnToWelcomeScreen(project))) {
(numberOfOpenedProjects == 1 && couldReturnToWelcomeScreen(project))
) {
closeProjectAndShowWelcomeFrameIfNoProjectOpened(project)
}
else {
@@ -74,4 +107,5 @@ open class CloseProjectWindowHelper {
return project?.let { SHOW_WELCOME_FRAME_FOR_PROJECT.get(project) }
?: (isShowWelcomeScreen && !PlatformUtils.isDataSpell() && !PlatformUtils.isDataGrip())
}
}
@@ -59,6 +59,7 @@ import java.util.*;
@ApiStatus.Internal
public final class WindowTabsComponent extends JBTabsImpl {
private static final String TITLE_LISTENER_KEY = "TitleListener";
public static final String CLOSE_TAB_KEY = "CloseTab";
private static final int TAB_HEIGHT = 30;
@@ -506,6 +507,7 @@ public final class WindowTabsComponent extends JBTabsImpl {
}
private static void closeTab(@NotNull IdeFrameImpl tabFrame, boolean closeOthers) {
tabFrame.getRootPane().putClientProperty(CLOSE_TAB_KEY, Boolean.TRUE);
Foundation.executeOnMainThread(true, false, () -> {
ID window = MacUtil.getWindowFromJavaWindow(tabFrame);
Foundation.invoke(window, closeOthers ? "performCloseOtherTabs:" : "performClose:", ID.NIL);
@@ -3,10 +3,19 @@ package com.intellij.wm
import com.intellij.openapi.project.Project
import com.intellij.openapi.wm.impl.CloseProjectWindowHelper
import com.intellij.testFramework.ProjectRule
import com.intellij.testFramework.assertions.Assertions.assertThat
import org.junit.ClassRule
import org.junit.Test
class CloseProjectWindowHelperTest {
companion object {
@JvmField
@ClassRule
val projectRule = ProjectRule()
}
@Test
fun `on Windows closing last project leads to exit`() {
val helper = object : TestCloseProjectWindowHelper() {
@@ -33,7 +42,7 @@ class CloseProjectWindowHelperTest {
assertThat(helper.wasShowWelcomeFrameIfNoProjectOpenedCalled).isTrue()
}
// well, not clear is listener will be called for case when no opened projects atl all, but just t o be sure
// well, not clear is listener will be called for the case when no opened projects at all, but just to be sure
@Test
fun `on Windows closing if no opened projects leads to exit`() {
val helper = object : TestCloseProjectWindowHelper() {
@@ -59,6 +68,51 @@ class CloseProjectWindowHelperTest {
assertThat(helper.wasQuitAppCalled).isTrue()
assertThat(helper.wasShowWelcomeFrameIfNoProjectOpenedCalled).isFalse()
}
@Test
fun `on macOS closing a tab with tabbed project view`() {
val helper = object : TestCloseProjectWindowHelper() {
override val isMacSystemMenu = true
override fun isMacOsTabbedProjectView(project: Project?): Boolean = true
override fun isCloseTab(project: Project?): Boolean = true
override fun couldReturnToWelcomeScreen(projects: Array<Project>): Boolean = false
}
helper.windowClosing(null)
assertThat(helper.wasQuitAppCalled).isFalse()
assertThat(helper.wasShowWelcomeFrameIfNoProjectOpenedCalled).isTrue()
}
@Test
fun `on macOS closing an application with tabbed project view when should show welcome screen`() {
val helper = object : TestCloseProjectWindowHelper() {
override val isMacSystemMenu = true
override fun isMacOsTabbedProjectView(project: Project?): Boolean = true
override fun isCloseTab(project: Project?): Boolean = false
override fun couldReturnToWelcomeScreen(projects: Array<Project>): Boolean = true
}
helper.windowClosing(null)
assertThat(helper.wasQuitAppCalled).isFalse()
assertThat(helper.wasShowWelcomeFrameIfNoProjectOpenedCalled).isTrue()
}
@Test
fun `on macOS closing an application with tabbed project view when should not show welcome screen`() {
val helper = object : TestCloseProjectWindowHelper() {
override val isMacSystemMenu = true
override fun isMacOsTabbedProjectView(project: Project?): Boolean = true
override fun isCloseTab(project: Project?): Boolean = false
override fun couldReturnToWelcomeScreen(projects: Array<Project>): Boolean = false
}
helper.windowClosing(null)
assertThat(helper.wasQuitAppCalled).isTrue()
assertThat(helper.wasShowWelcomeFrameIfNoProjectOpenedCalled).isFalse()
}
}
open class TestCloseProjectWindowHelper : CloseProjectWindowHelper() {