[PY-64403] move WhatsNew from rider to a platform module

Merge-request: IJ-MR-131247
Merged-by: Vladimir Koshelev <Vladimir.Koshelev@jetbrains.com>

GitOrigin-RevId: 73c9a19763d4f770d73da9223abd41368aca6a1d
This commit is contained in:
Vladimir Koshelev
2024-05-03 16:21:12 +00:00
committed by intellij-monorepo-bot
parent e681662c82
commit ce5999a4af
13 changed files with 763 additions and 0 deletions

1
.idea/modules.xml generated
View File

@@ -1125,6 +1125,7 @@
<module fileurl="file://$PROJECT_DIR$/platform/warmup/intellij.platform.warmup.iml" filepath="$PROJECT_DIR$/platform/warmup/intellij.platform.warmup.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/warmup/performanceTesting/intellij.platform.warmup.performanceTesting.iml" filepath="$PROJECT_DIR$/platform/warmup/performanceTesting/intellij.platform.warmup.performanceTesting.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.iml" filepath="$PROJECT_DIR$/platform/webSymbols/intellij.platform.webSymbols.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/whatsNew/intellij.platform.whatsNew.iml" filepath="$PROJECT_DIR$/platform/whatsNew/intellij.platform.whatsNew.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/workspace/jps/intellij.platform.workspace.jps.iml" filepath="$PROJECT_DIR$/platform/workspace/jps/intellij.platform.workspace.jps.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/workspace/jps/tests/intellij.platform.workspace.jps.tests.iml" filepath="$PROJECT_DIR$/platform/workspace/jps/tests/intellij.platform.workspace.jps.tests.iml" />
<module fileurl="file://$PROJECT_DIR$/platform/workspace/storage/intellij.platform.workspace.storage.iml" filepath="$PROJECT_DIR$/platform/workspace/storage/intellij.platform.workspace.storage.iml" />

View File

@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" packagePrefix="com.intellij.platform.whatsNew" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.ide" />
<orderEntry type="module" module-name="intellij.platform.statistics" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.ide.util.io" />
<orderEntry type="library" name="miglayout-swing" level="project" />
</component>
</module>

View File

@@ -0,0 +1,12 @@
<idea-plugin package="com.intellij.platform.whatsNew">
<extensions defaultExtensionNs="com.intellij">
<backgroundPostStartupActivity implementation="com.intellij.platform.whatsNew.WhatsNewShowOnStartCheckService" order="last"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.platform.whatsNew.reaction.ReactionCollector"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.platform.whatsNew.reaction.LegacyReactionCollector"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.platform.whatsNew.LegacyRiderWhatsNewCounterUsagesCollector"/>
<statistics.counterUsagesCollector implementationClass="com.intellij.platform.whatsNew.WhatsNewCounterUsageCollector"/>
<registryKey defaultValue="false" description="Whats new test mode" key="whats.new.test.mode" restartRequired="false" />
<registryKey defaultValue="true" description="Whats new enabled" key="whats.new.enabled" restartRequired="false" />
</extensions>
</idea-plugin>

View File

@@ -0,0 +1,4 @@
EapWhatsNewAction.text=What's _New
EapWhatsNewAction.description=Find out about the new features in this version of the IDE
useful.pane.text=Do you find this page useful?

View File

