mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-03-22 15:19:59 +07:00
ASPR-3010 Recommended plugin installation on IDEA startup
This commit is contained in:
@@ -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>
|
||||
19
plugins/ide-startup/importSettings/resources/icons/go.svg
Normal file
19
plugins/ide-startup/importSettings/resources/icons/go.svg
Normal 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 |
@@ -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 |
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -107,7 +107,7 @@ class OnboardingController private constructor(){
|
||||
}
|
||||
|
||||
service.onEnter()
|
||||
wizardController.goToThemePage(true)
|
||||
wizardController.goToPluginPage()
|
||||
|
||||
if(!dl.isShowing) {
|
||||
dl.initialize()
|
||||
|
||||
@@ -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() {
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user