mirror of
https://gitflic.ru/project/openide/openide.git
synced 2025-12-15 02:59:33 +07:00
OPENIDE #85 Add welcome screen for openide
This commit is contained in:
@@ -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)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<CefEditorModel> = 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String, Any>()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
259
platform/platform-impl/src/ru/openide/html/HtmlFileEditor.kt
Normal file
259
platform/platform-impl/src/ru/openide/html/HtmlFileEditor.kt
Normal file
@@ -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"
|
||||
}
|
||||
}
|
||||
63
platform/platform-impl/src/ru/openide/html/model.kt
Normal file
63
platform/platform-impl/src/ru/openide/html/model.kt
Normal file
@@ -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<JsQuery>()
|
||||
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?
|
||||
}
|
||||
@@ -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<OpenIdeProjectLocalState> {
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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<String, WelcomeScreenFile>()
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -211,6 +211,7 @@
|
||||
<virtualFileSystem implementationClass="com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl" key="file" physical="true"/>
|
||||
<virtualFileSystem implementationClass="com.intellij.openapi.vfs.impl.jar.JarFileSystemImpl" key="jar" physical="true"/>
|
||||
<virtualFileSystem implementationClass="com.intellij.openapi.vfs.ex.temp.TempFileSystem" key="temp" physical="true"/>
|
||||
<virtualFileSystem implementationClass="ru.openide.welcome.screen.editor.WelcomeScreenFileSystem" key="openide-welcome-fs" physical="false"/>
|
||||
|
||||
<applicationService serviceImplementation="com.intellij.ui.jcef.JBCefStartup" preload="notHeadless" os="mac"/>
|
||||
<cachesInvalidator implementation="com.intellij.ui.jcef.JBCefAppCacheInvalidator" order="last"/>
|
||||
@@ -1130,6 +1131,8 @@
|
||||
<fileEditorProvider implementation="com.intellij.openapi.fileEditor.impl.text.LargeFileEditorProvider" id="LargeFileEditor"/>
|
||||
<fileEditorProvider id="html-editor" implementation="com.intellij.openapi.fileEditor.impl.HTMLEditorProvider"/>
|
||||
<fileEditorProvider id="jcomponent-editor" implementation="com.intellij.openapi.fileEditor.impl.JComponentEditorProvider"/>
|
||||
<fileEditorProvider id="openide-html-editor" implementation="ru.openide.html.HtmlEditorProvider"/>
|
||||
<postStartupActivity implementation="ru.openide.welcome.screen.OpenIdeWelcomeScreenProjectActivity" order="last"/>
|
||||
<editorNotificationProvider implementation="com.intellij.openapi.fileEditor.impl.text.LargeFileNotificationProvider"/>
|
||||
<fileDocumentSynchronizationVetoer implementation="com.intellij.openapi.fileEditor.impl.LargeFileSavingVetoer"/>
|
||||
|
||||
|
||||
@@ -871,7 +871,7 @@
|
||||
<separator/>
|
||||
<group id="LearnGroup"/>
|
||||
<separator/>
|
||||
<action id="WhatsNewAction" class="com.intellij.ide.actions.WhatsNewAction"/>
|
||||
<action id="WhatsNewAction" class="ru.openide.action.OpenIdeWelcomeScreenOpenAction"/>
|
||||
<action id="MeetNewUIAction" class="com.intellij.ide.ui.experimental.meetNewUi.MeetNewUIAction"/>
|
||||
<action id="OnlineDocAction" class="com.intellij.ide.actions.OnlineDocAction"/>
|
||||
<action id="Help.JetBrainsTV" class="com.intellij.ide.actions.JetBrainsTvAction"/>
|
||||
|
||||
Reference in New Issue
Block a user