@@ -0,0 +1,280 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.whatsNew
import com.intellij.ide.DataManager
import com.intellij.ide.IdeBundle
import com.intellij.ide.actions.WhatsNewAction
import com.intellij.internal.statistic.collectors.fus.actions.persistence.ActionRuleValidator
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.application.ApplicationNamesInfo
import com.intellij.openapi.application.EDT
import com.intellij.openapi.application.ex.ApplicationInfoEx
import com.intellij.openapi.diagnostic.Logger
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.fileEditor.FileEditorManagerListener
import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.Companion.openEditor
import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.JsQueryHandler
import com.intellij.openapi.fileEditor.impl.HTMLEditorProvider.Request.Companion.url
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.registry.Registry
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.platform.whatsNew.reaction.FUSReactionChecker
import com.intellij.platform.whatsNew.reaction.ReactionsPanel
import com.intellij.ui.jcef.JBCefApp
import com.intellij.util.Urls.newFromEncoded
import com.intellij.util.application
import com.intellij.util.ui.StartupUiUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.IOException
import java.nio.charset.StandardCharsets
import java.util.*
import kotlin.let
import kotlin.run
import kotlin.text.isNotEmpty
class WhatsNewAction : AnAction(), com.intellij.openapi.project.DumbAware {
companion object {
private val DataContext.project: Project?
get() = CommonDataKeys.PROJECT.getData(this)
private val LOG = logger<WhatsNewAction>()
private const val PLACE = "WhatsNew"
private const val TEST_KEY = "whats.new.test.mode"
private const val REACTIONS_STATE = "whatsnew.reactions.state"
private val actionWhiteList = listOf("SearchEverywhere", "ChangeLaf", "ChangeIdeScale",
"SettingsSyncOpenSettingsAction", "BuildWholeSolutionAction",
"GitLab.Open.Settings.Page",
"AIAssistant.ToolWindow.ShowOrFocus", "ChangeMainToolbarColor",
"ShowEapDiagram", "multilaunch.RunMultipleProjects",
"EfCore.Shared.OpenQuickEfCoreActionsAction",
"OpenNewTerminalEAP", "CollectionsVisualizerEAP", "ShowDebugMonitoringToolEAP",
"LearnMoreStickyScrollEAP", "NewRiderProject", "BlazorHotReloadEAP")
private fun getHandler(dataContext: DataContext?): JsQueryHandler? {
dataContext ?: return null
return object : JsQueryHandler {
override suspend fun query(id: Long, request: String): String {
val contains = actionWhiteList.contains(request)
if (!contains) {
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew action $request not allowed")
}
WhatsNewCounterUsageCollector.actionNotAllowed(dataContext.project, request)
return "false"
}
if (request.isNotEmpty()) {
ActionManager.getInstance().getAction(request)?.let {
withContext(Dispatchers.EDT) {
it.actionPerformed(AnActionEvent.createFromAnAction(it, null, PLACE, dataContext))
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew action $request performed")
}
WhatsNewCounterUsageCollector.actionPerformed(dataContext.project, request)
}
return "true"
} ?: run {
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew action $request not found")
}
WhatsNewCounterUsageCollector.actionNotFound(dataContext.project, request)
}
}
return "false"
}
}
}
private val reactionChecker = FUSReactionChecker(REACTIONS_STATE)
fun refresh() {
reactionChecker.clearLikenessState()
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew reaction refresh")
}
}
private val isEap: Boolean
get() = if (Registry.`is`(TEST_KEY)) true else ApplicationInfoEx.getInstanceEx().isEAP
fun openWhatsNew(project: Project) {
if (!isEap) {
openWhatsNewPage(project)
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew: it's not EAP version")
}
return
}
val dataContextFromFocusAsync = DataManager.getInstance().dataContextFromFocusAsync
if (dataContextFromFocusAsync.isSucceeded) {
dataContextFromFocusAsync.onSuccess { dataContext ->
val queryHandler = getHandler(dataContext)
openWhatsNewPage(project, false, queryHandler)
}.onError {
openWhatsNewPage(project)
}
return
}
openWhatsNewPage(project)
}
private fun openWhatsNewPage(project: Project, url: String, byClient: Boolean = false, queryHandler: JsQueryHandler?) {
check(JBCefApp.isSupported()) { "JCEF is not supported on this system" }
val parameters = HashMap<String, String>()
parameters["var"] = "embed"
if (StartupUiUtil.isDarkTheme) {
parameters["theme"] = "dark"
}
val locale = Locale.getDefault()
if (locale != null) {
parameters["lang"] = locale.toLanguageTag().lowercase()
}
val request = url(newFromEncoded(url).addParameters(parameters).toExternalForm())
try {
WhatsNewAction::class.java.getResourceAsStream("whatsNewTimeoutText.html").use { stream ->
if (stream != null) {
request.withTimeoutHtml(String(stream.readAllBytes(), StandardCharsets.UTF_8).replace("__THEME__",
if (StartupUiUtil.isDarkTheme) "theme-dark" else "")
.replace("__TITLE__", IdeBundle.message("whats.new.timeout.title"))
.replace("__MESSAGE__", IdeBundle.message("whats.new.timeout.message"))
.replace("__ACTION__", IdeBundle.message("whats.new.timeout.action", url)))
}
}
}
catch (e: IOException) {
Logger.getInstance(WhatsNewAction::class.java).error(e)
}
request.withQueryHandler(queryHandler)
val title = IdeBundle.message("update.whats.new", ApplicationNamesInfo.getInstance().fullProductName)
openEditor(project, title, request)?.let {
FileEditorManager.getInstance(project).addTopComponent(it, ReactionsPanel.createPanel(PLACE, reactionChecker))
WhatsNewCounterUsageCollector.openedPerformed(project, byClient)
WhatsNewContentVersionChecker.saveLastShownUrl(url)
val disposable = Disposer.newDisposable(project)
val busConnection = application.messageBus.connect(disposable)
busConnection.subscribe(FileEditorManagerListener.FILE_EDITOR_MANAGER, object : FileEditorManagerListener {
override fun fileClosed(source: FileEditorManager, file: VirtualFile) {
if (it.file == file) {
WhatsNewCounterUsageCollector.closedPerformed(project)
Disposer.dispose(disposable)
}
}
})
}
}
private fun openWhatsNewPage(project: Project?, byClient: Boolean = false, queryHandler: JsQueryHandler? = null) {
val whatsNewUrl = WhatsNewContentVersionChecker.getUrl() ?: return
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew: openWhatsNewPage. queryHandler ${if (queryHandler != null) "enabled" else "disabled"}")
}
if (project != null && JBCefApp.isSupported()) {
openWhatsNewPage(project, whatsNewUrl, byClient, queryHandler)
}
else {
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew: can't be shown. JBCefApp isn't supported")
}
}
}
}
init {
templatePresentation.text = WhatsNewBundle.message("EapWhatsNewAction.text")
templatePresentation.description = WhatsNewBundle.message("EapWhatsNewAction.description")
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun update(e: AnActionEvent) {
val available = WhatsNewContentVersionChecker.getUrl() != null
e.presentation.isEnabledAndVisible = available
if (available) {
e.presentation.setText(IdeBundle.messagePointer("whats.new.action.custom.text", ApplicationNamesInfo.getInstance().fullProductName))
e.presentation.setDescription(
IdeBundle.messagePointer("whats.new.action.custom.description", ApplicationNamesInfo.getInstance().fullProductName))
}
}
override fun actionPerformed(e: AnActionEvent) {
openWhatsNewPage(e.project, true, if (isEap) getHandler(e.dataContext) else null)
}
}
internal enum class OpenedType { Auto, ByClient }
@Suppress("EnumEntryName")
internal enum class ActionFailedReason { Not_Allowed, Not_Found }
@Suppress("CompanionObjectInExtension")
internal object WhatsNewCounterUsageCollector : CounterUsagesCollector() {
private val eventLogGroup: EventLogGroup = EventLogGroup("whatsnew", 1)
private val opened = eventLogGroup.registerEvent("tab_opened", EventFields.Enum(("type"), OpenedType::class.java))
private val closed = eventLogGroup.registerEvent("tab_closed")
private val actionId = EventFields.StringValidatedByCustomRule("action_id", ActionRuleValidator::class.java)
private val perform = eventLogGroup.registerEvent("action_performed", actionId)
private val failed = eventLogGroup.registerEvent("action_failed", actionId, EventFields.Enum(("type"), ActionFailedReason::class.java))
fun openedPerformed(project: Project?, byClient: Boolean) {
opened.log(project, if (byClient) OpenedType.ByClient else OpenedType.Auto)
LegacyRiderWhatsNewCounterUsagesCollector.opened.log(project, if (byClient) OpenedType.ByClient else OpenedType.Auto)
}
fun closedPerformed(project: Project?) {
closed.log(project)
LegacyRiderWhatsNewCounterUsagesCollector.closed.log(project)
}
fun actionPerformed(project: Project?, id: String) {
perform.log(project, id)
LegacyRiderWhatsNewCounterUsagesCollector.perform.log(project, id)
}
fun actionNotAllowed(project: Project?, id: String) {
failed.log(project, id, ActionFailedReason.Not_Allowed)
LegacyRiderWhatsNewCounterUsagesCollector.failed.log(project, id, ActionFailedReason.Not_Allowed)
}
fun actionNotFound(project: Project?, id: String) {
failed.log(project, id, ActionFailedReason.Not_Found)
LegacyRiderWhatsNewCounterUsagesCollector.failed.log(project, id, ActionFailedReason.Not_Found)
}
override fun getGroup(): EventLogGroup {
return eventLogGroup
}
}
internal object LegacyRiderWhatsNewCounterUsagesCollector : CounterUsagesCollector() {
private val eventLogGroup: EventLogGroup = EventLogGroup("rider.whatsnew.eap", 3)
internal val opened = eventLogGroup.registerEvent("tab_opened", EventFields.Enum(("type"), OpenedType::class.java))
internal val closed = eventLogGroup.registerEvent("tab_closed")
internal val actionId = EventFields.StringValidatedByCustomRule("action_id", ActionRuleValidator::class.java)
internal val perform = eventLogGroup.registerEvent("action_performed", actionId)
internal val failed = eventLogGroup.registerEvent("action_failed", actionId, EventFields.Enum(("type"), ActionFailedReason::class.java))
override fun getGroup(): EventLogGroup {
return eventLogGroup
}
}

