diff --git a/idea/customization/base/src/OpenIdeExternalResourceUrls.kt b/idea/customization/base/src/OpenIdeExternalResourceUrls.kt
index 427b43320839..704706349624 100644
--- a/idea/customization/base/src/OpenIdeExternalResourceUrls.kt
+++ b/idea/customization/base/src/OpenIdeExternalResourceUrls.kt
@@ -45,8 +45,6 @@ class OpenIdeExternalResourceUrls : ExternalProductResourceUrls {
return productUrl.resolve("ide/docs/OpenIDE_ReferenceCard$suffix.pdf")
}
- override val whatIsNewPageUrl = productUrl.resolve("whatsnew")
-
override val gettingStartedPageUrl = productUrl.resolve("ide/resources")
override val helpPageUrl: ((topicId: String) -> Url)
diff --git a/platform/platform-impl/resources/fileTemplates/internal/openIdeWelcomeScreen.html.ft b/platform/platform-impl/resources/fileTemplates/internal/openIdeWelcomeScreen.html.ft
new file mode 100644
index 000000000000..c660928d0e6d
--- /dev/null
+++ b/platform/platform-impl/resources/fileTemplates/internal/openIdeWelcomeScreen.html.ft
@@ -0,0 +1,81 @@
+
+
+#[[
+
+
+
+
+
+
+ OpenIde Welcome
+
+
+
+ ]]#
+
+
+
+
diff --git a/platform/platform-impl/src/ru/openide/action/OpenIdeWelcomeScreenOpenAction.kt b/platform/platform-impl/src/ru/openide/action/OpenIdeWelcomeScreenOpenAction.kt
new file mode 100644
index 000000000000..4e8001012ca1
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/action/OpenIdeWelcomeScreenOpenAction.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.action
+
+import ru.openide.welcome.screen.WelcomeScreenHelper
+import com.intellij.ide.IdeBundle
+import com.intellij.openapi.actionSystem.ActionUpdateThread
+import com.intellij.openapi.actionSystem.AnAction
+import com.intellij.openapi.actionSystem.AnActionEvent
+import com.intellij.openapi.application.ApplicationNamesInfo
+import com.intellij.openapi.project.DumbAware
+
+class OpenIdeWelcomeScreenOpenAction : AnAction(), DumbAware {
+
+ override fun update(e: AnActionEvent) {
+ e.presentation.isEnabledAndVisible = true
+ 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 getActionUpdateThread() = ActionUpdateThread.BGT
+
+ override fun actionPerformed(e: AnActionEvent) {
+ val project = e.project ?: return
+ WelcomeScreenHelper(project).showWelcomeScreen()
+ }
+}
\ No newline at end of file
diff --git a/platform/platform-impl/src/ru/openide/html/HtmlEditorProvider.kt b/platform/platform-impl/src/ru/openide/html/HtmlEditorProvider.kt
new file mode 100644
index 000000000000..06de28903e9f
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/html/HtmlEditorProvider.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.html
+
+import ru.openide.welcome.screen.WelcomeScreenHelper
+import com.intellij.openapi.fileEditor.*
+import com.intellij.openapi.project.DumbAware
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.Key
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.ui.jcef.JBCefApp
+import com.intellij.util.application
+
+class HtmlEditorProvider : FileEditorProvider, DumbAware {
+
+ override fun createEditor(project: Project, file: VirtualFile): FileEditor {
+ val model = MODEL_KEY.get(file)
+ ?: WelcomeScreenHelper(project).createModel(file)!!.also { MODEL_KEY.set(file, it) }
+
+ return HtmlFileEditor(project, file, model)
+ }
+
+ override fun accept(project: Project, file: VirtualFile): Boolean = file.getUserData(MODEL_KEY) != null
+
+ override fun acceptRequiresReadAction(): Boolean = false
+
+ override fun getEditorTypeId(): String = "openide-html-editor"
+
+ override fun getPolicy(): FileEditorPolicy = FileEditorPolicy.HIDE_DEFAULT_EDITOR
+
+ @Suppress("CompanionObjectInExtension")
+ companion object {
+ val MODEL_KEY: Key = Key.create("openide.html.editor.model.key")
+
+ fun openEditor(project: Project, model: CefEditorModel, file: VirtualFile) {
+ if (!JBCefApp.isSupported()) return
+
+ MODEL_KEY.set(file, model)
+ application.invokeLaterOnWriteThread {
+ FileEditorManager.getInstance(project)
+ .openEditor(OpenFileDescriptor(project, file), true)
+ }
+ }
+ }
+}
diff --git a/platform/platform-impl/src/ru/openide/html/HtmlEditorTemplate.kt b/platform/platform-impl/src/ru/openide/html/HtmlEditorTemplate.kt
new file mode 100644
index 000000000000..9b3269e088e9
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/html/HtmlEditorTemplate.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.html
+
+import com.intellij.ide.fileTemplates.FileTemplateManager
+import com.intellij.openapi.project.Project
+
+class HtmlEditorTemplate(val name: String) {
+ private val params = mutableMapOf()
+
+ fun addParam(name: String, value: Any) = params.put(name, value)
+
+ fun createTextFromInternal(project: Project): String {
+ val templateManager = FileTemplateManager.getInstance(project)
+ val template = templateManager.getInternalTemplate(name)
+ val props = templateManager.defaultProperties
+ props.putAll(params)
+ return template.getText(props)
+ }
+
+}
\ No newline at end of file
diff --git a/platform/platform-impl/src/ru/openide/html/HtmlFileEditor.kt b/platform/platform-impl/src/ru/openide/html/HtmlFileEditor.kt
new file mode 100644
index 000000000000..523760bcbc87
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/html/HtmlFileEditor.kt
@@ -0,0 +1,259 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.html
+
+import com.intellij.CommonBundle
+import com.intellij.ide.IdeBundle
+import com.intellij.ide.plugins.MultiPanel
+import com.intellij.openapi.application.ApplicationInfo
+import com.intellij.openapi.application.invokeLater
+import com.intellij.openapi.editor.EditorBundle
+import com.intellij.openapi.fileEditor.FileEditor
+import com.intellij.openapi.fileEditor.FileEditorState
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.ActionCallback
+import com.intellij.openapi.util.SystemInfo
+import com.intellij.openapi.util.SystemInfoRt.isLinux
+import com.intellij.openapi.util.UserDataHolderBase
+import com.intellij.openapi.util.registry.Registry
+import com.intellij.openapi.util.registry.RegistryManager
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.wm.StatusBar
+import com.intellij.ui.components.JBLoadingPanel
+import com.intellij.ui.jcef.JBCefAppArmorUtils
+import com.intellij.ui.jcef.JBCefBrowserBase
+import com.intellij.ui.jcef.JCEFHtmlPanel
+import com.intellij.util.Alarm
+import com.intellij.util.SystemProperties
+import com.intellij.util.io.URLUtil
+import com.intellij.util.ui.UIUtil
+import org.cef.browser.CefBrowser
+import org.cef.browser.CefFrame
+import org.cef.browser.CefMessageRouter
+import org.cef.callback.CefQueryCallback
+import org.cef.handler.*
+import org.cef.network.CefRequest
+import org.jetbrains.concurrency.runAsync
+import java.awt.BorderLayout
+import java.beans.PropertyChangeListener
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.swing.JComponent
+
+class HtmlFileEditor(
+ private val project: Project,
+ private val file: VirtualFile,
+ private val model: CefEditorModel,
+) : UserDataHolderBase(), FileEditor {
+
+ private val loadingPanel = JBLoadingPanel(BorderLayout(), this)
+ private lateinit var contentPanel: JCEFHtmlPanel
+ private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, this)
+ private val initial = AtomicBoolean(true)
+ private val navigating = AtomicBoolean(false)
+
+ private val multiPanel = object : MultiPanel() {
+ override fun create(key: Int): JComponent = when (key) {
+ LOADING_KEY -> loadingPanel
+ CONTENT_KEY -> contentPanel.component
+ LINUX_APP_ARMOR_KEY -> JBCefAppArmorUtils.getUnprivilegedUserNamespacesRestrictedStubPanel()
+ else -> throw IllegalArgumentException("Unknown key: $key")
+ }
+
+ override fun select(key: Int, now: Boolean): ActionCallback {
+ val callback = super.select(key, now)
+ if (key == CONTENT_KEY) {
+ UIUtil.invokeLaterIfNeeded { contentPanel.component.requestFocusInWindow() }
+ }
+ return callback
+ }
+ }
+
+ init {
+ loadingPanel.setLoadingText(CommonBundle.getLoadingTreeNodeText())
+ if (needToFixLinuxCef()) {
+ multiPanel.select(LINUX_APP_ARMOR_KEY, true)
+ }
+ else {
+ initContentPanel()
+ }
+ }
+
+ override fun getComponent(): JComponent = multiPanel
+ override fun getPreferredFocusedComponent(): JComponent = multiPanel
+ override fun getName(): String = IdeBundle.message("tab.title.html.preview")
+ override fun setState(state: FileEditorState) {}
+ override fun isModified(): Boolean = false
+ override fun isValid(): Boolean = true
+ override fun addPropertyChangeListener(listener: PropertyChangeListener) {}
+ override fun removePropertyChangeListener(listener: PropertyChangeListener) {}
+ override fun dispose() {}
+ override fun getFile(): VirtualFile = file
+
+ //https://youtrack.haulmont.com/issue/ASPR-1589
+ private fun needToFixLinuxCef() =
+ ApplicationInfo.getInstance().build.baselineVersion < 242 && isLinux && useSandbox() && JBCefAppArmorUtils.areUnprivilegedUserNamespacesRestricted()
+
+ private fun initContentPanel() {
+ contentPanel = JCEFHtmlPanel(true, null, null)
+ val client = contentPanel.jbCefClient
+ val cefBrowser = contentPanel.cefBrowser
+
+ client.addLoadHandler(object : CefLoadHandlerAdapter() {
+ override fun onLoadingStateChange(
+ browser: CefBrowser,
+ isLoading: Boolean,
+ canGoBack: Boolean,
+ canGoForward: Boolean,
+ ) {
+ if (initial.get()) {
+ if (isLoading) {
+ invokeLater(runnable = ::startLoading)
+ }
+ else {
+ alarm.cancelAllRequests()
+ initial.set(false)
+ invokeLater(runnable = ::stopLoading)
+ }
+ }
+ }
+ }, cefBrowser)
+
+ client.addDisplayHandler(object : CefDisplayHandlerAdapter() {
+ override fun onStatusMessage(browser: CefBrowser, text: String) {
+ if (!text.startsWith(LOCAL_URL)) {
+ StatusBar.Info.set(text, project)
+ }
+ }
+ }, cefBrowser)
+
+ contentPanel.setErrorPage { errorCode, errorText, failedUrl ->
+ if (errorCode == CefLoadHandler.ErrorCode.ERR_ABORTED && navigating.getAndSet(false)) null
+ else JBCefBrowserBase.ErrorPage.DEFAULT.create(errorCode, errorText, failedUrl)
+ }
+
+ if (SystemInfo.isLinux) {
+ //remove default Intellij focusHandler on linux because of irretrievable focus interception
+ client.cefClient.removeFocusHandler()
+ }
+
+ multiPanel.select(CONTENT_KEY, true)
+ if (model is AsyncLoadCefEditorModel) {
+ startLoading()
+ runAsync { model.modelLoader.invoke() }.onProcessed {
+ if (it != null) {
+ initClient(it)
+ loadContent(it)
+ }
+ }
+ }
+ else {
+ initClient(model)
+ loadContent(model)
+ }
+ }
+
+ private fun loadContent(model: CefEditorModel) {
+ when (model) {
+ is HtmlCefEditorModel -> {
+ contentPanel.loadHTML(model.html, LOCAL_URL)
+ }
+
+ is UrlCefEditorModel -> {
+ val timeoutText = model.timeoutHtml ?: EditorBundle.message("message.html.editor.timeout")
+ alarm.addRequest(
+ { contentPanel.loadHTML(timeoutText) },
+ Registry.intValue("html.editor.timeout", URL_LOADING_TIMEOUT_MS)
+ )
+ contentPanel.loadURL(model.url)
+ }
+
+ is AsyncLoadCefEditorModel -> {
+ startLoading()
+ runAsync { model.modelLoader.invoke() }.onProcessed {
+ if (it != null) {
+ loadContent(it)
+ }
+ }
+ }
+ }
+ }
+
+ fun useSandbox() = SystemProperties.getBooleanProperty("jcef.use_sandbox", true)
+ && RegistryManager.getInstance().get("ide.browser.jcef.sandbox.enable").asBoolean()
+
+ private fun initClient(model: CefEditorModel) {
+ val client = contentPanel.jbCefClient
+ val cefBrowser = contentPanel.cefBrowser
+
+ client.addRequestHandler(object : CefRequestHandlerAdapter() {
+ override fun onBeforeBrowse(
+ cefBrowser: CefBrowser,
+ cefFrame: CefFrame,
+ cefRequest: CefRequest,
+ userGesture: Boolean,
+ isRedirect: Boolean,
+ ) = if (userGesture) {
+ navigating.set(true)
+ model.browseHandler(BrowseRequest(cefBrowser, cefFrame, cefRequest))
+ }
+ else {
+ false
+ }
+ }, cefBrowser)
+
+ client.addLifeSpanHandler(object : CefLifeSpanHandlerAdapter() {
+ override fun onBeforePopup(
+ cefBrowser: CefBrowser,
+ cefFrame: CefFrame,
+ targetUrl: String,
+ targetFrameName: String?,
+ ) = model.browseHandler(BrowseRequest(cefBrowser, cefFrame, targetUrl))
+ }, cefBrowser)
+
+ model.queries.groupBy(JsQuery::name).forEach { (name, queries) ->
+ val config = CefMessageRouter.CefMessageRouterConfig(name, "${name}Cancel")
+ val jsRouter = CefMessageRouter.create(config)
+ jsRouter.addHandler(object : CefMessageRouterHandlerAdapter() {
+ override fun onQuery(
+ cefBrowser: CefBrowser,
+ cefFrame: CefFrame,
+ id: Long,
+ request: String?,
+ persistent: Boolean,
+ callback: CefQueryCallback,
+ ): Boolean {
+ val query = queries.firstOrNull { it.request == request } ?: return false
+
+ query.handler(JsQueryRequest(cefBrowser, cefFrame, callback))
+ return true
+ }
+ }, true)
+ client.cefClient.addMessageRouter(jsRouter)
+ }
+ }
+
+ private fun startLoading() {
+ if (!loadingPanel.isLoading) {
+ loadingPanel.startLoading()
+ }
+ multiPanel.select(LOADING_KEY, true)
+ }
+
+ private fun stopLoading() {
+ if (loadingPanel.isLoading) {
+ loadingPanel.stopLoading()
+ }
+ multiPanel.select(CONTENT_KEY, true)
+ }
+
+ private companion object {
+ private const val LINUX_APP_ARMOR_KEY = 2
+ private const val LOADING_KEY = 1
+ private const val CONTENT_KEY = 0
+ private const val URL_LOADING_TIMEOUT_MS = 10000
+ private const val LOCAL_URL = "${URLUtil.FILE_PROTOCOL}:///openide"
+ }
+}
diff --git a/platform/platform-impl/src/ru/openide/html/model.kt b/platform/platform-impl/src/ru/openide/html/model.kt
new file mode 100644
index 000000000000..261eb1d4c468
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/html/model.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.html
+
+import com.intellij.ide.BrowserUtil
+import com.intellij.openapi.project.Project
+import org.cef.browser.CefBrowser
+import org.cef.browser.CefFrame
+import org.cef.callback.CefQueryCallback
+import org.cef.network.CefRequest
+import java.io.InputStream
+
+abstract class CefEditorModel {
+ var browseHandler: (BrowseRequest) -> Boolean = ::defaultBrowseHandle
+ val queries = mutableListOf()
+ var project: Project? = null
+
+ fun defaultBrowseHandle(request: BrowseRequest): Boolean {
+ BrowserUtil.browse(request.cefRequest.url)
+ return true
+ }
+
+ fun addJsQuery(name: String, request: String, handler: (JsQueryRequest) -> Unit) {
+ queries.add(JsQuery(name, request, handler))
+ }
+}
+
+class UrlCefEditorModel(val url: String, var timeoutHtml: String? = null) : CefEditorModel()
+
+class HtmlCefEditorModel(val html: String) : CefEditorModel()
+
+class AsyncLoadCefEditorModel(val modelLoader: () -> CefEditorModel) : CefEditorModel()
+
+class BrowseRequest(
+ val browser: CefBrowser,
+ val frame: CefFrame,
+ val cefRequest: CefRequest,
+) {
+ constructor(browser: CefBrowser, frame: CefFrame, url: String) : this(
+ browser,
+ frame,
+ CefRequest.create().apply { this.url = url }
+ )
+}
+
+class JsQuery(
+ val name: String,
+ val request: String,
+ val handler: (JsQueryRequest) -> Unit,
+)
+
+class JsQueryRequest(
+ val browser: CefBrowser,
+ val frame: CefFrame,
+ val callback: CefQueryCallback,
+)
+
+abstract class Resource(val mimeType: String?) {
+ abstract fun getInputStream(): InputStream?
+}
\ No newline at end of file
diff --git a/platform/platform-impl/src/ru/openide/welcome/screen/OpenIdeProjectLocalState.kt b/platform/platform-impl/src/ru/openide/welcome/screen/OpenIdeProjectLocalState.kt
new file mode 100644
index 000000000000..63d205273e49
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/welcome/screen/OpenIdeProjectLocalState.kt
@@ -0,0 +1,25 @@
+// Copyright (c) Haulmont 2025. All Rights Reserved.
+// Use is subject to license terms.
+package ru.openide.welcome.screen
+
+import com.intellij.openapi.components.*
+import com.intellij.openapi.project.Project
+import com.intellij.util.xmlb.XmlSerializerUtil
+
+
+@Service(Service.Level.PROJECT)
+@State(name = "OpenIdeProjectLocalState", storages = [Storage(StoragePathMacros.WORKSPACE_FILE)])
+class OpenIdeProjectLocalState : PersistentStateComponent {
+
+ companion object {
+ fun getInstance(project: Project): OpenIdeProjectLocalState = project.getService(OpenIdeProjectLocalState::class.java)
+ }
+
+ var isFirstOpen: Boolean = true
+
+ override fun getState(): OpenIdeProjectLocalState = this
+
+ override fun loadState(state: OpenIdeProjectLocalState) {
+ XmlSerializerUtil.copyBean(state, this)
+ }
+}
\ No newline at end of file
diff --git a/platform/platform-impl/src/ru/openide/welcome/screen/OpenIdeWelcomeScreenProjectActivity.kt b/platform/platform-impl/src/ru/openide/welcome/screen/OpenIdeWelcomeScreenProjectActivity.kt
new file mode 100644
index 000000000000..dbf02f5f739a
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/welcome/screen/OpenIdeWelcomeScreenProjectActivity.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.welcome.screen
+
+
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.application.ReadAction
+import com.intellij.openapi.extensions.ExtensionNotApplicableException
+import com.intellij.openapi.project.DumbAwareRunnable
+import com.intellij.openapi.project.DumbService
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.startup.ProjectActivity
+import com.intellij.openapi.startup.StartupManager
+import com.intellij.util.concurrency.AppExecutorUtil
+import java.util.concurrent.Callable
+
+class OpenIdeWelcomeScreenProjectActivity : ProjectActivity {
+ init {
+ val app = ApplicationManager.getApplication()
+ if (app.isCommandLine || app.isHeadlessEnvironment || app.isUnitTestMode) {
+ throw ExtensionNotApplicableException.create()
+ }
+ }
+
+ override suspend fun execute(project: Project) = StartupManager.getInstance(project).runAfterOpened(
+ DumbAwareRunnable {
+ ApplicationManager.getApplication().invokeLater(
+ {
+ DumbService.getInstance(project).smartInvokeLater {
+ smartOpenedAction(project)
+ }
+ }
+ ) { !project.isOpen || project.isDisposed }
+ }
+ )
+
+ private fun smartOpenedAction(project: Project) {
+ ReadAction.nonBlocking((Callable {
+ OpenIdeProjectLocalState.getInstance(project).isFirstOpen
+ }))
+ .inSmartMode(project)
+ .finishOnUiThread(ModalityState.any()) { showWelcomeScreen(project, it) }
+ .coalesceBy(javaClass, project)
+ .submit(AppExecutorUtil.getAppExecutorService())
+ }
+
+ private fun showWelcomeScreen(project: Project, isFirstOpen: Boolean) {
+ if (!isFirstOpen) return
+ OpenIdeProjectLocalState.getInstance(project).isFirstOpen = false
+ WelcomeScreenHelper(project).showWelcomeScreen()
+ }
+}
diff --git a/platform/platform-impl/src/ru/openide/welcome/screen/WelcomeScreenHelper.kt b/platform/platform-impl/src/ru/openide/welcome/screen/WelcomeScreenHelper.kt
new file mode 100644
index 000000000000..0b74b71e9bb4
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/welcome/screen/WelcomeScreenHelper.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.welcome.screen
+
+import com.intellij.ide.plugins.PluginManagerConfigurable
+import ru.openide.welcome.screen.editor.WelcomeScreenFile
+import ru.openide.welcome.screen.editor.WelcomeScreenFileSystem
+import ru.openide.welcome.screen.editor.WelcomeScreenFileType
+import com.intellij.openapi.application.invokeLater
+import com.intellij.openapi.extensions.PluginId
+import com.intellij.openapi.fileEditor.FileEditorManager
+import com.intellij.openapi.fileEditor.OpenFileDescriptor
+import com.intellij.openapi.options.ShowSettingsUtil
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.updateSettings.impl.pluginsAdvertisement.installAndEnable
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.ui.EditorNotifications
+import com.intellij.ui.jcef.JBCefApp
+import com.intellij.util.ui.StartupUiUtil
+import ru.openide.html.*
+
+class WelcomeScreenHelper(val project: Project) {
+
+ fun showWelcomeScreen() {
+ if (!JBCefApp.isSupported()) return
+
+ val editorManager = FileEditorManager.getInstance(project)
+ val openedWelcome = editorManager.allEditors.firstOrNull(WelcomeScreenFileType::isMyEditor)
+ if (openedWelcome != null) {
+ editorManager.openEditor(OpenFileDescriptor(project, openedWelcome.file), true)
+ return
+ }
+
+ val file = WelcomeScreenFileSystem.getFile(project) ?: WelcomeScreenFile(project.guessProjectDir())
+ val editorModel = createModel(file) ?: return
+ HtmlEditorProvider.openEditor(project, editorModel, file)
+ }
+
+ fun createModel(file: VirtualFile): HtmlCefEditorModel? {
+ if (!WelcomeScreenFileType.isMyFile(file)) return null
+ val htmlTemplate = HtmlEditorTemplate(TEMPLATE_NAME).apply {
+ addParam(THEME, if (StartupUiUtil.isDarkTheme) "theme-dark" else "")
+ }
+
+ return createEditorModel(htmlTemplate.createTextFromInternal(project))
+ }
+
+ private fun createEditorModel(html: String) = HtmlCefEditorModel(html).apply {
+ project = this@WelcomeScreenHelper.project
+
+ addJsQuery(JsQuery.OPEN_MARKETPLACE) {
+ invokeLater {
+ ShowSettingsUtil.getInstance().showSettingsDialog(
+ project,
+ PluginManagerConfigurable::class.java
+ )
+ }
+ }
+
+ addJsQuery(JsQuery.INSTALL_PYTHON_PLUGIN) {
+ invokeLater {
+ val project = project ?: return@invokeLater
+ installPlugin(PYTHON_PLUGIN_ID, project)
+ }
+ }
+
+ addJsQuery(JsQuery.INSTALL_DOCKER_PLUGIN) {
+ invokeLater {
+ val project = project ?: return@invokeLater
+ installPlugin(DOCKER_PLUGIN_ID, project)
+ }
+ }
+ }
+
+ private fun installPlugin(idString: String, project: Project) {
+ installAndEnable(project, setOf(PluginId.getId(idString)), true) {
+ EditorNotifications.getInstance(project).updateAllNotifications()
+ }
+ }
+
+ private fun CefEditorModel.addJsQuery(query: JsQuery, handler: (JsQueryRequest) -> Unit) =
+ addJsQuery(query.queryName, query.request, handler)
+
+ private enum class JsQuery(val queryName: String, val request: String) {
+ OPEN_MARKETPLACE(SUGGESTION_QUERY_NAME, "OPEN_MARKETPLACE"),
+ INSTALL_PYTHON_PLUGIN(SUGGESTION_QUERY_NAME, "INSTALL_PYTHON_PLUGIN"),
+ INSTALL_DOCKER_PLUGIN(SUGGESTION_QUERY_NAME, "INSTALL_DOCKER_PLUGIN"),
+ }
+
+ companion object {
+ private const val TEMPLATE_NAME = "openIdeWelcomeScreen.html"
+ private const val THEME = "THEME"
+ private const val SUGGESTION_QUERY_NAME = "suggestionQuery"
+ private const val PYTHON_PLUGIN_ID = "PythonCore"
+ private const val DOCKER_PLUGIN_ID = "ru.openide.docker"
+ }
+}
\ No newline at end of file
diff --git a/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFile.kt b/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFile.kt
new file mode 100644
index 000000000000..ab0b5fa70aff
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFile.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.welcome.screen.editor
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileSystem
+import com.intellij.testFramework.LightVirtualFile
+
+class WelcomeScreenFile(private val parent: VirtualFile? = null)
+ : LightVirtualFile(NAME, WelcomeScreenFileType.INSTANCE, "") {
+ override fun getParent() = parent
+
+ override fun isWritable() = false
+
+ override fun getFileSystem(): VirtualFileSystem = WelcomeScreenFileSystem.getInstance()
+
+ companion object {
+ val NAME = "Welcome"
+ fun getPath(project: Project) = "${project.guessProjectDir()?.path ?: "."}/$NAME"
+ }
+}
\ No newline at end of file
diff --git a/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFileSystem.kt b/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFileSystem.kt
new file mode 100644
index 000000000000..fd27575158f6
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFileSystem.kt
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+package ru.openide.welcome.screen.editor
+
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VfsUtil
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.openapi.vfs.VirtualFileManager
+import com.intellij.openapi.vfs.VirtualFileSystem
+import com.intellij.openapi.vfs.ex.temp.TempFileSystem
+import java.nio.file.Path
+
+class WelcomeScreenFileSystem : TempFileSystem() {
+
+ private val files = mutableMapOf()
+
+ override fun getProtocol() = PROTOCOL
+
+ override fun findFileByPath(path: String): WelcomeScreenFile? {
+ val parentPath = path.substringBeforeLast('/')
+ val file = files[parentPath]
+ if (file != null) {
+ return file
+ }
+
+ return if (path.endsWith(WelcomeScreenFile.NAME)) {
+ WelcomeScreenFile(VfsUtil.findFile(Path.of(parentPath), false)).also {
+ files[parentPath] = it
+ }
+ }
+ else {
+ null
+ }
+ }
+
+ override fun isWritable(file: VirtualFile) = false
+
+ override fun isReadOnly() = true
+
+ companion object {
+ const val PROTOCOL = "openide-welcome-fs"
+ fun getInstance(): VirtualFileSystem = VirtualFileManager.getInstance().getFileSystem(PROTOCOL)
+ fun getFile(project: Project) = getInstance().findFileByPath(WelcomeScreenFile.getPath(project))
+ }
+}
diff --git a/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFileType.kt b/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFileType.kt
new file mode 100644
index 000000000000..67ebb1a1d877
--- /dev/null
+++ b/platform/platform-impl/src/ru/openide/welcome/screen/editor/WelcomeScreenFileType.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) Haulmont 2024. All Rights Reserved.
+ * Use is subject to license terms.
+ */
+
+package ru.openide.welcome.screen.editor
+
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.fileEditor.FileEditor
+import com.intellij.openapi.fileTypes.ex.FakeFileType
+import com.intellij.openapi.vfs.VirtualFile
+
+class WelcomeScreenFileType : FakeFileType() {
+ companion object {
+ val INSTANCE = WelcomeScreenFileType()
+ fun isMyFile(file: VirtualFile) = file.fileType is WelcomeScreenFileType
+ fun isMyEditor(editor: FileEditor) = isMyFile(editor.file)
+ }
+
+ override fun getName() = "WelcomeScreen"
+
+ override fun getDescription() = ""
+
+ override fun isMyFileType(file: VirtualFile) = isMyFile(file)
+
+ override fun getIcon() = AllIcons.Nodes.PpWeb
+}
diff --git a/platform/platform-resources/src/META-INF/PlatformExtensions.xml b/platform/platform-resources/src/META-INF/PlatformExtensions.xml
index ed2392b33fa5..6d46a64d901d 100644
--- a/platform/platform-resources/src/META-INF/PlatformExtensions.xml
+++ b/platform/platform-resources/src/META-INF/PlatformExtensions.xml
@@ -211,6 +211,7 @@
+
@@ -1130,6 +1131,8 @@
+
+
diff --git a/platform/platform-resources/src/idea/PlatformActions.xml b/platform/platform-resources/src/idea/PlatformActions.xml
index 676456ee75d5..aeca01586be5 100644
--- a/platform/platform-resources/src/idea/PlatformActions.xml
+++ b/platform/platform-resources/src/idea/PlatformActions.xml
@@ -871,7 +871,7 @@
-
+