ASPR-3010 Recommended plugin installation on IDEA startup

This commit is contained in:
Artem Orlov
2026-02-13 15:40:08 +03:00
parent 264c999035
commit 94d5e69ef0
15 changed files with 601 additions and 64 deletions

View File

@@ -67,5 +67,6 @@
<orderEntry type="module" module-name="intellij.platform.ide.ui" />
<orderEntry type="module" module-name="intellij.platform.ide.initialConfigImport" />
<orderEntry type="module" module-name="intellij.platform.plugins.parser.impl" />
<orderEntry type="module" module-name="intellij.java.ui" />
</component>
</module>

View File

@@ -0,0 +1,19 @@
<!--
- Copyright (c) Haulmont 2026. All Rights Reserved.
- Use is subject to license terms.
-->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.84141 3.22307C3.30993 4.0174 3 4.97251 3 6V11C3 13.2091 4.79086 15 7 15H9C11.2091 15 13 13.2091 13 11V6C13 4.97279 12.6902 4.01793 12.159 3.22372L12.9393 1.71258C13.036 1.52547 13.0005 1.29735 12.8516 1.14844C12.7027 0.999529 12.4745 0.964032 12.2874 1.06065L10.7763 1.84097C9.98207 1.30976 9.02721 1 8 1C6.97251 1 6.0174 1.30993 5.22307 1.84141L3.71657 1.06441C3.52939 0.967868 3.30127 1.00342 3.15234 1.15234C3.00342 1.30127 2.96787 1.52939 3.06441 1.71657L3.84141 3.22307Z" fill="#4682FA"/>
<path d="M7.5 4.75C7.5 5.7165 6.7165 6.5 5.75 6.5C4.7835 6.5 4 5.7165 4 4.75C4 3.7835 4.7835 3 5.75 3C6.7165 3 7.5 3.7835 7.5 4.75Z" fill="white"/>
<path d="M12 4.75C12 5.7165 11.2165 6.5 10.25 6.5C9.2835 6.5 8.5 5.7165 8.5 4.75C8.5 3.7835 9.2835 3 10.25 3C11.2165 3 12 3.7835 12 4.75Z" fill="white"/>
<path d="M9 7.5H7V8.5C7 8.77614 7.22386 9 7.5 9H8.5C8.77614 9 9 8.77614 9 8.5V7.5Z" fill="white"/>
<path d="M8 8.25C8.69036 8.25 9.25 7.91421 9.25 7.5C9.25 7.08579 8.69036 6.75 8 6.75C7.30964 6.75 6.75 7.08579 6.75 7.5C6.75 7.91421 7.30964 8.25 8 8.25Z" fill="#CCA18A"/>
<path d="M4.71409 14.2829C4.32586 14.0121 3.98791 13.6741 3.71708 13.2859L3.20649 13.7965C2.93117 14.0718 2.93117 14.5182 3.20649 14.7935C3.48181 15.0688 3.92819 15.0688 4.20351 14.7935L4.71409 14.2829Z" fill="#CCA18A"/>
<path d="M11.2859 14.2829C11.6741 14.0121 12.0121 13.6741 12.2829 13.2859L12.7935 13.7965C13.0688 14.0718 13.0688 14.5182 12.7935 14.7935C12.5182 15.0688 12.0718 15.0688 11.7965 14.7935L11.2859 14.2829Z" fill="#CCA18A"/>
<path d="M12.0016 8.99849L13.0016 9.99849C13.2769 10.2738 13.7233 10.2738 13.9986 9.99849C14.2739 9.72317 14.2739 9.27678 13.9986 9.00146L12.9986 8.00146L12.0016 8.99849Z" fill="#CCA18A"/>
<path d="M2.99843 9.99849L3.99843 8.99849L3.00141 8.00146L2.00141 9.00146C1.72609 9.27678 1.72609 9.72317 2.00141 9.99849C2.27673 10.2738 2.72311 10.2738 2.99843 9.99849Z" fill="#CCA18A"/>
<path d="M6.5 4.75C6.5 5.16421 6.16421 5.5 5.75 5.5C5.33579 5.5 5 5.16421 5 4.75C5 4.33579 5.33579 4 5.75 4C6.16421 4 6.5 4.33579 6.5 4.75Z" fill="black"/>
<path d="M11 4.75C11 5.16421 10.6642 5.5 10.25 5.5C9.83579 5.5 9.5 5.16421 9.5 4.75C9.5 4.33579 9.83579 4 10.25 4C10.6642 4 11 4.33579 11 4.75Z" fill="black"/>
<path d="M8.5 7.5C8.77614 7.5 9.00684 7.27302 8.94406 7.00411C8.80756 6.41937 8.43643 6 8 6C7.56357 6 7.19244 6.41937 7.05594 7.00411C6.99316 7.27302 7.22386 7.5 7.5 7.5H8.5Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,19 @@
<!--
- Copyright (c) Haulmont 2026. All Rights Reserved.
- Use is subject to license terms.
-->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3.84141 3.22307C3.30993 4.0174 3 4.97251 3 6V11C3 13.2091 4.79086 15 7 15H9C11.2091 15 13 13.2091 13 11V6C13 4.97279 12.6902 4.01793 12.159 3.22372L12.9393 1.71258C13.036 1.52547 13.0005 1.29735 12.8516 1.14844C12.7027 0.999529 12.4745 0.964032 12.2874 1.06065L10.7763 1.84097C9.98207 1.30976 9.02721 1 8 1C6.97251 1 6.0174 1.30993 5.22307 1.84141L3.71657 1.06441C3.52939 0.967868 3.30127 1.00342 3.15234 1.15234C3.00342 1.30127 2.96787 1.52939 3.06441 1.71657L3.84141 3.22307Z" fill="#548AF7"/>
<path d="M7.5 4.75C7.5 5.7165 6.7165 6.5 5.75 6.5C4.7835 6.5 4 5.7165 4 4.75C4 3.7835 4.7835 3 5.75 3C6.7165 3 7.5 3.7835 7.5 4.75Z" fill="white"/>
<path d="M12 4.75C12 5.7165 11.2165 6.5 10.25 6.5C9.2835 6.5 8.5 5.7165 8.5 4.75C8.5 3.7835 9.2835 3 10.25 3C11.2165 3 12 3.7835 12 4.75Z" fill="white"/>
<path d="M9 7.5H7V8.5C7 8.77614 7.22386 9 7.5 9H8.5C8.77614 9 9 8.77614 9 8.5V7.5Z" fill="white"/>
<path d="M8 8.25C8.69036 8.25 9.25 7.91421 9.25 7.5C9.25 7.08579 8.69036 6.75 8 6.75C7.30964 6.75 6.75 7.08579 6.75 7.5C6.75 7.91421 7.30964 8.25 8 8.25Z" fill="#CCA18A"/>
<path d="M4.71409 14.2829C4.32586 14.0121 3.98791 13.6741 3.71708 13.2859L3.20649 13.7965C2.93117 14.0718 2.93117 14.5182 3.20649 14.7935C3.48181 15.0688 3.92819 15.0688 4.20351 14.7935L4.71409 14.2829Z" fill="#CCA18A"/>
<path d="M11.2859 14.2829C11.6741 14.0121 12.0121 13.6741 12.2829 13.2859L12.7935 13.7965C13.0688 14.0718 13.0688 14.5182 12.7935 14.7935C12.5182 15.0688 12.0718 15.0688 11.7965 14.7935L11.2859 14.2829Z" fill="#CCA18A"/>
<path d="M12.0016 8.99849L13.0016 9.99849C13.2769 10.2738 13.7233 10.2738 13.9986 9.99849C14.2739 9.72317 14.2739 9.27678 13.9986 9.00146L12.9986 8.00146L12.0016 8.99849Z" fill="#CCA18A"/>
<path d="M2.99843 9.99849L3.99843 8.99849L3.00141 8.00146L2.00141 9.00146C1.72609 9.27678 1.72609 9.72317 2.00141 9.99849C2.27673 10.2738 2.72311 10.2738 2.99843 9.99849Z" fill="#CCA18A"/>
<path d="M6.5 4.75C6.5 5.16421 6.16421 5.5 5.75 5.5C5.33579 5.5 5 5.16421 5 4.75C5 4.33579 5.33579 4 5.75 4C6.16421 4 6.5 4.33579 6.5 4.75Z" fill="#1E1F22"/>
<path d="M11 4.75C11 5.16421 10.6642 5.5 10.25 5.5C9.83579 5.5 9.5 5.16421 9.5 4.75C9.5 4.33579 9.83579 4 10.25 4C10.6642 4 11 4.33579 11 4.75Z" fill="#1E1F22"/>
<path d="M8.5 7.5C8.77614 7.5 9.00684 7.27302 8.94406 7.00411C8.80756 6.41937 8.43643 6 8 6C7.56357 6 7.19244 6.41937 7.05594 7.00411C6.99316 7.27302 7.22386 7.5 7.5 7.5H8.5Z" fill="#1E1F22"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -29,7 +29,7 @@
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceInterface="com.intellij.ide.startup.importSettings.data.StartupWizardService"
serviceImplementation="com.intellij.ide.startup.importSettings.data.DisabledStartupWizardPages"/>
serviceImplementation="com.intellij.ide.startup.importSettings.chooser.ui.OpenIdeStartupWizardService"/>
<transferSettings.thirdPartyProductSettingsTransfer id="VSCodeSettingsTransfer"
implementation="com.intellij.ide.startup.importSettings.transfer.VsCodeSettingsTransfer"/>

