mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
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:
@@ -0,0 +1 @@
|
||||
/build/
|
||||
@@ -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)
|
||||
}
|
||||
Vendored
+21
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
+234
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+100
@@ -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
|
||||
Executable
+169
@@ -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)
|
||||
}
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1 @@
|
||||
android.jetifier.ignorelist=common-30.2.2.jar
|
||||
Executable
+133
@@ -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>
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
Executable
BIN
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>
|
||||
Executable
BIN
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()
|
||||
}
|
||||
}
|
||||
+273
@@ -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
|
||||
}
|
||||
+64
@@ -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
|
||||
}
|
||||
}
|
||||
+42
@@ -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)
|
||||
}
|
||||
}
|
||||
+50
@@ -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)
|
||||
}
|
||||
}
|
||||
+51
@@ -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)
|
||||
}
|
||||
+100
@@ -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?
|
||||
+705
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+727
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+84
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+174
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+122
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+79
@@ -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)
|
||||
}
|
||||
+155
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
+103
@@ -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
|
||||
}
|
||||
}
|
||||
+77
@@ -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
|
||||
}
|
||||
}
|
||||
Executable
+108
@@ -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
Reference in New Issue
Block a user