diff --git a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java index 414d42a9667e..ac068e0ac8d3 100644 --- a/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java +++ b/python/ide/impl/src/com/intellij/pycharm/community/ide/impl/PyIdeCommonOptionsForm.java @@ -178,7 +178,7 @@ public class PyIdeCommonOptionsForm implements AbstractPyCommonOptionsForm { } public void updateSdkList(boolean preserveSelection, PyConfigurableInterpreterList myInterpreterList) { - myPythonSdks = myInterpreterList.getAllPythonSdks(myProject, null); + myPythonSdks = myInterpreterList.getAllPythonSdks(myProject, null, false); Sdk selection = preserveSelection ? (Sdk)content.interpreterComboBox.getSelectedItem() : null; if (!myPythonSdks.contains(selection)) { selection = null; diff --git a/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java b/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java index ce21d75412a4..272ef23f8036 100644 --- a/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java +++ b/python/python-psi-impl/src/com/jetbrains/python/sdk/PythonSdkUtil.java @@ -71,20 +71,38 @@ public final class PythonSdkUtil { private static final Key CACHED_SKELETON_HEADER = Key.create("CACHED_SKELETON_HEADER"); public static boolean isPythonSdk(@NotNull Sdk sdk) { + return isPythonSdk(sdk, false); + } + + @ApiStatus.Internal + public static boolean isPythonSdk(@NotNull Sdk sdk, boolean allowRemoteInFreeTier) { if (!PyNames.PYTHON_SDK_ID_NAME.equals(sdk.getSdkType().getName())) { return false; } // PY-79923: Should explicitly filter sdks created while pro was active - if (PlatformUtils.isPyCharm() && !PlatformUtils.isDataSpell()) { - return !isRemote(sdk) || !PluginManagerCore.isDisabled(ULTIMATE_PLUGIN_ID); + if (isFreeTier()) { + return allowRemoteInFreeTier || (!isRemote(sdk)); } return true; } - public static @Unmodifiable List getAllSdks() { - return ContainerUtil.filter(ProjectJdkTable.getInstance().getAllJdks(), PythonSdkUtil::isPythonSdk); + /** + * @return PyCharm with Pro mode disabled + */ + @ApiStatus.Internal + public static boolean isFreeTier() { + return PlatformUtils.isPyCharm() && (!PlatformUtils.isDataSpell()) && PluginManagerCore.isDisabled(ULTIMATE_PLUGIN_ID); + } + + public static @Unmodifiable @NotNull List<@NotNull Sdk> getAllSdks() { + return getAllSdks(false); + } + + @ApiStatus.Internal + public static @Unmodifiable @NotNull List<@NotNull Sdk> getAllSdks(boolean allowRemoteInFreeTier) { + return ContainerUtil.filter(ProjectJdkTable.getInstance().getAllJdks(), sdk -> isPythonSdk(sdk, allowRemoteInFreeTier)); } private static @Nullable PySkeletonHeader readSkeletonHeader(@NotNull VirtualFile file, @NotNull Sdk pythonSdk) { diff --git a/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java b/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java index 8c6ca4fc7395..254a718f2c8e 100644 --- a/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java +++ b/python/src/com/jetbrains/python/configuration/PyActiveSdkConfigurable.java @@ -4,7 +4,6 @@ package com.jetbrains.python.configuration; import com.intellij.icons.AllIcons; import com.intellij.ide.DataManager; import com.intellij.openapi.Disposable; -import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.application.ApplicationManager; @@ -24,7 +23,8 @@ import com.intellij.openapi.ui.popup.ListPopup; import com.intellij.openapi.util.Comparing; import com.intellij.openapi.util.Disposer; import com.intellij.openapi.util.Pair; -import com.intellij.ui.*; +import com.intellij.ui.CollectionComboBoxModel; +import com.intellij.ui.ComboboxSpeedSearch; import com.intellij.ui.components.DropDownLink; import com.intellij.util.NullableConsumer; import com.intellij.util.ui.JBUI; @@ -47,6 +47,7 @@ import java.util.Map; import java.util.function.Consumer; import static com.jetbrains.python.sdk.PySdkRenderingKt.groupModuleSdksByTypes; +import static com.jetbrains.python.sdk.PythonSdkUtil.isRemote; public class PyActiveSdkConfigurable implements UnnamedConfigurable { @@ -65,6 +66,7 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { private final @NotNull ComboBox mySdkCombo; private final @NotNull PyInstalledPackagesPanel myPackagesPanel; + private final @Nullable PyPanelWithPromo myPanelWithPromo; private final @Nullable Disposable myDisposable; @@ -85,6 +87,8 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { final PackagesNotificationPanel packagesNotificationPanel = new PyPackagesNotificationPanel(); myPackagesPanel = new PyInstalledPackagesPanel(myProject, packagesNotificationPanel); myPackagesPanel.setShowGrid(false); + boolean freeTier = PythonSdkUtil.isFreeTier(); + myPanelWithPromo = freeTier ? new PyPanelWithPromo(myPackagesPanel) : null; final PyCustomSdkUiProvider customUiProvider = PyCustomSdkUiProvider.getInstance(); myDisposable = customUiProvider == null ? null : Disposer.newDisposable(); @@ -95,7 +99,9 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { additionalAction = new DropDownLink<>(PyBundle.message("active.sdk.dialog.link.add.interpreter.text"), link -> createAddInterpreterPopup(project, module, link, this::updateSdkListAndSelect)); - myMainPanel = buildPanel(project, mySdkCombo, additionalAction, myPackagesPanel, packagesNotificationPanel, customizer); + myMainPanel = + buildPanel(project, mySdkCombo, additionalAction, freeTier ? myPanelWithPromo.getPanel() : myPackagesPanel, packagesNotificationPanel, + customizer); myInterpreterList = PyConfigurableInterpreterList.getInstance(myProject); myProjectSdksModel = myInterpreterList.getModel(); @@ -160,9 +166,10 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { private static @NotNull JPanel buildPanel(@NotNull Project project, @NotNull ComboBox sdkComboBox, @NotNull JComponent additionalAction, - @NotNull PyInstalledPackagesPanel installedPackagesPanel, + @NotNull JPanel promotionPanel, @NotNull PackagesNotificationPanel packagesNotificationPanel, @Nullable Pair customizer) { + final JPanel result = new JPanel(new GridBagLayout()); final GridBagConstraints c = new GridBagConstraints(); @@ -196,7 +203,7 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { c.gridwidth = 3; c.gridheight = GridBagConstraints.RELATIVE; c.fill = GridBagConstraints.BOTH; - result.add(installedPackagesPanel, c); + result.add(promotionPanel, c); c.gridheight = GridBagConstraints.REMAINDER; c.gridx = 0; @@ -218,6 +225,18 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { protected void onSdkSelected() { final Sdk sdk = getOriginalSelectedSdk(); + + if (sdk != null) { + // Non-null means we are in free tier mode, so must switch between packages and promo panel + if (myPanelWithPromo != null) { + boolean remote = isRemote(sdk); + myPanelWithPromo.setPromoMode(remote); + if (remote) { + return; + } + } + } + refreshPackages(sdk); } @@ -273,8 +292,9 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { Sdk sdk = null; if (myModule == null) { sdk = ProjectRootManager.getInstance(myProject).getProjectSdk(); - } else { - sdk = ModuleRootManager.getInstance(myModule).getSdk(); + } + else { + sdk = ModuleRootManager.getInstance(myModule).getSdk(); } if (sdk != null && PythonSdkUtil.isPythonSdk(sdk)) { @@ -315,7 +335,7 @@ public class PyActiveSdkConfigurable implements UnnamedConfigurable { } protected @NotNull List getAvailableSdks() { - return myInterpreterList.getAllPythonSdks(myProject, myModule); + return myInterpreterList.getAllPythonSdks(myProject, myModule, true); } private void updateSdkListAndSelect(@Nullable Sdk selectedSdk) { diff --git a/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java b/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java index 95be031afb1a..8d3fac49962e 100644 --- a/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java +++ b/python/src/com/jetbrains/python/configuration/PyConfigurableInterpreterList.java @@ -13,6 +13,7 @@ import com.jetbrains.python.run.PythonInterpreterTargetEnvironmentFactory; import com.jetbrains.python.run.TargetConfigurationWithLocalFsAccessExKt; import com.jetbrains.python.sdk.*; import com.jetbrains.python.sdk.flavors.PythonSdkFlavor; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -57,14 +58,15 @@ public final class PyConfigurableInterpreterList { /** * @param module if not null and module resides on certain target, returns only SDKs for this target */ - public @NotNull List getAllPythonSdks(final @Nullable Project project, @Nullable Module module) { + @ApiStatus.Internal + public @NotNull List getAllPythonSdks(final @Nullable Project project, @Nullable Module module, boolean allowRemoteInFreeTier) { var targetModuleSitsOn = (module != null) ? PythonInterpreterTargetEnvironmentFactory.Companion.getTargetModuleResidesOn(module) : null; List result = new ArrayList<>(); for (Sdk sdk : getModel().getSdks()) { - if (!PythonSdkUtil.isPythonSdk(sdk)) { + if (!PythonSdkUtil.isPythonSdk(sdk, allowRemoteInFreeTier)) { continue; } @@ -81,7 +83,7 @@ public final class PyConfigurableInterpreterList { } public @NotNull List getAllPythonSdks() { - return getAllPythonSdks(null, null); + return getAllPythonSdks(null, null, false); } private static class PyInterpreterComparator implements Comparator { diff --git a/python/src/com/jetbrains/python/configuration/PyPanelWithPromo.kt b/python/src/com/jetbrains/python/configuration/PyPanelWithPromo.kt new file mode 100644 index 000000000000..3dbc43c9639f --- /dev/null +++ b/python/src/com/jetbrains/python/configuration/PyPanelWithPromo.kt @@ -0,0 +1,25 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python.configuration + +import com.intellij.openapi.observable.properties.PropertyGraph +import com.intellij.openapi.observable.util.not +import com.intellij.ui.dsl.builder.* +import com.jetbrains.python.createPromoPanel +import javax.swing.JPanel + +/** + * Shows either promo-panel (if [promoMode]) or [whenPromoDisabled] + */ +internal class PyPanelWithPromo(private val whenPromoDisabled: JPanel) { + private val root = PropertyGraph() + private val _promoMode = root.property(false) + var promoMode: Boolean by _promoMode + val panel: JPanel = panel { + row { + cell(createPromoPanel()).resizableColumn().align(AlignY.CENTER + AlignX.FILL).applyToComponent { putClientProperty(DslComponentProperty.VERTICAL_COMPONENT_GAP, VerticalComponentGap(top = false, bottom = false)) } + }.resizableRow().visibleIf(_promoMode) + row { + cell(whenPromoDisabled).resizableColumn().align(AlignY.FILL + AlignX.FILL).applyToComponent { putClientProperty(DslComponentProperty.VERTICAL_COMPONENT_GAP, VerticalComponentGap(top = false, bottom = false)) } + }.resizableRow().visibleIf(_promoMode.not()) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt b/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt index f3138b6447ee..d9f1f104f143 100644 --- a/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt +++ b/python/src/com/jetbrains/python/configuration/PythonInterpreterMasterDetails.kt @@ -103,7 +103,7 @@ internal class PythonInterpreterMasterDetails(private val moduleOrProject: Modul } private val allPythonSdksInEdit: List - get() = pythonConfigurableInterpreterList.getAllPythonSdks(project, module) + get() = pythonConfigurableInterpreterList.getAllPythonSdks(project, module, false) override fun reset() { pythonPathsModified = false diff --git a/python/src/com/jetbrains/python/createPromoPanel.kt b/python/src/com/jetbrains/python/createPromoPanel.kt new file mode 100644 index 000000000000..1a1c4cef2f5d --- /dev/null +++ b/python/src/com/jetbrains/python/createPromoPanel.kt @@ -0,0 +1,38 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package com.jetbrains.python + +import com.intellij.ide.browsers.BrowserLauncher +import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI +import com.intellij.ui.dsl.builder.* +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.ui.JBFont +import com.jetbrains.python.icons.PythonIcons +import javax.swing.JPanel + +/** + * Panel that suggests user to buy Pro version + */ +@RequiresEdt +internal fun createPromoPanel(): JPanel = panel { + row { + icon(PythonIcons.Python.Pycharm32).resizableColumn().align(AlignX.RIGHT + AlignY.TOP) + panel { + row { + @Suppress("DialogTitleCapitalization") // PyCharm Pro is literally how we want to see it + text(PyBundle.message("pycharm.free.mode.upgrade.title")).align(AlignY.BOTTOM).applyToComponent { + font = JBFont.h1() + } + } + row { + text(PyBundle.message("pycharm.free.mode.upgrade.body")).align(AlignY.TOP).applyToComponent { putClientProperty(DslComponentProperty.VERTICAL_COMPONENT_GAP, VerticalComponentGap(top = false, bottom = false)) } + } + row { + button(PyBundle.message("pycharm.free.mode.upgrade.button")) { + BrowserLauncher.instance.open("https://www.jetbrains.com/pycharm/buy/?section=commercial&billing=yearly") + }.applyToComponent { + putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) + } + } + }.resizableColumn().align(AlignX.LEFT) + } +} \ No newline at end of file diff --git a/python/src/com/jetbrains/python/run/PythonCommandLineState.java b/python/src/com/jetbrains/python/run/PythonCommandLineState.java index c4e6bcab89db..0dab54a68d1d 100644 --- a/python/src/com/jetbrains/python/run/PythonCommandLineState.java +++ b/python/src/com/jetbrains/python/run/PythonCommandLineState.java @@ -64,6 +64,7 @@ import com.jetbrains.python.debugger.PyTargetPathMapper; import com.jetbrains.python.facet.LibraryContributingFacet; import com.jetbrains.python.facet.PythonPathContributingFacet; import com.jetbrains.python.library.PythonLibraryType; +import com.jetbrains.python.packaging.PyExecutionException; import com.jetbrains.python.remote.PyRemotePathMapper; import com.jetbrains.python.remote.PyRemoteSdkAdditionalData; import com.jetbrains.python.run.target.HelpersAwareTargetEnvironmentRequest; @@ -1047,22 +1048,21 @@ public abstract class PythonCommandLineState extends CommandLineState { return interpreterPath; } - private @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter() { - return getPythonTargetInterpreter(myConfig.getProject(), getSdk()); + private @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter() throws ExecutionException { + Sdk sdk = getSdk(); + if (sdk == null) { + throw new PyExecutionException(PyBundle.message("runcfg.error.message.cannot.find.python.interpreter")); + } + return getPythonTargetInterpreter(myConfig.getProject(), sdk); } - public static @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter(@NotNull Project project, @Nullable Sdk sdk) { - if (sdk == null) { - throw new IllegalStateException("SDK is not defined for Run Configuration"); - } - else { + public static @NotNull HelpersAwareTargetEnvironmentRequest getPythonTargetInterpreter(@NotNull Project project, @NotNull Sdk sdk) { HelpersAwareTargetEnvironmentRequest helpersAwareTargetRequest = PythonInterpreterTargetEnvironmentFactory.findPythonTargetInterpreter(sdk, project); if (helpersAwareTargetRequest == null) { throw new IllegalStateException("Cannot find execution environment for SDK " + sdk); } return helpersAwareTargetRequest; - } } /** diff --git a/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt b/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt index 1161e0122e34..89d6a14adcb7 100644 --- a/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt +++ b/python/src/com/jetbrains/python/run/PythonLockedRunConfiguration.kt @@ -11,9 +11,7 @@ import com.intellij.execution.runners.RunConfigurationWithSuppressedDefaultRunAc import com.intellij.facet.impl.invalid.FacetIgnorer import com.intellij.facet.impl.invalid.InvalidFacet import com.intellij.icons.AllIcons -import com.intellij.ide.browsers.BrowserLauncher import com.intellij.ide.plugins.PluginManager -import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI import com.intellij.openapi.extensions.ExtensionNotApplicableException import com.intellij.openapi.extensions.PluginId import com.intellij.openapi.options.SettingsEditor @@ -21,12 +19,10 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.JDOMUtil import com.intellij.openapi.util.NlsSafe import com.intellij.openapi.util.WriteExternalException -import com.intellij.ui.dsl.builder.* import com.intellij.util.PlatformUtils -import com.intellij.util.ui.JBFont import com.jetbrains.python.PYTHON_PROF_PLUGIN_ID import com.jetbrains.python.PyBundle -import com.jetbrains.python.icons.PythonIcons +import com.jetbrains.python.createPromoPanel import org.jdom.Element import org.jetbrains.annotations.Nls import org.jetbrains.annotations.NonNls @@ -40,29 +36,7 @@ private class PythonLockedRunConfigurationEditor : SettingsEditor