mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-20 13:31:28 +07:00
OPENIDE #85 Add welcome screen for openide
(cherry picked from commit 216f7d0bd79089bb6dc70ce76032a7652f90c294) (cherry picked from commite39da2aa11) (cherry picked from commitf22be17c9d) (cherry picked from commitf15893551b) (cherry picked from commit432e61d2b1)
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
|
||||
}
|
||||
@@ -218,6 +218,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"/>
|
||||
@@ -1227,6 +1228,8 @@
|
||||
<navbar implementation="com.intellij.openapi.options.newEditor.settings.SettingsNavBarModelExtension"/>
|
||||
|
||||
|
||||
<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"/>
|
||||
|
||||
|
||||
@@ -951,7 +951,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