mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-01-06 03:21:12 +07:00
[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:
committed by
intellij-monorepo-bot
parent
e681662c82
commit
ce5999a4af
1
.idea/modules.xml
generated
1
.idea/modules.xml
generated
@@ -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" />
|
||||
|
||||
19
platform/whatsNew/intellij.platform.whatsNew.iml
Normal file
19
platform/whatsNew/intellij.platform.whatsNew.iml
Normal 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>
|
||||
12
platform/whatsNew/resources/intellij.platform.whatsNew.xml
Normal file
12
platform/whatsNew/resources/intellij.platform.whatsNew.xml
Normal 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>
|
||||
@@ -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?
|
||||
280
platform/whatsNew/src/WhatsNewAction.kt
Normal file
280
platform/whatsNew/src/WhatsNewAction.kt
Normal 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
|
||||
}
|
||||
}
|
||||
19
platform/whatsNew/src/WhatsNewBundle.kt
Normal file
19
platform/whatsNew/src/WhatsNewBundle.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
74
platform/whatsNew/src/WhatsNewContentVersionChecker.kt
Normal file
74
platform/whatsNew/src/WhatsNewContentVersionChecker.kt
Normal 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)
|
||||
|
||||
}
|
||||
}
|
||||
131
platform/whatsNew/src/WhatsNewShowOnStartCheckService.kt
Normal file
131
platform/whatsNew/src/WhatsNewShowOnStartCheckService.kt
Normal 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
53
platform/whatsNew/src/reaction/FUSReactionChecker.kt
Normal file
53
platform/whatsNew/src/reaction/FUSReactionChecker.kt
Normal 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
|
||||
}
|
||||
}
|
||||
37
platform/whatsNew/src/reaction/ReactionCollector.kt
Normal file
37
platform/whatsNew/src/reaction/ReactionCollector.kt
Normal 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
|
||||
}
|
||||
131
platform/whatsNew/src/reaction/ReactionsPanel.kt
Normal file
131
platform/whatsNew/src/reaction/ReactionsPanel.kt
Normal 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()
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user