View File

@@ -87,13 +87,27 @@ choose.product.setting.sync.turned.off=Setting Sync Is Turned Off
choose.keymap.title = Choose Keymap
wizard.button.continue = Continue
plugins.page.title = Featured Plugins
plugins.page.ok.button.continue.without = Continue without Plugins
plugins.page.ok.button.install = Install Selected
plugins.page.choose.counter.no = No plugins selected for installation
plugins.page.choose.counter.one = 1 plugin selected for installation
plugins.page.choose.counter.multiple = {0} plugins selected for installation
install.plugins.page.title = Installing Plugins\u2026
plugins.page.title = Рекомендуемые Плагины
plugins.page.ok.button.continue.without = Продолжить без Плагинов
plugins.page.ok.button.install = Установить Выбранные
plugins.page.choose.counter.no = Нет плагинов для установки
plugins.page.choose.counter.one = 1 плагин выбран для установки
plugins.page.choose.counter.multiple = {0} плагина выбрано для установки
plugins.page.list.item.bundled = Предустановлен
# Plugin descriptions
plugin.description.mapstruct = Плагин для редактирования MapStruct мапперов
plugin.description.init.spring = Интеграция Init Spring для новых проектов
plugin.description.java.kotlin.support = Поддерживается OpenIDE
plugin.description.spring.support = Поддерживается Amplicode
plugin.description.python = Поддержка языка Python
plugin.description.sourcery = AI-рефакторинг кода для Python
plugin.description.webtools = Набор инструментов для веб-разработки
plugin.description.go = Поддержка языка Go
plugin.description.kilocode = Kilo Code AI Агент
plugin.description.continue = Open-source AI ассистент для кода
install.plugins.page.title = Установка Плагинов\u2026
theme.page.title = Choose Theme
theme.page.dark = Dark