View File

@@ -0,0 +1,19 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.whatsNew
import com.intellij.DynamicBundle
import org.jetbrains.annotations.Nls
import org.jetbrains.annotations.PropertyKey
object WhatsNewBundle {
private const val pathToBundle = "messages.WhatsNewBundle"
private val bundle by lazy { DynamicBundle(WhatsNewBundle::class.java, pathToBundle); }
@Nls
fun message(
@PropertyKey(resourceBundle = pathToBundle) key: String,
vararg params: Any
): String {
return bundle.getMessage(key, *params)
}
}

View File

@@ -0,0 +1,74 @@
package com.intellij.platform.whatsNew
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.ApplicationInfo
import com.intellij.openapi.diagnostic.logger
import com.intellij.platform.ide.customization.ExternalProductResourceUrls
class WhatsNewContentVersionChecker {
companion object {
private val LOG = logger<WhatsNewContentVersionChecker>()
private const val LAST_SHOWN_EAP_URL_PROP = "whats.new.last.shown.url"
private val linkRegEx = "^https://www\\.jetbrains\\.com/[a-zA-Z]+/whatsnew(-eap)?/(\\d+)-(\\d+)-(\\d+)/$".toRegex()
fun getUrl(): String? {
return ExternalProductResourceUrls.getInstance().whatIsNewPageUrl?.toDecodedForm()
}
fun productVersion(): ContentVersion? {
return try {
val year = ApplicationInfo.getInstance().majorVersion.toInt()
val release = ApplicationInfo.getInstance().minorVersion.toInt()
ContentVersion(year, release, 0)
} catch (e: NumberFormatException) {
LOG.warn("WhatsNew: unknown productVersion '$e'")
null
}
}
fun lastShownLinkVersion(): ContentVersion? {
return PropertiesComponent.getInstance().getValue(LAST_SHOWN_EAP_URL_PROP)?.let {
return parseUrl(it) ?: run {
if (LOG.isTraceEnabled) {
LOG.trace("WhatsNew: unknown lastShownLinkVersion: '$it'")
}
null
}
}
}
fun linkVersion(): ContentVersion? {
val url = getUrl()
if (url != null) {
return parseUrl(url)
}
return null
}
fun saveLastShownUrl(url: String) {
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew URL saved '$url'")
}
PropertiesComponent.getInstance().setValue(LAST_SHOWN_EAP_URL_PROP, url)
}
private fun parseUrl(link: String): ContentVersion? {
linkRegEx.matchEntire(link)?.let {
val year = it.groups[it.groups.size - 3]?.value?.toInt() ?: return@let null
val release = it.groups[it.groups.size - 2]?.value?.toInt() ?: return@let null
val eap = it.groups[it.groups.size - 1]?.value?.toInt() ?: return@let null
return ContentVersion(year, release, eap)
} ?: run {
if (LOG.isTraceEnabled) {
LOG.trace("EapWhatsNew: incompatible link '$link'")
}
}
return null
}
data class ContentVersion(val year: Int, val release: Int, val eap: Int)
}
}

