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 @@ - +