ASPR-3010 Recommended plugin installation on IDEA startup

This commit is contained in:
Artem Orlov
2026-03-26 13:12:37 +04:00
committed by Nikita Iarychenko
parent e0972d8d7a
commit 87d9534fc9
19 changed files with 1005 additions and 177 deletions
@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 60 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M59.024 19.4576C57.5531 18.4675 53.6878 18.0446 50.8776 18.8015C50.7261 16.0024 49.2829 13.6437 46.6426 11.5857L45.6651 10.9296L45.0138 11.9134C43.7333 13.8571 43.1938 16.4467 43.385 18.8007C43.5357 20.251 44.0403 21.8814 45.0138 23.0643C41.3563 25.1858 37.9852 24.7042 23.0555 24.7042H0.00465616C-0.062781 28.0753 0.479096 34.5603 4.60307 39.8395C5.05847 40.4226 5.5583 40.9867 6.10018 41.5302C9.45299 44.8877 14.5187 47.3496 22.0939 47.3567C33.6502 47.367 43.5516 41.1208 49.5741 26.0172C51.556 26.0498 56.7875 26.3727 59.3477 21.4252C59.4104 21.3419 59.9991 20.1129 59.9991 20.1129L59.0232 19.4568L59.024 19.4576ZM15.0479 16.3745H8.566V22.8564H15.0479V16.3745ZM23.422 16.3745H16.9401V22.8564H23.422V16.3745ZM31.7961 16.3745H25.3142V22.8564H31.7961V16.3745ZM40.1702 16.3745H33.6883V22.8564H40.1702V16.3745ZM6.67379 16.3745H0.191893V22.8564H6.67379V16.3745ZM15.0479 8.18766H8.566V14.6696H15.0479V8.18766ZM23.422 8.18766H16.9401V14.6696H23.422V8.18766ZM31.7961 8.18766H25.3142V14.6696H31.7961V8.18766ZM31.7961 0H25.3142V6.4819H31.7961V0Z" fill="#1D63ED"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,4 @@
<!-- Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill="#62B543" fill-opacity=".7" fill-rule="evenodd" d="M13.8189229,12.2851118 C11.7381304,15.0552815 7.29624189,14.1257561 4.44717245,14.2546012 C4.44717245,14.2546012 3.94283089,14.2832335 3.43439732,14.3670851 C3.43439732,14.3670851 3.62672229,14.2852786 3.87224354,14.2004045 C5.87221867,13.5091403 6.81849847,13.3700694 8.03485163,12.7452728 C10.3181992,11.5754411 12.5923397,9.02819378 13.0537151,6.3817562 C12.1841607,8.92798092 9.53969226,11.1183477 7.13256106,12.0059474 C5.4834767,12.61336 2.50243959,13.2064565 2.50243959,13.2074791 L2.38172498,13.1430565 C0.354128707,12.1572892 0.297863422,7.76939754 3.97863607,6.35516911 C5.59293826,5.73446293 7.13665308,6.07600472 8.88087692,5.65981457 C10.7417233,5.21805992 12.8961723,3.81917019 13.7718647,2 C14.7549727,4.91230844 15.9344977,9.46790327 13.8189229,12.2851118 Z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -1,68 +1,19 @@
<!-- Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<!-- -->
<!-- Modified by Nikita Iarychenko at 2025 as part of the OpenIDE project (https://openide.ru). -->
<!-- Any modifications are available on the same license terms as the original source code. -->
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3181_691)">
<mask id="mask0_3181_691" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
<rect width="20" height="20" rx="2.96875" fill="white"/>
</mask>
<g mask="url(#mask0_3181_691)">
<g filter="url(#filter0_f_3181_691)">
<circle cx="9.91837" cy="9.91837" r="9.91837" fill="#9B84E7"/>
</g>
<g filter="url(#filter1_f_3181_691)">
<ellipse cx="18.125" cy="9.21875" rx="10" ry="17.3438" fill="#9B84E7"/>
</g>
<g filter="url(#filter2_f_3181_691)">
<ellipse cx="7.30627" cy="22.7347" rx="8.77551" ry="9.10204" fill="#F24E4E"/>
</g>
<g filter="url(#filter3_f_3181_691)">
<ellipse cx="-0.857113" cy="19.5102" rx="5.83673" ry="8.4898" fill="#9B84E7"/>
</g>
<g filter="url(#filter4_f_3181_691)">
<ellipse cx="20.625" cy="21.25" rx="5.3125" ry="9.375" fill="white"/>
</g>
<g filter="url(#filter5_f_3181_691)">
<ellipse cx="3.59191" cy="3.38776" rx="9.87755" ry="11.4694" fill="#4D409B"/>
</g>
</g>
<path d="M12.6083 7.05312H15.9586C16.7326 7.05312 17.3599 6.46417 17.3599 5.73843V4.28846C17.3599 3.56272 16.7326 2.97377 15.9586 2.97377H12.6083C11.8343 2.97377 11.207 3.56272 11.207 4.28846V5.73843C11.207 6.46417 11.8343 7.05312 12.6083 7.05312Z" fill="white"/>
<path d="M16.1192 8.12994H12.448C11.6854 8.12994 11.0672 8.75081 11.0672 9.51683V14.2034C11.0672 14.4906 10.8516 14.7422 10.5663 14.7597C10.2569 14.7787 9.99962 14.5324 9.99962 14.2239V3.44719C9.99962 2.87496 9.49723 2.42887 8.93354 2.50943C5.29571 3.02923 2.5 6.17082 2.5 9.96748C2.50076 14.1236 5.86242 17.5 10.0004 17.5C14.1383 17.5 17.5 14.1274 17.5 9.96596V9.51607C17.5 8.75005 16.8818 8.12918 16.1192 8.12918V8.12994Z" fill="white"/>
</g>
<defs>
<filter id="filter0_f_3181_691" x="-4.57959" y="-4.57959" width="28.9961" height="28.9959" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.2898" result="effect1_foregroundBlur_3181_691"/>
</filter>
<filter id="filter1_f_3181_691" x="3.54541" y="-12.7046" width="29.1592" height="43.8467" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.2898" result="effect1_foregroundBlur_3181_691"/>
</filter>
<filter id="filter2_f_3181_691" x="-6.24475" y="8.85715" width="27.1018" height="27.7551" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.38776" result="effect1_foregroundBlur_3181_691"/>
</filter>
<filter id="filter3_f_3181_691" x="-11.2734" y="6.44082" width="20.8325" height="26.1388" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.2898" result="effect1_foregroundBlur_3181_691"/>
</filter>
<filter id="filter4_f_3181_691" x="8.56148" y="5.12398" width="24.127" height="32.252" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.37551" result="effect1_foregroundBlur_3181_691"/>
</filter>
<filter id="filter5_f_3181_691" x="-12.6203" y="-14.4163" width="32.4243" height="35.6082" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.16735" result="effect1_foregroundBlur_3181_691"/>
</filter>
<clipPath id="clip0_3181_691">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
<g clip-path="url(#clip0_3917_59730)">
<path d="M1.27273 20L12.1998 20C12.5373 20 12.8611 19.866 13.0998 19.6273L19.6273 13.1C19.866 12.8613 20 12.5376 20 12.2L20 1.27273C20 0.569819 19.4302 5.83484e-07 18.7273 5.22034e-07L7.80037 -4.33227e-07C7.46255 -4.6276e-07 7.13855 0.134364 6.89982 0.373273L0.372729 6.90782C0.134365 7.14636 0.000547716 7.47 0.000547686 7.80727L7.44565e-07 18.7273C6.83114e-07 19.4302 0.569819 20 1.27273 20Z" fill="url(#paint0_radial_3917_59730)"/>
<path d="M16.6654 3.33398H3.33203V16.6673H16.6654V3.33398Z" fill="black"/>
<path d="M10 14.166H5V14.9993H10V14.166Z" fill="white"/>
<path d="M5 9.12853H5.99293V5.87147H5V5H7.94633V5.87147H6.9534V9.12853H7.94633V10H5V9.12853Z" fill="white"/>
<path d="M8.44531 9.10008H9.16323C9.30838 9.10008 9.43767 9.06914 9.55074 9.00726C9.6638 8.94538 9.75127 8.85791 9.81315 8.74484C9.87503 8.63178 9.90597 8.50267 9.90597 8.35733V5H10.881V8.42494C10.881 8.72498 10.8118 8.99465 10.6737 9.23396C10.5356 9.47326 10.3452 9.66081 10.1023 9.79641C9.85937 9.9322 9.58683 10 9.2845 10H8.44531V9.10008Z" fill="white"/>
</g>
<defs>
<radialGradient id="paint0_radial_3917_59730" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(19.4793 0.520923) rotate(135) scale(27.0205)">
<stop offset="0.109181" stop-color="#FE2857"/>
<stop offset="0.7" stop-color="#007EFF"/>
</radialGradient>
<clipPath id="clip0_3917_59730">
<rect width="20" height="20" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -1,68 +1,19 @@
<!-- Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<!-- -->
<!-- Modified by Nikita Iarychenko at 2025 as part of the OpenIDE project (https://openide.ru). -->
<!-- Any modifications are available on the same license terms as the original source code. -->
<!-- Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_3181_705)">
<mask id="mask0_3181_705" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="0" y="0" width="24" height="24">
<rect width="24" height="24" rx="3.5625" fill="white"/>
</mask>
<g mask="url(#mask0_3181_705)">
<g filter="url(#filter0_f_3181_705)">
<circle cx="11.902" cy="11.902" r="11.902" fill="#9B84E7"/>
</g>
<g filter="url(#filter1_f_3181_705)">
<ellipse cx="21.75" cy="11.0625" rx="12" ry="20.8125" fill="#9B84E7"/>
</g>
<g filter="url(#filter2_f_3181_705)">
<ellipse cx="8.76743" cy="27.2816" rx="10.5306" ry="10.9224" fill="#F24E4E"/>
</g>
<g filter="url(#filter3_f_3181_705)">
<ellipse cx="-1.02863" cy="23.4122" rx="7.00408" ry="10.1878" fill="#9B84E7"/>
</g>
<g filter="url(#filter4_f_3181_705)">
<ellipse cx="24.75" cy="25.5" rx="6.375" ry="11.25" fill="white"/>
</g>
<g filter="url(#filter5_f_3181_705)">
<ellipse cx="4.31009" cy="4.06531" rx="11.8531" ry="13.7633" fill="#4D409B"/>
</g>
</g>
<path d="M15.1298 8.46374H19.1501C20.079 8.46374 20.8316 7.757 20.8316 6.88611V5.14615C20.8316 4.27526 20.079 3.56851 19.1501 3.56851H15.1298C14.2009 3.56851 13.4482 4.27526 13.4482 5.14615V6.88611C13.4482 7.757 14.2009 8.46374 15.1298 8.46374Z" fill="white"/>
<path d="M19.343 9.75593H14.9377C14.0224 9.75593 13.2807 10.501 13.2807 11.4202V17.0441C13.2807 17.3888 13.0219 17.6906 12.6796 17.7116C12.3082 17.7344 11.9995 17.4389 11.9995 17.0687V4.13663C11.9995 3.44995 11.3967 2.91465 10.7203 3.01131C6.35485 3.63507 3 7.40498 3 11.961C3.00091 16.9483 7.03491 21 12.0005 21C16.966 21 21 16.9529 21 11.9591V11.4193C21 10.5001 20.2582 9.75502 19.343 9.75502V9.75593Z" fill="white"/>
</g>
<defs>
<filter id="filter0_f_3181_705" x="-5.49551" y="-5.49551" width="34.7952" height="34.7951" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.74776" result="effect1_foregroundBlur_3181_705"/>
</filter>
<filter id="filter1_f_3181_705" x="4.25449" y="-15.2455" width="34.991" height="52.616" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.74776" result="effect1_foregroundBlur_3181_705"/>
</filter>
<filter id="filter2_f_3181_705" x="-7.4938" y="10.6286" width="32.5223" height="33.3061" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.86531" result="effect1_foregroundBlur_3181_705"/>
</filter>
<filter id="filter3_f_3181_705" x="-13.5282" y="7.72898" width="24.9993" height="31.3665" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="2.74776" result="effect1_foregroundBlur_3181_705"/>
</filter>
<filter id="filter4_f_3181_705" x="10.2738" y="6.14878" width="28.9524" height="38.7024" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="4.05061" result="effect1_foregroundBlur_3181_705"/>
</filter>
<filter id="filter5_f_3181_705" x="-15.1446" y="-17.2996" width="38.9093" height="42.7298" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="3.80082" result="effect1_foregroundBlur_3181_705"/>
</filter>
<clipPath id="clip0_3181_705">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
<g clip-path="url(#clip0_3917_59729)">
<path d="M1.52727 24L14.6398 24C15.0447 24 15.4333 23.8392 15.7198 23.5527L23.5527 15.72C23.8392 15.4335 24 15.0452 24 14.64L24 1.52727C24 0.683782 23.3162 2.90399e-07 22.4727 2.16659e-07L9.36044 -9.29654e-07C8.95506 -9.65094e-07 8.56626 0.161236 8.27978 0.447927L0.447274 8.28938C0.161238 8.57563 0.000656849 8.964 0.000656814 9.36873L4.83696e-07 22.4727C4.09955e-07 23.3162 0.683782 24 1.52727 24Z" fill="url(#paint0_radial_3917_59729)"/>
<path d="M20 4H4V20H20V4Z" fill="black"/>
<path d="M12 17H6V18H12V17Z" fill="white"/>
<path d="M6 10.9542H7.19152V7.04576H6V6H9.5356V7.04576H8.34408V10.9542H9.5356V12H6V10.9542Z" fill="white"/>
<path d="M10.1367 10.9201H10.9982C11.1724 10.9201 11.3276 10.883 11.4632 10.8087C11.5989 10.7345 11.7039 10.6295 11.7781 10.4938C11.8524 10.3581 11.8895 10.2032 11.8895 10.0288V6H13.0595V10.1099C13.0595 10.47 12.9765 10.7936 12.8108 11.0807C12.6451 11.3679 12.4166 11.593 12.1251 11.7557C11.8336 11.9186 11.5065 12 11.1437 12H10.1367V10.9201Z" fill="white"/>
</g>
<defs>
<radialGradient id="paint0_radial_3917_59729" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(23.3751 0.625108) rotate(135) scale(32.4245)">
<stop offset="0.109181" stop-color="#FE2857"/>
<stop offset="0.7" stop-color="#007EFF"/>
</radialGradient>
<clipPath id="clip0_3917_59729">
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -29,7 +29,7 @@
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceInterface="com.intellij.ide.startup.importSettings.data.StartupWizardService" open="true"
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"/>
@@ -41,11 +41,12 @@ exit.confirm.title = Settings Import Is in Progress
exit.confirm.prompt = Do you want to stop the import?<br>If you stop it now, only part of the settings will be imported.
stop.import = Stop Import
onboarding.wizard.finish-button=Finish
onboarding.wizard.finish-button=Готово
onboarding.wizard.getting-ready=Getting Ready\u2026
plugin-installation.progress.text-with-details={0} / {1}
plugin-installation.progress.determining-plugins-to-download=Determining plugins to download
plugin-installation.progress.downloading=Установка {0}...
settings.category.ui.name=UI settings
settings.category.ui.description=Includes theme, editor font, toolbar, and notifications
@@ -66,6 +67,7 @@ transfer.settings.message=Importing from {0}\u2026
import.settings.title = Importing settings\u2026
import.settings.ok = Import Settings
import.settings.back = Back
import.settings.back.ru = Назад
import.settings.sync.ok = Sync Settings
import.settings.sync.import.once = Import Once
@@ -87,13 +89,37 @@ 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.checkStyle = Статический анализ кода на соответствие стандартам кодирования
plugin.description.mavenHelper = Анализ зависимостей Maven и запуск из редактора
plugin.description.java.kotlin.support = Поддерживается OpenIDE
plugin.description.spring.support = Поддерживается Amplicode
plugin.description.python = Поддержка языка Python
plugin.description.django = Поддержка Django фреймворка для веб-приложений на Python
plugin.description.pylint = Статический анализатор кода Python для выявления ошибок
plugin.description.csvEditor = Редактор CSV и TSV файлов с подсветкой и форматированием
plugin.description.pydantic = Поддержка Pydantic для валидации данных в Python
plugin.description.webtools = Набор инструментов для веб-разработки
plugin.description.go = Поддержка языка Go
plugin.description.fileWatchers = Автоматический запуск внешних инструментов при изменении файлов
plugin.description.makefile = Подсветка синтаксиса и навигация для Makefile
plugin.description.protocolBuffers = Поддержка Protocol Buffers для сериализации данных
plugin.description.easyp = Линтинг и форматирование proto файлов с помощью easyp
plugin.description.kilocode = Kilo Code AI Агент
plugin.description.continue = Open-source AI ассистент для кода
plugin.description.docker = Поддержка Docker для управления контейнерами
install.plugins.page.title = Установка Плагинов\u2026
theme.page.title = Choose Theme
theme.page.dark = Dark
@@ -0,0 +1,30 @@
// 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;
import com.intellij.ui.IconManager;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
public final class OpenIdePluginIcons {
private static @NotNull Icon load(@NotNull String path, int cacheKey, int flags) {
return IconManager.getInstance().loadRasterizedIcon(path, OpenIdePluginIcons.class.getClassLoader(), cacheKey, flags);
}
public static final class Icons {
/** 60x48 */ public static final @NotNull Icon Docker = load("icons/docker.svg", -271658419, 0);
/** 16x16 */ public static final @NotNull Icon Spring = load("icons/spring.svg", 1614158219, 0);
}
}
@@ -1,4 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
//
// Modified by Artyom Orlov at 2026 as part of the OpenIDE project(https://openide.ru).
// Any modifications are available on the same license terms as the original source code.
package com.intellij.ide.startup.importSettings;
import com.intellij.ui.IconManager;
@@ -45,9 +48,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", -1970123830, 0);
/** 24x24 */ public static final @NotNull Icon IC_24 = load("ideIcons/IC_24.svg", 1159411326, 0);
/** 48x48 */ public static final @NotNull Icon IC_48 = load("ideIcons/IC_48.svg", 337830580, 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);
@@ -1,4 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
//
// Modified by Artyom Orlov at 2026 as part of the OpenIDE project(https://openide.ru).
// Any modifications are available on the same license terms as the original source code.
package com.intellij.ide.startup.importSettings.chooser.ui
import com.intellij.ide.startup.importSettings.data.StartupWizardService
@@ -107,7 +110,7 @@ class OnboardingController private constructor(){
}
service.onEnter()
wizardController.goToThemePage(true)
wizardController.goToPluginPage()
if(!dl.isShowing) {
dl.initialize()
@@ -0,0 +1,272 @@
// 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.OpenIdePluginIcons
import com.intellij.ide.startup.importSettings.data.*
import com.intellij.ide.startup.importSettings.openide.utils.PluginInstallationUtil
import com.intellij.ide.startup.importSettings.transfer.ProgressIndicatorAdapter
import com.intellij.ide.startup.importSettings.transfer.TransferSettingsProgressIndicator
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.application.ex.ApplicationManagerEx
import com.intellij.util.IconUtil
import com.jetbrains.rd.util.lifetime.Lifetime
import com.jetbrains.rd.util.reactive.Property
import com.jetbrains.rd.util.reactive.Signal
import kotlinx.coroutines.*
class OpenIdeStartupWizardService(private val coroutineScope: CoroutineScope) : StartupWizardService {
override val isActive = true
private val _shouldClose = Signal<Unit>()
override val shouldClose: Signal<Unit>
get() = _shouldClose
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 = "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"),
),
)
),
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 = "CheckStyle-IDEA",
icon = AllIcons.Plugins.PluginLogo,
name = "CheckStyle-IDEA",
description = ImportSettingsBundle.message("plugin.description.checkStyle"),
),
WizardPluginImpl(
id = "MavenRunHelper",
icon = AllIcons.Plugins.PluginLogo,
name = "Maven Helper",
description = ImportSettingsBundle.message("plugin.description.mavenHelper"),
),
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 = OpenIdePluginIcons.Icons.Spring,
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 = "com.haulmont.django",
icon = AllIcons.Plugins.PluginLogo,
name = "Django[beta]",
description = ImportSettingsBundle.message("plugin.description.django"),
),
WizardPluginImpl(
id = "com.leinardi.pycharm.pylint",
icon = AllIcons.Plugins.PluginLogo,
name = "Pylint",
description = ImportSettingsBundle.message("plugin.description.pylint"),
),
WizardPluginImpl(
id = "net.seesharpsoft.intellij.plugins.csv",
icon = AllIcons.Plugins.PluginLogo,
name = "CSV Editor",
description = ImportSettingsBundle.message("plugin.description.csvEditor"),
),
//WizardPluginImpl(
// id = "com.koxudaxi.pydantic",
// icon = AllIcons.Plugins.PluginLogo,
// name = "Pydantic",
// description = ImportSettingsBundle.message("plugin.description.pydantic"),
//),
)
),
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 = AllIcons.Language.GO,
plugins = listOf(
WizardPluginImpl(
id = "com.haulmont.go",
icon = AllIcons.Plugins.PluginLogo,
name = "Go [beta]",
description = ImportSettingsBundle.message("plugin.description.go"),
),
WizardPluginImpl(
id = "com.intellij.plugins.watcher",
icon = AllIcons.Plugins.PluginLogo,
name = "File Watchers",
description = ImportSettingsBundle.message("plugin.description.fileWatchers"),
),
WizardPluginImpl(
id = "name.kropp.intellij.makefile",
icon = AllIcons.Plugins.PluginLogo,
name = "Makefile Language",
description = ImportSettingsBundle.message("plugin.description.makefile"),
),
WizardPluginImpl(
id = "idea.plugin.protoeditor",
icon = AllIcons.Plugins.PluginLogo,
name = "Protocol Buffers",
description = ImportSettingsBundle.message("plugin.description.protocolBuffers"),
),
WizardPluginImpl(
id = "com.github.easyptech.easyp",
icon = AllIcons.Plugins.PluginLogo,
name = "easyp",
description = ImportSettingsBundle.message("plugin.description.easyp"),
),
)
),
WizardPluginGroupImpl(
id = "deployment",
name = "Deployment",
icon = OpenIdePluginIcons.Icons.Docker,
plugins = listOf(
WizardPluginImpl(
id = "ru.openide.docker",
icon = AllIcons.Plugins.PluginLogo,
name = "Docker",
description = ImportSettingsBundle.message("plugin.description.docker"),
),
)
),
)
override fun onStepEnter() {
}
override fun install(lifetime: Lifetime, ids: List<String>): PluginImportProgress {
val progressIndicator = TransferSettingsProgressIndicator()
val progressAdapter = ProgressIndicatorAdapter(progressIndicator)
val iconProperty = Property(AllIcons.Plugins.PluginLogo)
coroutineScope.launch {
var needRestart = false
try {
needRestart = PluginInstallationUtil.installPlugins(ids, progressAdapter) { icon ->
iconProperty.set(IconUtil.resizeSquared(icon, 64))
}
} catch (e: CancellationException) {
throw e
} catch (_: Exception) {
} finally {
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
if (needRestart) {
ApplicationManagerEx.getApplicationEx().restart(true)
} else {
_shouldClose.fire(Unit)
}
}
}
}
return object : PluginImportProgress {
override val icon = iconProperty
override val progressMessage = progressIndicator.progressMessage
override val progress = progressIndicator.progress
}
}
override fun skipPlugins() {
}
override fun shouldShowPage(pluginIdsMarkedForInstallation: List<String>): Boolean {
return true
}
}
}
override fun onEnter() {
}
override fun onCancel() {
}
override fun onExit() {
}
}
@@ -1,4 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
//
// Modified by Artyom Orlov at 2026 as part of the OpenIDE project(https://openide.ru).
// Any modifications are available on the same license terms as the original source code.
package com.intellij.ide.startup.importSettings.chooser.ui
import com.intellij.ide.startup.importSettings.data.StartupWizardService
@@ -64,9 +67,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)
@@ -1,4 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
//
// Modified by Artyom Orlov at 2026 as part of the OpenIDE project(https://openide.ru).
// Any modifications are available on the same license terms as the original source code.
package com.intellij.ide.startup.importSettings.data
import com.intellij.ide.startup.importSettings.ImportSettingsBundle
@@ -88,7 +91,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()
@@ -104,6 +107,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 {
@@ -1,4 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
//
// Modified by Artyom Orlov at 2026 as part of the OpenIDE project(https://openide.ru).
// Any modifications are available on the same license terms as the original source code.
package com.intellij.ide.startup.importSettings.data
import com.intellij.icons.AllIcons
@@ -91,7 +94,14 @@ class PluginServiceImpl : PluginService {
WizardPluginImpl(AllIcons.TransferSettings.Vscode, "Ideolog", "An interactive viewer for log files with customizible highlighting, filtration and navigation to source code."),
)
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() {}
@@ -136,6 +146,13 @@ class WizardPluginImpl(
override val id: String = UUID.randomUUID().toString(),
) : 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 {
override val keymaps: List<WizardKeymap> = listOf(
@@ -24,6 +24,7 @@ import com.intellij.ide.startup.importSettings.data.IconProductSize
import com.intellij.ide.startup.importSettings.data.JbService
import com.intellij.ide.startup.importSettings.data.Product
import com.intellij.ide.startup.importSettings.data.SettingsService
import com.intellij.ide.startup.importSettings.openide.utils.PluginInstallationUtil
import com.intellij.ide.startup.importSettings.statistics.ImportSettingsEventsCollector
import com.intellij.ide.startup.importSettings.transfer.TransferSettingsProgress
import com.intellij.l10n.LocalizationStateService
@@ -418,6 +419,8 @@ class JbImportServiceImpl(private val coroutineScope: CoroutineScope) : JbServic
filteredCategories.add(SettingsCategory.PLUGINS)
plugins2import = productInfo.getPluginsDescriptors().filter {
setting.selectedChildIds?.contains(it.key.idString) ?: false
}.filter { (_, descriptor) ->
PluginManagerCore.isCompatible(descriptor) && descriptor.namespace?.lowercase() != "jetbrains"
}
unselectedPlugins = setting.unselectedChildIds
logger.info(
@@ -660,11 +663,9 @@ private suspend fun calculatePluginsToInstall(alreadyInstalled: Set<PluginId>, t
if (pluginsToAttemptInstallation.isEmpty()) return emptyList()
reporter.text(ImportSettingsBundle.message("plugin-installation.progress.determining-plugins-to-download"))
val loadedPlugins = withContext(Dispatchers.IO) {
MarketplaceRequests.loadLastCompatiblePluginModels(pluginsToAttemptInstallation.toSet(), buildNumber = null, throwExceptions = true)
.mapNotNull { it.getDescriptor() as? PluginNode }
}
return loadedPlugins
val loadedPlugins = PluginInstallationUtil.getPluginsToInstall(pluginsToAttemptInstallation)
return loadedPlugins.filter { it.pluginId !in alreadyInstalled }
}
}
@@ -0,0 +1,53 @@
// 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.openide.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,134 @@
// 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.openide.utils
import com.intellij.ide.plugins.IdeaPluginDescriptor
import com.intellij.ide.plugins.PluginManagerCore
import com.intellij.ide.plugins.PluginNode
import com.intellij.ide.plugins.marketplace.MarketplaceRequests
import com.intellij.ide.startup.importSettings.ImportSettingsBundle
import com.intellij.ide.startup.importSettings.openide.service.PluginIconService
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ModalityState
import com.intellij.openapi.application.asContextElement
import com.intellij.openapi.extensions.PluginId
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.updateSettings.impl.PluginDownloader
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.swing.Icon
object PluginInstallationUtil {
/**
* Installs plugins by their IDs.
* @param onIconLoaded callback to update icon when plugin icon is loaded
* @return true if IDE restart is required after installation
*/
@Suppress("DEPRECATION")
suspend fun installPlugins(
pluginIds: List<String>,
progressIndicator: ProgressIndicator,
onIconLoaded: ((Icon) -> Unit)? = null,
): Boolean {
if (progressIndicator.isCanceled || pluginIds.isEmpty()) {
return false
}
val idsToInstall = pluginIds
.map { PluginId.Companion.getId(it) }
.filter { !PluginManagerCore.isPluginInstalled(it) }
if (idsToInstall.isEmpty()) {
progressIndicator.fraction = 1.0
return false
}
progressIndicator.text = ImportSettingsBundle.message("plugin-installation.progress.determining-plugins-to-download")
progressIndicator.fraction = 0.05
val pluginsToInstall: List<IdeaPluginDescriptor> = getPluginsToInstall(idsToInstall)
if (pluginsToInstall.isEmpty()) {
progressIndicator.fraction = 1.0
return false
}
var shouldRestart = false
val progressPerPlugin = 0.9 / pluginsToInstall.size.coerceAtLeast(1)
var currentProgress = 0.1
for (plugin in pluginsToInstall) {
if (progressIndicator.isCanceled) break
progressIndicator.text = ImportSettingsBundle.message("plugin-installation.progress.downloading", plugin.name)
// Load and set plugin icon
if (onIconLoaded != null) {
try {
val icon = PluginIconService.Companion.getInstance().loadIcon(plugin.pluginId.idString)
if (icon != null) {
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
onIconLoaded(icon)
}
}
}
catch (_: Exception) {
}
}
try {
val downloader = PluginDownloader.createDownloader(plugin).withErrorsConsumer { problem ->
}
withContext(Dispatchers.IO) {
downloader.prepareToInstall(null)
}
val appliedWithoutRestart = withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {
downloader.installDynamically(null)
}
if (!appliedWithoutRestart) {
shouldRestart = true
}
}
catch (_: Exception) {
}
currentProgress += progressPerPlugin
progressIndicator.fraction = currentProgress.coerceAtMost(0.99)
}
progressIndicator.fraction = 1.0
progressIndicator.text = null
return shouldRestart
}
suspend fun getPluginsToInstall(idsToInstall: List<PluginId>): List<PluginNode> = withContext(Dispatchers.IO) {
val directPlugins = MarketplaceRequests.loadLastCompatiblePluginDescriptors(idsToInstall.toSet(), null, true)
val dependencyIds = directPlugins
.asSequence()
.flatMap { it.dependencies }
.filter { !it.isOptional }
.map { it.pluginId }
.filter { it !in idsToInstall && !PluginManagerCore.isPluginInstalled(it) }
.toSet()
val dependencyPlugins = MarketplaceRequests.loadLastCompatiblePluginDescriptors(dependencyIds, null, true)
(directPlugins + dependencyPlugins).distinctBy { it.pluginId }
}
}
@@ -0,0 +1,173 @@
// 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.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 OpenIdeWizardPluginGroupPane(
private val group: WizardPluginGroup,
private val coroutineScope: CoroutineScope,
private val onSelectionChanged: () -> Unit
) {
private val pluginPanes = mutableListOf<OpenIdeWizardPluginPane>()
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 = OpenIdeWizardPluginPane(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<OpenIdeWizardPluginPane>
get() = pluginPanes.filter { !it.isBundled }
val selectedPlugins: List<OpenIdeWizardPluginPane>
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()
}
}
@@ -0,0 +1,173 @@
// 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.wizard.pluginChooser
import com.intellij.ide.startup.importSettings.ImportSettingsBundle
import com.intellij.ide.startup.importSettings.data.WizardPlugin
import com.intellij.ide.startup.importSettings.openide.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 OpenIdeWizardPluginPane(val plugin: WizardPlugin, private val coroutineScope: CoroutineScope, changeHandler: () -> Unit) {
private var checkBox = JBCheckBox().apply {
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 = 0
c.weightx = 0.0
c.anchor = GridBagConstraints.CENTER
add(checkBox, c)
c.gridx = 1
c.anchor = GridBagConstraints.CENTER
add(iconLabel.apply {
border = JBUI.Borders.empty(0, 10)
}, c)
c.gridx = 2
c.weightx = 1.0
c.fill = GridBagConstraints.HORIZONTAL
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)
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
c.gridy = 0
c.weightx = 0.0
add(checkBox, c)
c.gridx = 1
c.gridy = 0
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(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(8, 20)
}
val isBundled: Boolean = plugin.bundled
init {
loadIconAsync()
if (isBundled) {
checkBox.isSelected = true
setEnabled(false)
}
checkBox.addItemListener {
changeHandler()
}
}
private fun createLabel(): JEditorPane {
val dslLabel = DslLabel(DslLabelType.LABEL)
dslLabel.action = HyperlinkEventAction.HTML_HYPERLINK_INSTANCE
dslLabel.maxLineLength = MAX_LINE_LENGTH_WORD_WRAP
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()
}
}
}
}
}
@@ -1,4 +1,7 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
//
// Modified by Artyom Orlov at 2026 as part of the OpenIDE project(https://openide.ru).
// Any modifications are available on the same license terms as the original source code.
package com.intellij.ide.startup.importSettings.wizard.pluginChooser
import com.intellij.ide.startup.importSettings.ImportSettingsBundle
@@ -8,6 +11,7 @@ import com.intellij.ide.startup.importSettings.chooser.ui.ScrollSnapToFocused
import com.intellij.ide.startup.importSettings.chooser.ui.UiUtils
import com.intellij.ide.startup.importSettings.chooser.ui.WizardPagePane
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
@@ -29,14 +33,14 @@ import javax.swing.SwingUtilities
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<OpenIdeWizardPluginGroupPane>()
private val contentPage: JComponent
@@ -44,14 +48,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()
private fun getSelected(): List<OpenIdeWizardPluginPane> {
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")
@@ -61,7 +71,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")
}
}
@@ -74,14 +84,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 = OpenIdeWizardPluginGroupPane(group, controller.lifetime.coroutineScope) { changeHandler() }
groupPanes.add(groupPane)
add(groupPane.pane)
}
border = JBUI.Borders.empty(10, 0)
}
@@ -102,7 +112,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.ru"), goBackAction ?: {})
private val continueAction = controller.createDefaultButton(continueButtonTextOverride ?: ImportSettingsBundle.message("plugins.page.ok.button.continue.without")) {
val ids = getSelected().map { it.plugin.id }.toList()
@@ -110,10 +120,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)