View File

@@ -0,0 +1,131 @@
package com.intellij.platform.whatsNew
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.application.EDT
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity
import com.intellij.openapi.util.registry.Registry
import com.intellij.util.SystemProperties
import com.intellij.util.application
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.atomic.AtomicBoolean
class WhatsNewShowOnStartCheckService : ProjectActivity {
companion object {
private val LOG = logger<WhatsNewShowOnStartCheckService>()
}
private val ourStarted = AtomicBoolean(false)
private val isPlaybackMode = SystemProperties.getBooleanProperty("idea.is.playback", false)
private suspend fun checkConnectionAvailable(): Boolean {
return withContext(Dispatchers.IO) {
return@withContext try {
val url = WhatsNewContentVersionChecker.getUrl()?.let { URL(it) } ?: return@withContext false
val connection = url.openConnection() as HttpURLConnection
connection.setConnectTimeout(5000)
connection.instanceFollowRedirects = false
connection.connect()
if (connection.responseCode != 200) {
LOG.warn("WhatsNew page '$url' not available response code: ${connection.responseCode}")
false
}
else {
true
}
}
catch (e: Exception) {
LOG.warn("WhatsNew page connection error: '$e")
false
}
}
}
override suspend fun execute(project: Project) {
if (ourStarted.getAndSet(true)) return
if (application.isHeadlessEnvironment || application.isUnitTestMode || isPlaybackMode) return
withContext(Dispatchers.EDT) {
if (!Registry.`is`("whats.new.enabled")) {
if(LOG.isTraceEnabled){
LOG.trace("EapWhatsNew: DISABLED")
}
return@withContext
}
val isTestMode = Registry.`is`("whats.new.test.mode")
if(isTestMode) {
if(LOG.isTraceEnabled){
LOG.trace("WhatsNew: TEST MODE STARTED")
}
replaceAction()
openWhatsNew(project)
return@withContext
}
val productVersion = WhatsNewContentVersionChecker.productVersion() ?: run {
if(LOG.isTraceEnabled) {
LOG.trace("WhatsNew: unknown current version")
}
return@withContext
}
val linkVersion = WhatsNewContentVersionChecker.linkVersion() ?: run {
if(LOG.isTraceEnabled) {
LOG.trace("WhatsNew: unknown link version")
}
unregisterAction()
return@withContext
}
if(productVersion.year == linkVersion.year && productVersion.release == linkVersion.release) {
LOG.trace("WhatsNew: productVersion '$productVersion' linkVersion: '$linkVersion' ")
replaceAction()
WhatsNewContentVersionChecker.lastShownLinkVersion()?.let {
LOG.trace("WhatsNew: link version last: '$it' new: '$linkVersion' ")
if(it.eap < linkVersion.eap) {
openWhatsNew(project)
return@withContext
}
} ?: run {
LOG.trace("WhatsNew: link not saved")
openWhatsNew(project)
}
} else {
LOG.trace("WhatsNew: link '${WhatsNewContentVersionChecker.getUrl()}' incompatible with this product version: $productVersion ")
unregisterAction()
}
}
}
private suspend fun openWhatsNew(project: Project) {
if(checkConnectionAvailable()) {
WhatsNewAction.openWhatsNew(project)
}
}
private fun replaceAction() {
val actionManager = ActionManager.getInstance()
actionManager.replaceAction("WhatsNewAction", WhatsNewAction())
if(LOG.isTraceEnabled){
LOG.trace("WhatsNew: WhatsNewAction replaced by WhatsNewAction")
}
}
private fun unregisterAction() {
val actionManager = ActionManager.getInstance()
actionManager.unregisterAction("WhatsNewAction")
if(LOG.isTraceEnabled){
LOG.trace("EapWhatsNew: WhatsNewAction unregister")
}
}
}

