blob: f47289718373a8269846dd2883cfe468f388b84a [file] [log] [blame]
/*
* Copyright (C) 2013 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.templates
import com.android.annotations.concurrency.GuardedBy
import com.android.annotations.concurrency.Slow
import com.android.tools.idea.actions.NewAndroidComponentAction
import com.android.tools.idea.device.FormFactor
import com.android.tools.idea.flags.StudioFlags
import com.android.tools.idea.model.AndroidModel
import com.android.tools.idea.npw.model.ProjectSyncInvoker
import com.android.tools.idea.npw.model.ProjectSyncInvoker.DefaultProjectSyncInvoker
import com.android.tools.idea.npw.model.RenderTemplateModel
import com.android.tools.idea.npw.model.RenderTemplateModel.Companion.fromFacet
import com.android.tools.idea.npw.project.getModuleTemplates
import com.android.tools.idea.npw.project.getPackageForPath
import com.android.tools.idea.npw.template.ChooseActivityTypeStep
import com.android.tools.idea.npw.template.ChooseFragmentTypeStep
import com.android.tools.idea.npw.template.TemplateResolver
import com.android.tools.idea.ui.wizard.StudioWizardDialogBuilder
import com.android.tools.idea.util.androidFacet
import com.android.tools.idea.wizard.model.ModelWizard
import com.android.tools.idea.wizard.model.SkippableWizardStep
import com.android.tools.idea.wizard.model.WizardModel
import com.android.tools.idea.wizard.template.Category
import com.android.tools.idea.wizard.template.Template
import com.android.tools.idea.wizard.template.WizardUiContext
import com.google.common.collect.Table
import com.google.common.collect.TreeBasedTable
import com.intellij.ide.actions.NonEmptyActionGroup
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.LangDataKeys
import icons.AndroidIcons
import org.jetbrains.android.util.AndroidBundle.message
import org.jetbrains.annotations.PropertyKey
/**
* Handles locating templates and providing template metadata
*/
class TemplateManager private constructor() {
/** Lock protecting access to [_categoryTable] */
private val CATEGORY_TABLE_LOCK = Any()
/** Table mapping (Category, Template Name) -> Template File */
@GuardedBy("CATEGORY_TABLE_LOCK")
private var _categoryTable: Table<Category, String, Template>? = null
@get:GuardedBy("CATEGORY_TABLE_LOCK")
private val categoryTable: Table<Category, String, Template>?
get() {
if (_categoryTable == null) {
reloadCategoryTable()
}
return _categoryTable
}
private val topGroup = DefaultActionGroup("AndroidTemplateGroup", false)
@Slow
fun getTemplateCreationMenu(): ActionGroup {
refreshDynamicTemplateMenu()
return topGroup
}
@Slow
fun refreshDynamicTemplateMenu() = synchronized(CATEGORY_TABLE_LOCK) {
topGroup.apply {
removeAll()
addSeparator()
}
val am = ActionManager.getInstance()
reloadCategoryTable() // Force reload
for (category in categoryTable!!.rowKeySet()) {
// Create the menu group item
val categoryGroup: NonEmptyActionGroup = object : NonEmptyActionGroup() {
override fun update(e: AnActionEvent) {
updateAction(e, category.name, childrenCount > 0, false)
}
}
categoryGroup.isPopup = true
fillCategory(categoryGroup, category, am)
topGroup.add(categoryGroup)
setPresentation(category, categoryGroup)
}
}
@GuardedBy("CATEGORY_TABLE_LOCK")
private fun fillCategory(categoryGroup: NonEmptyActionGroup, category: Category, am: ActionManager) {
val categoryRow = _categoryTable!!.row(category)
fun addCategoryGroup(category: Category, name: String, @PropertyKey(resourceBundle = "messages.AndroidBundle") messageKey: String) {
val galleryAction: AnAction = object : AnAction() {
override fun update(e: AnActionEvent) {
updateAction(e, "Gallery...", true, true)
}
override fun actionPerformed(e: AnActionEvent) {
showWizardDialog(e, category.name, message(messageKey, FormFactor.MOBILE.id), "New $name")
}
}
categoryGroup.add(galleryAction)
categoryGroup.addSeparator()
setPresentation(category, galleryAction)
}
if (category == Category.Activity) {
addCategoryGroup(category, "Android Activity", "android.wizard.activity.add")
}
if (StudioFlags.NPW_SHOW_FRAGMENT_GALLERY.get() && category == Category.Fragment) {
addCategoryGroup(category, "Android Fragment", "android.wizard.fragment.add")
}
for (templateName in categoryRow.keys) {
val template = _categoryTable!![category, templateName]
val templateAction = NewAndroidComponentAction(category, templateName, template.minSdk, template.minCompileSdk, template.constraints)
val actionId = ACTION_ID_PREFIX + category + templateName
am.replaceAction(actionId, templateAction)
categoryGroup.add(templateAction)
}
val providers = AdditionalTemplateActionsProvider.EP_NAME.extensionList
for (provider in providers) {
for (anAction in provider.getAdditionalActions(category)) {
categoryGroup.add(anAction)
}
}
}
@Slow
@GuardedBy("CATEGORY_TABLE_LOCK")
private fun reloadCategoryTable() {
_categoryTable = TreeBasedTable.create()
TemplateResolver.getAllTemplates()
.filter { WizardUiContext.MenuEntry in it.uiContexts }
.forEach { addTemplateToTable(it) }
}
@GuardedBy("CATEGORY_TABLE_LOCK")
private fun addTemplateToTable(template: Template, userDefinedTemplate: Boolean = false) = with(template) {
if (category == Category.Compose && !StudioFlags.COMPOSE_WIZARD_TEMPLATES.get()) {
return
}
val existingTemplate = _categoryTable!![category, name]
if (existingTemplate == null || template.revision > existingTemplate.revision) {
_categoryTable!!.put(category, name, template)
}
}
companion object {
/**
* A directory relative to application home folder where we can find an extra template folder. This lets us ship more up-to-date
* templates with the application instead of waiting for SDK updates.
*/
private const val CATEGORY_ACTIVITY = "Activity"
private const val CATEGORY_FRAGMENT = "Fragment"
private const val ACTION_ID_PREFIX = "template.create."
@JvmStatic
val instance = TemplateManager()
private fun updateAction(event: AnActionEvent, actionText: String?, visible: Boolean, disableIfNotReady: Boolean) {
val module = event.getData(LangDataKeys.MODULE)
val facet = module?.androidFacet
val isProjectReady = facet != null && AndroidModel.get(facet) != null
event.presentation.apply {
text = actionText + (" (Project not ready)".takeUnless { isProjectReady } ?: "")
isVisible = visible && facet != null && AndroidModel.isRequired(facet)
isEnabled = !disableIfNotReady || isProjectReady
}
}
private fun showWizardDialog(e: AnActionEvent, category: String, commandName: String, dialogTitle: String) {
val projectSyncInvoker: ProjectSyncInvoker = DefaultProjectSyncInvoker()
val module = LangDataKeys.MODULE.getData(e.dataContext)!!
val targetFile = CommonDataKeys.VIRTUAL_FILE.getData(e.dataContext)!!
var targetDirectory = targetFile
if (!targetDirectory.isDirectory) {
targetDirectory = targetFile.parent
assert(targetDirectory != null)
}
val facet = module.androidFacet
assert(facet != null && AndroidModel.get(facet) != null)
val moduleTemplates = facet!!.getModuleTemplates(targetDirectory)
assert(moduleTemplates.isNotEmpty())
val initialPackageSuggestion = facet.getPackageForPath(moduleTemplates, targetDirectory)
val renderModel = fromFacet(
facet, initialPackageSuggestion, moduleTemplates[0],
commandName, projectSyncInvoker, true
)
val chooseTypeStep: SkippableWizardStep<WizardModel>
chooseTypeStep = when (category) {
CATEGORY_ACTIVITY -> ChooseActivityTypeStep.forActivityGallery(renderModel, targetDirectory)
CATEGORY_FRAGMENT -> ChooseFragmentTypeStep(renderModel, FormFactor.MOBILE, targetDirectory)
else -> throw RuntimeException("Invalid category name: $category")
}
val wizard = ModelWizard.Builder().addStep(chooseTypeStep).build()
StudioWizardDialogBuilder(wizard, dialogTitle).build().show()
}
private fun setPresentation(category: Category, categoryGroup: AnAction) {
categoryGroup.templatePresentation.apply {
icon = AndroidIcons.Android
text = category.name
}
}
}
}