From 0ffc947c7f78f6b83aea20c3504c9e3cbf32bbea Mon Sep 17 00:00:00 2001 From: Daniil Kalinin Date: Wed, 1 Oct 2025 11:08:57 +0200 Subject: [PATCH] PY-82344 Implement main promo components and project activities GitOrigin-RevId: 5fec5e7f2b88743a97d10b7f0d27f9173c8424fd --- .../src/com/intellij/ui/GotItTooltip.kt | 2 +- ...j.pycharm.community.ide.impl.promotion.xml | 5 +- ...yCharmCommunityToUnifiedScheduleService.kt | 45 ++++++ .../PyCommunityToUnifiedPromoDialog.kt | 110 ++++++++++++++ .../PyCommunityToUnifiedShowPromoActivity.kt | 95 ++++++++++++ .../PyCommunityToUnifiedTooltip.kt | 61 ++++++++ ...PyCommunityToUnifiedWelcomeScreenBanner.kt | 140 ++++++++++++++++++ .../PyPromoSharedComponents.kt | 27 ++++ 8 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCharmCommunityToUnifiedScheduleService.kt create mode 100644 python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedPromoDialog.kt create mode 100644 python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedShowPromoActivity.kt create mode 100644 python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedTooltip.kt create mode 100644 python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedWelcomeScreenBanner.kt create mode 100644 python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyPromoSharedComponents.kt diff --git a/platform/platform-impl/src/com/intellij/ui/GotItTooltip.kt b/platform/platform-impl/src/com/intellij/ui/GotItTooltip.kt index 21e0f9aaf99b..a1bb48f9b978 100644 --- a/platform/platform-impl/src/com/intellij/ui/GotItTooltip.kt +++ b/platform/platform-impl/src/com/intellij/ui/GotItTooltip.kt @@ -123,7 +123,7 @@ class GotItTooltip @ApiStatus.Internal constructor(@NonNls val id: String, /** * Add an optional image above the header or description */ - private fun withImage(image: Icon, withBorder: Boolean = true): GotItTooltip { + fun withImage(image: Icon, withBorder: Boolean = true): GotItTooltip { gotItBuilder.withImage(image, withBorder) return this } diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/resources/intellij.pycharm.community.ide.impl.promotion.xml b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/resources/intellij.pycharm.community.ide.impl.promotion.xml index 732601e56b42..f8585d957f5e 100644 --- a/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/resources/intellij.pycharm.community.ide.impl.promotion.xml +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/resources/intellij.pycharm.community.ide.impl.promotion.xml @@ -82,7 +82,10 @@ bundle="messages.PyCharmCommunityCustomizationBundle" key="promo.configurable.jupyter"/> - + + () + fun getInstance(): PyCharmCommunityToUnifiedScheduleService = service() + } + + private var remindLaterJob: Job? = null + + fun scheduleFallbackModal() { + val promoService = service() + // cancel previous scheduled task if any + remindLaterJob?.cancel() + val delayMs = promoService.getPromoIntervalMillis() + remindLaterJob = serviceScope.launch(Dispatchers.Default) { + try { + delay(delayMs) + // Only proceed if conditions still require showing modal + if (!promoService.shouldShowModal()) return@launch + val project = ProjectManager.getInstance().openProjects.firstOrNull() ?: run { + LOG.info("Skipping promo modal fallback: no open projects") + return@launch + } + PyCommunityToUnifiedShowPromoActivity.showModalPromo(project) + } + catch (t: Throwable) { + if (t is CancellationException) throw t + LOG.warn("Error in promo modal fallback task", t) + } + } + } +} \ No newline at end of file diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedPromoDialog.kt b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedPromoDialog.kt new file mode 100644 index 000000000000..e3ce4f3881de --- /dev/null +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedPromoDialog.kt @@ -0,0 +1,110 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.pycharm.community.ide.impl.promotion.communityToUnified + +import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.ui.PopupBorder +import com.intellij.ui.WindowRoundedCornersManager +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.panels.BackgroundRoundedPanel +import com.intellij.ui.dsl.builder.EmptySpacingConfiguration +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.gridLayout.UnscaledGaps +import com.intellij.ui.dsl.gridLayout.UnscaledGapsY +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import java.awt.BorderLayout +import java.awt.Dimension +import javax.swing.JComponent +import javax.swing.JPanel +import javax.swing.JRootPane +import javax.swing.border.Border + +internal class PyCommunityToUnifiedPromoDialog(project: Project?) : + DialogWrapper(project, null, false, + IdeModalityType.IDE, true, false) { + + private val imageIcon = PyPromoSharedComponents.popUpImg + + override fun createContentPaneBorder(): Border { + return JBUI.Borders.empty() + } + + override fun getStyle(): DialogStyle { + return super.getStyle() + } + + init { + setUndecorated(true) + rootPane.windowDecorationStyle = JRootPane.NONE + rootPane.border = PopupBorder.Factory.create(true, true) + WindowRoundedCornersManager.configure(this) + init() + } + + override fun createSouthPanel(): JComponent? { + return panel { + row { + button(PyPromoSharedComponents.updateNow) { + close(OK_EXIT_CODE) + }.focused() + .applyToComponent { + putClientProperty("gotItButton", true) + putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) + this@PyCommunityToUnifiedPromoDialog.rootPane.defaultButton = this + }.customize(customGaps = UnscaledGaps(right = 10)) + link(PyPromoSharedComponents.skip) { + close(CANCEL_EXIT_CODE) + } + } + }.also { + setSize(imageIcon.iconWidth, it.preferredSize.height) + it.border = JBUI.Borders.empty(0, 28, 28, 28) + } + } + + + override fun createCenterPanel(): JComponent { + val mainPanel = BackgroundRoundedPanel(16, BorderLayout()) + val imagePanel = createImagePanel() + val infoPanel = createInfoPanel() + mainPanel.add(imagePanel, BorderLayout.NORTH) + mainPanel.add(infoPanel, BorderLayout.SOUTH) + mainPanel.preferredSize = Dimension(imageIcon.iconWidth, 200) + pack() + return mainPanel + } + + private fun createImagePanel(): JPanel { + val imagePanel = JPanel(BorderLayout()) + val imageLabel = JBLabel(imageIcon) + imagePanel.add(imageLabel, BorderLayout.CENTER) + return imagePanel + } + + private fun createInfoPanel(): JPanel { + return panel { + customizeSpacingConfiguration(EmptySpacingConfiguration()) { + row { + label(PyPromoSharedComponents.headerTitle) + .applyToComponent { + font = JBFont.h2().asBold() + } + } + row { + text(PyPromoSharedComponents.mainText, maxLineLength = 40) + .applyToComponent { + foreground = PyPromoSharedComponents.infoFontColor + } + }.customize(customRowGaps = UnscaledGapsY(top = 4)) + row { + browserLink(PyPromoSharedComponents.learnMore, PyPromoSharedComponents.LEARN_MORE_URL) + }.customize(customRowGaps = UnscaledGapsY(top = 4, bottom = 12)) + } + }.also { + setSize(imageIcon.iconWidth, it.preferredSize.height) + it.border = JBUI.Borders.empty(28, 28, 0, 28) + } + } +} \ No newline at end of file diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedShowPromoActivity.kt b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedShowPromoActivity.kt new file mode 100644 index 000000000000..dde6999abe95 --- /dev/null +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedShowPromoActivity.kt @@ -0,0 +1,95 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.pycharm.community.ide.impl.promotion.communityToUnified + +import com.intellij.openapi.application.EDT +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.diagnostic.logger +import com.intellij.openapi.project.Project +import com.intellij.openapi.startup.ProjectActivity +import com.intellij.openapi.updateSettings.impl.PlatformUpdateDialog +import com.intellij.openapi.updateSettings.impl.PlatformUpdates +import com.intellij.openapi.updateSettings.impl.UpdateSettings +import com.intellij.util.PlatformUtils +import com.intellij.util.application +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class PyCommunityToUnifiedShowPromoActivity : ProjectActivity { + + override suspend fun execute(project: Project) { + showPromoIfNeeded(project) + } + + suspend fun showPromoIfNeeded(project: Project) { + if (project.isDisposed || application.isHeadlessEnvironment || !PlatformUtils.isPyCharmCommunity()) return + + val promoService = service() + val update = promoService.getAvailableUpdate() + if (update == null) return + + if (promoService.shouldShowTooltip()) { + showTooltip(project) + } + else if (promoService.shouldShowModal()) { + showModalPromo(project) + } + } + + companion object { + val LOG: Logger = logger() + + suspend fun showTooltip(project: Project) { + withContext(Dispatchers.EDT) { + PyCommunityToUnifiedTooltip.showTooltip(project) + } + } + + suspend fun showModalPromo(project: Project) { + withContext(Dispatchers.EDT) { + val result = PyCommunityToUnifiedPromoDialog(project).showAndGet() + if (!result) { + service().onRemindMeLaterClicked() + PyCharmCommunityToUnifiedScheduleService.getInstance().scheduleFallbackModal() + } + else { + launchUpdateDialog(project) + } + } + } + + fun launchUpdateDialog(project: Project?) { + val promoService = service() + promoService.serviceScope.launch { + val update = promoService.getAvailableUpdate() + if (update != null) { + showModalUpdateDialog(project, update) + } + else { + LOG.warn("Update dialog was launched but no update was found") + } + } + } + + private suspend fun showModalUpdateDialog(project: Project?, update: PlatformUpdates.Loaded) { + withContext(Dispatchers.EDT) { + val result = PlatformUpdateDialog(project, update, true, emptyList(), emptyList()).showAndGet() + val promoService = service() + if (!result) { + val buildNum = update.newBuild.number.asStringWithoutProductCode() + // ignore this update was clicked + if (UpdateSettings.getInstance().ignoredBuildNumbers.contains(buildNum)) { + LOG.info("Update declined for: $buildNum") + promoService.setUpdateDeclined() + } + else { + LOG.info("Remind me later clicked for: $buildNum") + promoService.onRemindMeLaterClicked() + PyCharmCommunityToUnifiedScheduleService.getInstance().scheduleFallbackModal() + } + } + } + } + } +} \ No newline at end of file diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedTooltip.kt b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedTooltip.kt new file mode 100644 index 000000000000..1ea725da2720 --- /dev/null +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedTooltip.kt @@ -0,0 +1,61 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.pycharm.community.ide.impl.promotion.communityToUnified + +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.impl.ActionButton +import com.intellij.openapi.project.Project +import com.intellij.openapi.wm.WindowManager +import com.intellij.ui.GotItTooltip +import com.intellij.util.IconUtil +import com.intellij.util.ui.UIUtil +import org.jetbrains.annotations.Nls + +object PyCommunityToUnifiedTooltip { + + private const val TOOLTIP_ID: String = "pycharm.community.to.unified.tooltip.id" + + fun showTooltip(project: Project) { + UIUtil.invokeLaterIfNeeded { + val frame = WindowManager.getInstance().getIdeFrame(project) ?: return@invokeLaterIfNeeded + val root = frame.component + + val actionId = "SettingsEntryPoint" + val targetAction: AnAction? = ActionManager.getInstance().getAction(actionId) + if (targetAction == null) return@invokeLaterIfNeeded + + val settingsButton: ActionButton? = UIUtil.uiTraverser(root) + .filter(ActionButton::class.java) + .firstOrNull { it.action == targetAction } + + + if (settingsButton != null) { + val tooltip = constructToolTip(project) + // TODO won't show properly in the corner :( + tooltip.show(settingsButton, GotItTooltip.BOTTOM_LEFT) + } + } + } + + private fun constructToolTip(project: Project): GotItTooltip { + val image = IconUtil.scale(PyPromoSharedComponents.popUpImg, null, 0.7f) + fun @Nls String.h3(): String = "$this" + + return GotItTooltip(TOOLTIP_ID, PyPromoSharedComponents.mainText) + .withImage(image) + .withHeader(PyPromoSharedComponents.headerTitle.h3()) + .withButtonLabel(PyPromoSharedComponents.updateNow) + .withLink(PyPromoSharedComponents.learnMore) { + PyPromoSharedComponents.learnMoreBrowserAction.invoke() + } + .withSecondaryButton(PyPromoSharedComponents.skip) { + PyCommunityToUnifiedPromoService.getInstance().onRemindMeLaterClicked() + PyCharmCommunityToUnifiedScheduleService.getInstance().scheduleFallbackModal() + } + .withGotItButtonAction { + PyCommunityToUnifiedShowPromoActivity.launchUpdateDialog(project) + } + .withShowCount(Int.MAX_VALUE) + .withFocus() + } +} \ No newline at end of file diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedWelcomeScreenBanner.kt b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedWelcomeScreenBanner.kt new file mode 100644 index 000000000000..d849cc212449 --- /dev/null +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyCommunityToUnifiedWelcomeScreenBanner.kt @@ -0,0 +1,140 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.pycharm.community.ide.impl.promotion.communityToUnified + +import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI +import com.intellij.openapi.application.EDT +import com.intellij.openapi.components.service +import com.intellij.openapi.wm.BannerStartPagePromoter +import com.intellij.openapi.wm.StartPagePromoter +import com.intellij.pycharm.community.ide.impl.promotion.icons.PycharmCommunityIdeImplPromotionIcons +import com.intellij.ui.components.panels.BackgroundRoundedPanel +import com.intellij.ui.components.panels.Wrapper +import com.intellij.ui.dsl.builder.EmptySpacingConfiguration +import com.intellij.ui.dsl.builder.TopGap +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.dsl.gridLayout.UnscaledGaps +import com.intellij.ui.dsl.gridLayout.UnscaledGapsY +import com.intellij.util.IconUtil +import com.intellij.util.PlatformUtils +import com.intellij.util.ui.JBFont +import com.intellij.util.ui.JBUI +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.annotations.ApiStatus +import org.jetbrains.annotations.Nls +import java.awt.Graphics +import java.awt.Graphics2D +import java.awt.geom.RoundRectangle2D +import javax.swing.BoxLayout +import javax.swing.Icon +import javax.swing.JComponent +import javax.swing.JPanel +import kotlin.math.min + +@ApiStatus.Internal +internal class PyCommunityToUnifiedWelcomeScreenBanner : BannerStartPagePromoter() { + override val headerLabel: @Nls String = "" + override val actionLabel: @Nls String = "" + override val description: @Nls String = "" + override val promoImage: Icon = PycharmCommunityIdeImplPromotionIcons.Backgrounds.Promotion_bg + + private val mainBannerPanel = createMainPanel() + + val container: Wrapper = Wrapper(mainBannerPanel).apply { isVisible = false } + + override fun getPromotion(isEmptyState: Boolean): JComponent { + val service = service() + service.serviceScope.launch { + val shouldShowBanner = service.shouldShowWelcomeScreenBanner() + if (shouldShowBanner) { + withContext(Dispatchers.EDT) { + container.setContent(mainBannerPanel) + container.isVisible = true + container.revalidate() + container.repaint() + } + } + } + return container + } + + override fun runAction() {} + + override fun canCreatePromo(isEmptyState: Boolean): Boolean { + return PlatformUtils.isPyCharmCommunity() + } + + override fun getPriorityLevel(): Int = StartPagePromoter.PRIORITY_LEVEL_HIGH + + private fun createMainPanel(): JPanel { + val panel = panel { + customizeSpacingConfiguration(EmptySpacingConfiguration()) { + row { + label(PyPromoSharedComponents.headerTitle) + .applyToComponent { + font = JBFont.h3().asBold() + } + }.customize(customRowGaps = UnscaledGapsY(top = 12)) + row { + text(PyPromoSharedComponents.mainText) + .applyToComponent { + foreground = PyPromoSharedComponents.infoFontColor + } + }.customize(customRowGaps = UnscaledGapsY(top = 4, bottom = 8)) + } + row { + button(PyPromoSharedComponents.updateNow) { + PyCommunityToUnifiedShowPromoActivity.launchUpdateDialog(null) + }.applyToComponent { putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) } + .focused() + .customize(customGaps = UnscaledGaps(right = 10)) + browserLink(PyPromoSharedComponents.learnMore, PyPromoSharedComponents.LEARN_MORE_URL) + }.topGap(TopGap.NONE) + }.apply { + isOpaque = false + border = JBUI.Borders.empty(0, 12, 16, 0) + } + return wrapIntoSvgBackgroundPanel(panel, JBUI.scale(16), promoImage) + } + + private fun wrapIntoSvgBackgroundPanel(panel: JPanel, arc: Int, bgImage: Icon): JPanel { + val svgPanel = SvgBackgroundRightAnchoredPanel(arc, bgImage).apply { + layout = BoxLayout(this, BoxLayout.X_AXIS) + } + svgPanel.add(panel) + return svgPanel + } + + private class SvgBackgroundRightAnchoredPanel( + private val arc: Int, + private val bgImage: Icon, + ) : BackgroundRoundedPanel(arc) { + + override fun paintComponent(g: Graphics) { + super.paintComponent(g) + if (width <= 0 || height <= 0) return + + val iw = bgImage.iconWidth + val ih = bgImage.iconHeight + if (iw <= 0 || ih <= 0) return + + val g2 = g.create() as Graphics2D + try { + g2.clip(RoundRectangle2D.Float(0f, 0f, width.toFloat(), height.toFloat(), arc.toFloat(), arc.toFloat())) + val sx = width.toFloat() / iw + val sy = height.toFloat() / ih + val scale = min(sx, sy) + + val scaled = IconUtil.scale(bgImage, this, scale) + // Right-anchored (no padding) + val x = width - scaled.iconWidth + val y = 0 + scaled.paintIcon(this, g2, x, y) + } + finally { + g2.dispose() + } + } + } +} \ No newline at end of file diff --git a/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyPromoSharedComponents.kt b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyPromoSharedComponents.kt new file mode 100644 index 000000000000..156c2b63f08a --- /dev/null +++ b/python/ide/impl/intellij.pycharm.community.ide.impl.promotion/src/com/intellij/pycharm/community/ide/impl/promotion/communityToUnified/PyPromoSharedComponents.kt @@ -0,0 +1,27 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.intellij.pycharm.community.ide.impl.promotion.communityToUnified + +import com.intellij.ide.BrowserUtil +import com.intellij.pycharm.community.ide.impl.PyCharmCommunityCustomizationBundle +import com.intellij.pycharm.community.ide.impl.promotion.icons.PycharmCommunityIdeImplPromotionIcons +import com.intellij.ui.JBColor +import java.awt.Color + +internal object PyPromoSharedComponents { + + val popUpImg = PycharmCommunityIdeImplPromotionIcons.Images.Popup_img + val infoFontColor = JBColor(Color.decode("#6C707E"), Color.decode("#A8ADBD")) + + // URLs used by promo-related messages + const val LEARN_MORE_URL: String = "https://blog.jetbrains.com/pycharm/2025/04/unified-pycharm/" + + // Actions + val learnMoreBrowserAction = { BrowserUtil.browse(LEARN_MORE_URL) } + + // Localized strings from PyCharmCommunityCustomizationBundle + val mainText = PyCharmCommunityCustomizationBundle.message("promotion.update.to.unified.info.text") + val learnMore = PyCharmCommunityCustomizationBundle.message("promotion.update.to.unified.learn.more.hyper.link") + val updateNow = PyCharmCommunityCustomizationBundle.message("promotion.update.to.unified.update.now.button.label") + val skip = PyCharmCommunityCustomizationBundle.message("promotion.update.to.unified.skip.button.label") + val headerTitle = PyCharmCommunityCustomizationBundle.message("promotion.update.to.unified.banner.header.title") +} \ No newline at end of file