build: reorganize and reconfigure modules to resolve dependency resolution errors

Gradle, for some reason, does not the path of modules to identify them uniquely. Instead, the 'build id' is used to do so, which consists of the maven group and artifact ID. So, even if two modules are located in separate directories, but have the name group and artifact ID, including both of them in other modules may result in dependency resolution errors.
This commit is contained in:
Akash Yadav
2024-05-29 13:11:44 +05:30
parent c0c185cfb7
commit 1efd6a04d5
2457 changed files with 1069 additions and 709 deletions
+1
View File
@@ -0,0 +1 @@
/build/
+50
View File
@@ -0,0 +1,50 @@
/*
* 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/>.
*/
import com.itsaky.androidide.build.config.BuildConfig
plugins{
id("com.android.library")
id("kotlin-android")
id("kotlin-kapt")
}
groupConfig {
groupIdSuffix.set("core")
}
android {
namespace = "${BuildConfig.packageName}.actions"
}
dependencies {
kapt(libs.google.auto.service)
api(libs.androidx.nav.fragment)
api(libs.androidx.nav.ui)
implementation(projects.core.common)
implementation(projects.core.resources)
implementation(libs.common.editor)
implementation(libs.common.kotlin)
implementation(libs.common.kotlin.coroutines.android)
implementation(libs.common.utilcode)
implementation(libs.google.auto.service.annotations)
implementation(libs.androidx.core.ktx)
implementation(libs.google.material)
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
+17
View File
@@ -0,0 +1,17 @@
<?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/>.
-->
<manifest/>
@@ -0,0 +1,54 @@
/*
* 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;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* @author Akash Yadav
*/
public class ActionData {
private final Map<Class<?>, Object> instances = new HashMap<>();
public <T> void put(@NonNull Class<T> type, @Nullable T object) {
Objects.requireNonNull(type);
this.instances.put(type, object);
}
@Nullable
public <T> T get(@NonNull Class<T> type) {
Objects.requireNonNull(type);
final var object = this.instances.get(type);
if (object == null) {
return null;
}
return type.cast(object);
}
public void clear() {
this.instances.clear();
}
}
@@ -0,0 +1,189 @@
/*
* 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
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import android.view.Menu
import android.view.View
import androidx.annotation.CallSuper
import com.itsaky.androidide.utils.resolveAttr
/**
* An action that can be registered using the [ActionsRegistry]
* [com.itsaky.androidide.actions.ActionsRegistry]
*
* @author Akash Yadav
*/
interface ActionItem {
/**
* A unique ID for this action.
*/
val id: String
/**
* The label for this action.
*/
var label: String
/**
* Whether the action should be visible to the user or not.
*/
var visible: Boolean
/**
* Whether the action should be enabled.
*/
var enabled: Boolean
/**
* Icon for this action.
*/
var icon: Drawable?
/**
* Whether the [execAction] method of this action must be executed on UI thread.
*/
var requiresUIThread: Boolean
/**
* The location of this [ActionItem].
*/
var location: Location
/**
* The order of this action item. This is used only at some locations and not everywhere.
*
* @see android.view.MenuItem.getOrder
*/
val order: Int
get() = Menu.NONE
/**
* The item ID that will be set to the menu item.
*/
val itemId: Int
get() = id.hashCode()
/**
* Prepare the action. Subclasses can modify the visual properties of this action here.
*
* @param data The data containing various information about the event.
*/
@CallSuper
fun prepare(data: ActionData) {
visible = true
enabled = true
}
/**
* Execute the action. The action executed in a background thread by default.
*
* @param data The data containing various information about the event.
* @return `true` if this action was executed successfully, `false` otherwise.
*/
suspend fun execAction(data: ActionData): Any
/**
* Called just after the [execAction] method executes **successfully** (i.e. returns `true`).
* Subclasses are free to do UI related work here as this method is called on UI thread.
*
* @param data The data containing various information about the event.
*/
fun postExec(data: ActionData, result: Any) = Unit
/**
* Called when the action item is to be destroyed. Any resource references must be released if
* held.
*/
fun destroy() = Unit
/**
* Return the show as action flags for the menu item.
*
* @return The show as action flags.
*/
fun getShowAsActionFlags(data: ActionData): Int = -1
/**
* Create custom action view for this action item.
*
* @return The custom action view or `null`.
*/
fun createActionView(data: ActionData): View? = null
/**
* Creates the color filter for this action's icon drawable.
*
* The default implementation returns a [PorterDuffColorFilter] instance with color [R.attr.colorOnSurface].
*/
fun createColorFilter(data: ActionData): ColorFilter? {
return data.getContext()?.let {
PorterDuffColorFilter(it.resolveAttr(R.attr.colorOnSurface), PorterDuff.Mode.SRC_ATOP)
}
}
/** Location where an action item will be shown. */
enum class Location(val id: String) {
/** Location marker for action items shown in editor activity's toolbar. */
EDITOR_TOOLBAR("ide.editor.toolbar"),
/**
* Location marker for action items shown in editor activity's sidebar (navigation rail in the drawer).
*/
EDITOR_SIDEBAR("ide.editor.sidebar"),
/**
* Location marker for action items shown in the default category of editor activity's sidebar (navigation rail in the drawer).
*/
EDITOR_SIDEBAR_DEFAULT_ITEMS("ide.editor.sidebar.defaultItems"),
/** Location marker for action items shown in editor's text action menu. */
EDITOR_TEXT_ACTIONS("ide.editor.textActions"),
/**
* Location marker for action items shown in 'Code actions' submenu in editor's text action
* menu.
*/
EDITOR_CODE_ACTIONS("ide.editor.codeActions"),
/** Location marker for action items shown when file tabs are reselected. */
EDITOR_FILE_TABS("ide.editor.fileTabs"),
/**
* Location marker for action items that are shown when the files in the editor activity's file
* tree are long clicked.
*/
EDITOR_FILE_TREE("ide.editor.fileTree"),
/** Location marker for action items shown in UI Designer activity's toolbar. */
UI_DESIGNER_TOOLBAR("ide.uidesigner.toolbar");
override fun toString(): String {
return id
}
fun forId(id: String): Location {
return entries.first { it.id == id }
}
}
}
@@ -0,0 +1,59 @@
/*
* 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
/**
* An action menu is an action which can contain child actions.
* @author Akash Yadav
*/
interface ActionMenu : ActionItem {
val children: MutableSet<ActionItem>
fun addAction(action: ActionItem) = children.add(action)
fun removeAction(action: ActionItem) = children.remove(action)
/**
* Find the action item with the given action ID.
*
* @return The action item or `null` if not found.
*/
fun findAction(id: String): ActionItem? {
return children.find { it.id == id }
}
override fun prepare(data: ActionData) {
super.prepare(data)
visible = children.isNotEmpty() && isAtLeastOneChildVisible(data)
enabled = visible
}
/** Action menus are not supposed to perform any action */
override suspend fun execAction(data: ActionData): Boolean {
return false
}
/**
* Calls [ActionItem.prepare] on each child action and returns `true` if at least one of them
* is [visible][ActionItem.visible].
*/
fun isAtLeastOneChildVisible(data: ActionData) : Boolean {
return children.firstOrNull { it.prepare(data); it.visible } != null
}
}
@@ -0,0 +1,121 @@
/*
* 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
import android.view.Menu
import com.itsaky.androidide.actions.internal.DefaultActionsRegistry
import com.itsaky.androidide.utils.ServiceLoader
import com.itsaky.androidide.utils.VMUtils
/** @author Akash Yadav */
abstract class ActionsRegistry {
companion object {
private var instance: ActionsRegistry? = null
@JvmStatic
fun getInstance(): ActionsRegistry {
if (instance == null) {
instance = if (VMUtils.isJvm()) {
DefaultActionsRegistry()
} else ServiceLoader.load(ActionsRegistry::class.java).findFirstOrThrow()
}
return instance as ActionsRegistry
}
}
/**
* Register the given action.
*
* @param action The action to register. Must not be `null`.
* @return `true` if the action was registered successfully. `false` otherwise.
*/
abstract fun registerAction(action: ActionItem): Boolean
/**
* Unregister the given action.
*
* @param action The action to unregister. Must not be `null`.
* @return `true` if the action was unregistered successfully. `false` otherwise.
*/
abstract fun unregisterAction(action: ActionItem): Boolean
/**
* Unregister the action with the given ID.
*
* @param id The ID of action to unregister. Must not be `null`.
* @return `true` if the action was unregistered successfully. `false` otherwise.
*/
abstract fun unregisterAction(id: String): Boolean
/**
* Find the action with the given ID.
*
* @param id The ID of the action to find.
* @return The found action or `null`.
*/
abstract fun findAction(location: ActionItem.Location, id: String): ActionItem?
/**
* Find the action with the given unique item ID.
*
* @param itemId The ID of the action to find.
* @return The found action or `null`.
*/
abstract fun findAction(location: ActionItem.Location, itemId: Int): ActionItem?
/**
* Fill the given menu with the registered actions.
*
* Subclasses must first call [ActionItem.prepare] to update the action.
*/
abstract fun fillMenu(params: FillMenuParams)
/** Get all the registered actions at the given location. */
abstract fun getActions(location: ActionItem.Location): Map<String, ActionItem>
/** Clear all the registered actions. */
abstract fun clearActions(location: ActionItem.Location)
/**
* Register a listener that will be called when any action is executed. The callbacks are called
* irrespective of the action's execution result.
*
* @param listener The listener to register.
*/
abstract fun registerActionExecListener(listener: ActionExecListener)
/**
* Unregister the given listener.
*
* @param listener The listener to unregister.
*/
abstract fun unregisterActionExecListener(listener: ActionExecListener)
/** Listener for listening to callbacks for action execution. */
interface ActionExecListener {
/**
* The given action was executed.
* @param action The action that was executed.
* @param result The result of the action execution.
*/
fun onExec(action: ActionItem, result: Any)
}
}
@@ -0,0 +1,34 @@
/*
* 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
/**
* A [ActionItem] related to the code editor. Subclasses can choose whether to dismiss the actions
* window when this action is performed.
*
* @author Akash Yadav
*/
interface EditorActionItem : ActionItem {
/**
* Whether the actions window should be dismissed or not once this action is performed.
*
* @return `true` if the actions window must be dismissed, `false` otherwise.
*/
fun dismissOnAction(): Boolean = true
}
@@ -0,0 +1,36 @@
/*
* 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
import android.view.Menu
import com.itsaky.androidide.actions.internal.DefaultActionsRegistry
/**
* Parameters required by [ActionsRegistry] to fill a menu.
*
* @author Akash Yadav
*/
data class FillMenuParams(
val data: ActionData,
val location: ActionItem.Location,
val menu: Menu,
val onMenuItemClick: OnActionClickListener = OnActionClickListener { registry, action, _, actionData ->
(registry as DefaultActionsRegistry).executeAction(action, actionData)
true
}
)
@@ -0,0 +1,38 @@
/*
* 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
import android.view.MenuItem
/**
* Listener for listening to click events on [ActionItem].
*
* @author Akash Yadav
*/
fun interface OnActionClickListener {
/**
* Called when the given [action] is clicked.
*
* @param registry The [ActionsRegistry].
* @param action The action that was clicked.
* @param data The action data associated with the action.
* @return Whether the click event was consumed.
*/
fun onClick(registry: ActionsRegistry, action: ActionItem, item: MenuItem, data: ActionData): Boolean
}
@@ -0,0 +1,45 @@
/*
* 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
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.FragmentNavigatorDestinationBuilder
import kotlin.reflect.KClass
/**
* An [ActionItem] that is shown in the editor activity's sidebar. If the action's [fragmentClass] is null,
* the [ActionItem.execAction] is called when the user clicks on the navigation item in the navigation rail.
*
* The [id] property of the action is used as the route for the navigation graph.
*
* @author Akash Yadav
*/
interface SidebarActionItem : ActionItem {
/**
* The fragment class for this action item. Implementations can provide a fragment class which
* will be shown when this action item is selected.
*/
val fragmentClass: KClass<out Fragment>?
/**
* Build/setup the fragment navigation for this action item. This method is called only if the [fragmentClass] property
* is non-null.
*/
fun FragmentNavigatorDestinationBuilder.buildNavigation() {}
}
@@ -0,0 +1,71 @@
/*
* 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
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.itsaky.androidide.utils.DialogUtils
import io.github.rosemoe.sora.widget.CodeEditor
import java.io.File
import java.nio.file.Path
fun ActionData.getContext(): Context? {
return get(Context::class.java)
}
fun ActionData.requireContext(): Context {
return getContext() ?: throw IllegalArgumentException("No context instance provided")
}
fun ActionData.requireFile(): File {
return get(File::class.java) ?: throw IllegalArgumentException("No file instance provided")
}
fun ActionData.requirePath(): Path {
return requireFile().toPath()
}
fun ActionData.requireEditor(): CodeEditor {
return get(CodeEditor::class.java)
?: throw IllegalArgumentException("An editor instance is required but none was provided")
}
fun newDialogBuilder(data: ActionData): MaterialAlertDialogBuilder {
val context = data.get(Context::class.java)!!
return DialogUtils.newMaterialDialogBuilder(context)
}
/**
* Checks if the given [ActionData] has instances of the given [types].
*
* @param types The type of objects to look for.
* @return `true` if this [ActionData] has the given [types], `false` otherwise.
*/
fun ActionData.hasRequiredData(vararg types: Class<*>): Boolean {
for (type in types) {
get(type) ?: return false
}
return true
}
/** Marks this action item as invisible. */
fun ActionItem.markInvisible() {
visible = false
enabled = false
}
@@ -0,0 +1,234 @@
/*
* 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.internal
import android.view.Menu
import android.view.MenuItem
import com.google.auto.service.AutoService
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.FillMenuParams
import com.itsaky.androidide.actions.OnActionClickListener
import com.itsaky.androidide.actions.locations.CodeActionsMenu
import com.itsaky.androidide.utils.withStopWatch
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import java.util.concurrent.ConcurrentHashMap
/**
* Default implementation for the [ActionsRegistry]
*
* @author Akash Yadav
*/
@AutoService(ActionsRegistry::class)
class DefaultActionsRegistry : ActionsRegistry() {
private val actions = ConcurrentHashMap<String, ConcurrentHashMap<String, ActionItem>>()
private val listeners = HashSet<ActionExecListener>()
private val actionsCoroutineScope = CoroutineScope(Dispatchers.Default) +
CoroutineName("DefaultActionsRegistry")
companion object {
private val log = LoggerFactory.getLogger(DefaultActionsRegistry::class.java)
}
init {
registerAction(CodeActionsMenu)
}
override fun getActions(location: ActionItem.Location): MutableMap<String, ActionItem> {
if (actions[location.id] == null) {
actions[location.id] = ConcurrentHashMap()
}
return actions[location.id]!!
}
override fun registerAction(action: ActionItem): Boolean {
val actions = getActions(action.location)
actions[action.id] = action
return true
}
override fun unregisterAction(action: ActionItem): Boolean {
val actions = getActions(action.location)
val older = actions.remove(action.id)
if (older != null) {
older.destroy()
return true
}
return false
}
override fun unregisterAction(id: String): Boolean {
for (locations in this.actions.values) {
val older = locations.remove(id)
if (older != null) {
older.destroy()
return true
}
}
return false
}
override fun findAction(location: ActionItem.Location, id: String): ActionItem? =
getActions(location)[id]
override fun findAction(location: ActionItem.Location, itemId: Int): ActionItem? {
for (action in getActions(location)) {
if (action.value.itemId == itemId) {
return action.value
}
}
return null
}
override fun clearActions(location: ActionItem.Location) {
val actions = getActions(location)
actions.forEach { it.value.destroy() }
actions.clear()
}
override fun registerActionExecListener(listener: ActionExecListener) {
listeners.add(listener)
}
override fun unregisterActionExecListener(listener: ActionExecListener) {
listeners.remove(listener)
}
override fun fillMenu(params: FillMenuParams) {
val (data, location, menu, onClickListener) = params
val actions = getActions(location)
for (action in actions.values) {
action.prepare(data)
if (!action.visible) {
continue
}
addActionToMenu(menu, action, data, onClickListener)
}
}
private fun addActionToMenu(
menu: Menu,
action: ActionItem,
data: ActionData,
onClickListener: OnActionClickListener
) {
val item: MenuItem = if (action is ActionMenu) {
val sub = menu.addSubMenu(Menu.NONE, action.itemId, action.order, action.label)
var shouldBeEnabled = false
for (subItem in action.children) {
subItem.prepare(data)
if (subItem.visible) {
addActionToMenu(sub, subItem, data, onClickListener)
}
if (action.enabled && subItem.enabled && !shouldBeEnabled) {
shouldBeEnabled = true
}
}
action.enabled = shouldBeEnabled
sub.item
} else {
menu.add(Menu.NONE, action.itemId, action.order, action.label)
}
item.isEnabled = action.enabled
item.icon = action.icon?.apply {
colorFilter = action.createColorFilter(data)
alpha = if (action.enabled) 255 else 76
}
var showAsAction = action.getShowAsActionFlags(data)
if (showAsAction == -1) {
showAsAction = if (action.icon != null) {
MenuItem.SHOW_AS_ACTION_IF_ROOM
} else {
MenuItem.SHOW_AS_ACTION_NEVER
}
}
if (!action.enabled) {
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER
}
item.setShowAsAction(showAsAction)
action.createActionView(data)?.let { item.actionView = it }
if (action !is ActionMenu) {
item.setOnMenuItemClickListener {
onClickListener.onClick(this, action, it, data)
}
}
}
/** Executes the given action item with the given */
fun executeAction(action: ActionItem, data: ActionData): Job {
val onMainThread = action.requiresUIThread
val context = if (onMainThread) Dispatchers.Main.immediate else Dispatchers.Default
return actionsCoroutineScope.launch(context) {
val result = withStopWatch("Action '${action.id}'") {
action.execAction(data)
}
val post = fun() = run {
action.postExec(data, result)
notifyActionExec(action, result)
}
if (onMainThread) {
post()
} else {
withContext(Dispatchers.Main.immediate) {
post()
}
}
}.also { job ->
job.invokeOnCompletion { error ->
if (error != null) {
log.error("An error occurred when performing action '{}'", action.id, error)
}
}
}
}
private fun notifyActionExec(action: ActionItem, result: Any) {
for (listener in listeners) {
listener.onExec(action, result)
}
}
}
@@ -0,0 +1,50 @@
/*
* 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.locations
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
object CodeActionsMenu : ActionMenu {
const val ID = "ide.editor.code.actions"
override val children: MutableSet<ActionItem> = mutableSetOf()
override val id: String = ID
override var label: String = "Code actions"
override var visible = true
override var enabled: Boolean = true
override var icon: Drawable? = null
override var requiresUIThread: Boolean = false
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
override fun prepare(data: ActionData) {
super.prepare(data)
if (icon == null) {
icon = ContextCompat.getDrawable(data[Context::class.java]!!, R.drawable.ic_code)
}
}
}
+100
View File
@@ -0,0 +1,100 @@
# aapt2 file
src/arm64-v8a/assets/data/arm64-v8a/aapt2
src/armeabi-v7a/assets/data/arm64-v8a/aapt2
src/x86_64/assets/data/arm64-v8a/aapt2
# Directories that are created when generating signed APK in Android Studio
/debug
/release
/arm64-v8a
/armeabi-v7a
# Built application files
*.apk
*.aar
*.ap_
*.aab
# Files for the ART/Dalvik VM
*.dex
# Java class files
*.class
# Generated files
/bin/
/gen/
/out/
# Uncomment the following line in case you need and you don't have the release build type files in your app
# release/
# Gradle files
.gradle/
/build/
# Local configuration file (sdk path, etc)
local.properties
# Proguard folder generated by Eclipse
proguard/
# Log Files
*.log
# Android Studio Navigation editor temp files
.navigation/
# Android Studio captures folder
captures/
# IntelliJ
*.iml
.idea/workspace.xml
.idea/tasks.xml
.idea/gradle.xml
.idea/assetWizardSettings.xml
.idea/dictionaries
.idea/libraries
# Android Studio 3 in .gitignore file.
.idea/caches
.idea/modules.xml
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
.idea/navEditor.xml
# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
.cxx/
# Google Services (e.g. APIs or Firebase)
# google-services.json
# Freeline
freeline.py
freeline/
freeline_project_description.json
# fastlane
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
fastlane/readme.md
# Version control
vcs.xml
# lint
lint/intermediates/
lint/generated/
lint/outputs/
lint/tmp/
# lint/reports/
# Proguard files
dictionary.txt
proguard-rules.pro
+169
View File
@@ -0,0 +1,169 @@
@file:Suppress("UnstableApiUsage")
import com.itsaky.androidide.build.config.BuildConfig
import com.itsaky.androidide.desugaring.utils.JavaIOReplacements.applyJavaIOReplacements
import com.itsaky.androidide.plugins.AndroidIDEAssetsPlugin
import com.itsaky.androidide.plugins.AndroidIDECoreAppPlugin
plugins {
id("com.android.application")
id("kotlin-android")
id("kotlin-kapt")
id("kotlin-parcelize")
id("realm-android")
id("androidx.navigation.safeargs.kotlin")
id("com.itsaky.androidide.desugaring")
}
apply {
plugin(AndroidIDEAssetsPlugin::class.java)
plugin(AndroidIDECoreAppPlugin::class.java)
}
buildscript {
dependencies {
classpath(libs.logging.logback.core)
classpath(libs.composite.desugaringCore)
}
}
groupConfig {
groupIdSuffix.set("core")
}
android {
namespace = BuildConfig.packageName
defaultConfig {
applicationId = BuildConfig.packageName
vectorDrawables.useSupportLibrary = true
}
androidResources {
generateLocaleConfig = true
}
buildTypes {
release {
isShrinkResources = true
}
}
lint {
abortOnError = false
disable.addAll(arrayOf("VectorPath", "NestedWeights", "ContentDescription", "SmallSp"))
}
}
kapt {
arguments {
arg("eventBusIndex", "${BuildConfig.packageName}.events.AppEventsIndex")
}
}
desugaring {
replacements {
includePackage(
"org.eclipse.jgit",
)
applyJavaIOReplacements()
}
}
dependencies {
debugImplementation(libs.common.leakcanary)
// Annotation processors
kapt(libs.common.glide.ap)
kapt(libs.google.auto.service)
kapt(projects.annotation.processors)
implementation(libs.common.editor)
implementation(libs.common.utilcode)
implementation(libs.common.glide)
implementation(libs.common.jsoup)
implementation(libs.common.kotlin.coroutines.android)
implementation(libs.common.retrofit)
implementation(libs.common.retrofit.gson)
implementation(libs.common.charts)
implementation(libs.common.hiddenApiBypass)
implementation(libs.google.auto.service.annotations)
implementation(libs.google.gson)
implementation(libs.google.guava)
// Git
implementation(libs.git.jgit)
// AndroidX
implementation(libs.androidx.splashscreen)
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.cardview)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.coordinatorlayout)
implementation(libs.androidx.drawer)
implementation(libs.androidx.grid)
implementation(libs.androidx.nav.fragment)
implementation(libs.androidx.nav.ui)
implementation(libs.androidx.preference)
implementation(libs.androidx.recyclerview)
implementation(libs.androidx.transition)
implementation(libs.androidx.vectors)
implementation(libs.androidx.animated.vectors)
implementation(libs.androidx.work)
implementation(libs.androidx.work.ktx)
implementation(libs.google.material)
implementation(libs.google.flexbox)
// Kotlin
implementation(libs.androidx.core.ktx)
implementation(libs.common.kotlin)
// Dependencies in composite build
implementation(libs.composite.appintro)
implementation(libs.composite.desugaringCore)
implementation(libs.composite.javapoet)
// Local projects here
implementation(projects.core.actions)
implementation(projects.core.common)
implementation(projects.core.indexing)
implementation(projects.core.lspApi)
implementation(projects.core.projects)
implementation(projects.core.resources)
implementation(projects.editor.impl)
implementation(projects.editor.lexers)
implementation(projects.event.eventbus)
implementation(projects.event.eventbusAndroid)
implementation(projects.event.eventbusEvents)
implementation(projects.java.javacServices)
implementation(projects.java.lsp)
implementation(projects.logging.idestats)
implementation(projects.logging.logsender)
implementation(projects.termux.application)
implementation(projects.termux.view)
implementation(projects.termux.emulator)
implementation(projects.termux.shared)
implementation(projects.tooling.api)
implementation(projects.tooling.pluginConfig)
implementation(projects.utilities.buildInfo)
implementation(projects.utilities.lookup)
implementation(projects.utilities.preferences)
implementation(projects.utilities.templatesApi)
implementation(projects.utilities.templatesImpl)
implementation(projects.utilities.treeview)
implementation(projects.utilities.uidesigner)
implementation(projects.utilities.xmlInflater)
implementation(projects.xml.aaptcompiler)
implementation(projects.xml.lsp)
implementation(projects.xml.utils)
// This is to build the tooling-api-impl project before the app is built
// So we always copy the latest JAR file to assets
compileOnly(projects.tooling.impl)
testImplementation(projects.testing.unitTest)
androidTestImplementation(projects.testing.androidTest)
}
BIN
View File
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
android.jetifier.ignorelist=common-30.2.2.jar
+133
View File
@@ -0,0 +1,133 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:installLocation="internalOnly"
tools:ignore="Instantiatable">
<permission
android:description="@string/permdesc_bind_logger_service"
android:label="@string/permlab_bind_logger_service"
android:name="com.itsaky.androidide.permission.BIND_LOG_SERVICE"
android:protectionLevel="normal"
tools:ignore="ManifestOrder" />
<uses-permission android:name="com.itsaky.androidide.permission.BIND_LOG_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:allowBackup="false"
android:allowNativeHeapPointerTagging="false"
android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:name=".app.IDEApplication"
android:preserveLegacyExternalStorage="true"
android:requestLegacyExternalStorage="true"
android:resizeableActivity="false"
android:supportsRtl="true"
android:theme="@style/Theme.AndroidIDE"
tools:ignore="DiscouragedApi,MediaCapabilities,UnusedAttribute">
<activity
android:exported="true"
android:name=".activities.CrashHandlerActivity"
android:windowSoftInputMode="stateAlwaysHidden">
<intent-filter>
<action android:name="com.itsaky.androidide.REPORT_CRASH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:exported="true"
android:name=".activities.SplashActivity"
android:theme="@style/AppTheme.Splash">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:exported="false"
android:name=".activities.OnboardingActivity" />
<activity
android:exported="false"
android:name=".activities.MainActivity"
android:theme="@style/Theme.AndroidIDE" />
<activity
android:configChanges="orientation|screenSize"
android:launchMode="singleTop"
android:name=".activities.editor.EditorActivityKt"/>
<activity android:name=".activities.PreferencesActivity" />
<activity android:name=".activities.AboutActivity" />
<activity android:name=".activities.ContributorsActivity" />
<activity
android:name=".activities.TerminalActivity"
android:windowSoftInputMode="adjustResize" />
<provider
android:authorities="com.itsaky.androidide.documents"
android:exported="true"
android:grantUriPermissions="true"
android:name=".provider.IDEDocumentsProvider"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
</intent-filter>
</provider>
<provider
android:authorities="${applicationId}.providers.fileprovider"
android:exported="false"
android:grantUriPermissions="true"
android:name=".provider.IDEFileProvider">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/ide_file_provider_paths" />
</provider>
<receiver
android:enabled="true"
android:exported="true"
android:name=".services.InstallationResultReceiver"
tools:ignore="ExportedReceiver">
<intent-filter>
<action android:name="com.itsaky.androidide.installer.INSTALL_PACKAGE" />
</intent-filter>
</receiver>
<service
android:exported="false"
android:name=".services.builder.GradleBuildService" />
<service
android:exported="true"
android:name=".services.log.LogReceiverService"
android:permission="com.itsaky.androidide.permission.BIND_LOG_SERVICE">
<intent-filter>
<action android:name="com.itsaky.androidide.LOG_SERVICE_ACTION" />
</intent-filter>
</service>
<service
android:enabled="false"
android:exported="false"
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>
</application>
</manifest>
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/colorPrimary" />
<corners
android:topLeftRadius="8dp"
android:topRightRadius="8dp" />
</shape>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/coordinator"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/colorPrimary"
app:popupTheme="@style/ThemeOverlay.MaterialComponents.Light" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<!-- Your content below appbar right here -->
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_item1"
android:name="Menu Item" />
</menu>
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -0,0 +1,45 @@
/*
* 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
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.projects.builder.BuildService
/**
* Marker class for actions that execute build related tasks.
*
* @author Akash Yadav
*/
abstract class BaseBuildAction : EditorActivityAction() {
protected val buildService: BuildService?
get() = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE)
override fun prepare(data: ActionData) {
super.prepare(data)
val context = data.getActivity()
if (context == null) {
visible = false
return
} else {
visible = true
}
enabled = buildService?.let { !it.isBuildInProgress } == true
}
}
@@ -0,0 +1,63 @@
/*
* 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
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.itsaky.androidide.editor.ui.IDEEditor
/** @author Akash Yadav */
abstract class BaseEditorAction : EditorActionItem {
override var label: String = ""
override var visible: Boolean = true
override var enabled: Boolean = true
override var icon: Drawable? = null
override var requiresUIThread: Boolean = true // all editor actions must be executed on UI thread
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
override fun prepare(data: ActionData) {
super.prepare(data)
getEditor(data)
?: kotlin.run {
visible = false
enabled = false
return
}
visible = true
enabled = true
}
fun getEditor(data: ActionData): IDEEditor? {
return data.get(IDEEditor::class.java)
}
fun getContext(data: ActionData): Context? {
val editor = getEditor(data) ?: return null
return editor.context
}
fun tintDrawable(context: Context, drawable: Drawable): Drawable {
drawable.setTint(
ContextCompat.getColor(context, com.itsaky.androidide.resources.R.color.primaryIconColor)
)
return drawable
}
}
@@ -0,0 +1,62 @@
/*
* 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
import android.content.Context
import android.graphics.drawable.Drawable
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
import com.itsaky.androidide.tasks.cancelIfActive
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
/** @author Akash Yadav */
abstract class EditorActivityAction : ActionItem {
override var enabled: Boolean = true
override var visible: Boolean = true
override var icon: Drawable? = null
override var label: String = ""
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TOOLBAR
override var requiresUIThread: Boolean = false
protected val actionScope = CoroutineScope(Dispatchers.Default) +
CoroutineName("${javaClass.simpleName}Scope")
override fun prepare(data: ActionData) {
super.prepare(data)
if (!data.hasRequiredData(Context::class.java)) {
markInvisible()
}
}
fun ActionData.getActivity(): EditorHandlerActivity? {
return this[Context::class.java] as? EditorHandlerActivity
}
fun ActionData.requireActivity(): EditorHandlerActivity {
return getActivity()!!
}
override fun destroy() {
super.destroy()
actionScope.cancelIfActive("Action is being destroyed")
}
}
@@ -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.actions
import com.itsaky.androidide.editor.ui.IDEEditor
import com.itsaky.androidide.ui.CodeEditorView
/** @author Akash Yadav */
abstract class EditorRelatedAction : EditorActivityAction(), EditorActionItem {
override var requiresUIThread: Boolean = true
override fun prepare(data: ActionData) {
super<EditorActionItem>.prepare(data)
super<EditorActivityAction>.prepare(data)
val editor =
data.getEditor()
?: run {
visible = false
enabled = false
return
}
val file = editor.file
visible = file != null
enabled = visible
}
fun ActionData.getEditor(): IDEEditor? = get(IDEEditor::class.java)
fun ActionData.getEditorView(): CodeEditorView? = get(CodeEditorView::class.java)
}
@@ -0,0 +1,50 @@
/*
* 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.build
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.resources.R.string
/**
* Triggers a project sync request.
*
* @author Akash Yadav
*/
class ProjectSyncAction(context: Context, override val order: Int) : BaseBuildAction() {
override val id: String = "ide.editor.syncProject"
override var requiresUIThread = false
init {
label = context.getString(string.title_sync_project)
icon = ContextCompat.getDrawable(context, R.drawable.ic_sync)
}
override suspend fun execAction(data: ActionData): Any {
return data.requireActivity().saveAll(requestSync = false)
}
override fun postExec(data: ActionData, result: Any) {
val activity = data.requireActivity()
activity.initializeProject()
}
}
@@ -0,0 +1,273 @@
/*
* 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.build
import android.content.Context
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.actions.getContext
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.openApplicationModuleChooser
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.models.ApkMetadata
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import com.itsaky.androidide.tooling.api.models.BasicAndroidVariantMetadata
import com.itsaky.androidide.utils.ApkInstaller
import com.itsaky.androidide.utils.InstallationResultHandler
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.resolveAttr
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.slf4j.LoggerFactory
import java.io.File
/**
* The 'Quick Run' and 'Cancel build' action in the editor activity.
*
* If a build is in progress, executing this action cancels the build. Otherwise, the selected
* build variant is built and installed to the device.
*
* @author Akash Yadav
*/
class QuickRunWithCancellationAction(context: Context, override val order: Int) :
BaseBuildAction() {
companion object {
private val log = LoggerFactory.getLogger(QuickRunWithCancellationAction::class.java)
}
init {
label = context.getString(R.string.quick_run_debug)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_outline)
enabled = false
}
override val id: String = "ide.editor.build.quickRun"
// Execute on UI thread as this action might try to show dialogs to the user
override var requiresUIThread: Boolean = true
override fun prepare(data: ActionData) {
super.prepare(data)
val context = data.getActivity() ?: run {
markInvisible()
return
}
if (data.isBuildInProgress()) {
label = context.getString(R.string.title_cancel_build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop_daemons)
} else {
label = context.getString(R.string.quick_run_debug)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_outline)
}
visible = true
enabled = true
}
override fun createColorFilter(data: ActionData): ColorFilter? {
return data.getContext()?.let {
PorterDuffColorFilter(it.resolveAttr(
if (data.isBuildInProgress())
R.attr.colorError
else R.attr.colorSuccess
), PorterDuff.Mode.SRC_ATOP)
}
}
override suspend fun execAction(data: ActionData): Boolean {
if (data.isBuildInProgress()) {
return cancelBuild()
}
return quickRun(data)
}
private fun quickRun(data: ActionData): Boolean {
openApplicationModuleChooser(data) { module ->
val activity = data.requireActivity()
val variant = module.getSelectedVariant() ?: run {
activity.flashError(
activity.getString(R.string.err_selected_variant_not_found))
return@openApplicationModuleChooser
}
val taskName = "${module.path}:${variant.mainArtifact.assembleTaskName}"
log.info(
"Running task '$taskName' to assemble variant '${variant.name}' of project '${module.path}'")
onModuleSelected(data, module, variant, taskName)
}
return true
}
private fun onModuleSelected(
data: ActionData,
module: AndroidModule,
variant: BasicAndroidVariantMetadata,
taskName: String
) {
val buildService = this.buildService ?: run {
log.error("Cannot execute task '{}'. BuildService not found.", taskName)
return
}
if (!buildService.isToolingServerStarted()) {
flashError(string.msg_tooling_server_unavailable)
return
}
val activity =
data.getActivity()
?: run {
log.error(
"Cannot execute task '{}'. Activity instance not provided in ActionData.", taskName)
return
}
actionScope.launch(Dispatchers.Default) {
activity.saveAllResult()
val result = withContext(Dispatchers.IO) {
buildService.executeTasks(taskName).get()
}
log.debug("Task execution result: {}", result)
if (result?.isSuccessful != true) {
log.error("Tasks failed to execute: '{}'", taskName)
return@launch
}
handleResult(data, result, module, variant)
}.invokeOnCompletion { error ->
if (error != null) {
log.error("Failed to run '{}'", taskName, error)
}
}
}
private fun cancelBuild(): Boolean {
log.info("Sending build cancellation request...")
val builder = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE)
if (builder?.isToolingServerStarted() != true) {
flashError(com.itsaky.androidide.projects.R.string.msg_tooling_server_unavailable)
return false
}
builder.cancelCurrentBuild().whenComplete { result,
error ->
if (error != null) {
log.error("Failed to send build cancellation request", error)
return@whenComplete
}
if (!result.wasEnqueued) {
log.warn(
"Unable to enqueue cancellation request reason={} reason.message={}",
result.failureReason,
result.failureReason!!.message
)
return@whenComplete
}
log.info("Build cancellation request was successfully enqueued...")
}
return true
}
private fun handleResult(
data: ActionData,
result: TaskExecutionResult?,
module: AndroidModule,
variant: BasicAndroidVariantMetadata
) {
if (result == null || !result.isSuccessful) {
log.debug("Cannot install APK. Task execution failed.")
return
}
log.debug("Installing APK(s) for project: '{}' variant: '{}'", module.path, variant.name)
val main = variant.mainArtifact
val outputListingFile = main.assembleTaskOutputListingFile
if (outputListingFile == null) {
log.error("No output listing file provided with project model")
return
}
log.trace("Parsing metadata")
val apkFile = ApkMetadata.findApkFile(outputListingFile)
if (apkFile == null) {
log.error("No apk file specified in output listing file: {}", outputListingFile)
return
}
if (!apkFile.exists()) {
log.error("APK file specified in output listing file does not exist! {}", apkFile)
return
}
install(data, apkFile)
}
private fun install(data: ActionData, apk: File) {
val activity =
data.getActivity()
?: run {
log.error("Cannot install APK. Unable to get activity instance.")
return
}
activity.runOnUiThread {
log.debug("Installing APK: {}", apk)
if (!apk.exists()) {
log.error("APK file does not exist!")
return@runOnUiThread
}
ApkInstaller.installApk(
activity,
InstallationResultHandler.createEditorActivitySender(activity),
apk,
activity.installationSessionCallback()
)
}
}
private fun ActionData.isBuildInProgress(): Boolean {
val context = getActivity()
val buildService = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE)
return context?.editorViewModel?.let { it.isInitializing || it.isBuildInProgress } == true || buildService?.isBuildInProgress == true
}
}
@@ -0,0 +1,62 @@
/*
* 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.build
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.fragments.RunTasksDialogFragment
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class RunTasksAction(context: Context, override val order: Int) : BaseBuildAction() {
override val id: String = "ide.editor.build.runTasks"
private var dialog: RunTasksDialogFragment? = null
init {
label = context.getString(R.string.title_run_tasks)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_tasks)
}
override suspend fun execAction(data: ActionData): Any {
dialog?.dismiss()
dialog = null
dialog = RunTasksDialogFragment()
return dialog!!
}
override fun postExec(data: ActionData, result: Any) {
if (result !is RunTasksDialogFragment) {
return
}
val activity = data.getActivity()!!
result.show(activity.supportFragmentManager, this.id)
}
override fun destroy() {
super.destroy()
try {
dialog?.dismiss()
} catch (e: Exception) {
// ignored
}
dialog = null
}
}
@@ -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.editor
import android.content.Context
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class CopyAction(context: Context, override val order: Int) : BaseEditorAction() {
init {
label = context.getString(R.string.copy)
val arr = context.obtainStyledAttributes(intArrayOf(android.R.attr.actionModeCopyDrawable))
icon = arr.getDrawable(0)?.let { tintDrawable(context, it) }
arr.recycle()
}
override val id: String = "ide.editor.code.text.copy"
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.copyText()
return true
}
}
@@ -0,0 +1,52 @@
/*
* 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.editor
import android.content.Context
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
/** @author Akash Yadav */
class CutAction(context: Context, override val order: Int) : BaseEditorAction() {
init {
label = context.getString(android.R.string.cut)
val arr = context.obtainStyledAttributes(intArrayOf(android.R.attr.actionModeCutDrawable))
icon = arr.getDrawable(0)?.let { tintDrawable(context, it) }
arr.recycle()
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
visible = getEditor(data)?.isEditable ?: false
enabled = visible
}
override val id: String = "ide.editor.code.text.cut"
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.cutText()
return true
}
}
@@ -0,0 +1,44 @@
/*
* 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.editor
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.resources.R
/**
* Expands the current selection using the language server set in editor.
*
* @author Akash Yadav
*/
class ExpandSelectionAction(context: Context, override val order: Int) : EditorRelatedAction() {
override val id: String = "ide.editor.code.actions.expandSelection"
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
init {
label = context.getString(R.string.action_expand_selection)
icon = ContextCompat.getDrawable(context, R.drawable.ic_expand_selection)
}
override suspend fun execAction(data: ActionData): Any {
return data.getEditor()?.expandSelection() ?: false
}
}
@@ -0,0 +1,51 @@
/*
* 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.editor
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.actions.markInvisible
/**
* An action to long select text in the editor.
*
* @author Akash Yadav
*/
class LongSelectAction(context: Context, override val order: Int) : EditorRelatedAction() {
override val id: String = "ide.editor.code.text.longSelect"
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
init {
label = context.getString(R.string.title_begin_long_select)
icon = ContextCompat.getDrawable(context, R.drawable.editor_text_select_start)
}
override fun prepare(data: ActionData) {
super.prepare(data)
data.getEditor() ?: markInvisible()
}
override suspend fun execAction(data: ActionData): Any {
return data.getEditor()?.beginLongSelect() ?: false
}
}
@@ -0,0 +1,53 @@
/*
* 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.editor
import android.content.Context
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
/** @author Akash Yadav */
class PasteAction(context: Context, override val order: Int) : BaseEditorAction() {
init {
label = context.getString(android.R.string.paste)
val arr = context.obtainStyledAttributes(intArrayOf(android.R.attr.actionModePasteDrawable))
icon = arr.getDrawable(0)?.let { tintDrawable(context, it) }
arr.recycle()
}
override val id: String = "ide.editor.code.text.paste"
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
visible = getEditor(data)?.isEditable ?: false
enabled = visible
}
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.pasteText()
return true
}
}
@@ -0,0 +1,44 @@
/*
* 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.editor
import android.content.Context
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
/** @author Akash Yadav */
class SelectAllAction(context: Context, override val order: Int) : BaseEditorAction() {
init {
label = context.getString(android.R.string.selectAll)
val arr = context.obtainStyledAttributes(intArrayOf(android.R.attr.actionModeSelectAllDrawable))
icon = arr.getDrawable(0)?.let { tintDrawable(context, it) }
arr.recycle()
}
override val id: String = "ide.editor.code.text.selectAll"
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.selectAll()
return true
}
override fun dismissOnAction() = false
}
@@ -0,0 +1,64 @@
/*
* 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.etc
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.services.log.lookupLogService
/**
* An action to disconnect all connected LogSenders at once.
*
* @author Akash Yadav
*/
class DisconnectLogSendersAction(context: Context, override val order: Int) : EditorActivityAction() {
override val id: String = "ide.editor.service.logreceiver.disconnectSenders"
init {
label = context.getString(R.string.title_disconnect_log_senders)
icon = ContextCompat.getDrawable(context, R.drawable.ic_logs_disconnect)
}
override fun prepare(data: ActionData) {
super.prepare(data)
data.getActivity() ?: run {
markInvisible()
return
}
val receiverService = lookupLogService()
if (receiverService == null) {
markInvisible()
return
}
}
override suspend fun execAction(data: ActionData): Any {
val receiverService = lookupLogService()
receiverService?.disconnectAll()
markInvisible()
data.getActivity()?.invalidateOptionsMenu()
return true
}
}
@@ -0,0 +1,47 @@
/*
* 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.etc
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class FindActionMenu(context: Context, override val order: Int) : EditorActivityAction(),
ActionMenu {
override val children: MutableSet<ActionItem> = mutableSetOf()
override val id: String = "ide.editor.find"
init {
label = context.getString(R.string.menu_find)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search)
addAction(FindInFileAction(context, 0))
addAction(FindInProjectAction(context, 1))
}
override fun prepare(data: ActionData) {
super<EditorActivityAction>.prepare(data)
super<ActionMenu>.prepare(data)
}
}
@@ -0,0 +1,45 @@
/*
* 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.etc
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class FindInFileAction() : EditorRelatedAction() {
override val id: String = "ide.editor.find.inFile"
override var requiresUIThread: Boolean = true
override var order: Int = 0
constructor(context: Context, order: Int) : this() {
this.label = context.getString(R.string.menu_find_file)
this.icon = ContextCompat.getDrawable(context, R.drawable.ic_search_file)
this.order = order
}
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.getEditorView() ?: return false
editor.beginSearch()
return true
}
}
@@ -0,0 +1,69 @@
/*
* 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.etc
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class FindInProjectAction() : EditorActivityAction() {
override var requiresUIThread: Boolean = true
override var order: Int = 0
constructor(context: Context, order: Int) : this() {
this.label = context.getString(R.string.menu_find_project)
this.icon = ContextCompat.getDrawable(context, R.drawable.ic_search_project)
this.order = order
}
override val id: String = "ide.editor.find.inProject"
override fun prepare(data: ActionData) {
super.prepare(data)
data.getActivity()
?: run {
markInvisible()
return
}
val project = IProjectManager.getInstance().rootProject
if (project == null || project.subProjects.isEmpty()) {
markInvisible()
return
}
visible = true
enabled = true
}
override suspend fun execAction(data: ActionData): Boolean {
val context = data.getActivity() ?: return false
val dialog = context.findInProjectDialog
return run {
dialog.show()
true
}
}
}
@@ -0,0 +1,94 @@
/*
* 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.etc
import android.content.Context
import android.view.MenuItem
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.openApplicationModuleChooser
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.utils.IntentUtils
import com.itsaky.androidide.utils.flashError
import org.slf4j.LoggerFactory
/**
* An action to launch the already installed application on the device.
*
* @author Akash Yadav
*/
class LaunchAppAction(context: Context, override val order: Int) : EditorActivityAction() {
override val id: String = "ide.editor.launchInstalledApp"
override var requiresUIThread: Boolean = true
init {
label = context.getString(R.string.title_launch_app)
icon = ContextCompat.getDrawable(context, R.drawable.ic_open_external)
}
companion object {
private val log = LoggerFactory.getLogger(LaunchAppAction::class.java)
}
override fun prepare(data: ActionData) {
super.prepare(data)
data.getActivity() ?: run {
markInvisible()
return
}
visible = true
val projectManager = IProjectManager.getInstance()
enabled = projectManager.getAndroidAppModules().isNotEmpty()
}
override suspend fun execAction(data: ActionData) {
openApplicationModuleChooser(data) { app ->
val variant = app.getSelectedVariant()
log.debug("Selected variant: {}", variant?.name)
if (variant == null) {
flashError(R.string.err_selected_variant_not_found)
return@openApplicationModuleChooser
}
val applicationId = variant.mainArtifact.applicationId
if (applicationId == null) {
log.error("Unable to launch application. variant.mainArtifact.applicationId is null")
flashError(R.string.err_cannot_determine_package)
return@openApplicationModuleChooser
}
log.info("Launching application: {}", applicationId)
val activity = data.requireActivity()
IntentUtils.launchApp(activity, applicationId, logError = false)
}
}
override fun getShowAsActionFlags(data: ActionData): Int {
// prefer showing this in the overflow menu
return MenuItem.SHOW_AS_ACTION_IF_ROOM
}
}
@@ -0,0 +1,113 @@
/*
* 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.etc
import android.content.Context
import android.content.Intent
import android.view.MenuItem
import androidx.core.content.ContextCompat
import com.android.aaptcompiler.AaptResourceType.LAYOUT
import com.android.aaptcompiler.extractPathData
import com.blankj.utilcode.util.KeyboardUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
import com.itsaky.androidide.editor.ui.IDEEditor
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.uidesigner.UIDesignerActivity
import java.io.File
/** @author Akash Yadav */
class PreviewLayoutAction(context: Context, override val order: Int) : EditorRelatedAction() {
override val id: String = "ide.editor.previewLayout"
override var requiresUIThread: Boolean = false
init {
label = context.getString(R.string.title_preview_layout)
icon = ContextCompat.getDrawable(context, R.drawable.ic_preview_layout)
}
override fun prepare(data: ActionData) {
super.prepare(data)
val viewModel = data.requireActivity().editorViewModel
if (viewModel.isInitializing) {
visible = true
enabled = false
return
}
if (!visible) {
return
}
val editor = data.requireEditor()
val file = editor.file!!
val isXml = file.name.endsWith(".xml")
if (!isXml) {
markInvisible()
return
}
val type = try {
extractPathData(file).type
} catch (err: Throwable) {
markInvisible()
return
}
visible = type == LAYOUT
enabled = visible
}
override fun getShowAsActionFlags(data: ActionData): Int {
val activity = data.getActivity() ?: return super.getShowAsActionFlags(data)
return if (KeyboardUtils.isSoftInputVisible(activity)) {
MenuItem.SHOW_AS_ACTION_IF_ROOM
} else {
MenuItem.SHOW_AS_ACTION_ALWAYS
}
}
override suspend fun execAction(data: ActionData): Boolean {
val activity = data.requireActivity()
activity.saveAll()
return true
}
override fun postExec(data: ActionData, result: Any) {
val activity = data.requireActivity()
activity.previewLayout(data.requireEditor().file!!)
}
private fun EditorHandlerActivity.previewLayout(file: File) {
val intent = Intent(this, UIDesignerActivity::class.java)
intent.putExtra(UIDesignerActivity.EXTRA_FILE, file.absolutePath)
uiDesignerResultLauncher?.launch(intent)
}
private fun ActionData.requireEditor(): IDEEditor {
return this.getEditor() ?: throw IllegalArgumentException(
"An editor instance is required but none was provided")
}
}
@@ -0,0 +1,64 @@
/*
* 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.etc
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.tasks.launchAsyncWithProgress
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Action which reloads the editor color schemes.
*
* @author Akash Yadav
*/
class ReloadColorSchemesAction(context: Context, override val order: Int) : EditorActivityAction() {
override val id: String = "ide.editor.colorScheme.reload"
// Schemes are reloaded in a background thread
// This property is set to true just to make sure that the ProgressDialog instance is created on
// the UI thread
override var requiresUIThread: Boolean = true
init {
label = context.getString(R.string.title_reload_color_schemes)
icon = ContextCompat.getDrawable(context, R.drawable.ic_reload)
}
override suspend fun execAction(data: ActionData): Boolean {
val context = data.requireActivity()
context.lifecycleScope.launchAsyncWithProgress(
context = Dispatchers.Default,
configureFlashbar = { builder, _ ->
builder.message(R.string.please_wait)
}) { flashbar, _ ->
IDEColorSchemeProvider.reload()
withContext(Dispatchers.Main) {
flashbar.dismiss()
}
}
return true
}
}
@@ -0,0 +1,46 @@
/*
* 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.file
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
/**
* Closes all opened files.
*
* @author Akash Yadav
*/
class CloseAllFilesAction(context: Context, override val order: Int) : FileTabAction() {
override val id: String = "ide.editor.fileTab.close.all"
init {
label = context.getString(R.string.action_closeAll)
icon = ContextCompat.getDrawable(context, R.drawable.ic_close_all)
}
override fun EditorHandlerActivity.doAction(data: ActionData): Boolean {
closeAll {
invalidateOptionsMenu()
}
return true
}
}
@@ -0,0 +1,48 @@
/*
* 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.file
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
/**
* Closes the current file.
*
* @author Akash Yadav
*/
class CloseFileAction(context: Context, override val order: Int) : FileTabAction() {
override val id: String = "ide.editor.fileTab.close.current"
init {
label = context.getString(R.string.action_closeThis)
icon = ContextCompat.getDrawable(context, R.drawable.ic_close_this)
}
override fun EditorHandlerActivity.doAction(data: ActionData): Boolean {
content.tabs.selectedTabPosition.let { index ->
closeFile(index) {
invalidateOptionsMenu()
}
}
return true
}
}
@@ -0,0 +1,44 @@
/*
* 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.file
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
/**
* Closes all opened files except the selected one.
*
* @author Akash Yadav
*/
class CloseOtherFilesAction(context: Context, override val order: Int) : FileTabAction() {
override val id: String = "ide.editor.fileTab.close.others"
init {
label = context.getString(R.string.action_closeOthers)
icon = ContextCompat.getDrawable(context, R.drawable.ic_close_others)
}
override fun EditorHandlerActivity.doAction(data: ActionData): Boolean {
closeOthers()
return true
}
}
@@ -0,0 +1,61 @@
/*
* 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.file
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem.Location
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_FILE_TABS
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
/**
* Action related to file tabs. Shown only when there is at least one file opened.
*
* @author Akash Yadav
*/
abstract class FileTabAction : EditorActivityAction() {
override var location: Location = EDITOR_FILE_TABS
override var requiresUIThread: Boolean = true
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
val activity =
data.getActivity()
?: run {
markInvisible()
return
}
visible = activity.editorViewModel.getOpenedFiles().isNotEmpty()
enabled = visible
}
override suspend fun execAction(data: ActionData): Any {
val activity = data.getActivity() ?: return false
return activity.doAction(data)
}
abstract fun EditorHandlerActivity.doAction(data: ActionData): Boolean
}
@@ -0,0 +1,52 @@
/*
* 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.file
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.resources.R
/**
* Action that formats the code in the editor.
*
* @author Akash Yadav
*/
class FormatCodeAction(context: Context, override val order: Int) : EditorRelatedAction() {
override val id: String = "ide.editor.code.text.format"
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
init {
label = context.getString(R.string.title_format_code)
icon = ContextCompat.getDrawable(context, R.drawable.ic_format_code)
}
override suspend fun execAction(data: ActionData): Any {
val editor = data.getEditor()!!
val cursor = editor.text.cursor
if (cursor.isSelected) {
editor.formatCodeAsync(cursor.left(), cursor.right())
} else {
editor.formatCodeAsync()
}
return true
}
}
@@ -0,0 +1,105 @@
/*
* 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.file
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess
import org.slf4j.LoggerFactory
/** @author Akash Yadav */
class SaveFileAction(context: Context, override val order: Int) : EditorRelatedAction() {
override val id: String = "ide.editor.files.saveAll"
override var requiresUIThread: Boolean = false
companion object {
private val log = LoggerFactory.getLogger(SaveFileAction::class.java)
}
init {
label = context.getString(R.string.save)
icon = ContextCompat.getDrawable(context, R.drawable.ic_save)
}
override fun prepare(data: ActionData) {
super.prepare(data)
val context = data.getActivity() ?: run {
visible = false
enabled = false
return
}
visible = context.editorViewModel.getOpenedFiles().isNotEmpty()
enabled = context.areFilesModified() && !context.areFilesSaving()
}
override suspend fun execAction(data: ActionData): ResultWrapper {
val context = data.getActivity() ?: return ResultWrapper()
if (context.areFilesSaving()) {
return ResultWrapper(isAlreadySaving = true)
}
return try {
// Cannot use context.saveAll() because this.execAction is called on non-UI thread
// and saveAll call will result in UI actions
ResultWrapper(result = context.saveAllResult())
} catch (error: Throwable) {
log.error("Failed to save file", error)
ResultWrapper()
}
}
override fun postExec(data: ActionData, result: Any) {
if (result is ResultWrapper && result.result != null) {
val context = data.requireActivity()
if (result.isAlreadySaving) {
context.flashError(R.string.msg_files_being_saved)
return
}
// show save notification before calling 'notifySyncNeeded' so that the file save notification
// does not overlap the sync notification
context.flashSuccess(R.string.all_saved)
val saveResult = result.result
if (saveResult.xmlSaved) {
ProjectManagerImpl.getInstance().generateSources()
}
if (saveResult.gradleSaved) {
context.editorViewModel.isSyncNeeded = true
}
context.invalidateOptionsMenu()
} else {
log.error("Failed to save file")
flashError(R.string.save_failed)
}
}
inner class ResultWrapper(val isAlreadySaving: Boolean = false, val result: SaveResult? = null)
}
@@ -0,0 +1,52 @@
/*
* 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.filetree
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.requireFile
/**
* Base class for action items for directory nodes.
*
* @author Akash Yadav
*/
abstract class BaseDirNodeAction(context: Context,
@StringRes labelRes: Int? = null,
@DrawableRes iconRes: Int? = null) : BaseFileTreeAction(context, labelRes, iconRes) {
override fun prepare(data: ActionData) {
super.prepare(data)
if (!data.hasFileTreeData()) {
markInvisible()
return
}
val file = data.requireFile()
if (!file.isDirectory) {
markInvisible()
return
}
visible = true
enabled = true
}
}
@@ -0,0 +1,90 @@
/*
* 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.filetree
import android.content.Context
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.actions.hasRequiredData
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.eventbus.events.Event
import com.itsaky.androidide.events.ExpandTreeNodeRequestEvent
import com.itsaky.androidide.events.ListProjectFilesRequestEvent
import com.unnamed.b.atv.model.TreeNode
import org.greenrobot.eventbus.EventBus
import java.io.File
/**
* Base class for actions related to the file tree.
*
* @author Akash Yadav
*/
abstract class BaseFileTreeAction(
context: Context,
@StringRes labelRes: Int? = null,
@DrawableRes iconRes: Int? = null
) : EditorActivityAction() {
override var requiresUIThread: Boolean = true
override var location: ActionItem.Location = ActionItem.Location.EDITOR_FILE_TREE
init {
labelRes?.let { label = context.getString(it) }
iconRes?.let { icon = ContextCompat.getDrawable(context, it) }
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!data.hasFileTreeData()) {
markInvisible()
return
}
visible = true
enabled = true
}
protected open fun ActionData.hasFileTreeData(): Boolean {
return hasRequiredData(Context::class.java, File::class.java, TreeNode::class.java)
}
protected fun ActionData.getTreeNode() : TreeNode? {
return this[TreeNode::class.java]
}
protected fun ActionData.requireTreeNode() : TreeNode {
return getTreeNode()!!
}
protected fun Event.putData(context: Context): Event {
put(Context::class.java, context)
return this
}
protected fun requestFileListing() {
EventBus.getDefault().post(ListProjectFilesRequestEvent())
}
protected fun requestExpandNode(node: TreeNode) {
EventBus.getDefault().post(ExpandTreeNodeRequestEvent(node))
}
}
@@ -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.filetree
import android.content.Context
import com.blankj.utilcode.util.ClipboardUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.flashSuccess
/**
* Action to copy the absolute path of the selected file.
*
* @author Akash Yadav
*/
class CopyPathAction(context: Context, override val order: Int) :
BaseFileTreeAction(context, labelRes = R.string.copy_path, iconRes = R.drawable.ic_copy) {
override val id: String = "ide.editor.fileTree.copyPath"
override suspend fun execAction(data: ActionData) {
val file = data.requireFile()
ClipboardUtils.copyText("[AndroidIDE] Copied File Path", file.absolutePath)
flashSuccess(R.string.copied)
}
}
@@ -0,0 +1,107 @@
/*
* 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.filetree
import android.app.ProgressDialog
import android.content.Context
import com.blankj.utilcode.util.FileUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent
import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.FlashType
import com.itsaky.androidide.utils.flashMessage
import org.greenrobot.eventbus.EventBus
import java.io.File
/**
* File tree action to delete files.
*
* @author Akash Yadav
*/
class DeleteAction(context: Context, override val order: Int) :
BaseFileTreeAction(context, labelRes = R.string.delete_file, iconRes = R.drawable.ic_delete) {
override val id: String = "ide.editor.fileTree.delete"
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
val builder = DialogUtils.newMaterialDialogBuilder(context)
builder
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { dialogInterface, _ ->
dialogInterface.dismiss()
@Suppress("DEPRECATION")
val progressDialog =
ProgressDialog.show(context, null, context.getString(R.string.please_wait), true, false)
executeAsync({ FileUtils.delete(file) }) {
progressDialog.dismiss()
val deleted = it ?: false
flashMessage(
if (deleted) R.string.deleted else R.string.delete_failed,
if (deleted) FlashType.SUCCESS else FlashType.ERROR
)
if (!deleted) {
return@executeAsync
}
notifyFileDeleted(file, context)
if (lastHeld != null) {
val parent = lastHeld.parent
parent.deleteChild(lastHeld)
requestExpandNode(parent)
} else {
requestFileListing()
}
val frag = context.getEditorForFile(file)
if (frag != null) {
context.closeFile(context.findIndexOfEditorByFile(frag.file))
}
}
}
.setTitle(R.string.title_confirm_delete)
.setMessage(
context.getString(
R.string.msg_confirm_delete,
String.format("%s [%s]", file.name, file.absolutePath)
)
)
.setCancelable(false)
.create()
.show()
}
private fun notifyFileDeleted(file: File, context: Context) {
val deletionEvent = FileDeletionEvent(file)
// Notify FileManager first
FileManager.onFileDeleted(deletionEvent)
EventBus.getDefault().post(deletionEvent.putData(context))
}
}
@@ -0,0 +1,422 @@
/*
* 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.filetree
import android.content.Context
import android.view.LayoutInflater
import androidx.core.view.isVisible
import com.blankj.utilcode.util.FileIOUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder
import com.itsaky.androidide.databinding.LayoutCreateFileJavaBinding
import com.itsaky.androidide.eventbus.events.file.FileCreationEvent
import com.itsaky.androidide.preferences.databinding.LayoutDialogTextInputBinding
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.ProjectWriter
import com.itsaky.androidide.utils.SingleTextWatcher
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess
import com.unnamed.b.atv.model.TreeNode
import jdkx.lang.model.SourceVersion
import org.greenrobot.eventbus.EventBus
import org.slf4j.LoggerFactory
import java.io.File
import java.util.Objects
import java.util.regex.Pattern
/**
* File tree action to create a new file.
*
* @author Akash Yadav
*/
class NewFileAction(context: Context, override val order: Int) :
BaseDirNodeAction(
context = context,
labelRes = R.string.new_file,
iconRes = R.drawable.ic_new_file
) {
override val id: String = "ide.editor.fileTree.newFile"
companion object {
const val RES_PATH_REGEX = "/.*/src/.*/res"
const val LAYOUT_RES_PATH_REGEX = "/.*/src/.*/res/layout"
const val MENU_RES_PATH_REGEX = "/.*/src/.*/res/menu"
const val DRAWABLE_RES_PATH_REGEX = "/.*/src/.*/res/drawable"
const val JAVA_PATH_REGEX = "/.*/src/.*/java"
private val log = LoggerFactory.getLogger(NewFileAction::class.java)
}
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val node = data.getTreeNode()
try {
createNewFile(context, node, file, false)
} catch (e: Exception) {
log.error("Failed to create new file", e)
flashError(e.cause?.message ?: e.message)
}
}
private fun createNewFile(
context: Context,
node: TreeNode?,
file: File,
forceUnknownType: Boolean
) {
if (forceUnknownType) {
createNewEmptyFile(context, node, file)
return
}
val projectDir = IProjectManager.getInstance().projectDirPath
Objects.requireNonNull(projectDir)
val isJava =
Pattern.compile(Pattern.quote(projectDir) + JAVA_PATH_REGEX).matcher(file.absolutePath).find()
val isRes =
Pattern.compile(Pattern.quote(projectDir) + RES_PATH_REGEX).matcher(file.absolutePath).find()
val isLayoutRes =
Pattern.compile(Pattern.quote(projectDir) + LAYOUT_RES_PATH_REGEX)
.matcher(file.absolutePath)
.find()
val isMenuRes =
Pattern.compile(Pattern.quote(projectDir) + MENU_RES_PATH_REGEX)
.matcher(file.absolutePath)
.find()
val isDrawableRes =
Pattern.compile(Pattern.quote(projectDir) + DRAWABLE_RES_PATH_REGEX)
.matcher(file.absolutePath)
.find()
if (isJava) {
createJavaClass(context, node, file)
return
}
if (isLayoutRes && file.name == "layout") {
createLayoutRes(context, node, file)
return
}
if (isMenuRes && file.name == "menu") {
createMenuRes(context, node, file)
return
}
if (isDrawableRes && file.name == "drawable") {
createDrawableRes(context, node, file)
return
}
if (isRes && file.name == "res") {
createNewResource(context, node, file)
return
}
createNewEmptyFile(context, node, file)
}
private fun createJavaClass(context: Context, node: TreeNode?, file: File) {
val builder = DialogUtils.newMaterialDialogBuilder(context)
val binding: LayoutCreateFileJavaBinding =
LayoutCreateFileJavaBinding.inflate(LayoutInflater.from(context))
binding.typeGroup.addOnButtonCheckedListener { _, _, _ ->
binding.createLayout.isVisible = binding.typeGroup.checkedButtonId == binding.typeActivity.id
}
binding.name.editText?.addTextChangedListener(
object : SingleTextWatcher() {
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (isValidJavaName(s)) {
binding.name.isErrorEnabled = true
binding.name.error = context.getString(R.string.msg_invalid_name)
} else {
binding.name.isErrorEnabled = false
}
}
}
)
builder.setView(binding.root)
builder.setTitle(R.string.new_java_class)
builder.setPositiveButton(R.string.text_create) { dialogInterface, _ ->
dialogInterface.dismiss()
try {
doCreateJavaFile(binding, file, context, node)
} catch (e: Exception) {
log.error("Failed to create Java file", e)
flashError(e.cause?.message ?: e.message)
}
}
builder.setNegativeButton(android.R.string.cancel, null)
builder.setCancelable(false)
builder.create().show()
}
private fun doCreateJavaFile(
binding: LayoutCreateFileJavaBinding,
file: File,
context: Context,
node: TreeNode?
) {
if (binding.name.isErrorEnabled) {
flashError(R.string.msg_invalid_name)
return
}
val name: String = binding.name.editText!!.text.toString().trim()
if (name.isBlank()) {
flashError(R.string.msg_invalid_name)
return
}
val autoLayout =
binding.typeGroup.checkedButtonId == binding.typeActivity.id &&
binding.createLayout.isChecked
val pkgName = ProjectWriter.getPackageName(file)
if (pkgName == null || pkgName.trim { it <= ' ' }.isEmpty()) {
flashError(R.string.msg_get_package_failed)
return
}
val id: Int = binding.typeGroup.checkedButtonId
val javaName = if (name.endsWith(".java")) name else "$name.java"
val className = if (!name.contains(".")) name else name.substring(0, name.lastIndexOf("."))
val created =
when (id) {
binding.typeClass.id ->
createFile(
context,
node,
file,
javaName,
ProjectWriter.createJavaClass(pkgName, className)
)
binding.typeInterface.id ->
createFile(
context,
node,
file,
javaName,
ProjectWriter.createJavaInterface(pkgName, className)
)
binding.typeEnum.id ->
createFile(
context,
node,
file,
javaName,
ProjectWriter.createJavaEnum(pkgName, className)
)
binding.typeActivity.id ->
createFile(
context,
node,
file,
javaName,
ProjectWriter.createActivity(pkgName, className)
)
else -> createFile(context, node, file, name, "")
}
if (created && autoLayout) {
val packagePath = pkgName.toString().replace(".", "/")
createAutoLayout(context, file, name, packagePath)
}
}
private fun isValidJavaName(s: CharSequence?) =
s == null || !SourceVersion.isName(s) || SourceVersion.isKeyword(s)
private fun createLayoutRes(context: Context, node: TreeNode?, file: File) {
createNewFileWithContent(
context,
node,
Environment.mkdirIfNotExits(file),
ProjectWriter.createLayout(),
".xml"
)
}
private fun createAutoLayout(
context: Context,
directory: File,
fileName: String,
packagePath: String
) {
val dir = directory.toString().replace("java/$packagePath", "res/layout/")
val layoutName = ProjectWriter.createLayoutName(fileName.replace(".java", ".xml"))
val newFileLayout = File(dir, layoutName)
if (newFileLayout.exists()) {
flashError(R.string.msg_layout_file_exists)
return
}
if (!FileIOUtils.writeFileFromString(newFileLayout, ProjectWriter.createLayout())) {
flashError(R.string.msg_layout_file_creation_failed)
return
}
notifyFileCreated(newFileLayout, context)
}
private fun createMenuRes(context: Context, node: TreeNode?, file: File) {
createNewFileWithContent(
context,
node,
Environment.mkdirIfNotExits(file),
ProjectWriter.createMenu(),
".xml"
)
}
private fun createDrawableRes(context: Context, node: TreeNode?, file: File) {
createNewFileWithContent(
context,
node,
Environment.mkdirIfNotExits(file),
ProjectWriter.createDrawable(),
".xml"
)
}
private fun createNewResource(context: Context, node: TreeNode?, file: File) {
val labels =
arrayOf(
context.getString(R.string.restype_drawable),
context.getString(R.string.restype_layout),
context.getString(R.string.restype_menu),
context.getString(R.string.restype_other)
)
val builder = DialogUtils.newMaterialDialogBuilder(context)
builder.setTitle(R.string.new_xml_resource)
builder.setItems(labels) { _, position ->
when (position) {
0 -> createDrawableRes(context, node, File(file, "drawable"))
1 -> createLayoutRes(context, node, File(file, "layout"))
2 -> createMenuRes(context, node, File(file, "menu"))
3 -> createNewFile(context, node, file, true)
}
}
builder.create().show()
}
private fun createNewEmptyFile(context: Context, node: TreeNode?, file: File) {
createNewFileWithContent(context, node, file, "")
}
private fun createNewFileWithContent(
context: Context,
node: TreeNode?,
file: File,
content: String
) {
createNewFileWithContent(context, node, file, content, null)
}
private fun createNewFileWithContent(
context: Context,
node: TreeNode?,
folder: File,
content: String,
extension: String?,
) {
val binding = LayoutDialogTextInputBinding.inflate(LayoutInflater.from(context))
val builder = DialogUtils.newMaterialDialogBuilder(context)
binding.name.editText!!.setHint(R.string.file_name)
builder.setTitle(R.string.new_file)
builder.setMessage(
context.getString(R.string.msg_can_contain_slashes) +
"\n\n" +
context.getString(R.string.msg_newfile_dest, folder.absolutePath)
)
builder.setView(binding.root)
builder.setCancelable(false)
builder.setPositiveButton(R.string.text_create) { dialogInterface, _ ->
dialogInterface.dismiss()
var name = binding.name.editText!!.text.toString().trim()
if (name.isBlank()) {
flashError(R.string.msg_invalid_name)
return@setPositiveButton
}
if (extension != null && extension.trim { it <= ' ' }.isNotEmpty()) {
name = if (name.endsWith(extension)) name else name + extension
}
try {
createFile(context, node, folder, name, content)
} catch (e: Exception) {
log.error("Failed to create file", e)
flashError(e.cause?.message ?: e.message)
}
}
builder.setNegativeButton(android.R.string.cancel, null)
builder.create().show()
}
private fun createFile(
context: Context,
node: TreeNode?,
directory: File,
name: String,
content: String
): Boolean {
if (name.length !in 1..40 || name.startsWith("/")) {
flashError(R.string.msg_invalid_name)
return false
}
val newFile = File(directory, name)
if (newFile.exists()) {
flashError(R.string.msg_file_exists)
return false
}
if (!FileIOUtils.writeFileFromString(newFile, content)) {
flashError(R.string.msg_file_creation_failed)
return false
}
notifyFileCreated(newFile, context)
// TODO Notify language servers about file created event
flashSuccess(R.string.msg_file_created)
if (node != null) {
val newNode = TreeNode(newFile)
newNode.viewHolder = FileTreeViewHolder(context)
node.addChild(newNode)
requestExpandNode(node)
} else {
requestFileListing()
}
return true
}
private fun notifyFileCreated(file: File, context: Context) {
EventBus.getDefault().post(FileCreationEvent(file).putData(context))
}
}
@@ -0,0 +1,90 @@
/*
* 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.filetree
import android.content.Context
import android.view.LayoutInflater
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder
import com.itsaky.androidide.preferences.databinding.LayoutDialogTextInputBinding
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashSuccess
import com.unnamed.b.atv.model.TreeNode
import java.io.File
/**
* File tree action to create a new folder.
*
* @author Akash Yadav
*/
class NewFolderAction(context: Context, override val order: Int) :
BaseDirNodeAction(
context = context,
labelRes = R.string.new_folder,
iconRes = R.drawable.ic_new_folder
) {
override val id: String = "ide.editor.fileTree.newFolder"
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val currentDir = data.requireFile()
val lastHeld = data.getTreeNode()
val binding = LayoutDialogTextInputBinding.inflate(LayoutInflater.from(context))
val builder = DialogUtils.newMaterialDialogBuilder(context)
binding.name.editText!!.setHint(R.string.folder_name)
builder.setTitle(R.string.new_folder)
builder.setMessage(R.string.msg_can_contain_slashes)
builder.setView(binding.root)
builder.setCancelable(false)
builder.setPositiveButton(R.string.text_create) { dialogInterface, _ ->
dialogInterface.dismiss()
val name: String = binding.name.editText!!.text.toString().trim()
if (name.length !in 1..40 || name.startsWith("/")) {
flashError(R.string.msg_invalid_name)
return@setPositiveButton
}
val newDir = File(currentDir, name)
if (newDir.exists()) {
flashError(R.string.msg_folder_exists)
return@setPositiveButton
}
if (!newDir.mkdirs()) {
flashError(R.string.msg_folder_creation_failed)
return@setPositiveButton
}
flashSuccess(R.string.msg_folder_created)
if (lastHeld != null) {
val node = TreeNode(newDir)
node.viewHolder = FileTreeViewHolder(context)
lastHeld.addChild(node)
requestExpandNode(lastHeld)
} else {
requestFileListing()
}
}
builder.setNegativeButton(android.R.string.cancel, null)
builder.create().show()
}
}
@@ -0,0 +1,44 @@
/*
* 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.filetree
import android.content.Context
import android.content.Intent
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.IntentUtils
/**
* File tree action to open files with external applications.
*
* @author Akash Yadav
*/
class OpenWithAction(context: Context, override val order: Int) :
BaseFileTreeAction(
context = context,
labelRes = R.string.open_with,
iconRes = R.drawable.ic_open_with
) {
override val id: String = "ide.editor.fileTree.openWith"
override suspend fun execAction(data: ActionData) {
IntentUtils.startIntent(data.requireActivity(), data.requireFile(), "*/*", Intent.ACTION_VIEW)
}
}
@@ -0,0 +1,116 @@
/*
* 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.filetree
import android.content.Context
import android.view.LayoutInflater
import com.blankj.utilcode.util.FileUtils
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder
import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
import com.itsaky.androidide.preferences.databinding.LayoutDialogTextInputBinding
import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.tasks.launchAsyncWithProgress
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.FlashType
import com.itsaky.androidide.utils.flashMessage
import com.unnamed.b.atv.model.TreeNode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import java.io.File
/**
* Action to rename the selected file.
*
* @author Akash Yadav
*/
class RenameAction(context: Context, override val order: Int) :
BaseFileTreeAction(
context,
labelRes = R.string.rename_file,
iconRes = R.drawable.ic_file_rename
) {
override val id: String = "ide.editor.fileTree.rename"
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
val binding = LayoutDialogTextInputBinding.inflate(LayoutInflater.from(context))
val builder = DialogUtils.newMaterialDialogBuilder(context)
binding.name.editText!!.hint =
context.getString(com.itsaky.androidide.resources.R.string.new_name)
binding.name.editText!!.setText(file.name)
builder.setTitle(com.itsaky.androidide.resources.R.string.rename_file)
builder.setMessage(com.itsaky.androidide.resources.R.string.msg_rename_file)
builder.setView(binding.root)
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton(com.itsaky.androidide.resources.R.string.rename_file) {
dialogInterface,
_ ->
dialogInterface.dismiss()
actionScope.launchAsyncWithProgress(
configureFlashbar = { builder, cancelChecker ->
builder.message(com.itsaky.androidide.resources.R.string.please_wait)
},
action = { _, _ ->
val name: String = binding.name.editText!!.text.toString().trim()
val renamed = name.length in 1..40 && FileUtils.rename(file, name)
if (renamed) {
notifyFileRenamed(file, name, context)
}
withContext(Dispatchers.Main) {
flashMessage(
if (renamed) com.itsaky.androidide.resources.R.string.renamed
else com.itsaky.androidide.resources.R.string.rename_failed,
if (renamed) FlashType.SUCCESS else FlashType.ERROR)
if (!renamed) {
return@withContext
}
if (lastHeld != null) {
val parent = lastHeld.parent
parent.deleteChild(lastHeld)
val node = TreeNode(File(file.parentFile, name))
node.viewHolder = FileTreeViewHolder(context)
parent.addChild(node)
requestExpandNode(parent)
} else {
requestFileListing()
}
}
})
}
builder.create().show()
}
private fun notifyFileRenamed(file: File, name: String, context: Context) {
val renameEvent = FileRenameEvent(file, File(file.parent, name))
// Notify FileManager first
FileManager.onFileRenamed(renameEvent)
EventBus.getDefault().post(renameEvent.apply { putData(context) })
}
}
@@ -0,0 +1,71 @@
/*
* 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
import android.content.Context
import com.itsaky.androidide.R
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.api.AndroidModule
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.flashError
/**
* @see openApplicationModuleChooser
*/
inline fun openApplicationModuleChooser(data: ActionData,
crossinline callback: (AndroidModule) -> Unit) =
openApplicationModuleChooser(data.requireContext(), callback)
/**
* Shows a dialog to let the user choose between Android application modules in case the project has
* multiple subproject with `com.android.application` plugin. If the project contains only a single
* application module, it is selected by default and the dialog is not shown to the user.
*
* @param
*/
inline fun openApplicationModuleChooser(context: Context,
crossinline callback: (AndroidModule) -> Unit) {
val projectManager = IProjectManager.getInstance()
val applications = projectManager.getAndroidAppModules()
if (applications.isEmpty()) {
flashError(R.string.msg_launch_failure_no_app_module)
ILogger.ROOT.error("Cannot run application. No application modules found in project.")
return
}
if (applications.size == 1) {
// Only one application module in available in the project.
callback(applications.first())
return
}
// there are multiple application modules in the project
// ask the user to select the application module to build
val builder = DialogUtils.newSingleChoiceDialog(
context,
context.getString(R.string.title_choose_application),
applications.map { it.path }.toTypedArray(),
0) { selection ->
val app = applications[selection]
ILogger.ROOT.info("Selected application: '{}'", app.path)
callback(app)
}
builder.show()
}
@@ -0,0 +1,46 @@
/*
* 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.graphics.drawable.Drawable
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.SidebarActionItem
/**
* @author Akash Yadav
*/
abstract class AbstractSidebarAction : SidebarActionItem {
// sidebar actions should always be executed on UI thread
override var requiresUIThread = true
override var visible = true
override var enabled = true
// should never change
final override var location = ActionItem.Location.EDITOR_SIDEBAR
// Subclasses should accept a Context in their constructor and initialize these values
// when the object instance is initialized
override var icon: Drawable? = null
override var label: String = ""
override suspend fun execAction(data: ActionData): Any {
return false
}
}
@@ -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,50 @@
/*
* 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.actions.ActionData
import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.activities.editor.BaseEditorActivity
import com.itsaky.androidide.resources.R
import kotlin.reflect.KClass
/**
* Sidebar action for closing the project.
*
* @author Akash Yadav
*/
class CloseProjectSidebarAction(context: Context, override val order: Int) :
AbstractSidebarAction() {
override val id: String = "ide.editor.sidebar.closeProject"
override val fragmentClass: KClass<out Fragment>? = null
init {
label = context.getString(R.string.title_close_project)
icon = ContextCompat.getDrawable(context, R.drawable.ic_folder_close)
}
override suspend fun execAction(data: ActionData): Any {
val context = data.requireContext() as BaseEditorActivity
context.doConfirmProjectClose()
return true
}
}
@@ -0,0 +1,45 @@
/*
* 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.FileTreeFragment
import kotlin.reflect.KClass
/**
* Sidebar action for showing file tree.
*
* @author Akash Yadav
*/
class FileTreeSidebarAction(context: Context, override val order: Int) : AbstractSidebarAction() {
companion object {
const val ID ="ide.editor.sidebar.fileTree"
}
override val id: String = ID
override val fragmentClass: KClass<out Fragment> = FileTreeFragment::class
init {
label = context.getString(R.string.msg_file_tree)
icon = ContextCompat.getDrawable(context, R.drawable.ic_folder)
}
}
@@ -0,0 +1,51 @@
/*
* 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 android.content.Intent
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.activities.PreferencesActivity
import kotlin.reflect.KClass
/**
* A sidebar action to navigate the user to IDE Preferences.
*
* @author Akash Yadav
*/
class PreferencesSidebarAction(context: Context, override val order: Int) : AbstractSidebarAction() {
override val id: String = "ide.editor.sidebar.preferences"
// TODO : Should we show the preferences in the sidebar itself?
override val fragmentClass: KClass<out Fragment>? = null
init {
label = context.getString(R.string.ide_preferences)
icon = ContextCompat.getDrawable(context, R.drawable.ic_settings)
}
override suspend fun execAction(data: ActionData): Any {
val context = data.requireContext()
return context.startActivity(Intent(context, PreferencesActivity::class.java))
}
}
@@ -0,0 +1,73 @@
/*
* 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 android.content.Intent
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.activities.TerminalActivity
import com.itsaky.androidide.projects.IProjectManager
import com.termux.shared.termux.TermuxConstants.TERMUX_APP.TERMUX_ACTIVITY
import java.util.Objects
import kotlin.reflect.KClass
/**
* Sidebar action for opening the terminal.
*
* @author Akash Yadav
*/
class TerminalSidebarAction(context: Context, override val order: Int) : AbstractSidebarAction() {
override val id: String = ID
override val fragmentClass: KClass<out Fragment>? = null
init {
label = context.getString(R.string.title_terminal)
icon = ContextCompat.getDrawable(context, R.drawable.ic_terminal)
}
companion object {
const val ID = "ide.editor.sidebar.terminal"
fun startTerminalActivity(data: ActionData, isFailsafe: Boolean) {
val context = data.requireContext()
val intent = Intent(context, TerminalActivity::class.java).apply {
putExtra(
TERMUX_ACTIVITY.EXTRA_SESSION_WORKING_DIR,
Objects.requireNonNull(IProjectManager.getInstance().projectDirPath)
)
putExtra(
TERMUX_ACTIVITY.EXTRA_SESSION_NAME,
IProjectManager.getInstance().rootProject?.rootProject?.name
)
putExtra(TERMUX_ACTIVITY.EXTRA_FAILSAFE_SESSION, isFailsafe)
}
context.startActivity(intent)
}
}
override suspend fun execAction(data: ActionData): Any {
startTerminalActivity(data, false)
return true
}
}
@@ -0,0 +1,73 @@
/*
* 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.text
import android.app.Activity
import android.content.Context
import android.view.MenuItem
import androidx.core.content.ContextCompat
import com.blankj.utilcode.util.KeyboardUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class RedoAction(context: Context, override val order: Int) : EditorRelatedAction() {
init {
label = context.getString(R.string.redo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_redo)
}
override val id: String = "ide.editor.code.text.redo"
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
val editor = data.getEditor() ?: run {
markInvisible()
return
}
enabled = editor.canRedo()
}
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.getEditor() ?: run {
markInvisible()
return false
}
editor.redo()
data.getActivity()?.invalidateOptionsMenu()
return true
}
override fun getShowAsActionFlags(data: ActionData): Int {
return if (KeyboardUtils.isSoftInputVisible(data.get(Context::class.java) as Activity)) {
MenuItem.SHOW_AS_ACTION_IF_ROOM
} else {
MenuItem.SHOW_AS_ACTION_NEVER
}
}
}
@@ -0,0 +1,57 @@
/*
* 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.text
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
import com.itsaky.androidide.resources.R
/** @author Akash Yadav */
class UndoAction(context: Context, override val order: Int) : EditorRelatedAction() {
override val id: String = "ide.editor.code.text.undo"
init {
label = context.getString(R.string.undo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_undo)
}
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
val editor = data.getEditor()!!
enabled = editor.canUndo()
}
override suspend fun execAction(data: ActionData): Any {
val editor = data.getEditor()
return if (editor != null) {
editor.undo()
data.getActivity()?.invalidateOptionsMenu()
true
} else {
false
}
}
}
@@ -0,0 +1,306 @@
/*
* 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.activities
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.SpannableStringBuilder
import android.text.style.ForegroundColorSpan
import android.view.View
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.Insets
import androidx.core.view.updatePaddingRelative
import com.blankj.utilcode.util.ClipboardUtils
import com.blankj.utilcode.util.SizeUtils
import com.itsaky.androidide.BuildConfig
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.SimpleIconTitleDescriptionAdapter
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.databinding.ActivityAboutBinding
import com.itsaky.androidide.models.IconTitleDescriptionItem
import com.itsaky.androidide.models.SimpleIconTitleDescriptionItem
import com.itsaky.androidide.utils.BuildInfoUtils
import com.itsaky.androidide.utils.flashSuccess
import com.itsaky.androidide.utils.resolveAttr
class AboutActivity : EdgeToEdgeIDEActivity() {
private var _binding: ActivityAboutBinding? = null
private val binding: ActivityAboutBinding
get() = checkNotNull(_binding) {
"Activity has been destroyed"
}
override fun bindLayout(): View {
_binding = ActivityAboutBinding.inflate(layoutInflater)
return _binding!!.root
}
companion object {
private var id = 0
private val ACTION_WEBSITE = id++
private val ACTION_EMAIL = id++
private val ACTION_TG_CHANNEL = id++
private val ACTION_TG_GROUP = id++
private val ACTION_CONTRIBUTE = id++
private val ACTION_CONTRIBUTORS = id++
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.apply {
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setTitle(R.string.about)
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
aboutHeader.apply {
ideVersion.text = createVersionText()
ideVersion.isClickable = true
ideVersion.isFocusable = true
ideVersion.setBackgroundResource(R.drawable.bg_ripple)
ideVersion.setOnClickListener {
ClipboardUtils.copyText(BuildInfoUtils.getBuildInfoHeader())
flashSuccess(R.string.copied)
}
}
socials.apply {
sectionTitle.setText(R.string.title_socials)
sectionItems.adapter = AboutSocialItemsAdapter(createSocialItems(), ::handleActionClick)
}
misc.apply {
sectionTitle.setText(R.string.title_misc)
sectionItems.adapter = AboutSocialItemsAdapter(createMiscItems(), ::handleActionClick)
}
}
}
override fun onApplySystemBarInsets(insets: Insets) {
binding.toolbar.apply {
setPaddingRelative(
paddingStart + insets.left,
paddingTop,
paddingEnd + insets.right,
paddingBottom
)
}
}
private fun handleActionClick(action: SimpleIconTitleDescriptionItem) {
when (action.id) {
ACTION_WEBSITE -> app.openWebsite()
ACTION_EMAIL -> app.emailUs()
ACTION_TG_GROUP -> app.openTelegramGroup()
ACTION_TG_CHANNEL -> app.openTelegramChannel()
ACTION_CONTRIBUTE -> app.openUrl(BaseApplication.CONTRIBUTOR_GUIDE_URL)
ACTION_CONTRIBUTORS -> startActivity(Intent(this, ContributorsActivity::class.java))
}
}
private fun createSocialItems(): List<IconTitleDescriptionItem> {
return mutableListOf<IconTitleDescriptionItem>().apply {
add(
createSimpleIconTextItem(
this@AboutActivity,
ACTION_WEBSITE,
R.drawable.ic_website,
R.string.about_option_website,
BuildInfo.PROJECT_SITE
)
)
add(
createSimpleIconTextItem(
this@AboutActivity,
ACTION_EMAIL,
R.drawable.ic_email,
R.string.about_option_email,
BaseApplication.EMAIL
)
)
add(
createSimpleIconTextItem(
this@AboutActivity,
ACTION_TG_GROUP,
R.drawable.ic_telegram,
R.string.discussions_on_telegram,
BaseApplication.TELEGRAM_GROUP_URL
)
)
add(
createSimpleIconTextItem(
this@AboutActivity,
ACTION_TG_CHANNEL,
R.drawable.ic_telegram,
R.string.official_tg_channel,
BaseApplication.TELEGRAM_CHANNEL_URL
)
)
}
}
private fun createMiscItems(): List<IconTitleDescriptionItem> {
return mutableListOf<IconTitleDescriptionItem>().apply {
add(
SimpleIconTitleDescriptionItem.create(
this@AboutActivity,
ACTION_CONTRIBUTE,
R.drawable.ic_code,
R.string.title_contribute,
R.string.summary_contribute
)
)
add(
SimpleIconTitleDescriptionItem.create(
this@AboutActivity,
ACTION_CONTRIBUTORS,
R.drawable.ic_heart_outline,
R.string.title_contributors,
R.string.summary_contributors
)
)
}
}
private fun createSimpleIconTextItem(
context: Context,
id: Int,
@DrawableRes icon: Int,
@StringRes title: Int,
description: CharSequence
): SimpleIconTitleDescriptionItem {
return SimpleIconTitleDescriptionItem(
id,
ContextCompat.getDrawable(context, icon),
ContextCompat.getString(context, title),
description
)
}
/**
* Create the version name string that should be displayed to the user.
*
* Format of the version name string is :
*
* `v[version-name]-[variant] ([build-type]/[[UN]OFFICIAL])`
*/
@Suppress("KDocUnresolvedReference")
private fun createVersionText(): CharSequence {
val builder = SpannableStringBuilder()
builder.append("v")
builder.append(BuildInfo.VERSION_NAME_SIMPLE)
builder.append("-")
builder.append(IDEBuildConfigProvider.getInstance().cpuAbiName)
builder.append(" ")
val colorPositive = ContextCompat.getColor(this, R.color.color_success)
val colorNegative = ContextCompat.getColor(this, R.color.color_error)
appendBuildType(builder, colorPositive, colorNegative)
return builder
}
private fun appendBuildType(
builder: SpannableStringBuilder,
@ColorInt
colorPositive: Int,
@ColorInt
colorNegative: Int
) {
@Suppress("KotlinConstantConditions")
var color = if (BuildConfig.BUILD_TYPE != "release") {
colorNegative
} else {
colorPositive
}
builder.append("(")
appendForegroundSpan(builder, BuildConfig.BUILD_TYPE, color)
val isOfficialBuild = BuildInfoUtils.isOfficialBuild(this)
color = if (isOfficialBuild) {
colorPositive
} else {
colorNegative
}
builder.append("/")
appendForegroundSpan(
builder,
BuildInfoUtils.getBuildType(this).lowercase(),
color
)
builder.append(")")
}
private fun appendForegroundSpan(
builder: SpannableStringBuilder,
text: CharSequence,
color: Int
) {
builder.append(
text,
ForegroundColorSpan(color),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
class AboutSocialItemsAdapter(
items: List<IconTitleDescriptionItem>,
private val onClickListener: (SimpleIconTitleDescriptionItem) -> Unit
) : SimpleIconTitleDescriptionAdapter(items) {
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
val binding = holder.binding
val item = getItem(position) as SimpleIconTitleDescriptionItem
val dp8 = SizeUtils.dp2px(8f)
binding.icon.updatePaddingRelative(dp8, dp8, dp8, dp8)
binding.title.setTextAppearance(R.style.TextAppearance_Material3_TitleSmall)
binding.description.maxLines = 3
binding.description.setTextAppearance(R.style.TextAppearance_Material3_BodySmall)
binding.description.setTextColor(binding.description.context.resolveAttr(R.attr.colorPrimary))
binding.root.isClickable = true
binding.root.isFocusable = true
binding.root.setBackgroundResource(R.drawable.bg_ripple)
binding.root.setOnClickListener {
onClickListener(item)
}
}
}
}
@@ -0,0 +1,112 @@
/*
* 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.activities
import android.os.Bundle
import android.view.View
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.ContributorsGridAdapter
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityContributorsBinding
import com.itsaky.androidide.utils.getConnectionInfo
import com.itsaky.androidide.viewmodel.ContributorsViewModel
/**
* @author Akash Yadav
*/
class ContributorsActivity : EdgeToEdgeIDEActivity() {
private var _binding: ActivityContributorsBinding? = null
private val binding: ActivityContributorsBinding
get() = checkNotNull(_binding) {
"Activity has been destroyed"
}
private val viewModel by viewModels<ContributorsViewModel>()
override fun bindLayout(): View {
_binding = ActivityContributorsBinding.inflate(layoutInflater)
return binding.root
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.apply {
setSupportActionBar(toolbar)
supportActionBar!!.setTitle(R.string.title_contributors)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
githubContributors.apply {
sectionTitle.setText(R.string.title_github_contributors)
}
translationContributors.apply {
sectionTitle.setText(R.string.title_crowdin_translators)
}
noConnection.root.setText(R.string.msg_no_internet)
loadingProgress.isVisible = false
}
viewModel._crowdinTranslators.observe(this) { translators ->
binding.translationContributors.sectionItems.adapter = ContributorsGridAdapter(translators)
}
viewModel._githubContributors.observe(this) { githubContributors ->
binding.githubContributors.sectionItems.adapter = ContributorsGridAdapter(githubContributors)
}
val connectionInfo = getConnectionInfo(this)
binding.apply {
noConnection.root.isVisible = !connectionInfo.isConnected
githubContributorsCard.isVisible = connectionInfo.isConnected
translationContributorsCard.isVisible = connectionInfo.isConnected
if (connectionInfo.isConnected) {
viewModel.observeLoadingState(this@ContributorsActivity) { isLoading ->
binding.loadingProgress.isVisible = isLoading
}
viewModel.fetchAll()
}
}
}
override fun onApplySystemBarInsets(insets: Insets) {
super.onApplySystemBarInsets(insets)
binding.toolbar.apply {
setPaddingRelative(
paddingStart + insets.left,
paddingTop,
paddingEnd + insets.right,
paddingBottom
)
}
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -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.activities;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.IDEActivity;
import com.itsaky.androidide.databinding.ActivityCrashHandlerBinding;
import com.itsaky.androidide.fragments.CrashReportFragment;
public class CrashHandlerActivity extends IDEActivity {
public static final String REPORT_ACTION = "com.itsaky.androidide.REPORT_CRASH";
public static final String TRACE_KEY = "crash_trace";
private ActivityCrashHandlerBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final var extra = getIntent().getExtras();
if (extra == null) {
finishAffinity();
return;
}
final var report = extra.getString(TRACE_KEY, "Unable to get logs.");
final var fragment = CrashReportFragment.newInstance(report);
getSupportFragmentManager()
.beginTransaction()
.replace(binding.getRoot().getId(), fragment, "crash_report_fragment")
.addToBackStack(null)
.commit();
}
@Override
@NonNull
protected View bindLayout() {
binding = ActivityCrashHandlerBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
}
}
@@ -0,0 +1,208 @@
/*
* 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.activities
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.activity.viewModels
import androidx.core.graphics.Insets
import androidx.core.view.isVisible
import androidx.transition.TransitionManager
import androidx.transition.doOnEnd
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.activities.editor.EditorActivityKt
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityMainBinding
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.templates.ITemplateProvider
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.flashInfo
import com.itsaky.androidide.viewmodel.MainViewModel
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_MAIN
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_TEMPLATE_DETAILS
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_TEMPLATE_LIST
import java.io.File
class MainActivity : EdgeToEdgeIDEActivity() {
private val viewModel by viewModels<MainViewModel>()
private var _binding: ActivityMainBinding? = null
private val onBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
viewModel.apply {
// Ignore back press if project creating is in progress
if (creatingProject.value == true) {
return@apply
}
val newScreen = when (currentScreen.value) {
SCREEN_TEMPLATE_DETAILS -> SCREEN_TEMPLATE_LIST
SCREEN_TEMPLATE_LIST -> SCREEN_MAIN
else -> SCREEN_MAIN
}
if (currentScreen.value != newScreen) {
setScreen(newScreen)
}
}
}
}
private val binding: ActivityMainBinding
get() = checkNotNull(_binding)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
openLastProject()
viewModel.currentScreen.observe(this) { screen ->
if (screen == -1) {
return@observe
}
onScreenChanged(screen)
onBackPressedCallback.isEnabled = screen != SCREEN_MAIN
}
// Data in a ViewModel is kept between activity rebuilds on
// configuration changes (i.e. screen rotation)
// * previous == -1 and current == -1 -> this is an initial instantiation of the activity
if (viewModel.currentScreen.value == -1 && viewModel.previousScreen == -1) {
viewModel.setScreen(SCREEN_MAIN)
} else {
onScreenChanged(viewModel.currentScreen.value)
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
}
override fun onApplySystemBarInsets(insets: Insets) {
binding.fragmentContainersParent.setPadding(
insets.left,
0,
insets.right,
insets.bottom
)
}
private fun onScreenChanged(screen: Int?) {
val previous = viewModel.previousScreen
if (previous != -1) {
// template list -> template details
// ------- OR -------
// template details -> template list
val setAxisToX =
(previous == SCREEN_TEMPLATE_LIST || previous == SCREEN_TEMPLATE_DETAILS) && (screen == SCREEN_TEMPLATE_LIST || screen == SCREEN_TEMPLATE_DETAILS)
val axis = if (setAxisToX) {
MaterialSharedAxis.X
} else {
MaterialSharedAxis.Y
}
val isForward = (screen ?: 0) - previous == 1
val transition = MaterialSharedAxis(axis, isForward)
transition.doOnEnd {
viewModel.isTransitionInProgress = false
onBackPressedCallback.isEnabled = viewModel.currentScreen.value != SCREEN_MAIN
}
viewModel.isTransitionInProgress = true
TransitionManager.beginDelayedTransition(binding.root, transition)
}
val currentFragment = when (screen) {
SCREEN_MAIN -> binding.main
SCREEN_TEMPLATE_LIST -> binding.templateList
SCREEN_TEMPLATE_DETAILS -> binding.templateDetails
else -> throw IllegalArgumentException("Invalid screen id: '$screen'")
}
for (fragment in arrayOf(binding.main, binding.templateList, binding.templateDetails)) {
fragment.isVisible = fragment == currentFragment
}
}
override fun bindLayout(): View {
_binding = ActivityMainBinding.inflate(layoutInflater)
return binding.root
}
private fun openLastProject() {
binding.root.post { tryOpenLastProject() }
}
private fun tryOpenLastProject() {
if (!GeneralPreferences.autoOpenProjects) {
return
}
val openedProject = GeneralPreferences.lastOpenedProject
if (GeneralPreferences.NO_OPENED_PROJECT == openedProject) {
return
}
if (TextUtils.isEmpty(openedProject)) {
app
flashInfo(string.msg_opened_project_does_not_exist)
return
}
val project = File(openedProject)
if (!project.exists()) {
flashInfo(string.msg_opened_project_does_not_exist)
return
}
if (GeneralPreferences.confirmProjectOpen) {
askProjectOpenPermission(project)
return
}
openProject(project)
}
private fun askProjectOpenPermission(root: File) {
val builder = DialogUtils.newMaterialDialogBuilder(this)
builder.setTitle(string.title_confirm_open_project)
builder.setMessage(getString(string.msg_confirm_open_project, root.absolutePath))
builder.setCancelable(false)
builder.setPositiveButton(string.yes) { _, _ -> openProject(root) }
builder.setNegativeButton(string.no, null)
builder.show()
}
internal fun openProject(root: File) {
ProjectManagerImpl.getInstance().projectPath = root.absolutePath
startActivity(Intent(this, EditorActivityKt::class.java))
}
override fun onDestroy() {
ITemplateProvider.getInstance().release()
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,260 @@
/*
* 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.activities
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.github.appintro.AppIntro2
import com.github.appintro.AppIntroPageTransformerType
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider
import com.itsaky.androidide.app.configuration.IJdkDistributionProvider
import com.itsaky.androidide.fragments.onboarding.GreetingFragment
import com.itsaky.androidide.fragments.onboarding.IdeSetupConfigurationFragment
import com.itsaky.androidide.fragments.onboarding.OnboardingInfoFragment
import com.itsaky.androidide.fragments.onboarding.PermissionsFragment
import com.itsaky.androidide.fragments.onboarding.StatisticsFragment
import com.itsaky.androidide.models.JdkDistribution
import com.itsaky.androidide.preferences.internal.StatPreferences
import com.itsaky.androidide.preferences.internal.prefManager
import com.itsaky.androidide.tasks.launchAsyncWithProgress
import com.itsaky.androidide.ui.themes.IThemeManager
import com.itsaky.androidide.utils.Environment
import com.termux.shared.android.PackageUtils
import com.termux.shared.markdown.MarkdownUtils
import com.termux.shared.termux.TermuxConstants
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.withContext
class OnboardingActivity : AppIntro2() {
private val terminalActivityCallback = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()) {
Log.d(TAG, "TerminalActivity: resultCode=${it.resultCode}")
if (!isFinishing) {
reloadJdkDistInfo {
tryNavigateToMainIfSetupIsCompleted()
}
}
}
private val activityScope =
CoroutineScope(Dispatchers.Main + CoroutineName("OnboardingActivity"))
private var listJdkInstallationsJob: Job? = null
companion object {
private const val TAG = "OnboardingActivity"
private const val KEY_ARCHCONFIG_WARNING_IS_SHOWN = "ide.archConfig.experimentalWarning.isShown"
}
override fun onCreate(savedInstanceState: Bundle?) {
IThemeManager.getInstance().applyTheme(this)
super.onCreate(savedInstanceState)
if (tryNavigateToMainIfSetupIsCompleted()) {
return
}
setSwipeLock(true)
setTransformer(AppIntroPageTransformerType.Fade)
setProgressIndicator()
showStatusBar(true)
isIndicatorEnabled = true
isWizardMode = true
addSlide(GreetingFragment())
if (!PackageUtils.isCurrentUserThePrimaryUser(this)) {
val errorMessage = getString(string.bootstrap_error_not_primary_user_message,
MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_PREFIX_DIR_PATH, false))
addSlide(OnboardingInfoFragment.newInstance(
getString(string.title_unsupported_user),
errorMessage,
R.drawable.ic_alert,
ContextCompat.getColor(this, R.color.color_error)
))
return
}
if (isInstalledOnSdCard()) {
val errorMessage = getString(string.bootstrap_error_installed_on_portable_sd,
MarkdownUtils.getMarkdownCodeForString(TermuxConstants.TERMUX_PREFIX_DIR_PATH, false))
addSlide(OnboardingInfoFragment.newInstance(
getString(string.title_install_location_error),
errorMessage,
R.drawable.ic_alert,
ContextCompat.getColor(this, R.color.color_error)
))
return
}
if (!checkDeviceSupported()) {
return
}
if (!StatPreferences.statConsentDialogShown) {
addSlide(StatisticsFragment.newInstance(this))
StatPreferences.statConsentDialogShown = true
}
if (!PermissionsFragment.areAllPermissionsGranted(this)) {
addSlide(PermissionsFragment.newInstance(this))
}
if (!checkToolsIsInstalled()) {
addSlide(IdeSetupConfigurationFragment.newInstance(this))
}
}
override fun onResume() {
super.onResume()
reloadJdkDistInfo {
tryNavigateToMainIfSetupIsCompleted()
}
}
override fun onDestroy() {
super.onDestroy()
activityScope.cancel("Activity is being destroyed")
}
override fun onNextPressed(currentFragment: Fragment?) {
(currentFragment as? StatisticsFragment?)?.updateStatOptInStatus()
}
override fun onDonePressed(currentFragment: Fragment?) {
(currentFragment as? StatisticsFragment?)?.updateStatOptInStatus()
if (!IDEBuildConfigProvider.getInstance().supportsCpuAbi()) {
finishAffinity()
return
}
if (!checkToolsIsInstalled() && currentFragment is IdeSetupConfigurationFragment) {
val intent = Intent(this, TerminalActivity::class.java)
if (currentFragment.isAutoInstall()) {
intent.putExtra(TerminalActivity.EXTRA_ONBOARDING_RUN_IDESETUP, true)
intent.putExtra(TerminalActivity.EXTRA_ONBOARDING_RUN_IDESETUP_ARGS,
currentFragment.buildIdeSetupArguments())
}
terminalActivityCallback.launch(intent)
return
}
tryNavigateToMainIfSetupIsCompleted()
}
private fun checkToolsIsInstalled(): Boolean {
return IJdkDistributionProvider.getInstance().installedDistributions.isNotEmpty()
&& Environment.ANDROID_HOME.exists()
}
private fun isSetupCompleted(): Boolean {
return checkToolsIsInstalled()
&& StatPreferences.statConsentDialogShown
&& PermissionsFragment.areAllPermissionsGranted(this)
}
private fun tryNavigateToMainIfSetupIsCompleted(): Boolean {
if (isSetupCompleted()) {
startActivity(Intent(this, MainActivity::class.java))
finish()
return true
}
return false
}
private inline fun reloadJdkDistInfo(crossinline distConsumer: (List<JdkDistribution>) -> Unit) {
listJdkInstallationsJob?.cancel("Reloading JDK distributions")
listJdkInstallationsJob = activityScope.launchAsyncWithProgress(Dispatchers.Default,
configureFlashbar = { builder, _ ->
builder.message(string.please_wait)
}) { _, _ ->
val distributionProvider = IJdkDistributionProvider.getInstance()
distributionProvider.loadDistributions()
withContext(Dispatchers.Main) {
distConsumer(distributionProvider.installedDistributions)
}
}.also {
it?.invokeOnCompletion {
listJdkInstallationsJob = null
}
}
}
private fun isInstalledOnSdCard(): Boolean {
// noinspection SdCardPath
return PackageUtils.isAppInstalledOnExternalStorage(this) &&
TermuxConstants.TERMUX_FILES_DIR_PATH != filesDir.absolutePath
.replace("^/data/user/0/".toRegex(), "/data/data/")
}
private fun checkDeviceSupported(): Boolean {
val configProvider = IDEBuildConfigProvider.getInstance()
if (!configProvider.supportsCpuAbi()) {
addSlide(OnboardingInfoFragment.newInstance(
getString(string.title_unsupported_device),
getString(
string.msg_unsupported_device,
configProvider.cpuArch.abi,
configProvider.deviceArch.abi
),
R.drawable.ic_alert,
ContextCompat.getColor(this, R.color.color_error)
))
return false
}
if (configProvider.cpuArch != configProvider.deviceArch) {
// IDE's build flavor is NOT the primary arch of the device
// warn the user
if (!archConfigExperimentalWarningIsShown()) {
addSlide(OnboardingInfoFragment.newInstance(
getString(string.title_experiment_flavor),
getString(string.msg_experimental_flavor,
configProvider.cpuArch.abi,
configProvider.deviceArch.abi
),
R.drawable.ic_alert,
ContextCompat.getColor(this, R.color.color_warning)
))
prefManager.putBoolean(KEY_ARCHCONFIG_WARNING_IS_SHOWN, true)
}
}
return true
}
private fun archConfigExperimentalWarningIsShown() =
prefManager.getBoolean(KEY_ARCHCONFIG_WARNING_IS_SHOWN, false)
}
@@ -0,0 +1,100 @@
/*
* 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.activities
import android.os.Bundle
import android.view.View
import androidx.core.graphics.Insets
import androidx.fragment.app.Fragment
import com.itsaky.androidide.R
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityPreferencesBinding
import com.itsaky.androidide.fragments.IDEPreferencesFragment
import com.itsaky.androidide.preferences.addRootPreferences
import com.itsaky.androidide.preferences.IDEPreferences as prefs
class PreferencesActivity : EdgeToEdgeIDEActivity() {
private var _binding: ActivityPreferencesBinding? = null
private val binding: ActivityPreferencesBinding
get() = checkNotNull(_binding) { "Activity has been destroyed" }
private val rootFragment by lazy {
IDEPreferencesFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(binding.toolbar)
supportActionBar!!.setTitle(R.string.ide_preferences)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
if (savedInstanceState != null) {
return
}
(prefs.children as MutableList?)?.clear()
prefs.addRootPreferences()
val args = Bundle()
args.putParcelableArrayList(
IDEPreferencesFragment.EXTRA_CHILDREN,
ArrayList(prefs.children)
)
rootFragment.arguments = args
loadFragment(rootFragment)
}
override fun onApplySystemBarInsets(insets: Insets) {
val toolbar: View = binding.toolbar
toolbar.setPadding(
toolbar.paddingLeft + insets.left,
toolbar.paddingTop,
toolbar.paddingRight + insets.right,
toolbar.paddingBottom
)
val fragmentContainer: View = binding.fragmentContainerParent
fragmentContainer.setPadding(
fragmentContainer.paddingLeft + insets.left,
fragmentContainer.paddingTop,
fragmentContainer.paddingRight + insets.right,
fragmentContainer.paddingBottom
)
}
override fun bindLayout(): View {
_binding = ActivityPreferencesBinding.inflate(
layoutInflater
)
return binding.root
}
private fun loadFragment(fragment: Fragment) {
super.loadFragment(fragment, binding.fragmentContainer.id)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -0,0 +1,34 @@
/*
* 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.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
/**
* @author Akash Yadav
*/
class SplashActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
startActivity(Intent(this, OnboardingActivity::class.java))
finish()
}
}
@@ -0,0 +1,811 @@
/*
* 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.activities.editor
import android.content.Intent
import android.content.pm.PackageInstaller.SessionCallback
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Process
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.viewModels
import androidx.annotation.GravityInt
import androidx.annotation.StringRes
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.collection.MutableIntIntMap
import androidx.core.graphics.Insets
import androidx.core.view.GravityCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative
import com.blankj.utilcode.constant.MemoryConstants
import com.blankj.utilcode.util.ConvertUtils.byte2MemorySize
import com.blankj.utilcode.util.FileUtils
import com.blankj.utilcode.util.ThreadUtils
import com.github.mikephil.charting.components.AxisBase
import com.github.mikephil.charting.data.Entry
import com.github.mikephil.charting.data.LineData
import com.github.mikephil.charting.data.LineDataSet
import com.github.mikephil.charting.formatter.IAxisValueFormatter
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.Tab
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_FILE_TABS
import com.itsaky.androidide.adapters.DiagnosticsAdapter
import com.itsaky.androidide.adapters.SearchListAdapter
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityEditorBinding
import com.itsaky.androidide.databinding.ContentEditorBinding
import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding
import com.itsaky.androidide.events.InstallationResultEvent
import com.itsaky.androidide.fragments.SearchResultFragment
import com.itsaky.androidide.fragments.sidebar.EditorSidebarFragment
import com.itsaky.androidide.fragments.sidebar.FileTreeFragment
import com.itsaky.androidide.handlers.EditorActivityLifecyclerObserver
import com.itsaky.androidide.handlers.LspHandler.registerLanguageServers
import com.itsaky.androidide.interfaces.DiagnosticClickListener
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.models.DiagnosticGroup
import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SearchResult
import com.itsaky.androidide.preferences.internal.BuildPreferences
import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.tasks.cancelIfActive
import com.itsaky.androidide.ui.CodeEditorView
import com.itsaky.androidide.ui.ContentTranslatingDrawerLayout
import com.itsaky.androidide.ui.SwipeRevealLayout
import com.itsaky.androidide.uidesigner.UIDesignerActivity
import com.itsaky.androidide.utils.ActionMenuUtils.createMenu
import com.itsaky.androidide.utils.ApkInstallationSessionCallback
import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder
import com.itsaky.androidide.utils.InstallationResultHandler.onResult
import com.itsaky.androidide.utils.IntentUtils
import com.itsaky.androidide.utils.MemoryUsageWatcher
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.resolveAttr
import com.itsaky.androidide.viewmodel.EditorViewModel
import com.itsaky.androidide.xml.resources.ResourceTableRegistry
import com.itsaky.androidide.xml.versions.ApiVersionsRegistry
import com.itsaky.androidide.xml.widgets.WidgetTableRegistry
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.MAIN
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.io.File
import kotlin.math.roundToInt
import kotlin.math.roundToLong
/**
* Base class for EditorActivity which handles most of the view related things.
*
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSelectedListener,
DiagnosticClickListener {
protected val mLifecycleObserver = EditorActivityLifecyclerObserver()
protected var diagnosticInfoBinding: LayoutDiagnosticInfoBinding? = null
protected var filesTreeFragment: FileTreeFragment? = null
protected var editorBottomSheet: BottomSheetBehavior<out View?>? = null
protected val memoryUsageWatcher = MemoryUsageWatcher()
protected val pidToDatasetIdxMap = MutableIntIntMap(initialCapacity = 3)
var isDestroying = false
protected set
/**
* Editor activity's [CoroutineScope] for executing tasks in the background.
*/
protected val editorActivityScope = CoroutineScope(Dispatchers.Default)
internal var installationCallback: ApkInstallationSessionCallback? = null
var uiDesignerResultLauncher: ActivityResultLauncher<Intent>? = null
val editorViewModel by viewModels<EditorViewModel>()
internal var _binding: ActivityEditorBinding? = null
val binding: ActivityEditorBinding
get() = checkNotNull(_binding) { "Activity has been destroyed" }
val content: ContentEditorBinding
get() = binding.content
override val subscribeToEvents: Boolean
get() = true
private val onBackPressedCallback: OnBackPressedCallback = object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
if (binding.root.isDrawerOpen(GravityCompat.START)) {
binding.root.closeDrawer(GravityCompat.START)
} else if (editorBottomSheet?.state != BottomSheetBehavior.STATE_COLLAPSED) {
editorBottomSheet?.setState(BottomSheetBehavior.STATE_COLLAPSED)
} else if (binding.swipeReveal.isOpen) {
binding.swipeReveal.close()
} else {
doConfirmProjectClose()
}
}
}
private val memoryUsageListener = MemoryUsageWatcher.MemoryUsageListener { memoryUsage ->
memoryUsage.forEachValue { proc ->
_binding?.memUsageView?.chart?.apply {
val dataset = (data.getDataSetByIndex(pidToDatasetIdxMap[proc.pid]) as LineDataSet?)
?: run {
log.error("No dataset found for process: {}: {}", proc.pid, proc.pname)
return@forEachValue
}
dataset.entries.mapIndexed { index, entry ->
entry.y = byte2MemorySize(proc.usageHistory[index], MemoryConstants.MB).toFloat()
}
dataset.label = "%s - %.2fMB".format(proc.pname, dataset.entries.last().y)
dataset.notifyDataSetChanged()
data.notifyDataChanged()
notifyDataSetChanged()
invalidate()
}
}
}
private var isImeVisible = false
private var contentCardRealHeight: Int? = null
private val editorSurfaceContainerBackground by lazy {
resolveAttr(R.attr.colorSurfaceDim)
}
private val editorLayoutCorners by lazy {
resources.getDimensionPixelSize(R.dimen.editor_container_corners).toFloat()
}
private var optionsMenuInvalidator: Runnable? = null
companion object {
@JvmStatic
protected val PROC_IDE = "IDE"
@JvmStatic
protected val PROC_GRADLE_TOOLING = "Gradle Tooling"
@JvmStatic
protected val PROC_GRADLE_DAEMON = "Gradle Daemon"
@JvmStatic
protected val log: Logger = LoggerFactory.getLogger(BaseEditorActivity::class.java)
private const val OPTIONS_MENU_INVALIDATION_DELAY = 150L
const val EDITOR_CONTAINER_SCALE_FACTOR = 0.87f
const val KEY_BOTTOM_SHEET_SHOWN = "editor_bottomSheetShown"
const val KEY_PROJECT_PATH = "saved_projectPath"
}
protected abstract fun provideCurrentEditor(): CodeEditorView?
protected abstract fun provideEditorAt(index: Int): CodeEditorView?
protected abstract fun doOpenFile(file: File, selection: Range?)
protected abstract fun doDismissSearchProgress()
protected abstract fun getOpenedFiles(): List<OpenedFile>
internal abstract fun doConfirmProjectClose()
protected open fun preDestroy() {
_binding = null
optionsMenuInvalidator?.also {
ThreadUtils.getMainHandler().removeCallbacks(it)
}
optionsMenuInvalidator = null
installationCallback?.destroy()
installationCallback = null
if (isDestroying) {
memoryUsageWatcher.stopWatching(true)
memoryUsageWatcher.listener = null
editorActivityScope.cancelIfActive("Activity is being destroyed")
}
}
protected open fun postDestroy() {
if (isDestroying) {
Lookup.getDefault().unregisterAll()
ApiVersionsRegistry.getInstance().clear()
ResourceTableRegistry.getInstance().clear()
WidgetTableRegistry.getInstance().clear()
}
}
override fun bindLayout(): View {
this._binding = ActivityEditorBinding.inflate(layoutInflater)
this.diagnosticInfoBinding = this.content.diagnosticInfo
return this.binding.root
}
override fun onApplyWindowInsets(insets: WindowInsetsCompat) {
super.onApplyWindowInsets(insets)
val height = contentCardRealHeight ?: return
val imeInsets = insets.getInsets(WindowInsetsCompat.Type.ime())
_binding?.content?.bottomSheet?.setImeVisible(imeInsets.bottom > 0)
_binding?.contentCard?.updateLayoutParams<ViewGroup.LayoutParams> {
this.height = height - imeInsets.bottom
}
val isImeVisible = imeInsets.bottom > 0
if (this.isImeVisible != isImeVisible) {
this.isImeVisible = isImeVisible
onSoftInputChanged()
}
}
override fun onApplySystemBarInsets(insets: Insets) {
super.onApplySystemBarInsets(insets)
this._binding?.apply {
drawerSidebar.getFragment<EditorSidebarFragment>()
.onApplyWindowInsets(insets)
content.apply {
editorAppBarLayout.updatePadding(
top = insets.top
)
editorToolbar.updatePaddingRelative(
start = editorToolbar.paddingStart + insets.left,
end = editorToolbar.paddingEnd + insets.right
)
}
}
}
@Subscribe(threadMode = MAIN)
open fun onInstallationResult(event: InstallationResultEvent) {
val intent = event.intent
if (isDestroying) {
return
}
val packageName = onResult(this, intent) ?: return
if (BuildPreferences.launchAppAfterInstall) {
IntentUtils.launchApp(this, packageName)
return
}
Snackbar.make(content.realContainer, string.msg_action_open_application, Snackbar.LENGTH_LONG)
.setAction(string.yes) { IntentUtils.launchApp(this, packageName) }.show()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
this.optionsMenuInvalidator = Runnable { super.invalidateOptionsMenu() }
registerLanguageServers()
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_PROJECT_PATH)) {
ProjectManagerImpl.getInstance().projectPath = savedInstanceState.getString(
KEY_PROJECT_PATH
)!!
}
onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
lifecycle.addObserver(mLifecycleObserver)
setSupportActionBar(content.editorToolbar)
setupDrawers()
content.tabs.addOnTabSelectedListener(this)
setupViews()
setupContainers()
setupDiagnosticInfo()
uiDesignerResultLauncher = registerForActivityResult(
StartActivityForResult(),
this::handleUiDesignerResult
)
setupMemUsageChart()
watchMemory()
}
private fun onSwipeRevealDragProgress(progress: Float) {
_binding?.apply {
contentCard.progress = progress
val insetsTop = systemBarInsets?.top ?: 0
content.editorAppBarLayout.updatePadding(
top = (insetsTop * (1f - progress)).roundToInt()
)
memUsageView.chart.updateLayoutParams<ViewGroup.MarginLayoutParams> {
topMargin = (insetsTop * progress).roundToInt()
}
}
}
private fun setupMemUsageChart() {
binding.memUsageView.chart.apply {
val colorAccent = resolveAttr(R.attr.colorAccent)
isDragEnabled = false
description.isEnabled = false
xAxis.axisLineColor = colorAccent
axisRight.axisLineColor = colorAccent
setPinchZoom(false)
setBackgroundColor(editorSurfaceContainerBackground)
setDrawGridBackground(true)
setScaleEnabled(true)
axisLeft.isEnabled = false
axisRight.valueFormatter = object :
IAxisValueFormatter {
override fun getFormattedValue(value: Float, axis: AxisBase?): String {
return "%dMB".format(value.roundToLong())
}
}
}
}
private fun watchMemory() {
memoryUsageWatcher.listener = memoryUsageListener
memoryUsageWatcher.watchProcess(Process.myPid(), PROC_IDE)
resetMemUsageChart()
}
protected fun resetMemUsageChart() {
val processes = memoryUsageWatcher.getMemoryUsages()
val datasets = Array(processes.size) { index ->
LineDataSet(
List(MemoryUsageWatcher.MAX_USAGE_ENTRIES) { Entry(it.toFloat(), 0f) },
processes[index].pname
)
}
val bgColor = editorSurfaceContainerBackground
val textColor = resolveAttr(R.attr.colorOnSurface)
for ((index, proc) in processes.withIndex()) {
val dataset = datasets[index]
dataset.color = getMemUsageLineColorFor(proc)
dataset.setDrawIcons(false)
dataset.setDrawCircles(false)
dataset.setDrawCircleHole(false)
dataset.setDrawValues(false)
dataset.formLineWidth = 1f
dataset.formSize = 15f
dataset.isHighlightEnabled = false
pidToDatasetIdxMap[proc.pid] = index
}
binding.memUsageView.chart.setBackgroundColor(bgColor)
binding.memUsageView.chart.apply {
data = LineData(*datasets)
axisRight.textColor = textColor
axisLeft.textColor = textColor
legend.textColor = textColor
data.setValueTextColor(textColor)
setBackgroundColor(bgColor)
setGridBackgroundColor(bgColor)
notifyDataSetChanged()
invalidate()
}
}
private fun getMemUsageLineColorFor(proc: MemoryUsageWatcher.ProcessMemoryInfo): Int {
return when (proc.pname) {
PROC_IDE -> Color.BLUE
PROC_GRADLE_TOOLING -> Color.RED
PROC_GRADLE_DAEMON -> Color.GREEN
else -> throw IllegalArgumentException("Unknown process: $proc")
}
}
override fun onPause() {
super.onPause()
memoryUsageWatcher.listener = null
memoryUsageWatcher.stopWatching(false)
this.isDestroying = isFinishing
getFileTreeFragment()?.saveTreeState()
}
override fun onResume() {
super.onResume()
invalidateOptionsMenu()
memoryUsageWatcher.listener = memoryUsageListener
memoryUsageWatcher.startWatching()
try {
getFileTreeFragment()?.listProjectFiles()
} catch (th: Throwable) {
log.error("Failed to update files list", th)
flashError(string.msg_failed_list_files)
}
}
override fun onStop() {
super.onStop()
checkIsDestroying()
}
override fun onDestroy() {
checkIsDestroying()
preDestroy()
super.onDestroy()
postDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putString(KEY_PROJECT_PATH, IProjectManager.getInstance().projectDirPath)
super.onSaveInstanceState(outState)
}
override fun invalidateOptionsMenu() {
val mainHandler = ThreadUtils.getMainHandler()
optionsMenuInvalidator?.also {
mainHandler.removeCallbacks(it)
mainHandler.postDelayed(it, OPTIONS_MENU_INVALIDATION_DELAY)
}
}
override fun onTabSelected(tab: Tab) {
val position = tab.position
editorViewModel.displayedFileIndex = position
val editorView = provideEditorAt(position)!!
editorView.onEditorSelected()
editorViewModel.setCurrentFile(position, editorView.file)
refreshSymbolInput(editorView)
invalidateOptionsMenu()
}
override fun onTabUnselected(tab: Tab) {}
override fun onTabReselected(tab: Tab) {
createMenu(this, tab.view, EDITOR_FILE_TABS, true).show()
}
override fun onGroupClick(group: DiagnosticGroup?) {
if (group?.file?.exists() == true && FileUtils.isUtf8(group.file)) {
doOpenFile(group.file, null)
hideBottomSheet()
}
}
override fun onDiagnosticClick(file: File, diagnostic: DiagnosticItem) {
doOpenFile(file, diagnostic.range)
hideBottomSheet()
}
open fun handleSearchResults(map: Map<File, List<SearchResult>>?) {
val results = map ?: emptyMap()
setSearchResultAdapter(SearchListAdapter(results, { file ->
doOpenFile(file, null)
hideBottomSheet()
}) { match ->
doOpenFile(match.file, match)
hideBottomSheet()
})
showSearchResults()
doDismissSearchProgress()
}
open fun setSearchResultAdapter(adapter: SearchListAdapter) {
content.bottomSheet.setSearchResultAdapter(adapter)
}
open fun setDiagnosticsAdapter(adapter: DiagnosticsAdapter) {
content.bottomSheet.setDiagnosticsAdapter(adapter)
}
open fun hideBottomSheet() {
if (editorBottomSheet?.state != BottomSheetBehavior.STATE_COLLAPSED) {
editorBottomSheet?.state = BottomSheetBehavior.STATE_COLLAPSED
}
}
open fun showSearchResults() {
if (editorBottomSheet?.state != BottomSheetBehavior.STATE_EXPANDED) {
editorBottomSheet?.state = BottomSheetBehavior.STATE_EXPANDED
}
val index = content.bottomSheet.pagerAdapter.findIndexOfFragmentByClass(
SearchResultFragment::class.java
)
if (index >= 0 && index < content.bottomSheet.binding.tabs.tabCount) {
content.bottomSheet.binding.tabs.getTabAt(index)?.select()
}
}
open fun handleDiagnosticsResultVisibility(errorVisible: Boolean) {
content.bottomSheet.handleDiagnosticsResultVisibility(errorVisible)
}
open fun handleSearchResultVisibility(errorVisible: Boolean) {
content.bottomSheet.handleSearchResultVisibility(errorVisible)
}
open fun showFirstBuildNotice() {
newMaterialDialogBuilder(this).setPositiveButton(android.R.string.ok, null)
.setTitle(string.title_first_build).setMessage(string.msg_first_build).setCancelable(false)
.create().show()
}
open fun getFileTreeFragment(): FileTreeFragment? {
if (filesTreeFragment == null) {
filesTreeFragment = supportFragmentManager.findFragmentByTag(
FileTreeFragment.TAG
) as FileTreeFragment?
}
return filesTreeFragment
}
fun doSetStatus(text: CharSequence, @GravityInt gravity: Int) {
editorViewModel.statusText = text
editorViewModel.statusGravity = gravity
}
fun refreshSymbolInput() {
provideCurrentEditor()?.also { refreshSymbolInput(it) }
}
fun refreshSymbolInput(editor: CodeEditorView) {
content.bottomSheet.refreshSymbolInput(editor)
}
private fun checkIsDestroying() {
if (!isDestroying && isFinishing) {
isDestroying = true
}
}
private fun handleUiDesignerResult(result: ActivityResult) {
if (result.resultCode != RESULT_OK || result.data == null) {
log.warn(
"UI Designer returned invalid result: resultCode={}, data={}", result.resultCode,
result.data
)
return
}
val generated = result.data!!.getStringExtra(UIDesignerActivity.RESULT_GENERATED_XML)
if (TextUtils.isEmpty(generated)) {
log.warn("UI Designer returned blank generated XML code")
return
}
val view = provideCurrentEditor()
val text = view?.editor?.text ?: run {
log.warn("No file opened to append UI designer result")
return
}
val endLine = text.lineCount - 1
text.replace(0, 0, endLine, text.getColumnCount(endLine), generated)
}
private fun setupDrawers() {
val toggle = ActionBarDrawerToggle(
this, binding.editorDrawerLayout, content.editorToolbar,
string.app_name, string.app_name
)
binding.editorDrawerLayout.addDrawerListener(toggle)
toggle.syncState()
binding.apply {
editorDrawerLayout.apply {
childId = contentCard.id
translationBehaviorStart = ContentTranslatingDrawerLayout.TranslationBehavior.FULL
translationBehaviorEnd = ContentTranslatingDrawerLayout.TranslationBehavior.FULL
setScrimColor(Color.TRANSPARENT)
}
}
}
private fun onBuildStatusChanged() {
log.debug(
"onBuildStatusChanged: isInitializing: ${editorViewModel.isInitializing}, isBuildInProgress: ${editorViewModel.isBuildInProgress}"
)
val visible = editorViewModel.isBuildInProgress || editorViewModel.isInitializing
content.progressIndicator.visibility = if (visible) View.VISIBLE else View.GONE
invalidateOptionsMenu()
}
private fun setupViews() {
editorViewModel._isBuildInProgress.observe(this) { onBuildStatusChanged() }
editorViewModel._isInitializing.observe(this) { onBuildStatusChanged() }
editorViewModel._statusText.observe(this) { content.bottomSheet.setStatus(it.first, it.second) }
editorViewModel.observeFiles(this) { files ->
content.apply {
if (files.isNullOrEmpty()) {
tabs.visibility = View.GONE
viewContainer.displayedChild = 1
} else {
tabs.visibility = View.VISIBLE
viewContainer.displayedChild = 0
}
}
invalidateOptionsMenu()
}
setupNoEditorView()
setupBottomSheet()
if (!app.prefManager.getBoolean(
KEY_BOTTOM_SHEET_SHOWN
) && editorBottomSheet?.state != BottomSheetBehavior.STATE_EXPANDED
) {
editorBottomSheet?.state = BottomSheetBehavior.STATE_EXPANDED
ThreadUtils.runOnUiThreadDelayed({
editorBottomSheet?.state = BottomSheetBehavior.STATE_COLLAPSED
app.prefManager.putBoolean(KEY_BOTTOM_SHEET_SHOWN, true)
}, 1500)
}
binding.contentCard.progress = 0f
binding.swipeReveal.dragListener = object : SwipeRevealLayout.OnDragListener {
override fun onDragStateChanged(swipeRevealLayout: SwipeRevealLayout, state: Int) {}
override fun onDragProgress(swipeRevealLayout: SwipeRevealLayout, progress: Float) {
onSwipeRevealDragProgress(progress)
}
}
}
private fun setupNoEditorView() {
content.noEditorSummary.movementMethod = LinkMovementMethod()
val filesSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
binding.root.openDrawer(GravityCompat.START)
}
}
val bottomSheetSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
editorBottomSheet?.state = BottomSheetBehavior.STATE_EXPANDED
}
}
val sb = SpannableStringBuilder()
appendClickableSpan(sb, string.msg_drawer_for_files, filesSpan)
appendClickableSpan(sb, string.msg_swipe_for_output, bottomSheetSpan)
content.noEditorSummary.text = sb
}
private fun appendClickableSpan(
sb: SpannableStringBuilder,
@StringRes textRes: Int,
span: ClickableSpan,
) {
val str = getString(textRes)
val split = str.split("@@", limit = 3)
if (split.size != 3) {
// Not a valid format
sb.append(str)
sb.append('\n')
return
}
sb.append(split[0])
sb.append(split[1], span, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.append(split[2])
sb.append('\n')
}
private fun setupBottomSheet() {
editorBottomSheet = BottomSheetBehavior.from<View>(content.bottomSheet)
editorBottomSheet?.addBottomSheetCallback(object : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
val editor = provideCurrentEditor()
editor?.editor?.ensureWindowsDismissed()
}
}
override fun onSlide(bottomSheet: View, slideOffset: Float) {
content.apply {
val editorScale = 1 - slideOffset * (1 - EDITOR_CONTAINER_SCALE_FACTOR)
this.bottomSheet.onSlide(slideOffset)
this.viewContainer.scaleX = editorScale
this.viewContainer.scaleY = editorScale
}
}
})
val observer: OnGlobalLayoutListener = object : OnGlobalLayoutListener {
override fun onGlobalLayout() {
contentCardRealHeight = binding.contentCard.height
content.also {
it.realContainer.pivotX = it.realContainer.width.toFloat() / 2f
it.realContainer.pivotY =
(it.realContainer.height.toFloat() / 2f) + (systemBarInsets?.run { bottom - top }
?: 0)
it.viewContainer.viewTreeObserver.removeOnGlobalLayoutListener(this)
}
}
}
content.apply {
viewContainer.viewTreeObserver.addOnGlobalLayoutListener(observer)
bottomSheet.setOffsetAnchor(editorAppBarLayout)
}
}
private fun setupDiagnosticInfo() {
val gd = GradientDrawable()
gd.shape = GradientDrawable.RECTANGLE
gd.setColor(-0xdededf)
gd.setStroke(1, -0x1)
gd.cornerRadius = 8f
diagnosticInfoBinding?.root?.background = gd
diagnosticInfoBinding?.root?.visibility = View.GONE
}
private fun setupContainers() {
handleDiagnosticsResultVisibility(true)
handleSearchResultVisibility(true)
}
private fun onSoftInputChanged() {
if (!isDestroying) {
invalidateOptionsMenu()
content.bottomSheet.onSoftInputChanged()
}
}
private fun showNeedHelpDialog() {
val builder = newMaterialDialogBuilder(this)
builder.setTitle(string.need_help)
builder.setMessage(string.msg_need_help)
builder.setPositiveButton(android.R.string.ok, null)
builder.create().show()
}
open fun installationSessionCallback(): SessionCallback {
return ApkInstallationSessionCallback(this).also { installationCallback = it }
}
}
@@ -0,0 +1,25 @@
/*
* 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.activities.editor
/**
* @author Akash Yadav
*/
class EditorActivityKt : EditorHandlerActivity()
// TODO: Should we adjust to the display cutout insets?
@@ -0,0 +1,705 @@
/*
* 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.activities.editor
import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.MenuItem
import android.view.ViewGroup.LayoutParams
import androidx.annotation.DrawableRes
import androidx.appcompat.view.menu.MenuBuilder
import androidx.collection.MutableIntObjectMap
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.GravityCompat
import com.blankj.utilcode.util.ImageUtils
import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR
import com.itsaky.androidide.actions.ActionsRegistry.Companion.getInstance
import com.itsaky.androidide.actions.FillMenuParams
import com.itsaky.androidide.editor.language.treesitter.JavaLanguage
import com.itsaky.androidide.editor.language.treesitter.JsonLanguage
import com.itsaky.androidide.editor.language.treesitter.KotlinLanguage
import com.itsaky.androidide.editor.language.treesitter.LogLanguage
import com.itsaky.androidide.editor.language.treesitter.TSLanguageRegistry
import com.itsaky.androidide.editor.language.treesitter.XMLLanguage
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.editor.ui.IDEEditor
import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
import com.itsaky.androidide.interfaces.IEditorHandler
import com.itsaky.androidide.models.FileExtension
import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.ui.CodeEditorView
import com.itsaky.androidide.utils.DialogUtils.newYesNoDialog
import com.itsaky.androidide.utils.IntentUtils.openImage
import com.itsaky.androidide.utils.UniqueNameBuilder
import com.itsaky.androidide.utils.flashSuccess
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.collections.set
/**
* Base class for EditorActivity. Handles logic for working with file editors.
*
* @author Akash Yadav
*/
open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
protected val isOpenedFilesSaved = AtomicBoolean(false)
override fun doOpenFile(file: File, selection: Range?) {
openFileAndSelect(file, selection)
}
override fun doCloseAll(runAfter: () -> Unit) {
closeAll(runAfter)
}
override fun provideCurrentEditor(): CodeEditorView? {
return getCurrentEditor()
}
override fun provideEditorAt(index: Int): CodeEditorView? {
return getEditorAtIndex(index)
}
override fun preDestroy() {
super.preDestroy()
TSLanguageRegistry.instance.destroy()
editorViewModel.removeAllFiles()
}
override fun onCreate(savedInstanceState: Bundle?) {
mBuildEventListener.setActivity(this)
super.onCreate(savedInstanceState)
editorViewModel._displayedFile.observe(
this) { this.content.editorContainer.displayedChild = it }
editorViewModel._startDrawerOpened.observe(this) { opened ->
this.binding.editorDrawerLayout.apply {
if (opened) openDrawer(GravityCompat.START) else closeDrawer(GravityCompat.START)
}
}
editorViewModel._filesModified.observe(this) { invalidateOptionsMenu() }
editorViewModel._filesSaving.observe(this) { invalidateOptionsMenu() }
editorViewModel.observeFiles(this) {
// rewrite the cached files index if there are any opened files
val currentFile =
getCurrentEditor()?.editor?.file?.absolutePath
?: run {
editorViewModel.writeOpenedFiles(null)
editorViewModel.openedFilesCache = null
return@observeFiles
}
getOpenedFiles().also {
val cache = OpenedFilesCache(currentFile, it)
editorViewModel.writeOpenedFiles(cache)
editorViewModel.openedFilesCache = cache
}
}
executeAsync {
TSLanguageRegistry.instance.register(JavaLanguage.TS_TYPE, JavaLanguage.FACTORY)
TSLanguageRegistry.instance.register(KotlinLanguage.TS_TYPE_KT, KotlinLanguage.FACTORY)
TSLanguageRegistry.instance.register(KotlinLanguage.TS_TYPE_KTS, KotlinLanguage.FACTORY)
TSLanguageRegistry.instance.register(LogLanguage.TS_TYPE, LogLanguage.FACTORY)
TSLanguageRegistry.instance.register(JsonLanguage.TS_TYPE, JsonLanguage.FACTORY)
TSLanguageRegistry.instance.register(XMLLanguage.TS_TYPE, XMLLanguage.FACTORY)
IDEColorSchemeProvider.initIfNeeded()
}
}
override fun onPause() {
super.onPause()
// if the user manually closes the project, this will be true
// in this case, don't overwrite the already saved cache
if (!isOpenedFilesSaved.get()) {
saveOpenedFiles()
}
}
override fun onResume() {
super.onResume()
isOpenedFilesSaved.set(false)
}
override fun saveOpenedFiles() {
writeOpenedFilesCache(getOpenedFiles(), getCurrentEditor()?.editor?.file)
}
private fun writeOpenedFilesCache(openedFiles: List<OpenedFile>, selectedFile: File?) {
if (selectedFile == null || openedFiles.isEmpty()) {
editorViewModel.writeOpenedFiles(null)
editorViewModel.openedFilesCache = null
log.debug("[onPause] No opened files. Opened files cache reset to null.")
isOpenedFilesSaved.set(true)
return
}
val cache = OpenedFilesCache(selectedFile = selectedFile.absolutePath, allFiles = openedFiles)
editorViewModel.writeOpenedFiles(cache)
editorViewModel.openedFilesCache = if (!isDestroying) cache else null
log.debug("[onPause] Opened files cache reset to {}", editorViewModel.openedFilesCache)
isOpenedFilesSaved.set(true)
}
override fun onStart() {
super.onStart()
try {
editorViewModel.getOrReadOpenedFilesCache(this::onReadOpenedFilesCache)
editorViewModel.openedFilesCache = null
} catch (err: Throwable) {
log.error("Failed to reopen recently opened files", err)
}
}
private fun onReadOpenedFilesCache(cache: OpenedFilesCache?) {
cache ?: return
cache.allFiles.forEach { file ->
openFile(File(file.filePath), file.selection)
}
openFile(File(cache.selectedFile))
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
prepareOptionsMenu(menu)
return true
}
@SuppressLint("RestrictedApi")
override fun onCreateOptionsMenu(menu: Menu): Boolean {
if (menu is MenuBuilder) {
menu.setOptionalIconsVisible(true)
}
val data = createToolbarActionData()
getInstance().fillMenu(FillMenuParams(data, EDITOR_TOOLBAR, menu))
return true
}
open fun prepareOptionsMenu(menu: Menu) {
val data = createToolbarActionData()
val actions = getInstance().getActions(EDITOR_TOOLBAR)
actions.forEach { (_, action) ->
menu.findItem(action.itemId)?.let { item ->
action.prepare(data)
item.isVisible = action.visible
item.isEnabled = action.enabled
item.title = action.label
item.icon = action.icon?.apply {
colorFilter = action.createColorFilter(data)
alpha = if (action.enabled) 255 else 76
}
var showAsAction = action.getShowAsActionFlags(data)
if (showAsAction == -1) {
showAsAction = if (action.icon != null) {
MenuItem.SHOW_AS_ACTION_IF_ROOM
} else {
MenuItem.SHOW_AS_ACTION_NEVER
}
}
if (!action.enabled) {
showAsAction = MenuItem.SHOW_AS_ACTION_NEVER
}
item.setShowAsAction(showAsAction)
action.createActionView(data)?.let { item.actionView = it }
}
}
content.editorToolbar.updateMenuDisplay()
}
private fun createToolbarActionData(): ActionData {
val data = ActionData()
val currentEditor = getCurrentEditor()
data.put(Context::class.java, this)
data.put(CodeEditorView::class.java, currentEditor)
if (currentEditor != null) {
data.put(IDEEditor::class.java, currentEditor.editor)
data.put(File::class.java, currentEditor.file)
}
return data
}
override fun getCurrentEditor(): CodeEditorView? {
return if (editorViewModel.getCurrentFileIndex() != -1) {
getEditorAtIndex(editorViewModel.getCurrentFileIndex())
} else null
}
override fun getEditorAtIndex(index: Int): CodeEditorView? {
return _binding?.content?.editorContainer?.getChildAt(index) as CodeEditorView?
}
override fun openFileAndSelect(file: File, selection: Range?) {
openFile(file, selection)
getEditorForFile(file)?.editor?.also { editor ->
editor.postInLifecycle {
if (selection == null) {
editor.setSelection(0, 0)
return@postInLifecycle
}
editor.validateRange(selection)
editor.setSelection(selection)
}
}
}
override fun openFile(file: File, selection: Range?): CodeEditorView? {
val range = selection ?: Range.NONE
if (ImageUtils.isImage(file)) {
openImage(this, file)
return null
}
val index = openFileAndGetIndex(file, range)
val tab = content.tabs.getTabAt(index)
if (tab != null && index >= 0 && !tab.isSelected) {
tab.select()
}
editorViewModel.startDrawerOpened = false
editorViewModel.displayedFileIndex = index
return try {
getEditorAtIndex(index)
} catch (th: Throwable) {
log.error("Unable to get editor fragment at opened file index {}", index, th)
null
}
}
override fun openFileAndGetIndex(file: File, selection: Range?): Int {
val openedFileIndex = findIndexOfEditorByFile(file)
if (openedFileIndex != -1) {
return openedFileIndex
}
if (!file.exists()) {
return -1
}
val position = editorViewModel.getOpenedFileCount()
log.info("Opening file at index {} file:{}", position, file)
val editor = CodeEditorView(this, file, selection!!)
editor.layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
content.editorContainer.addView(editor)
content.tabs.addTab(content.tabs.newTab())
editorViewModel.addFile(file)
editorViewModel.setCurrentFile(position, file)
updateTabs()
return position
}
override fun getEditorForFile(file: File): CodeEditorView? {
for (i in 0 until editorViewModel.getOpenedFileCount()) {
val editor = content.editorContainer.getChildAt(i) as? CodeEditorView
if (file == editor?.file) return editor
}
return null
}
override fun findIndexOfEditorByFile(file: File?): Int {
if (file == null) {
log.error("Cannot find index of a null file.")
return -1
}
for (i in 0 until editorViewModel.getOpenedFileCount()) {
val opened: File = editorViewModel.getOpenedFile(i)
if (opened == file) {
return i
}
}
return -1
}
override fun saveAllAsync(
notify: Boolean,
requestSync: Boolean,
processResources: Boolean,
progressConsumer: ((Int, Int) -> Unit)?,
runAfter: (() -> Unit)?
) {
editorActivityScope.launch {
saveAll(notify, requestSync, processResources, progressConsumer)
runAfter?.invoke()
}
}
override suspend fun saveAll(
notify: Boolean,
requestSync: Boolean,
processResources: Boolean,
progressConsumer: ((Int, Int) -> Unit)?
): Boolean {
val result = saveAllResult(progressConsumer)
// don't bother to switch the context if we don't need to
if (notify || (result.gradleSaved && requestSync)) {
withContext(Dispatchers.Main) {
if (notify) {
flashSuccess(string.all_saved)
}
if (result.gradleSaved && requestSync) {
editorViewModel.isSyncNeeded = true
}
}
}
if (processResources) {
ProjectManagerImpl.getInstance().generateSources()
}
return result.gradleSaved
}
override suspend fun saveAllResult(progressConsumer: ((Int, Int) -> Unit)?): SaveResult {
return performFileSave {
val result = SaveResult()
for (i in 0 until editorViewModel.getOpenedFileCount()) {
saveResultInternal(i, result)
progressConsumer?.invoke(i + 1, editorViewModel.getOpenedFileCount())
}
return@performFileSave result
}
}
override suspend fun saveResult(index: Int, result: SaveResult) {
performFileSave {
saveResultInternal(index, result)
}
}
private suspend fun saveResultInternal(
index: Int,
result: SaveResult
) : Boolean {
if (index < 0 || index >= editorViewModel.getOpenedFileCount()) {
return false
}
val frag = getEditorAtIndex(index) ?: return false
val fileName = frag.file?.name ?: return false
run {
// Must be called before frag.save()
// Otherwise, it'll always return false
val modified = frag.isModified
if (!frag.save()) {
return false
}
val isGradle = fileName.endsWith(".gradle") || fileName.endsWith(".gradle.kts")
val isXml: Boolean = fileName.endsWith(".xml")
if (!result.gradleSaved) {
result.gradleSaved = modified && isGradle
}
if (!result.xmlSaved) {
result.xmlSaved = modified && isXml
}
}
val hasUnsaved = hasUnsavedFiles()
withContext(Dispatchers.Main) {
editorViewModel.areFilesModified = hasUnsaved
// set tab as unmodified
val tab = content.tabs.getTabAt(index) ?: return@withContext
if (tab.text!!.startsWith('*')) {
tab.text = tab.text!!.substring(startIndex = 1)
}
}
return true
}
private fun hasUnsavedFiles() = editorViewModel.getOpenedFiles().any { file ->
getEditorForFile(file)?.isModified == true
}
private suspend inline fun <T : Any?> performFileSave(crossinline action: suspend () -> T) : T {
setFilesSaving(true)
try {
return action()
} finally {
setFilesSaving(false)
}
}
private suspend fun setFilesSaving(saving: Boolean) {
withContext(Dispatchers.Main.immediate) {
editorViewModel.areFilesSaving = saving
}
}
override fun areFilesModified(): Boolean {
return editorViewModel.areFilesModified
}
override fun areFilesSaving(): Boolean {
return editorViewModel.areFilesSaving
}
override fun closeFile(index: Int, runAfter: () -> Unit) {
if (index < 0 || index >= editorViewModel.getOpenedFileCount()) {
log.error("Invalid file index. Cannot close.")
return
}
val opened = editorViewModel.getOpenedFile(index)
log.info("Closing file: {}", opened)
val editor = getEditorAtIndex(index)
if (editor?.isModified == true) {
log.info("File has been modified: {}", opened)
notifyFilesUnsaved(listOf(editor)) {
closeFile(index, runAfter)
}
return
}
editor?.close() ?: run {
log.error("Cannot save file before close. Editor instance is null")
}
editorViewModel.removeFile(index)
content.apply {
tabs.removeTabAt(index)
editorContainer.removeViewAt(index)
}
editorViewModel.areFilesModified = hasUnsavedFiles()
updateTabs()
runAfter()
}
override fun closeOthers() {
if (editorViewModel.getOpenedFileCount() == 0) {
return
}
val unsavedFiles =
editorViewModel.getOpenedFiles().map(::getEditorForFile)
.filter { it != null && it.isModified }
if (unsavedFiles.isNotEmpty()) {
notifyFilesUnsaved(unsavedFiles) { closeOthers() }
return
}
val file = editorViewModel.getCurrentFile()
var index = 0
// keep closing the file at index 0
// if openedFiles[0] == file, then keep closing files at index 1
while (editorViewModel.getOpenedFileCount() != 1) {
val editor = getEditorAtIndex(index)
if (editor == null) {
log.error("Unable to save file at index {}", index)
continue
}
// Index of files changes as we keep close files
// So we compare the files instead of index
if (file != editor.file) {
closeFile(index)
} else {
index = 1
}
}
}
override fun closeAll(runAfter: () -> Unit) {
val count = editorViewModel.getOpenedFileCount()
val unsavedFiles =
editorViewModel.getOpenedFiles().map(this::getEditorForFile)
.filter { it != null && it.isModified }
if (unsavedFiles.isNotEmpty()) {
// There are unsaved files
notifyFilesUnsaved(unsavedFiles) { closeAll(runAfter) }
return
}
// Files were already saved, close all files one by one
for (i in 0 until count) {
getEditorAtIndex(i)?.close() ?: run {
log.error("Unable to close file at index {}", i)
}
}
editorViewModel.removeAllFiles()
content.apply {
tabs.removeAllTabs()
tabs.requestLayout()
editorContainer.removeAllViews()
}
runAfter()
}
override fun getOpenedFiles() =
editorViewModel.getOpenedFiles().mapNotNull {
val editor = getEditorForFile(it)?.editor ?: return@mapNotNull null
OpenedFile(it.absolutePath, editor.cursorLSPRange)
}
private fun notifyFilesUnsaved(unsavedEditors: List<CodeEditorView?>, invokeAfter: Runnable) {
if (isDestroying) {
// Do not show unsaved files dialog if the activity is being destroyed
// TODO Use a service to save files and to avoid file content loss
for (editor in unsavedEditors) {
editor?.markUnmodified()
}
invokeAfter.run()
return
}
val mapped = unsavedEditors.mapNotNull { it?.file?.absolutePath }
val builder =
newYesNoDialog(
context = this,
title = getString(string.title_files_unsaved),
message = getString(string.msg_files_unsaved, TextUtils.join("\n", mapped)),
positiveClickListener = { dialog, _ ->
dialog.dismiss()
saveAllAsync(notify = true, runAfter = { runOnUiThread(invokeAfter) })
}
) { dialog, _ ->
dialog.dismiss()
// Mark all the files as saved, then try to close them all
for (editor in unsavedEditors) {
editor?.markAsSaved()
}
invokeAfter.run()
}
builder.show()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onFileRenamed(event: FileRenameEvent) {
val index = findIndexOfEditorByFile(event.file)
if (index < 0 || index >= content.tabs.tabCount) {
return
}
val editor = getEditorAtIndex(index) ?: return
editorViewModel.updateFile(index, event.newFile)
editor.updateFile(event.newFile)
updateTabs()
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDocumentChange(event: DocumentChangeEvent) {
// update content modification status
editorViewModel.areFilesModified = true
val index = findIndexOfEditorByFile(event.file.toFile())
if (index == -1) {
return
}
val tab = content.tabs.getTabAt(index)!!
if (tab.text?.startsWith('*') == true) {
return
}
// mark as modified
tab.text = "*${tab.text}"
}
private fun updateTabs() {
editorActivityScope.launch {
val files = editorViewModel.getOpenedFiles()
val dupliCount = mutableMapOf<String, Int>()
val names = MutableIntObjectMap<Pair<String, @DrawableRes Int>>()
val nameBuilder = UniqueNameBuilder<File>("", File.separator)
files.forEach {
var count = dupliCount[it.name] ?: 0
dupliCount[it.name] = ++count
nameBuilder.addPath(it, it.path)
}
for (index in 0 until content.tabs.tabCount) {
val file = files.getOrNull(index) ?: continue
val count = dupliCount[file.name] ?: 0
val isModified = getEditorAtIndex(index)?.isModified ?: false
var name = if (count > 1) nameBuilder.getShortPath(file) else file.name
if (isModified) {
name = "*${name}"
}
names[index] = name to FileExtension.Factory.forFile(file).icon
}
withContext(Dispatchers.Main) {
names.forEach { index, (name, iconId) ->
val tab = content.tabs.getTabAt(index) ?: return@forEach
tab.icon = ResourcesCompat.getDrawable(resources, iconId, theme)
tab.text = name
}
}
}
}
}
@@ -0,0 +1,109 @@
/*
* 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.activities.editor
import android.os.Process
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.transferToStream
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Reads the logs from AndroidIDE and saves it to a file in the projects directory.
*
* @author Akash Yadav
*/
class IDELogcatReader {
private var job: Job? = null
private var shouldRun = false
companion object {
private val log = LoggerFactory.getLogger(IDELogcatReader::class.java)
}
/**
* Start reading the logs.
*/
fun start() {
shouldRun = true
check(job == null) {
"Logcat reader is already running"
}
job = CoroutineScope(Dispatchers.IO).launch {
run()
}
}
/**
* Stop the log reader.
*/
fun stop() {
shouldRun = false
job?.cancel("User requested cancellation")
job = null
}
private fun run() {
val date = Date()
val dateFormat = SimpleDateFormat("yyyy-MM-dd_HH:mm:ss.SSS", Locale.US)
val outputFile = File(Environment.ANDROIDIDE_HOME,
"logs/AndroidIDE-LOG-${dateFormat.format(date)}.txt")
log.debug("Creating output file: {}", outputFile)
outputFile.parentFile!!.mkdirs()
try {
outputFile.createNewFile()
} catch (e: Exception) {
log.error("Failed to create output file for log", e)
return
}
outputFile.outputStream().buffered().use { writer ->
try {
val process = ProcessBuilder(
"logcat",
"--pid=${Process.myPid()}",
"-v",
"threadtime"
).let { builder ->
builder.redirectErrorStream(true)
builder.start()
}
process.inputStream.transferToStream(writer)
writer.flush()
log.info("Process ended with exit code: {}", process.waitFor())
} catch (err: Throwable) {
log.error("Failed to read logs", err)
}
}
}
}
@@ -0,0 +1,727 @@
/*
* 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.activities.editor
import android.content.Intent
import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup.MarginLayoutParams
import android.widget.CheckBox
import androidx.activity.viewModels
import androidx.annotation.GravityInt
import androidx.appcompat.app.AlertDialog
import com.blankj.utilcode.util.SizeUtils
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.databinding.LayoutSearchProjectBinding
import com.itsaky.androidide.flashbar.Flashbar
import com.itsaky.androidide.fragments.sheets.ProgressSheet
import com.itsaky.androidide.handlers.EditorBuildEventListener
import com.itsaky.androidide.handlers.LspHandler.connectClient
import com.itsaky.androidide.handlers.LspHandler.destroyLanguageServers
import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.IDELanguageClientImpl
import com.itsaky.androidide.lsp.java.utils.CancelChecker
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.projects.api.GradleProject
import com.itsaky.androidide.projects.builder.BuildService
import com.itsaky.androidide.services.builder.GradleBuildService
import com.itsaky.androidide.services.builder.GradleBuildServiceConnnection
import com.itsaky.androidide.services.builder.gradleDistributionParams
import com.itsaky.androidide.tasks.executeAsyncProvideError
import com.itsaky.androidide.tasks.executeWithProgress
import com.itsaky.androidide.tooling.api.messages.AndroidInitializationParams
import com.itsaky.androidide.tooling.api.messages.InitializeProjectParams
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_DIRECTORY_INACCESSIBLE
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_DIRECTORY
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult.Failure.PROJECT_NOT_FOUND
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import com.itsaky.androidide.tooling.api.models.mapToSelectedVariants
import com.itsaky.androidide.utils.DURATION_INDEFINITE
import com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder
import com.itsaky.androidide.utils.RecursiveFileSearcher
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.flashbarBuilder
import com.itsaky.androidide.utils.resolveAttr
import com.itsaky.androidide.utils.showOnUiThread
import com.itsaky.androidide.utils.withIcon
import com.itsaky.androidide.viewmodel.BuildVariantsViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
import java.util.concurrent.CompletableFuture
import java.util.regex.Pattern
import java.util.stream.Collectors
/** @author Akash Yadav */
@Suppress("MemberVisibilityCanBePrivate")
abstract class ProjectHandlerActivity : BaseEditorActivity() {
protected val buildVariantsViewModel by viewModels<BuildVariantsViewModel>()
protected var mSearchingProgress: ProgressSheet? = null
protected var mFindInProjectDialog: AlertDialog? = null
protected var syncNotificationFlashbar: Flashbar? = null
protected var isFromSavedInstance = false
protected var shouldInitialize = false
protected var initializingFuture: CompletableFuture<out InitializeResult?>? = null
val findInProjectDialog: AlertDialog
get() {
if (mFindInProjectDialog == null) {
createFindInProjectDialog()
}
return mFindInProjectDialog!!
}
protected val mBuildEventListener = EditorBuildEventListener()
private val buildServiceConnection = GradleBuildServiceConnnection()
companion object {
const val STATE_KEY_FROM_SAVED_INSTANACE = "ide.editor.isFromSavedInstance"
const val STATE_KEY_SHOULD_INITIALIZE = "ide.editor.isInitializing"
}
abstract fun doCloseAll(runAfter: () -> Unit)
abstract fun saveOpenedFiles()
override fun doDismissSearchProgress() {
if (mSearchingProgress?.isShowing == true) {
mSearchingProgress!!.dismiss()
}
}
override fun doConfirmProjectClose() {
confirmProjectClose()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
savedInstanceState?.let {
this.shouldInitialize = it.getBoolean(STATE_KEY_SHOULD_INITIALIZE, true)
this.isFromSavedInstance = it.getBoolean(STATE_KEY_FROM_SAVED_INSTANACE, false)
}
?: run {
this.shouldInitialize = true
this.isFromSavedInstance = false
}
editorViewModel._isSyncNeeded.observe(this) { isSyncNeeded ->
if (!isSyncNeeded) {
// dismiss if already showing
syncNotificationFlashbar?.dismiss()
return@observe
}
if (syncNotificationFlashbar?.isShowing() == true) {
// already shown
return@observe
}
notifySyncNeeded()
}
startServices()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.apply {
putBoolean(STATE_KEY_SHOULD_INITIALIZE, !editorViewModel.isInitializing)
putBoolean(STATE_KEY_FROM_SAVED_INSTANACE, true)
}
}
override fun onPause() {
super.onPause()
if (isDestroying) {
// reset these values here
// sometimes, when the IDE closed and reopened instantly, these values prevent initialization
// of the project
ProjectManagerImpl.getInstance().destroy()
editorViewModel.isInitializing = false
editorViewModel.isBuildInProgress = false
}
}
override fun preDestroy() {
syncNotificationFlashbar?.dismiss()
syncNotificationFlashbar = null
if (isDestroying) {
releaseServerListener()
this.initializingFuture?.cancel(true)
this.initializingFuture = null
closeProject(false)
}
if (IDELanguageClientImpl.isInitialized()) {
IDELanguageClientImpl.shutdown()
}
super.preDestroy()
if (isDestroying) {
try {
stopLanguageServers()
} catch (err: Exception) {
log.error("Failed to stop editor services.")
}
try {
unbindService(buildServiceConnection)
buildServiceConnection.onConnected = {}
} catch (err: Throwable) {
log.error("Unable to unbind service")
} finally {
Lookup.getDefault().apply {
(lookup(BuildService.KEY_BUILD_SERVICE) as? GradleBuildService?)
?.setEventListener(null)
unregister(BuildService.KEY_BUILD_SERVICE)
}
mBuildEventListener.release()
editorViewModel.isBoundToBuildSerice = false
}
}
}
fun setStatus(status: CharSequence) {
setStatus(status, Gravity.CENTER)
}
fun setStatus(status: CharSequence, @GravityInt gravity: Int) {
doSetStatus(status, gravity)
}
fun appendBuildOutput(str: String) {
content.bottomSheet.appendBuildOut(str)
}
fun notifySyncNeeded() {
notifySyncNeeded { initializeProject() }
}
private fun notifySyncNeeded(onConfirm: () -> Unit) {
val buildService = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE)
if (buildService == null || editorViewModel.isInitializing || buildService.isBuildInProgress) return
this.syncNotificationFlashbar?.dismiss()
this.syncNotificationFlashbar = flashbarBuilder(
duration = DURATION_INDEFINITE,
backgroundColor = resolveAttr(R.attr.colorSecondaryContainer),
messageColor = resolveAttr(R.attr.colorOnSecondaryContainer)
)
.withIcon(R.drawable.ic_sync, colorFilter = resolveAttr(R.attr.colorOnSecondaryContainer))
.message(string.msg_sync_needed)
.positiveActionText(string.btn_sync)
.positiveActionTapListener {
onConfirm()
it.dismiss()
}
.negativeActionText(string.btn_ignore_changes)
.negativeActionTapListener(Flashbar::dismiss)
.build()
this.syncNotificationFlashbar?.showOnUiThread()
}
fun startServices() {
val service = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) as GradleBuildService?
if (editorViewModel.isBoundToBuildSerice && service != null) {
log.info("Reusing already started Gradle build service")
onGradleBuildServiceConnected(service)
return
} else {
log.info("Binding to Gradle build service...")
}
buildServiceConnection.onConnected = this::onGradleBuildServiceConnected
if (
bindService(
Intent(this, GradleBuildService::class.java),
buildServiceConnection,
BIND_AUTO_CREATE or BIND_IMPORTANT
)
) {
log.info("Bind request for Gradle build service was successful...")
} else {
log.error("Gradle build service doesn't exist or the IDE is not allowed to access it.")
}
initLspClient()
}
/**
* Initialize (sync) the project.
*
* @param buildVariantsProvider A function which returns the map of project paths to the selected build variants.
* This function is called asynchronously.
*/
fun initializeProject(buildVariantsProvider: () -> Map<String, String>) {
executeWithProgress { progress ->
executeAsyncProvideError(buildVariantsProvider::invoke) { result, error ->
com.itsaky.androidide.tasks.runOnUiThread {
progress.dismiss()
}
if (result == null || error != null) {
val msg = getString(string.msg_build_variants_fetch_failed)
flashError(msg)
log.error(msg, error)
return@executeAsyncProvideError
}
com.itsaky.androidide.tasks.runOnUiThread {
initializeProject(result)
}
}
}
}
fun initializeProject() {
val currentVariants = buildVariantsViewModel._buildVariants.value
// no information about the build variants is available
// use the default variant selections
if (currentVariants == null) {
log.debug(
"No variant selection information available. Default build variants will be selected.")
initializeProject(emptyMap())
return
}
// variant selection information is available
// but there are updated & unsaved variant selections
// use the updated variant selections to initialize the project
if (buildVariantsViewModel.updatedBuildVariants.isNotEmpty()) {
val newSelections = currentVariants.toMutableMap()
newSelections.putAll(buildVariantsViewModel.updatedBuildVariants)
initializeProject {
newSelections.mapToSelectedVariants().also {
log.debug("Initializing project with new build variant selections: {}", it)
}
}
return
}
// variant selection information is available but no variant selections have been updated
// the user might be trying to sync the project from options menu
// initialize the project with the existing selected variants
initializeProject {
log.debug("Re-initializing project with existing build variant selections")
currentVariants.mapToSelectedVariants()
}
}
/**
* Initialize (sync) the project.
*
* @param buildVariants A map of project paths to the selected build variants.
*/
fun initializeProject(buildVariants: Map<String, String>) {
val manager = ProjectManagerImpl.getInstance()
val projectDir = File(manager.projectPath)
if (!projectDir.exists()) {
log.error("GradleProject directory does not exist. Cannot initialize project")
return
}
val initialized = manager.projectInitialized && manager.cachedInitResult != null
log.debug("Is project initialized: {}", initialized)
// When returning after a configuration change between the initialization process,
// we do not want to start another project initialization
if (isFromSavedInstance && initialized && !shouldInitialize) {
log.debug("Skipping init process because initialized && !wasInitializing")
return
}
//noinspection ConstantConditions
ThreadUtils.runOnUiThread { preProjectInit() }
val buildService = Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE)
if (buildService == null) {
log.error("No build service found. Cannot initialize project.")
return
}
if (!buildService.isToolingServerStarted()) {
flashError(string.msg_tooling_server_unavailable)
return
}
this.initializingFuture =
if (shouldInitialize || (!isFromSavedInstance && !initialized)) {
log.debug("Sending init request to tooling server..")
buildService.initializeProject(createProjectInitParams(projectDir, buildVariants))
} else {
// The project initialization was in progress before the configuration change
// In this case, we should not start another project initialization
log.debug("Using cached initialize result as the project is already initialized")
CompletableFuture.supplyAsync {
log.warn("GradleProject has already been initialized. Skipping initialization process.")
manager.cachedInitResult
}
}
this.initializingFuture!!.whenCompleteAsync { result, error ->
releaseServerListener()
if (result == null || !result.isSuccessful || error != null) {
if (!CancelChecker.isCancelled(error)) {
log.error("An error occurred initializing the project with Tooling API", error)
}
ThreadUtils.runOnUiThread {
postProjectInit(false, result?.failure)
}
return@whenCompleteAsync
}
onProjectInitialized(result)
}
}
private fun createProjectInitParams(
projectDir: File,
buildVariants: Map<String, String>
): InitializeProjectParams {
return InitializeProjectParams(
projectDir.absolutePath,
gradleDistributionParams,
createAndroidParams(buildVariants)
)
}
private fun createAndroidParams(buildVariants: Map<String, String>): AndroidInitializationParams {
if (buildVariants.isEmpty()) {
return AndroidInitializationParams.DEFAULT
}
return AndroidInitializationParams(buildVariants)
}
private fun releaseServerListener() {
// Release reference to server listener in order to prevent memory leak
(Lookup.getDefault().lookup(BuildService.KEY_BUILD_SERVICE) as? GradleBuildService?)
?.setServerListener(null)
}
fun stopLanguageServers() {
try {
destroyLanguageServers(isChangingConfigurations)
} catch (err: Throwable) {
log.error("Unable to stop editor services. Please report this issue.", err)
}
}
protected fun onGradleBuildServiceConnected(service: GradleBuildService) {
log.info("Connected to Gradle build service")
buildServiceConnection.onConnected = null
editorViewModel.isBoundToBuildSerice = true
Lookup.getDefault().update(BuildService.KEY_BUILD_SERVICE, service)
service.setEventListener(mBuildEventListener)
if (!service.isToolingServerStarted()) {
service.startToolingServer { pid ->
memoryUsageWatcher.watchProcess(pid, PROC_GRADLE_TOOLING)
resetMemUsageChart()
service.metadata().whenComplete { metadata, err ->
if (metadata == null || err != null) {
log.error("Failed to get tooling server metadata")
return@whenComplete
}
if (pid != metadata.pid) {
log.warn(
"Tooling server pid mismatch. Expected: {}, Actual: {}. Replacing memory watcher...",
pid, metadata.pid)
memoryUsageWatcher.watchProcess(metadata.pid, PROC_GRADLE_TOOLING)
resetMemUsageChart()
}
}
initializeProject()
}
} else {
initializeProject()
}
}
protected open fun onProjectInitialized(result: InitializeResult) {
val manager = ProjectManagerImpl.getInstance()
if (isFromSavedInstance && manager.projectInitialized && result == manager.cachedInitResult) {
log.debug("Not setting up project as this a configuration change")
return
}
manager.cachedInitResult = result
editorActivityScope.launch(Dispatchers.IO) {
manager.setupProject()
manager.notifyProjectUpdate()
updateBuildVariants(manager.androidBuildVariants)
com.itsaky.androidide.tasks.runOnUiThread {
postProjectInit(true, null)
}
}
}
protected open fun preProjectInit() {
setStatus(getString(string.msg_initializing_project))
editorViewModel.isInitializing = true
}
protected open fun postProjectInit(isSuccessful: Boolean, failure: TaskExecutionResult.Failure?) {
val manager = ProjectManagerImpl.getInstance()
if (!isSuccessful) {
val initFailed = getString(string.msg_project_initialization_failed)
setStatus(initFailed)
val msg = when (failure) {
PROJECT_DIRECTORY_INACCESSIBLE -> string.msg_project_dir_inaccessible
PROJECT_NOT_DIRECTORY -> string.msg_file_is_not_dir
PROJECT_NOT_FOUND -> string.msg_project_dir_doesnt_exist
else -> null
}?.let {
"$initFailed: ${getString(it)}"
}
flashError(msg ?: initFailed)
editorViewModel.isInitializing = false
manager.projectInitialized = false
return
}
initialSetup()
setStatus(getString(string.msg_project_initialized))
editorViewModel.isInitializing = false
manager.projectInitialized = true
if (mFindInProjectDialog?.isShowing == true) {
mFindInProjectDialog!!.dismiss()
}
mFindInProjectDialog = null // Create the dialog again if needed
}
private fun updateBuildVariants(buildVariants: Map<String, BuildVariantInfo>) {
// avoid using the 'runOnUiThread' method defined in the activity
com.itsaky.androidide.tasks.runOnUiThread {
buildVariantsViewModel.buildVariants = buildVariants
buildVariantsViewModel.resetUpdatedSelections()
}
}
protected open fun createFindInProjectDialog(): AlertDialog? {
val manager = ProjectManagerImpl.getInstance()
if (manager.rootProject == null) {
log.warn("No root project model found. Is the project initialized?")
flashError(getString(string.msg_project_not_initialized))
return null
}
val moduleDirs =
try {
manager.rootProject!!.subProjects.stream().map(GradleProject::projectDir)
.collect(Collectors.toList())
} catch (e: Throwable) {
flashError(getString(string.msg_no_modules))
emptyList()
}
return createFindInProjectDialog(moduleDirs)
}
protected open fun createFindInProjectDialog(moduleDirs: List<File>): AlertDialog? {
val srcDirs = mutableListOf<File>()
val binding = LayoutSearchProjectBinding.inflate(layoutInflater)
binding.modulesContainer.removeAllViews()
for (i in moduleDirs.indices) {
val module = moduleDirs[i]
val src = File(module, "src")
if (!module.exists() || !module.isDirectory || !src.exists() || !src.isDirectory) {
continue
}
val check = CheckBox(this)
check.text = module.name
check.isChecked = true
val params = MarginLayoutParams(-2, -2)
params.bottomMargin = SizeUtils.dp2px(4f)
binding.modulesContainer.addView(check, params)
srcDirs.add(src)
}
val builder = newMaterialDialogBuilder(this)
builder.setTitle(string.menu_find_project)
builder.setView(binding.root)
builder.setCancelable(false)
builder.setPositiveButton(string.menu_find) { dialog, _ ->
val text = binding.input.editText!!.text.toString().trim()
if (text.isEmpty()) {
flashError(string.msg_empty_search_query)
return@setPositiveButton
}
val searchDirs = mutableListOf<File>()
for (i in 0 until binding.modulesContainer.childCount) {
val check = binding.modulesContainer.getChildAt(i) as CheckBox
if (check.isChecked) {
searchDirs.add(srcDirs[i])
}
}
val extensions = binding.filter.editText!!.text.toString().trim()
val extensionList = mutableListOf<String>()
if (extensions.isNotEmpty()) {
if (extensions.contains("|")) {
for (str in
extensions
.split(Pattern.quote("|").toRegex())
.dropLastWhile { it.isEmpty() }
.toTypedArray()) {
if (str.trim().isEmpty()) {
continue
}
extensionList.add(str)
}
} else {
extensionList.add(extensions)
}
}
if (searchDirs.isEmpty()) {
flashError(string.msg_select_search_modules)
} else {
dialog.dismiss()
getProgressSheet(string.msg_searching_project)?.apply {
show(supportFragmentManager, "search_in_project_progress")
}
RecursiveFileSearcher.searchRecursiveAsync(text, extensionList, searchDirs) { results ->
handleSearchResults(results)
}
}
}
builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() }
mFindInProjectDialog = builder.create()
return mFindInProjectDialog
}
private fun initialSetup() {
val manager = ProjectManagerImpl.getInstance()
GeneralPreferences.lastOpenedProject = manager.projectDirPath
try {
val project = manager.rootProject
if (project == null) {
log.warn("GradleProject not initialized. Skipping initial setup...")
return
}
var projectName = project.rootProject.name
if (projectName.isEmpty()) {
projectName = manager.projectDir.name
}
supportActionBar!!.subtitle = projectName
} catch (th: Throwable) {
// ignored
}
}
private fun closeProject(manualFinish: Boolean) {
if (manualFinish) {
// if the user is manually closing the project,
// save the opened files cache
// this is needed because in this case, the opened files cache will be empty
// when onPause will be called.
saveOpenedFiles()
// reset the lastOpenedProject if the user explicitly chose to close the project
GeneralPreferences.lastOpenedProject = GeneralPreferences.NO_OPENED_PROJECT
}
// Make sure we close files
// This will make sure that file contents are not erased.
doCloseAll {
if (manualFinish) {
finish()
}
}
}
private fun confirmProjectClose() {
val builder = newMaterialDialogBuilder(this)
builder.setTitle(string.title_confirm_project_close)
builder.setMessage(string.msg_confirm_project_close)
builder.setNegativeButton(string.no, null)
builder.setPositiveButton(string.yes) { dialog, _ ->
dialog.dismiss()
closeProject(true)
}
builder.show()
}
private fun initLspClient() {
if (!IDELanguageClientImpl.isInitialized()) {
IDELanguageClientImpl.initialize(this as EditorHandlerActivity)
}
connectClient(IDELanguageClientImpl.getInstance())
}
open fun getProgressSheet(msg: Int): ProgressSheet? {
doDismissSearchProgress()
mSearchingProgress =
ProgressSheet().also {
it.isCancelable = false
it.setMessage(getString(msg))
it.setSubMessageEnabled(false)
}
return mSearchingProgress
}
}
@@ -0,0 +1,100 @@
/*
* 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.core.widget.addTextChangedListener
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.LayoutBuildVariantItemBinding
import com.itsaky.androidide.tooling.api.IAndroidProject
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo
import com.itsaky.androidide.tooling.api.models.BuildVariantInfo.Companion.withSelection
import com.itsaky.androidide.viewmodel.BuildVariantsViewModel
import java.util.Objects
/**
* [RecyclerView] adapter for showing the list of Android modules and their selected build variant.
*
* @property items
* @author Akash Yadav
*/
class BuildVariantsAdapter(
private val viewModel: BuildVariantsViewModel,
private var 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.projectPath
binding.variantName.apply {
val viewModel = viewModel
setAdapter(
ArrayAdapter(binding.root.context, R.layout.support_simple_spinner_dropdown_item,
variantInfo.buildVariants
)
)
var listSelection = variantInfo.buildVariants.indexOf(variantInfo.selectedVariant)
if (listSelection < 0 || listSelection >= variantInfo.buildVariants.size) {
listSelection = 0
}
this.listSelection = listSelection
setText(variantInfo.selectedVariant, false)
addTextChangedListener { editable ->
// update the changed build variants map
viewModel.updatedBuildVariants = viewModel.updatedBuildVariants.also { variants ->
// the newly selected build variant
// if this is different that the variant that was used while initializing the project,
// then the user is notified to re-sync the project
// else the selection is cleared
val newSelection = editable?.toString() ?: IAndroidProject.DEFAULT_VARIANT
if (!Objects.equals(variantInfo.selectedVariant, newSelection)) {
variants[variantInfo.projectPath] = variantInfo.withSelection(newSelection)
} else {
variants.remove(variantInfo.projectPath)
}
}
}
}
}
}
@@ -0,0 +1,110 @@
/*
* 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.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.itsaky.androidide.R
import com.itsaky.androidide.app.IDEApplication
import com.itsaky.androidide.contributors.Contributor
import com.itsaky.androidide.databinding.LayoutContributorsItemBinding
/**
* @author Akash Yadav
*/
class ContributorsGridAdapter(
contributors: List<Contributor>
) : RecyclerView.Adapter<ContributorsGridAdapter.ViewHolder>() {
private val contributors = contributors.toMutableList()
class ViewHolder(val binding: LayoutContributorsItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun getItemCount(): Int {
return contributors.size
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutContributorsItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
val contributor = contributors[position]
if (contributor is EmptyContributor) {
binding.root.visibility = View.INVISIBLE
return
}
Glide.with(binding.root)
.load(contributor.avatarUrl)
.placeholder(R.drawable.ic_account)
.transition(DrawableTransitionOptions.withCrossFade(100))
.into(binding.root)
binding.root.setOnClickListener {
IDEApplication.instance.openUrl(contributor.profileUrl)
}
}
object EmptyContributor : Contributor {
override val id: Int
get() = 0
override val username: String
get() = ""
override val avatarUrl: String
get() = ""
override val profileUrl: String
get() = ""
}
internal fun fillDiff(extras: Int) {
val count = itemCount
for (i in 1..extras) {
contributors.add(EmptyContributor)
}
val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return count
}
override fun getNewListSize(): Int {
return count + extras
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newItemPosition < count && oldItemPosition == newItemPosition
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return areItemsTheSame(oldItemPosition, newItemPosition)
}
})
diff.dispatchUpdatesTo(this)
}
}
@@ -0,0 +1,99 @@
/*
* 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.graphics.PorterDuff;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.itsaky.androidide.databinding.LayoutDiagnosticItemBinding;
import com.itsaky.androidide.interfaces.DiagnosticClickListener;
import com.itsaky.androidide.lsp.models.DiagnosticItem;
import com.itsaky.androidide.lsp.models.DiagnosticSeverity;
import com.itsaky.androidide.resources.R;
import java.io.File;
import java.util.List;
import java.util.Objects;
public class DiagnosticItemAdapter extends RecyclerView.Adapter<DiagnosticItemAdapter.VH> {
private final List<DiagnosticItem> diagnostics;
private final File file;
private final DiagnosticClickListener listener;
public DiagnosticItemAdapter(
List<DiagnosticItem> diagnostics, File file, DiagnosticClickListener listener) {
this.diagnostics = Objects.requireNonNull(diagnostics);
this.file = file;
this.listener = listener;
}
@NonNull
@Override
public DiagnosticItemAdapter.VH onCreateViewHolder(@NonNull ViewGroup p1, int p2) {
return new VH(
LayoutDiagnosticItemBinding.inflate(LayoutInflater.from(p1.getContext()), p1, false));
}
@Override
public void onBindViewHolder(DiagnosticItemAdapter.VH p1, int p2) {
final DiagnosticItem diagnostic = diagnostics.get(p2);
final LayoutDiagnosticItemBinding binding = p1.binding;
binding.icon.setImageResource(getDiagnosticIconId(diagnostic));
binding.icon.setColorFilter(
ContextCompat.getColor(
binding.icon.getContext(),
diagnostic.getSeverity() == DiagnosticSeverity.ERROR
? R.color.diagnostic_error
: R.color.diagnostic_warning),
PorterDuff.Mode.SRC_ATOP);
binding.title.setText(diagnostic.getMessage());
binding
.getRoot()
.setOnClickListener(
v -> {
if (listener != null) {
listener.onDiagnosticClick(file, diagnostic);
}
});
}
@Override
public int getItemCount() {
return diagnostics.size();
}
private int getDiagnosticIconId(DiagnosticItem diagnostic) {
if (diagnostic.getSeverity() == DiagnosticSeverity.ERROR)
return R.drawable.ic_compilation_error;
return R.drawable.ic_info;
}
public static class VH extends RecyclerView.ViewHolder {
private LayoutDiagnosticItemBinding binding;
public VH(LayoutDiagnosticItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
@@ -0,0 +1,84 @@
/*
* 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 androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.itsaky.androidide.databinding.LayoutDiagnosticGroupBinding;
import com.itsaky.androidide.interfaces.DiagnosticClickListener;
import com.itsaky.androidide.models.DiagnosticGroup;
import java.util.List;
import java.util.Objects;
public class DiagnosticsAdapter extends RecyclerView.Adapter<DiagnosticsAdapter.VH> {
private final List<DiagnosticGroup> diagnostics;
private final DiagnosticClickListener listener;
public DiagnosticsAdapter(List<DiagnosticGroup> diagnostics, DiagnosticClickListener listener) {
this.diagnostics = Objects.requireNonNull(diagnostics);
this.listener = listener;
}
@NonNull
@Override
public DiagnosticsAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) {
return new VH(
LayoutDiagnosticGroupBinding.inflate(LayoutInflater.from(p1.getContext()), p1, false));
}
@Override
public void onBindViewHolder(DiagnosticsAdapter.VH p1, int p2) {
final DiagnosticGroup group = diagnostics.get(p2);
final LayoutDiagnosticGroupBinding binding = p1.binding;
binding.info.icon.setImageResource(group.icon);
binding.info.title.setText(group.text);
binding.diagnostics.setLayoutManager(new LinearLayoutManager(binding.diagnostics.getContext()));
binding.diagnostics.setAdapter(
new DiagnosticItemAdapter(group.diagnostics, group.file, listener));
binding
.info
.getRoot()
.setOnClickListener(
v -> {
if (listener != null) {
listener.onGroupClick(group);
}
});
}
@Override
public int getItemCount() {
return diagnostics.size();
}
public class VH extends RecyclerView.ViewHolder {
private LayoutDiagnosticGroupBinding binding;
public VH(LayoutDiagnosticGroupBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
@@ -0,0 +1,174 @@
/*
* 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 androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.collection.LongSparseArray;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager2.adapter.FragmentStateAdapter;
import com.itsaky.androidide.fragments.DiagnosticsListFragment;
import com.itsaky.androidide.fragments.SearchResultFragment;
import com.itsaky.androidide.fragments.output.AppLogFragment;
import com.itsaky.androidide.fragments.output.BuildOutputFragment;
import com.itsaky.androidide.fragments.output.IDELogFragment;
import com.itsaky.androidide.resources.R;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EditorBottomSheetTabAdapter extends FragmentStateAdapter {
private static final Logger LOG = LoggerFactory.getLogger(EditorBottomSheetTabAdapter.class);
private final List<Tab> fragments;
public EditorBottomSheetTabAdapter(@NonNull FragmentActivity fragmentActivity) {
super(fragmentActivity);
var index = -1;
this.fragments = new ArrayList<>();
this.fragments.add(
new Tab(
fragmentActivity.getString(R.string.build_output),
BuildOutputFragment.class,
++index));
this.fragments.add(
new Tab(fragmentActivity.getString(R.string.app_logs), AppLogFragment.class, ++index));
this.fragments.add(
new Tab(fragmentActivity.getString(R.string.ide_logs), IDELogFragment.class, ++index));
this.fragments.add(
new Tab(
fragmentActivity.getString(R.string.view_diags),
DiagnosticsListFragment.class,
++index));
this.fragments.add(
new Tab(
fragmentActivity.getString(R.string.view_search_results),
SearchResultFragment.class,
++index));
}
public Fragment getFragmentAtIndex(int index) {
return getFragmentById(getItemId(index));
}
@Nullable
public Fragment getFragmentById(long itemId) {
final var fragments = getFragments();
if (fragments != null) {
return fragments.get(itemId);
}
return null;
}
@Nullable
private LongSparseArray<Fragment> getFragments() {
try {
final var field = FragmentStateAdapter.class.getDeclaredField("mFragments");
field.setAccessible(true);
return (LongSparseArray<Fragment>) field.get(this);
} catch (Throwable th) {
LOG.error("Unable to reflect fragment list from adapter.");
}
return null;
}
@NonNull
@Override
public Fragment createFragment(int position) {
try {
final var tab = fragments.get(position);
final var klass = Class.forName(tab.name).asSubclass(Fragment.class);
final var constructor = klass.getDeclaredConstructor();
constructor.setAccessible(true);
return constructor.newInstance();
} catch (Throwable th) {
throw new RuntimeException("Unable to create fragment", th);
}
}
@Override
public int getItemCount() {
return fragments.size();
}
public String getTitle(int position) {
return fragments.get(position).title;
}
@Nullable
public BuildOutputFragment getBuildOutputFragment() {
return findFragmentByClass(BuildOutputFragment.class);
}
@Nullable
private <T extends Fragment> T findFragmentByClass(Class<T> clazz) {
final var name = clazz.getName();
for (final var tab : this.fragments) {
if (tab.name.equals(name)) {
return (T) getFragmentById(tab.itemId);
}
}
return null;
}
@Nullable
public AppLogFragment getLogFragment() {
return findFragmentByClass(AppLogFragment.class);
}
@Nullable
public DiagnosticsListFragment getDiagnosticsFragment() {
return findFragmentByClass(DiagnosticsListFragment.class);
}
@Nullable
public SearchResultFragment getSearchResultFragment() {
return findFragmentByClass(SearchResultFragment.class);
}
public <T extends Fragment> int findIndexOfFragmentByClass(@NonNull Class<T> tClass) {
final var name = tClass.getName();
for (int i = 0; i < this.fragments.size(); i++) {
final var tab = this.fragments.get(i);
if (tab.name.equals(name)) {
return i;
}
}
return -1;
}
static class Tab {
final String title;
final String name;
final long itemId;
public Tab(String title, @NonNull Class<? extends Fragment> fragment, long id) {
this.title = title;
this.name = fragment.getName();
this.itemId = id;
}
}
}
@@ -0,0 +1,122 @@
/*
* 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.annotation.SuppressLint
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.DiffUtil.Callback
import androidx.recyclerview.widget.DiffUtil.DiffResult
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
/**
* Filterable [RecyclerView.Adapter].
*
* @author Akash Yadav
*/
abstract class FilterableRecyclerViewAdapter<V : RecyclerView.ViewHolder, D>(val items: List<D>) :
RecyclerView.Adapter<V>() {
protected var filtered: List<D> = mutableListOf<D>().apply { addAll(items) }
private var filterJob: Job? = null
/**
* Filter the list with the given query.
*
* @param query The query.
*/
@SuppressLint("NotifyDataSetChanged")
fun filter(query: String?) {
filterJob?.cancel(CancellationException("A new query has been submitted for filtering"))
val items = this.items
filterJob = CoroutineScope(Dispatchers.Default).launch {
val (filtered, result) = doFilter(query?.trim(), items)
withContext(Dispatchers.Main) {
val adapter = this@FilterableRecyclerViewAdapter
if (result == null) {
adapter.filtered = adapter.items
notifyDataSetChanged()
return@withContext
}
adapter.filtered = filtered
result.dispatchUpdatesTo(adapter)
}
}
}
private fun doFilter(
query: String?,
items: List<D>,
): Pair<List<D>, DiffResult?> {
if (query.isNullOrBlank()) {
return items to null
}
val filtered = items.filter {
onFilter(it, query)
}
val result =
DiffUtil.calculateDiff(
object : Callback() {
override fun getOldListSize(): Int {
return items.size
}
override fun getNewListSize(): Int {
return filtered.size
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return items[oldItemPosition] == filtered[newItemPosition]
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return items[oldItemPosition] == filtered[newItemPosition]
}
}
)
return filtered to result
}
/** Get the list item at given index. */
fun getItem(index: Int): D {
return filtered[index]
}
override fun getItemCount(): Int {
return filtered.size
}
/** Get the query candidate for the given list item. */
abstract fun getQueryCandidate(item: D): String
/** Called on every item when filtering the data. */
protected open fun onFilter(item: D, query: String): Boolean {
return getQueryCandidate(item).contains(query, ignoreCase = true)
}
}
@@ -0,0 +1,60 @@
/*
* 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 androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.databinding.LayoutMainActionItemBinding
import com.itsaky.androidide.models.MainScreenAction
/**
* Adapter for the actions available on the main screen.
*
* @author Akash Yadav
*/
class MainActionsListAdapter
@JvmOverloads
constructor(val actions: List<MainScreenAction> = emptyList()) :
RecyclerView.Adapter<MainActionsListAdapter.VH>() {
class VH(val binding: LayoutMainActionItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
VH(LayoutMainActionItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
override fun getItemCount(): Int = actions.size
fun getAction(index: Int) = actions[index]
override fun onBindViewHolder(holder: VH, position: Int) {
val action = getAction(index = position)
val binding = holder.binding
binding.root.apply {
setText(action.text)
setIconResource(action.icon)
setOnClickListener {
action.onClick?.invoke(action, it)
}
action.onLongClick?.let { onLongClick ->
setOnLongClickListener {
onLongClick(action, it)
}
}
}
}
}
@@ -0,0 +1,79 @@
/************************************************************************************
* 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 androidx.recyclerview.widget.RecyclerView;
import com.itsaky.androidide.databinding.LayoutOptionssheetItemBinding;
import com.itsaky.androidide.models.SheetOption;
import java.util.List;
public class OptionsSheetAdapter extends RecyclerView.Adapter<OptionsSheetAdapter.VH> {
private OnOptionsClickListener listener;
private List<SheetOption> options;
public OptionsSheetAdapter(List<SheetOption> options, OnOptionsClickListener listener) {
this.options = options;
this.listener = listener;
}
@Override
public OptionsSheetAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) {
return new VH(
LayoutOptionssheetItemBinding.inflate(LayoutInflater.from(p1.getContext()), p1, false));
}
@Override
public void onBindViewHolder(OptionsSheetAdapter.VH p1, int p2) {
final LayoutOptionssheetItemBinding binding = p1.binding;
final SheetOption option = options.get(p2);
binding.text.setText(option.title);
binding.icon.setImageDrawable(option.icon);
binding
.getRoot()
.setOnClickListener(
v -> {
if (listener != null) listener.onOptionClick(option);
});
}
@Override
public int getItemCount() {
return options.size();
}
public class VH extends RecyclerView.ViewHolder {
private LayoutOptionssheetItemBinding binding;
public VH(LayoutOptionssheetItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
public static interface OnOptionsClickListener {
public void onOptionClick(SheetOption option);
}
}
@@ -0,0 +1,66 @@
/*
* 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 androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.adapters.RunTasksListAdapter.VH
import com.itsaky.androidide.databinding.LayoutRunTaskItemBinding
import com.itsaky.androidide.models.Checkable
import com.itsaky.androidide.tooling.api.models.GradleTask
/**
* Adapter for showing tasks list in [RunTaskDialogFragment]
* [com.itsaky.androidide.fragments.RunTasksDialogFragment].
*
* @author Akash Yadav
*/
class RunTasksListAdapter
@JvmOverloads
constructor(
tasks: List<Checkable<GradleTask>>,
val onCheckChanged: (Checkable<GradleTask>) -> Unit = {}
) : FilterableRecyclerViewAdapter<VH, Checkable<GradleTask>>(tasks) {
data class VH(val binding: LayoutRunTaskItemBinding) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH {
return VH(LayoutRunTaskItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: VH, position: Int) {
val binding = holder.binding
val data = getItem(position)
val task = data.data
binding.check.isChecked = data.isChecked
binding.taskPath.text = task.path
binding.taskDesc.text = task.description
binding.root.setOnClickListener {
data.isChecked = !data.isChecked
binding.check.isChecked = data.isChecked
onCheckChanged(data)
}
}
override fun getQueryCandidate(item: Checkable<GradleTask>): String {
return item.data.path
}
}
@@ -0,0 +1,98 @@
/**
* 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:></https:>//www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.adapters
import android.graphics.PorterDuff.Mode.SRC_ATOP
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView.Adapter
import androidx.recyclerview.widget.RecyclerView.ViewHolder
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.SearchListAdapter.VH
import com.itsaky.androidide.databinding.LayoutSearchResultGroupBinding
import com.itsaky.androidide.databinding.LayoutSearchResultItemBinding
import com.itsaky.androidide.models.FileExtension
import com.itsaky.androidide.models.SearchResult
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE
import com.itsaky.androidide.syntax.highlighters.JavaHighlighter
import com.itsaky.androidide.utils.resolveAttr
import java.io.File
import java.util.concurrent.CompletableFuture
class SearchListAdapter(
private val results: Map<File, List<SearchResult>?>,
private val onFileClick: (File) -> Unit,
private val onMatchClick: (SearchResult) -> Unit,
private val keys: List<File>
) : Adapter<VH>() {
constructor(
results: Map<File, List<SearchResult>?>,
onFileClick: (File) -> Unit,
onMatchClick: (SearchResult) -> Unit
) : this(results, onFileClick, onMatchClick, results.keys.toList())
override fun onCreateViewHolder(p1: ViewGroup, p2: Int): VH {
return VH(LayoutSearchResultGroupBinding.inflate(LayoutInflater.from(p1.context)))
}
override fun onBindViewHolder(p1: VH, p2: Int) {
val binding = p1.binding
val file = keys[p2]
val matches = results[file] ?: listOf()
val color = binding.icon.context.resolveAttr(R.attr.colorPrimary)
binding.title.text = file.name
binding.icon.setImageResource(FileExtension.Factory.forFile(file).icon)
binding.icon.setColorFilter(color, SRC_ATOP)
binding.items.layoutManager = LinearLayoutManager(binding.items.context)
binding.items.adapter = ChildAdapter(matches)
binding.root.setOnClickListener { onFileClick(file) }
}
override fun getItemCount(): Int {
return results.size
}
inner class ChildAdapter(val matches: List<SearchResult>) : Adapter<ChildVH>() {
override fun onCreateViewHolder(p1: ViewGroup, p2: Int): ChildVH {
return ChildVH(LayoutSearchResultItemBinding.inflate(LayoutInflater.from(p1.context)))
}
override fun onBindViewHolder(p1: ChildVH, p2: Int) {
val match = matches[p2]
val binding = p1.binding
CompletableFuture.runAsync {
try {
val scheme = SchemeAndroidIDE.newInstance(binding.text.context)
val sb = JavaHighlighter().highlight(scheme, match.line, match.match)
ThreadUtils.runOnUiThread { binding.text.text = sb }
} catch (e: Exception) {
ThreadUtils.runOnUiThread { binding.text.text = match.match }
}
}
binding.root.setOnClickListener { onMatchClick(match) }
}
override fun getItemCount(): Int {
return matches.size
}
}
class VH(val binding: LayoutSearchResultGroupBinding) : ViewHolder(binding.root)
class ChildVH(val binding: LayoutSearchResultItemBinding) : ViewHolder(binding.root)
}
@@ -0,0 +1,155 @@
/*
* 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 static com.itsaky.androidide.utils.ResourceUtilsKt.resolveAttr;
import android.annotation.SuppressLint;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.itsaky.androidide.R;
import com.itsaky.androidide.databinding.LayoutSymbolItemBinding;
import com.itsaky.androidide.editor.ui.IDEEditor;
import com.itsaky.androidide.models.Symbol;
import io.github.rosemoe.sora.widget.SelectionMovement;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
public class SymbolInputAdapter extends RecyclerView.Adapter<SymbolInputAdapter.VH> {
private static final List<Character> pairs;
static {
pairs = new ArrayList<>();
pairs.add('}');
pairs.add(')');
pairs.add(']');
pairs.add('"');
pairs.add('\'');
pairs.add('>');
}
private IDEEditor editor;
private final List<Symbol> symbols;
public SymbolInputAdapter(IDEEditor editor) {
this(editor, null);
}
public SymbolInputAdapter(IDEEditor editor, List<Symbol> symbols) {
this.editor = editor;
this.symbols = new ArrayList<>();
this.updateItems(symbols);
}
private void updateItems(List<Symbol> symbols) {
if (symbols == null) {
return;
}
this.symbols.clear();
this.symbols.addAll(symbols);
this.symbols.removeIf(Objects::isNull);
}
@SuppressLint("NotifyDataSetChanged")
public void refresh(IDEEditor editor, List<Symbol> newSymbols) {
this.editor = Objects.requireNonNull(editor);
if (this.symbols.equals(newSymbols)) {
// no need to update symbols
return;
}
updateItems(newSymbols);
notifyDataSetChanged();
}
@NonNull
@Override
public VH onCreateViewHolder(@NonNull ViewGroup parent, int itemType) {
return new VH(
LayoutSymbolItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false));
}
@Override
public void onBindViewHolder(@NonNull VH holder, int position) {
final Symbol symbol = symbols.get(position);
holder.binding.symbol.setText(symbol.getLabel());
holder.binding.symbol.setTextColor(
resolveAttr(holder.binding.symbol.getContext(), R.attr.colorOnSurface));
holder.binding.symbol.setOnClickListener(
__ -> insertSymbol(symbol.getCommit(), symbol.getOffset()));
}
@Override
public int getItemCount() {
return symbols.size();
}
void insertSymbol(String text, int selectionOffset) {
if (selectionOffset < 0 || selectionOffset > text.length()) {
return;
}
final var controller = editor.getSnippetController();
if ("\t".equals(text) && controller.isInSnippet()) {
controller.shiftToNextTabStop();
return;
}
if ("\t".equals(text)) {
editor.indentOrCommitTab();
return;
}
var cur = editor.getText().getCursor();
if (cur.isSelected()) {
editor
.getText()
.delete(cur.getLeftLine(), cur.getLeftColumn(), cur.getRightLine(), cur.getRightColumn());
editor.notifyIMEExternalCursorChange();
}
if (cur.getLeftColumn() < editor.getText().getColumnCount(cur.getLeftLine())
&& text.length() == 1
&& text.charAt(0) == editor.getText().charAt(cur.getLeftLine(), cur.getLeftColumn())
&& pairs.contains(text.charAt(0))) {
editor.moveSelection(SelectionMovement.RIGHT);
} else {
editor.commitText(text);
if (selectionOffset != text.length()) {
editor.setSelection(
cur.getRightLine(), cur.getRightColumn() - (text.length() - selectionOffset));
}
}
}
public static class VH extends RecyclerView.ViewHolder {
LayoutSymbolItemBinding binding;
public VH(LayoutSymbolItemBinding binding) {
super(binding.getRoot());
this.binding = binding;
}
}
}
@@ -0,0 +1,104 @@
/*
* 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.View
import android.view.ViewGroup
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.blankj.utilcode.util.ConvertUtils
import com.google.android.material.shape.CornerFamily
import com.itsaky.androidide.adapters.TemplateListAdapter.ViewHolder
import com.itsaky.androidide.databinding.LayoutTemplateListItemBinding
import com.itsaky.androidide.templates.Template
/**
* [RecyclerView.Adapter] for showing templates in a [RecyclerView].
*
* @author Akash Yadav
*/
class TemplateListAdapter(
templates: List<Template<*>>,
private val onClick: ((Template<*>, ViewHolder) -> Unit)? = null
) : RecyclerView.Adapter<ViewHolder>() {
private val templates = templates.toMutableList()
class ViewHolder(internal val binding: LayoutTemplateListItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(LayoutTemplateListItemBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
))
}
override fun getItemCount(): Int {
return templates.size
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
holder.binding.apply {
val template = templates[position]
if (template == Template.EMPTY) {
root.visibility = View.INVISIBLE
return@apply
}
templateName.setText(template.templateName)
templateIcon.setImageResource(template.thumb)
templateIcon.shapeAppearanceModel =
templateIcon.shapeAppearanceModel.toBuilder()
.setAllCorners(CornerFamily.ROUNDED, ConvertUtils.dp2px(8f).toFloat())
.build()
root.setOnClickListener {
onClick?.invoke(template, holder)
}
}
}
internal fun fillDiff(extras: Int) {
val count = itemCount
for (i in 1..extras) {
templates.add(Template.EMPTY)
}
val diff = DiffUtil.calculateDiff(object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return count
}
override fun getNewListSize(): Int {
return count + extras
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return newItemPosition < count && oldItemPosition == newItemPosition
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return areItemsTheSame(oldItemPosition, newItemPosition)
}
})
diff.dispatchUpdatesTo(this)
}
}
@@ -0,0 +1,63 @@
/*
* 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.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.adapters.TemplateWidgetsListAdapter.WidgetViewHolder
import com.itsaky.androidide.databinding.LayoutTemplateWidgetlistItemBinding
import com.itsaky.androidide.templates.ITemplateWidgetViewProvider
import com.itsaky.androidide.templates.Widget
/**
* A [RecyclerView.Adapter] that is used to show the widgets from templates.
*
* @author Akash Yadav
*/
class TemplateWidgetsListAdapter(private val widgets: List<Widget<*>>) :
RecyclerView.Adapter<WidgetViewHolder>() {
class WidgetViewHolder(
internal val binding: LayoutTemplateWidgetlistItemBinding
) : RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int
): WidgetViewHolder {
return WidgetViewHolder(LayoutTemplateWidgetlistItemBinding.inflate(
LayoutInflater.from(parent.context), parent, false))
}
override fun getItemCount(): Int {
return widgets.size
}
override fun onBindViewHolder(holder: WidgetViewHolder, position: Int) {
holder.binding.apply {
val viewProvider = ITemplateWidgetViewProvider.getInstance()
val widget = widgets[position]
val view = viewProvider.createView(root.context, widget)
root.removeAllViews()
root.addView(view,
LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT))
}
}
}
@@ -0,0 +1,103 @@
/*
* 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.onboarding
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.itsaky.androidide.databinding.LayoutOnboardingItemBinding
import com.itsaky.androidide.models.OnboardingItem
/**
* Default implmentation of [RecyclerView.Adapter] for showing [OnboardingItem]s.
*
* @author Akash Yadav
*/
open class DefaultOnboardingItemAdapter<T : OnboardingItem>(
protected val items: List<T>,
protected val onItemClickListener: OnItemClickListener<T>? = null,
protected val onItemLongClickListener: OnItemLongClickListener<T>? = null
) : RecyclerView.Adapter<DefaultOnboardingItemAdapter.ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutOnboardingItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
doBindViewHolder(holder, position, getItem(position), holder.binding)
}
protected open fun doBindViewHolder(
holder: ViewHolder,
position: Int,
item: T,
binding: LayoutOnboardingItemBinding
) {
binding.content.title.text = item.title
if (item.description.isNotBlank()) {
binding.content.description.text = item.description
} else {
binding.content.description.visibility = View.INVISIBLE
}
if (item.icon != 0) {
binding.content.icon.setImageResource(item.icon)
if (item.iconTint != 0) {
binding.content.icon.supportImageTintList = ColorStateList.valueOf(item.iconTint)
}
} else {
binding.content.icon.visibility = View.INVISIBLE
}
binding.root.isClickable = item.isClickable
binding.root.isFocusable = item.isClickable
if (item.isClickable && onItemClickListener != null) {
binding.root.setOnClickListener { onItemClickListener.onClick(item, position, binding) }
}
if (item.isLongClickable && onItemLongClickListener != null) {
binding.root.setOnLongClickListener { onItemLongClickListener.onLongClick(item, position, binding) }
}
}
override fun getItemCount(): Int {
return items.size
}
fun getItem(index: Int): T {
return items[index]
}
class ViewHolder(val binding: LayoutOnboardingItemBinding) :
RecyclerView.ViewHolder(binding.root)
fun interface OnItemClickListener<T : OnboardingItem> {
fun onClick(item: T, position: Int, binding: LayoutOnboardingItemBinding)
}
fun interface OnItemLongClickListener<T : OnboardingItem> {
fun onLongClick(item: T, position: Int, binding: LayoutOnboardingItemBinding): Boolean
}
}
@@ -0,0 +1,77 @@
/*
* 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.onboarding
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.button.MaterialButton
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.LayoutOnboardingPermissionItemBinding
import com.itsaky.androidide.models.OnboardingPermissionItem
/**
* @author Akash Yadav
*/
class OnboardingPermissionsAdapter(private val permissions: List<OnboardingPermissionItem>,
private val requestPermission: (String) -> Unit) :
RecyclerView.Adapter<OnboardingPermissionsAdapter.ViewHolder>() {
class ViewHolder(val binding: LayoutOnboardingPermissionItemBinding) :
RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
LayoutOnboardingPermissionItemBinding.inflate(LayoutInflater.from(parent.context), parent,
false))
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val binding = holder.binding
val permission = permissions[position]
binding.infoContent.apply {
title.setText(permission.title)
description.setText(permission.description)
}
binding.grantButton.setOnClickListener {
requestPermission(permission.permission)
}
if (permission.isGranted) {
binding.grantButton.apply {
isEnabled = false
text = ""
icon = ContextCompat.getDrawable(binding.root.context, R.drawable.ic_ok)
iconTint = ColorStateList.valueOf(
ContextCompat.getColor(binding.root.context, R.color.green_500))
iconGravity = MaterialButton.ICON_GRAVITY_TEXT_TOP
iconPadding = 0
iconSize = SizeUtils.dp2px(28f)
}
}
}
override fun getItemCount(): Int {
return permissions.size
}
}
@@ -0,0 +1,108 @@
/************************************************************************************
* 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.viewholders;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.LinearLayout;
import androidx.transition.ChangeImageTransform;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.SizeUtils;
import com.itsaky.androidide.databinding.LayoutFiletreeItemBinding;
import com.itsaky.androidide.models.FileExtension;
import com.itsaky.androidide.resources.R;
import com.unnamed.b.atv.model.TreeNode;
import java.io.File;
public class FileTreeViewHolder extends TreeNode.BaseNodeViewHolder<File> {
private LayoutFiletreeItemBinding binding;
public FileTreeViewHolder(Context context) {
super(context);
}
@Override
public View createNodeView(TreeNode node, File file) {
this.binding = LayoutFiletreeItemBinding.inflate(LayoutInflater.from(context));
final var dp15 = SizeUtils.dp2px(15);
final var icon = getIconForFile(file);
final var chevron = binding.filetreeChevron;
binding.filetreeName.setText(file.getName());
binding.filetreeIcon.setImageResource(icon);
final var root = applyPadding(node, binding, dp15);
if (file.isDirectory()) {
chevron.setVisibility(View.VISIBLE);
updateChevronIcon(node.isExpanded());
} else {
chevron.setVisibility(View.INVISIBLE);
}
return root;
}
private void updateChevronIcon(boolean expanded) {
final int chevronIcon;
if (expanded) {
chevronIcon = R.drawable.ic_chevron_down;
} else {
chevronIcon = R.drawable.ic_chevron_right;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeImageTransform());
binding.filetreeChevron.setImageResource(chevronIcon);
}
protected LinearLayout applyPadding(
final TreeNode node, final LayoutFiletreeItemBinding binding, final int padding) {
final var root = binding.getRoot();
root.setPaddingRelative(
root.getPaddingLeft() + (padding * (node.getLevel() - 1)),
root.getPaddingTop(),
root.getPaddingRight(),
root.getPaddingBottom());
return root;
}
protected int getIconForFile(final File file) {
return FileExtension.Factory.forFile(file).getIcon();
}
public void updateChevron(boolean expanded) {
setLoading(false);
updateChevronIcon(expanded);
}
public void setLoading(boolean loading) {
final int viewIndex;
if (loading) {
viewIndex = 1;
} else {
viewIndex = 0;
}
binding.chevronLoadingSwitcher.setDisplayedChild(viewIndex);
}
}

Some files were not shown because too many files have changed in this diff Show More