View File

@@ -0,0 +1,53 @@
package com.intellij.platform.whatsNew.reaction
import com.intellij.ide.util.PropertiesComponent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
class FUSReactionChecker(private val stateKey: String): ReactionChecker {
override fun onLike(project: Project?, place: String?) {
ApplicationManager.getApplication().assertIsDispatchThread()
val value = if (getLikenessState() == ReactionChecker.State.Liked) {
0
}
else 1
putValue(value)
ReactionCollector.reactedPerformed(project, place, ReactionType.Like,
if (value == 0) ReationAction.Unset
else ReationAction.Set)
}
override fun onDislike(project: Project?, place: String?) {
ApplicationManager.getApplication().assertIsDispatchThread()
val value = if (getLikenessState() == ReactionChecker.State.Disliked) {
0
}
else -1
putValue(value)
ReactionCollector.reactedPerformed(project, place, ReactionType.Dislike,
if (value == 0) ReationAction.Unset
else ReationAction.Set)
}
internal fun putValue(value: Int) {
val propertiesComponent = PropertiesComponent.getInstance()
propertiesComponent.setValue(stateKey, value, 0)
}
private fun getLikenessState(): ReactionChecker.State {
ApplicationManager.getApplication().assertIsDispatchThread()
val propertiesComponent = PropertiesComponent.getInstance()
return ReactionChecker.State.stateByIndex(propertiesComponent.getInt(stateKey, 0))
}
override fun clearLikenessState() {
putValue(0)
}
override fun checkState(state: ReactionChecker.State): Boolean {
return getLikenessState() == state
}
}

