feat: add UI for build variants list

This commit is contained in:
Akash Yadav
2023-08-25 17:01:11 +05:30
parent 172f3bfc2a
commit 8c21fbc002
9 changed files with 244 additions and 54 deletions
@@ -0,0 +1,42 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.actions.sidebar
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.itsaky.androidide.R
import com.itsaky.androidide.fragments.sidebar.BuildVariantsFragment
import kotlin.reflect.KClass
/**
* Sidebar action for showing the build variants of Android modules in the project.
*
* @author Akash Yadav
*/
class BuildVariantsSidebarAction(context: Context, override val order: Int) :
AbstractSidebarAction() {
override val fragmentClass: KClass<out Fragment> = BuildVariantsFragment::class
override val id: String = "ide.editor.sidebar.buildVariants"
init {
label = context.getString(R.string.title_build_variants)
icon = ContextCompat.getDrawable(context, R.drawable.ic_android)
}
}
@@ -0,0 +1,65 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.LayoutBuildVariantItemBinding
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
/**
* [RecyclerView] adapter for showing the list of Android modules and their selected build variant.
*
* @property items
* @author Akash Yadav
*/
class BuildVariantsAdapter(
private val items: List<BuildVariantInfo>
) : RecyclerView.Adapter<BuildVariantsAdapter.ViewHolder>() {
class ViewHolder(internal val binding: LayoutBuildVariantItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val binding = LayoutBuildVariantItemBinding.inflate(LayoutInflater.from(parent.context), parent,
false)
return ViewHolder(binding)
}
override fun getItemCount(): Int {
return items.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
val variantInfo = items[position]
binding.moduleName.text = variantInfo.modulePath
binding.variantName.setAdapter(
ArrayAdapter(binding.root.context, R.layout.support_simple_spinner_dropdown_item,
variantInfo.buildVariants
)
)
binding.variantName.listSelection = variantInfo.selectedVariantIndex
}
}
@@ -22,7 +22,6 @@ import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.FragmentRecyclerviewBinding
/**
@@ -47,12 +46,19 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
return LinearLayoutManager(requireContext())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/**
* Sets up the recycler view in the fragment.
*/
protected open fun onSetupRecyclerView() {
binding.root.apply {
layoutManager = onCreateLayoutManager()
adapter = unsavedAdapter ?: onCreateAdapter()
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onSetupRecyclerView()
unsavedAdapter = null
@@ -0,0 +1,49 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.sidebar
import android.os.Bundle
import android.view.View
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.adapters.BuildVariantsAdapter
import com.itsaky.androidide.fragments.RecyclerViewFragment
import com.itsaky.androidide.viewmodel.EditorViewModel
/**
* A fragment to show the list of Android modules and its build variants.
*
* @author Akash Yadav
*/
class BuildVariantsFragment : RecyclerViewFragment<BuildVariantsAdapter>() {
private val viewModel by viewModels<EditorViewModel>(ownerProducer = { requireActivity() })
override fun onCreateAdapter(): RecyclerView.Adapter<*> {
return BuildVariantsAdapter(viewModel.buildVariants)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel._buildVariants.observe(viewLifecycleOwner) {
// Setup the recyclerview each time build variants list is updated
onSetupRecyclerView()
}
}
}
@@ -40,10 +40,12 @@ import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.FillMenuParams
import com.itsaky.androidide.actions.SidebarActionItem
import com.itsaky.androidide.actions.internal.DefaultActionsRegistry
import com.itsaky.androidide.actions.sidebar.BuildVariantsSidebarAction
import com.itsaky.androidide.actions.sidebar.CloseProjectSidebarAction
import com.itsaky.androidide.actions.sidebar.FileTreeSidebarAction
import com.itsaky.androidide.actions.sidebar.PreferencesSidebarAction
import com.itsaky.androidide.actions.sidebar.TerminalSidebarAction
import com.itsaky.androidide.fragments.sidebar.BuildVariantsFragment
import com.itsaky.androidide.fragments.sidebar.EditorSidebarFragment
import java.lang.ref.WeakReference
@@ -60,7 +62,10 @@ internal object EditorSidebarActions {
fun registerActions(context: Context) {
val registry = ActionsRegistry.getInstance()
var order = -1
@Suppress("KotlinConstantConditions")
registry.registerAction(FileTreeSidebarAction(context, ++order))
registry.registerAction(BuildVariantsSidebarAction(context, ++order))
registry.registerAction(TerminalSidebarAction(context, ++order))
registry.registerAction(PreferencesSidebarAction(context, ++order))
registry.registerAction(CloseProjectSidebarAction(context, ++order))
@@ -88,6 +93,7 @@ internal object EditorSidebarActions {
val data = ActionData()
data.put(Context::class.java, context) // needed for inflating the menu
val titleRef = WeakReference(binding.title)
val params = FillMenuParams(data, ActionItem.Location.EDITOR_SIDEBAR,
rail.menu) { actionsRegistry, action, item, actionsData ->
@@ -104,18 +110,15 @@ internal object EditorSidebarActions {
controller.navigate(action.id, navOptions {
launchSingleTop = true
restoreState = true
popUpTo(controller.graph.findStartDestination().id) {
inclusive = false
saveState = true
}
})
// Return true only if the destination we've navigated to matches the MenuItem
val result = controller.currentDestination?.matchDestination(action.id) == true
if (result) {
item.isChecked = true
titleRef.get()?.text = item.title
}
result
} catch (e: IllegalArgumentException) {
false
@@ -147,36 +150,6 @@ internal object EditorSidebarActions {
destination(builder)
}
}
val railRef = WeakReference(rail)
val titleRef = WeakReference(binding.title)
controller.addOnDestinationChangedListener(
object : NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
val railView = railRef.get()
if (railView == null) {
controller.removeOnDestinationChangedListener(this)
return
}
railView.menu.forEach { item ->
if (destination.matchDestination(item.itemId)) {
item.isChecked = true
titleRef.get()?.text = item.title
}
}
}
})
// make sure the 'File tree' item is checked by default
rail.menu.findItem(FileTreeSidebarAction.ID.hashCode())?.also {
it.isChecked = true
binding.title.text = it.title
}
}
/**
@@ -188,15 +161,6 @@ internal object EditorSidebarActions {
internal fun NavDestination.matchDestination(route: String): Boolean =
hierarchy.any { it.route == route }
/**
* Determines whether the given `destId` matches the NavDestination. This handles
* both the default case (the destination's id matches the given id) and the nested case where
* the given id is a parent/grandparent/etc of the destination.
*/
@JvmStatic
internal fun NavDestination.matchDestination(@IdRes destId: Int): Boolean =
hierarchy.any { it.id == destId }
@JvmStatic
internal fun ShapeAppearanceModel.roundedOnRight(cornerSize: Float = 28f): ShapeAppearanceModel {
return toBuilder().run {
@@ -27,6 +27,7 @@ import com.google.gson.GsonBuilder
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import com.itsaky.androidide.utils.Environment
import java.io.File
@@ -52,6 +53,8 @@ class EditorViewModel : ViewModel() {
*/
private val mCurrentFile = MutableLiveData<Pair<Int, File?>?>(null)
internal val _buildVariants = MutableLiveData<List<BuildVariantInfo>>(null)
var openedFilesCache: OpenedFilesCache?
get() = _openedFiles.value
set(value) {
@@ -100,6 +103,12 @@ class EditorViewModel : ViewModel() {
_startDrawerOpened.value = value
}
var buildVariants: List<BuildVariantInfo>
get() = this._buildVariants.value ?: emptyList()
set(value) {
this._buildVariants.value = value
}
/**
* Add the given file to the list of opened files.
*
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/module_name"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center_vertical"
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
app:layout_constraintBottom_toBottomOf="@id/variant_name_layout"
app:layout_constraintEnd_toStartOf="@id/variant_name_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text=":app" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/variant_name_layout"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/module_name"
app:layout_constraintTop_toTopOf="parent"
tools:text=":app">
<AutoCompleteTextView
android:id="@+id/variant_name"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:inputType="none" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -594,4 +594,5 @@
<string name="msg_codeaction_failed">Failed to perform code action</string>
<string name="idepref_devOptions_enableLogsender_title">Enable LogSender</string>
<string name="idepref_devOptions_enableLogsender_summary">When disabled, logs from applications won\'t be shown in AndroidIDE</string>
<string name="title_build_variants">Build variants</string>
</resources>
@@ -15,15 +15,15 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.sidebar
import com.itsaky.androidide.fragments.EmptyStateFragment
import com.itsaky.androidide.fragments.RecyclerViewFragment
package com.itsaky.androidide.tooling.api.models
/**
* A fragment to show the list of modules and the selected build variants in the
* Information about the build variants of an Android module.
*
* @property modulePath The project path of the Android module project.
* @property buildVariants The build variants of the Android module project.
* @property selectedVariantIndex The index of the build variant in the [buildVariants] list which is currently selected.
* @author Akash Yadav
*/
class BulidVariantsFragment {
}
data class BuildVariantInfo(val modulePath: String, val buildVariants: List<String>,
val selectedVariantIndex: Int)