View File

@@ -25,6 +25,7 @@ public final class StartupImportIcons {
/** 16x32 */ public static final @NotNull Icon Build = load("icons/build.svg", 263576131, 0);
/** 16x16 */ public static final @NotNull Icon ColorPicker = load("icons/colorPicker.svg", -2131734344, 0);
/** 20x20 */ public static final @NotNull Icon ConfigFile = load("icons/configFile.svg", 1186090686, 0);
/** 16x16 */ public static final @NotNull Icon GO = load("icons/go.svg", -1523302431, 2);
/** 16x32 */ public static final @NotNull Icon Json = load("icons/json.svg", -322377413, 0);
/** 16x16 */ public static final @NotNull Icon Keyboard = load("icons/keyboard.svg", -1617681176, 0);
/** 16x16 */ public static final @NotNull Icon Plugin = load("icons/plugin.svg", 62738285, 0);
@@ -51,9 +52,9 @@ public final class StartupImportIcons {
/** 20x20 */ public static final @NotNull Icon GO_20 = load("ideIcons/GO_20.svg", -1868538832, 0);
/** 24x24 */ public static final @NotNull Icon GO_24 = load("ideIcons/GO_24.svg", 1440377958, 0);
/** 48x48 */ public static final @NotNull Icon GO_48 = load("ideIcons/GO_48.svg", 1561543628, 0);
/** 20x20 */ public static final @NotNull Icon IC_20 = load("ideIcons/IC_20.svg", -1970123839, 0);
/** 24x24 */ public static final @NotNull Icon IC_24 = load("ideIcons/IC_24.svg", 1159411325, 0);
/** 48x48 */ public static final @NotNull Icon IC_48 = load("ideIcons/IC_48.svg", 337830589, 0);
/** 20x20 */ public static final @NotNull Icon IC_20 = load("ideIcons/IC_20.svg", 502576943, 0);
/** 24x24 */ public static final @NotNull Icon IC_24 = load("ideIcons/IC_24.svg", -1033029982, 0);
/** 48x48 */ public static final @NotNull Icon IC_48 = load("ideIcons/IC_48.svg", 1976499955, 0);
/** 20x20 */ public static final @NotNull Icon IU_20 = load("ideIcons/IU_20.svg", 1667621169, 0);
/** 24x24 */ public static final @NotNull Icon IU_24 = load("ideIcons/IU_24.svg", 458021303, 0);
/** 48x48 */ public static final @NotNull Icon IU_48 = load("ideIcons/IU_48.svg", 897538403, 0);
@@ -94,7 +95,7 @@ public final class StartupImportIcons {
/** 140x29 */ public static final @NotNull Icon DG = load("ideNames/DG.svg", 1972725857, 2);
/** 143x29 */ public static final @NotNull Icon DS = load("ideNames/DS.svg", 1754791872, 2);
/** 119x24 */ public static final @NotNull Icon GO = load("ideNames/GO.svg", -693864257, 2);
/** 291x67 */ public static final @NotNull Icon IC = load("ideNames/IC.svg", -1523330134, 2);
/** 290x67 */ public static final @NotNull Icon IC = load("ideNames/IC.svg", -1642048260, 2);
/** 312x25 */ public static final @NotNull Icon IU = load("ideNames/IU.svg", 1956857230, 2);
/** 67x24 */ public static final @NotNull Icon MPS = load("ideNames/MPS.svg", 2042563748, 2);
/** 291x66 */ public static final @NotNull Icon PC = load("ideNames/PC.svg", -790053601, 2);

View File

@@ -107,7 +107,7 @@ class OnboardingController private constructor(){
}
service.onEnter()
wizardController.goToThemePage(true)
wizardController.goToPluginPage()
if(!dl.isShowing) {
dl.initialize()

View File

@@ -0,0 +1,177 @@
// OpenIDE Project
// Copyright (C) 2026 “Open Development Platform” Ltd. (https://openide.ru)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License version 3 or later as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see http://www.gnu.org/licenses/.
package com.intellij.ide.startup.importSettings.chooser.ui
import com.intellij.icons.AllIcons
import com.intellij.ide.startup.importSettings.ImportSettingsBundle
import com.intellij.ide.startup.importSettings.StartupImportIcons
import com.intellij.ide.startup.importSettings.data.*
import com.intellij.java.ui.icons.JavaUIIcons
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.reactive.IVoidSource
import com.jetbrains.rd.util.reactive.OptProperty
import com.jetbrains.rd.util.reactive.Property
class OpenIdeStartupWizardService : StartupWizardService {
override val isActive = true
override val shouldClose: IVoidSource
get() = object : IVoidSource {
override fun advise(lifetime: Lifetime, handler: (Unit) -> Unit) {
}
}
override fun getKeymapService(): KeymapService {
TODO("Not yet implemented")
}
override fun getThemeService(): ThemeService {
TODO("Not yet implemented")
}
override fun getPluginService(): PluginService {
return object : PluginService {
override val pluginGroups: List<WizardPluginGroup>
get() = listOf(
WizardPluginGroupImpl(
id = "java-kotlin",
name = "Java / Kotlin",
icon = AllIcons.FileTypes.Java,
plugins = listOf(
WizardPluginImpl(
id = "org.mapstruct.intellij",
icon = AllIcons.Plugins.PluginLogo,
name = "MapStruct Support",
description = ImportSettingsBundle.message("plugin.description.mapstruct"),
),
WizardPluginImpl(
id = "pro.nikolaev.init-spring",
icon = AllIcons.Plugins.PluginLogo,
name = "Init Spring",
description = ImportSettingsBundle.message("plugin.description.init.spring"),
),
WizardPluginImpl(
id = "bundled.java.kotlin",
icon = AllIcons.FileTypes.Java,
name = "Java / Kotlin support",
description = ImportSettingsBundle.message("plugin.description.java.kotlin.support"),
bundled = true,
),
WizardPluginImpl(
id = "bundled.spring",
icon = JavaUIIcons.SpringPromo,
name = "Spring support",
description = ImportSettingsBundle.message("plugin.description.spring.support"),
bundled = true,
),
)
),
WizardPluginGroupImpl(
id = "python",
name = "Python",
icon = AllIcons.Language.Python,
plugins = listOf(
WizardPluginImpl(
id = "PythonCore",
icon = AllIcons.Plugins.PluginLogo,
name = "Python Community Edition",
description = ImportSettingsBundle.message("plugin.description.python"),
),
WizardPluginImpl(
id = "sourcery.pycharm-plugin",
icon = AllIcons.Plugins.PluginLogo,
name = "Sourcery",
description = ImportSettingsBundle.message("plugin.description.sourcery"),
),
)
),
WizardPluginGroupImpl(
id = "web",
name = "Web",
icon = AllIcons.FileTypes.Html,
plugins = listOf(
WizardPluginImpl(
id = "com.haulmont.webtools",
icon = AllIcons.Plugins.PluginLogo,
name = "Frontend/Web [beta]",
description = ImportSettingsBundle.message("plugin.description.webtools"),
),
)
),
WizardPluginGroupImpl(
id = "go",
name = "Go",
icon = StartupImportIcons.Icons.GO,
plugins = listOf(
WizardPluginImpl(
id = "org.jetbrains.plugins.go",
icon = AllIcons.Plugins.PluginLogo,
name = "Go [beta]",
description = ImportSettingsBundle.message("plugin.description.go"),
),
)
),
WizardPluginGroupImpl(
id = "ai",
name = "AI",
icon = AllIcons.Actions.Lightning,
plugins = listOf(
WizardPluginImpl(
id = "ai.kilocode.jetbrains",
icon = AllIcons.Plugins.PluginLogo,
name = "Kilo Code",
description = ImportSettingsBundle.message("plugin.description.kilocode"),
),
WizardPluginImpl(
id = "com.github.continuedev.continueintellijextension",
icon = AllIcons.Plugins.PluginLogo,
name = "Continue",
description = ImportSettingsBundle.message("plugin.description.continue"),
),
)
),
)
override fun onStepEnter() {
}
override fun install(lifetime: Lifetime, ids: List<String>): PluginImportProgress {
return object : PluginImportProgress {
override val icon = Property(AllIcons.Plugins.PluginLogo)
override val progressMessage = Property(null)
override val progress = OptProperty<Int>()
}
}
override fun skipPlugins() {
}
override fun shouldShowPage(pluginIdsMarkedForInstallation: List<String>): Boolean {
return true
}
}
}
override fun onEnter() {
}
override fun onCancel() {
}
override fun onExit() {
}
}

View File

@@ -64,9 +64,8 @@ internal class WizardControllerImpl(dialog: OnboardingDialog,
}
override fun goToPluginPage() {
val page = WizardPluginsPage(this, service.getPluginService(), goBackAction = {
goToKeymapPage(isForwardDirection = false)
}, goForwardAction = { ids ->
val page = WizardPluginsPage(this, service.getPluginService(), goBackAction = null,
goForwardAction = { ids ->
goToInstallPluginPage(ids)
}, continueButtonTextOverride = null)
dialog.changePage(page)

View File

@@ -87,7 +87,7 @@ data class WizardScheme(
)
interface PluginService {
val plugins: List<WizardPlugin>
val pluginGroups: List<WizardPluginGroup>
fun onStepEnter()
fun install(lifetime: Lifetime, ids: List<String>): PluginImportProgress
fun skipPlugins()
@@ -103,6 +103,15 @@ interface WizardPlugin {
val icon: Icon
val name: String
val description: String?
val bundled: Boolean
get() = false
}
interface WizardPluginGroup {
val id: String
val name: String
val icon: Icon
val plugins: List<WizardPlugin>
}
interface KeymapService {

View File

@@ -113,7 +113,14 @@ class PluginServiceImpl : PluginService {
)
override val plugins: List<WizardPlugin> = listOf
override val pluginGroups: List<WizardPluginGroup> = listOf(
WizardPluginGroupImpl(
id = "mock.group",
name = "Mock Group",
icon = AllIcons.Plugins.PluginLogo,
plugins = listOf
)
)
override fun onStepEnter() {}
@@ -154,8 +161,15 @@ class TestPluginImportProgress(lifetime: Lifetime) : TestImportProgress(lifetime
class WizardPluginImpl(override val icon: Icon,
override val name: String,
override val description: String? = null,
override val id: String = UUID.randomUUID().toString()) : WizardPlugin {
}
override val id: String = UUID.randomUUID().toString(),
override val bundled: Boolean = false) : WizardPlugin
class WizardPluginGroupImpl(
override val id: String,
override val name: String,
override val icon: Icon,
override val plugins: List<WizardPlugin>
) : WizardPluginGroup
@Suppress("HardCodedStringLiteral")
class TestKeymapService : KeymapService {

View File

@@ -0,0 +1,44 @@
/*
* Copyright (c) Haulmont 2026. All Rights Reserved.
* Use is subject to license terms.
*/
package com.intellij.ide.startup.importSettings.service
import com.intellij.ide.plugins.marketplace.MarketplaceRequests
import com.intellij.ide.plugins.newui.PluginLogo
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.util.IntellijInternalApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.swing.Icon
@Service(Service.Level.APP)
class PluginIconService {
suspend fun loadIcon(pluginId: String): Icon? {
return withContext(Dispatchers.IO) {
loadFromMarketplace(pluginId)
}
}
@OptIn(IntellijInternalApi::class)
private fun loadFromMarketplace(pluginId: String): Icon? {
return try {
val marketplaceRequests = MarketplaceRequests.getInstance()
val pluginNode = marketplaceRequests.getLastCompatiblePluginUpdateModel(
PluginId.getId(pluginId),
buildNumber = null
) ?: return null
PluginLogo.getIcon(pluginNode.getDescriptor(), false, false, false)
} catch (_: Exception) {
null
}
}
companion object {
fun getInstance(): PluginIconService = service()
}
}

View File

@@ -0,0 +1,161 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.startup.importSettings.wizard.pluginChooser
import com.intellij.icons.AllIcons
import com.intellij.ide.startup.importSettings.data.WizardPluginGroup
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.panels.VerticalLayout
import com.intellij.util.IconUtil
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.ThreeStateCheckBox
import com.intellij.util.ui.UIUtil
import java.awt.BorderLayout
import java.awt.Cursor
import java.awt.FlowLayout
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import kotlinx.coroutines.CoroutineScope
import javax.swing.Icon
import javax.swing.JLabel
import javax.swing.JPanel
class WizardPluginGroupPane(
private val group: WizardPluginGroup,
private val coroutineScope: CoroutineScope,
private val onSelectionChanged: () -> Unit
) {
private val pluginPanes = mutableListOf<WizardPluginPane>()
private val groupCheckbox = ThreeStateCheckBox()
private val counterLabel = JLabel()
private val contentPanel = JPanel(VerticalLayout(0)).apply {
isOpaque = false
isVisible = false
border = JBUI.Borders.emptyLeft(28)
group.plugins.forEach { plugin ->
val pluginPane = WizardPluginPane(plugin, coroutineScope) {
updateGroupCheckboxState()
onSelectionChanged()
}
pluginPanes.add(pluginPane)
add(pluginPane.pane)
}
}
private val expandIcon = JLabel()
private var isExpanded = false
val pane: JPanel = createPane()
private val selectablePanes: List<WizardPluginPane>
get() = pluginPanes.filter { !it.isBundled }
val selectedPlugins: List<WizardPluginPane>
get() = pluginPanes.filter { it.selected && !it.isBundled }
val selectedCount: Int
get() = selectablePanes.count { it.selected }
val totalCount: Int
get() = selectablePanes.size
private fun createPane(): JPanel {
return JPanel(BorderLayout()).apply {
isOpaque = false
border = JBUI.Borders.empty(0, 16)
add(createHeader(), BorderLayout.NORTH)
add(contentPanel, BorderLayout.CENTER)
updateGroupCheckboxState()
}
}
private fun createHeader(): JPanel {
groupCheckbox.apply {
isOpaque = false
isThirdStateEnabled = false
addActionListener {
val shouldSelect = state == ThreeStateCheckBox.State.SELECTED
selectablePanes.forEach { it.setSelected(shouldSelect) }
updateGroupCheckboxState()
onSelectionChanged()
}
}
counterLabel.apply {
foreground = UIUtil.getContextHelpForeground()
font = JBFont.label()
}
expandIcon.apply {
icon = getExpandIcon()
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
toggleExpanded()
}
})
}
val scaledIcon = IconUtil.scale(group.icon, null, 1.5f)
val groupIcon = JLabel(scaledIcon).apply {
border = JBUI.Borders.emptyRight(8)
}
val groupName = JBLabel(group.name).apply {
font = JBFont.h4()
}
val headerClickArea = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
isOpaque = false
add(expandIcon)
add(groupCheckbox)
add(groupIcon)
add(groupName)
add(JLabel(" ").apply { border = JBUI.Borders.emptyLeft(4) })
add(counterLabel)
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
addMouseListener(object : MouseAdapter() {
override fun mouseClicked(e: MouseEvent) {
if (e.source !== expandIcon && e.source !== groupCheckbox) {
toggleExpanded()
}
}
})
}
return JPanel(BorderLayout()).apply {
isOpaque = false
border = JBUI.Borders.empty(4, 0)
add(headerClickArea, BorderLayout.CENTER)
}
}
private fun updateGroupCheckboxState() {
val selectable = selectablePanes
val selectedCount = selectable.count { it.selected }
groupCheckbox.state = when (selectedCount) {
0 -> ThreeStateCheckBox.State.NOT_SELECTED
selectable.size -> ThreeStateCheckBox.State.SELECTED
else -> ThreeStateCheckBox.State.DONT_CARE
}
counterLabel.text = "($selectedCount/${selectable.size})"
}
private fun toggleExpanded() {
isExpanded = !isExpanded
contentPanel.isVisible = isExpanded
expandIcon.icon = getExpandIcon()
}
private fun getExpandIcon(): Icon {
return if (isExpanded) AllIcons.General.ArrowDown else AllIcons.General.ArrowRight
}
fun setAllSelected(selected: Boolean) {
selectablePanes.forEach { it.setSelected(selected) }
updateGroupCheckboxState()
}
}

View File

@@ -1,70 +1,97 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.ide.startup.importSettings.wizard.pluginChooser
import com.intellij.ide.startup.importSettings.ImportSettingsBundle
import com.intellij.ide.startup.importSettings.data.WizardPlugin
import com.intellij.ide.startup.importSettings.service.PluginIconService
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.dsl.builder.HyperlinkEventAction
import com.intellij.ui.dsl.builder.MAX_LINE_LENGTH_WORD_WRAP
import com.intellij.ui.dsl.builder.components.DslLabel
import com.intellij.ui.dsl.builder.components.DslLabelType
import com.intellij.util.IconUtil
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import java.awt.GridBagConstraints
import java.awt.GridBagLayout
import javax.swing.JEditorPane
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.SwingUtilities
class WizardPluginPane(val plugin: WizardPlugin, private val coroutineScope: CoroutineScope, changeHandler: () -> Unit) {
class WizardPluginPane(val plugin: WizardPlugin, changeHandler: () -> Unit) {
private var checkBox = JBCheckBox().apply {
addItemListener { e ->
changeHandler()
}
isOpaque = false
}
val selected: Boolean
get() = checkBox.isSelected
fun setSelected(value: Boolean) {
checkBox.isSelected = value
}
fun setEnabled(value: Boolean) {
checkBox.isEnabled = value
pane.isEnabled = value
nameLabel.isEnabled = value
}
private val iconLabel = JLabel(IconUtil.resizeSquared(plugin.icon, 24))
private val nameLabel = createLabel()
private val bundledLabel = JLabel(ImportSettingsBundle.message("plugins.page.list.item.bundled")).apply {
foreground = UIUtil.getContextHelpForeground()
border = JBUI.Borders.emptyRight(10)
}
val pane = JPanel(GridBagLayout()).apply {
val c = GridBagConstraints()
plugin.description?.let {
c.gridx = 0
c.gridy = 1
c.gridy = 0
c.weightx = 0.0
c.anchor = GridBagConstraints.CENTER
add(checkBox, c)
c.gridx = 1
c.gridy = 0
c.gridheight = 3
c.anchor = GridBagConstraints.NORTH
add(JLabel(plugin.icon).apply {
c.anchor = GridBagConstraints.CENTER
add(iconLabel.apply {
border = JBUI.Borders.empty(0, 10)
}, c)
c.gridx = 2
c.gridy = 0
c.gridheight = 1
c.anchor = GridBagConstraints.CENTER
add(JPanel().apply {
isOpaque = false }
)
c.gridx = 2
c.gridy = 1
c.weightx = 1.0
c.fill = GridBagConstraints.HORIZONTAL
add(createLabel().apply {
text = plugin.name
add(JPanel(GridBagLayout()).apply {
isOpaque = false
val inner = GridBagConstraints()
inner.gridx = 0
inner.gridy = 0
inner.weightx = 1.0
inner.fill = GridBagConstraints.HORIZONTAL
inner.anchor = GridBagConstraints.WEST
add(nameLabel.apply {
text = plugin.name
}, inner)
inner.gridy = 1
add(createLabel().apply {
text = plugin.description
foreground = UIUtil.getLabelDisabledForeground()
}, inner)
}, c)
c.gridx = 2
c.gridy = 2
add(createLabel().apply {
text = plugin.description
foreground = UIUtil.getLabelDisabledForeground()
}, c)
if (plugin.bundled) {
c.gridx = 3
c.weightx = 0.0
c.fill = GridBagConstraints.NONE
c.anchor = GridBagConstraints.EAST
add(bundledLabel, c)
}
} ?: run {
c.gridx = 0
@@ -73,20 +100,43 @@ class WizardPluginPane(val plugin: WizardPlugin, changeHandler: () -> Unit) {
add(checkBox, c)
c.gridx = 1
c.gridy = 0
add(JLabel(plugin.icon).apply {
add(iconLabel.apply {
border = JBUI.Borders.empty(0, 10)
}, c)
c.gridx = 2
c.gridy = 0
c.weightx = 1.0
c.fill = GridBagConstraints.HORIZONTAL
add(createLabel().apply {
add(nameLabel.apply {
text = plugin.name
}, c)
if (plugin.bundled) {
c.gridx = 3
c.weightx = 0.0
c.fill = GridBagConstraints.NONE
c.anchor = GridBagConstraints.EAST
add(bundledLabel, c)
}
}
}.apply {
isOpaque = false
border = JBUI.Borders.empty(0, 20)
border = JBUI.Borders.empty(8, 20)
}
val isBundled: Boolean = plugin.bundled
init {
loadIconAsync()
if (isBundled) {
checkBox.isSelected = true
setEnabled(false)
}
checkBox.addItemListener {
changeHandler()
}
}
private fun createLabel(): JEditorPane {
@@ -96,4 +146,16 @@ class WizardPluginPane(val plugin: WizardPlugin, changeHandler: () -> Unit) {
return dslLabel
}
private fun loadIconAsync() {
coroutineScope.launch {
val icon = PluginIconService.getInstance().loadIcon(plugin.id)
if (icon != null) {
SwingUtilities.invokeLater {
iconLabel.icon = IconUtil.resizeSquared(icon, 24)
pane.repaint()
}
}
}
}
}

View File

@@ -4,6 +4,7 @@ package com.intellij.ide.startup.importSettings.wizard.pluginChooser
import com.intellij.ide.startup.importSettings.ImportSettingsBundle
import com.intellij.ide.startup.importSettings.chooser.ui.*
import com.intellij.ide.startup.importSettings.data.PluginService
import com.intellij.ide.startup.importSettings.data.WizardPluginGroup
import com.intellij.openapi.util.NlsActions.ActionText
import com.intellij.openapi.util.SystemInfo
import com.intellij.platform.ide.bootstrap.StartupWizardStage
@@ -20,14 +21,14 @@ import javax.swing.*
internal class WizardPluginsPage(
val controller: BaseController,
private val pluginService: PluginService,
goBackAction: () -> Unit,
goBackAction: (() -> Unit)?,
goForwardAction: (List<String>) -> Unit,
private val continueButtonTextOverride: @ActionText String?
) : OnboardingPage {
override val stage: StartupWizardStage = StartupWizardStage.WizardPluginPage
private val pluginPanes = mutableListOf<WizardPluginPane>()
private val groupPanes = mutableListOf<WizardPluginGroupPane>()
private val contentPage: JComponent
@@ -35,14 +36,20 @@ internal class WizardPluginsPage(
border = JBUI.Borders.emptyLeft(26)
foreground = UIUtil.getContextHelpForeground()
}
override fun confirmExit(parentComponent: Component?): Boolean = true
private fun getSelected(): List<WizardPluginPane> {
return pluginPanes.filter { it.selected }.toList()
return groupPanes.flatMap { it.selectedPlugins }
}
private fun getSelectedCount(): Int {
return groupPanes.sumOf { it.selectedCount }
}
private fun changeHandler() {
val selected = getSelected()
when(selected.size) {
val selectedSize = getSelected().size
when(selectedSize) {
0 -> {
leftLabel.text = ImportSettingsBundle.message("plugins.page.choose.counter.no")
continueAction.text = continueButtonTextOverride ?: ImportSettingsBundle.message("plugins.page.ok.button.continue.without")
@@ -52,7 +59,7 @@ internal class WizardPluginsPage(
continueAction.text = continueButtonTextOverride ?: ImportSettingsBundle.message("plugins.page.ok.button.install")
}
else -> {
leftLabel.text = ImportSettingsBundle.message("plugins.page.choose.counter.multiple", selected.size)
leftLabel.text = ImportSettingsBundle.message("plugins.page.choose.counter.multiple", selectedSize)
continueAction.text = continueButtonTextOverride ?: ImportSettingsBundle.message("plugins.page.ok.button.install")
}
}
@@ -65,14 +72,14 @@ internal class WizardPluginsPage(
border = JBUI.Borders.empty(18, 20)
})
val plugins = pluginService.plugins
val pluginGroups: List<WizardPluginGroup> = pluginService.pluginGroups
val listPane = JPanel(VerticalLayout(JBUI.scale(4))).apply {
isOpaque = false
plugins.forEach {
val pl = WizardPluginPane(it) { changeHandler() }
pluginPanes.add(pl)
add(pl.pane)
pluginGroups.forEach { group: WizardPluginGroup ->
val groupPane = WizardPluginGroupPane(group, controller.lifetime.coroutineScope) { changeHandler() }
groupPanes.add(groupPane)
add(groupPane.pane)
}
border = JBUI.Borders.empty(10, 0)
}
@@ -93,7 +100,7 @@ internal class WizardPluginsPage(
})
}
private val backAction = controller.createButton(ImportSettingsBundle.message("import.settings.back"), goBackAction)
private val backAction = controller.createButton(ImportSettingsBundle.message("import.settings.back"), goBackAction ?: {})
private val continueAction = controller.createDefaultButton(continueButtonTextOverride ?: ImportSettingsBundle.message("plugins.page.ok.button.continue.without")) {
val ids = getSelected().map { it.plugin.id }.toList()
@@ -101,10 +108,20 @@ internal class WizardPluginsPage(
}
init {
val buttons: List<JButton> = if (SystemInfo.isMac) {
listOf(backAction, continueAction)
val buttons: List<JButton> = buildList {
if (SystemInfo.isMac) {
if (goBackAction != null) {
add(backAction)
}
add(continueAction)
}
else {
add(continueAction)
if (goBackAction != null) {
add(backAction)
}
}
}
else listOf(continueAction, backAction)
contentPage = WizardPagePane(pane, buttons, leftLabel)