View File

@@ -0,0 +1,37 @@
package com.intellij.platform.whatsNew.reaction
import com.intellij.internal.statistic.eventLog.EventLogGroup
import com.intellij.internal.statistic.eventLog.events.EventFields
import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesCollector
import com.intellij.openapi.project.Project
internal enum class ReactionType { Like, Dislike }
internal enum class ReationAction { Set, Unset }
internal object ReactionCollector : CounterUsagesCollector() {
private val eventLogGroup: EventLogGroup = EventLogGroup("whatsnew.reactions", 1)
private val reacted = eventLogGroup.registerEvent("reacted",
EventFields.ActionPlace,
EventFields.Enum(("type"), ReactionType::class.java),
EventFields.Enum("action", ReationAction::class.java))
fun reactedPerformed(project: Project?, place: String?, type: ReactionType, action: ReationAction) {
reacted.log(project, place, type, action)
LegacyReactionCollector.reacted.log(project, place, type, action)
}
override fun getGroup(): EventLogGroup = eventLogGroup
}
internal object LegacyReactionCollector : CounterUsagesCollector() {
private val eventLogGroup: EventLogGroup = EventLogGroup("rider.reactions", 2)
internal val reacted = eventLogGroup.registerEvent("reacted",
EventFields.ActionPlace,
EventFields.Enum(("type"), ReactionType::class.java),
EventFields.Enum("action", ReationAction::class.java))
override fun getGroup(): EventLogGroup = eventLogGroup
}

View File

@@ -0,0 +1,131 @@
// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.platform.whatsNew.reaction
import com.intellij.CommonBundle
import com.intellij.icons.AllIcons
import com.intellij.ide.DataManager
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.ex.ActionButtonLook
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.NlsActions
import com.intellij.platform.whatsNew.WhatsNewBundle
import com.intellij.ui.JBColor
import net.miginfocom.swing.MigLayout
import org.jetbrains.annotations.ApiStatus
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
class ReactionsPanel {
companion object {
@ApiStatus.Internal
@JvmField
val STATE_CHECKER_KEY = DataKey.create<ReactionChecker>("RunWidgetSlot")
private val group: ActionGroup = DefaultActionGroup(mutableListOf(LikeReactionAction(), DislikeUsefulAction()))
fun createPanel(place: String,
stateChecker: ReactionChecker): JComponent {
return JPanel(MigLayout("ins 0, gap 7", "push[min!][pref!]push")).apply {
add(JLabel(WhatsNewBundle.message("useful.pane.text")))
DataManager.registerDataProvider(this) { key ->
if (STATE_CHECKER_KEY.`is`(key))
stateChecker
else null
}
val look = object : ActionButtonLook() {}
val toolbar = object : ActionToolbarImpl(place, group, true) {
override fun getActionButtonLook(): ActionButtonLook {
return look
}
}
toolbar.border = null
toolbar.targetComponent = this
add(toolbar)
// isOpaque = false
background = JBColor.WHITE
toolbar.isOpaque = false
}
}
}
}
private class LikeReactionAction() : ReactionAction(CommonBundle.message("button.without.mnemonic.yes"), AllIcons.Ide.LikeDimmed,
AllIcons.Ide.Like,
AllIcons.Ide.LikeSelected) {
override fun isSelected(e: AnActionEvent): Boolean {
return getReactionStateChecker(e)?.checkState(ReactionChecker.State.Liked) == true
}
override fun actionPerformed(e: AnActionEvent) {
getReactionStateChecker(e)?.onLike(e.project, e.place)
}
}
private class DislikeUsefulAction() : ReactionAction(CommonBundle.message("button.without.mnemonic.no"), AllIcons.Ide.DislikeDimmed,
AllIcons.Ide.Dislike, AllIcons.Ide.DislikeSelected) {
override fun isSelected(e: AnActionEvent): Boolean {
return getReactionStateChecker(e)?.checkState(ReactionChecker.State.Disliked) == true
}
override fun actionPerformed(e: AnActionEvent) {
getReactionStateChecker(e)?.onDislike(e.project, e.place)
}
}
private abstract class ReactionAction(text: @NlsActions.ActionText String,
val icon: Icon,
val hoveredIcon: Icon,
val selectedIcon: Icon) : AnAction(text, null, icon), DumbAware {
override fun update(e: AnActionEvent) {
val selected = isSelected(e)
val presentation = e.presentation
presentation.icon = if (selected) selectedIcon else icon
presentation.hoveredIcon = if (selected) selectedIcon else hoveredIcon
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT
}
abstract fun isSelected(e: AnActionEvent): Boolean
fun getReactionStateChecker(e: AnActionEvent): ReactionChecker? {
return e.dataContext.getData(ReactionsPanel.STATE_CHECKER_KEY)
}
}
interface ReactionChecker {
enum class State(val index: Int) {
Liked (1),
Disliked (-1),
Undefined (0);
companion object {
fun stateByIndex(ind: Int?): State {
return ind?.let { values().firstOrNull { it.index == ind } ?: Undefined } ?: Undefined
}
}
}
fun onLike(project: Project?, place: String?)
fun onDislike(project: Project?, place: String?)
fun checkState(state: State): Boolean
fun clearLikenessState()
}

View File

@@ -28,6 +28,7 @@ class PyCharmCommunityProperties(private val communityHome: Path) : PyCharmPrope
"intellij.xml.dom.impl",
"intellij.platform.main",
"intellij.pycharm.community",
"intellij.platform.whatsNew",
)
productLayout.bundledPluginModules.addAll(
listOf(

View File

@@ -25,6 +25,7 @@
<orderEntry type="module" module-name="intellij.python.featuresTrainer" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="library" name="http-client" level="project" />
<orderEntry type="module" module-name="intellij.platform.whatsNew" />
<orderEntry type="module" module-name="intellij.pycharm.community.ide.impl.promotion" scope="RUNTIME" />
</component>
</module>