Dynamically add toolbar actions using ActionsRegistry

This commit is contained in:
Akash Yadav
2022-04-08 20:41:21 +05:30
parent 2a06e020fe
commit abc847c8df
41 changed files with 1760 additions and 204 deletions
+7
View File
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KtfmtSettings">
<option name="enabled" value="true" />
<option name="uiFormatterStyle" value="Dropbox" />
</component>
</project>
+4
View File
@@ -30,11 +30,14 @@
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_copy.xml" value="0.188" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_github.xml" value="0.21837606837606838" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_linear_layout.xml" value="0.175" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_lint.xml" value="0.151" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_min_sdk.xml" value="0.147" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_password_input.xml" value="0.147" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_pick_color.xml" value="0.147" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_quickfix.xml" value="0.14" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_relative_layout.xml" value="0.175" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_run.xml" value="0.151" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_run_debug.xml" value="0.151" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_save.xml" value="0.26205128205128203" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_signature_help.xml" value="0.19907407407407407" />
<entry key="..\:/Projects/AndroidStudioProjects/AndroidIDE/app/src/main/res/drawable/ic_text_actions.xml" value="0.147" />
@@ -127,6 +130,7 @@
<entry key="..\:/Users/Akash Yadav/AndroidStudioProjects/android-ide/app/src/main/res/layout/layout_create_project_content.xml" value="0.33" />
<entry key="..\:/Users/Akash Yadav/AndroidStudioProjects/android-ide/app/src/main/res/layout/layout_widget_item.xml" value="0.264" />
<entry key="..\:/Users/Akash Yadav/AndroidStudioProjects/android-ide/app/src/main/res/menu/menu_designer_widgets.xml" value="0.20854700854700856" />
<entry key="..\:/itsaky/.gradle/caches/transforms-3/e4862cae80f8754ca1a451ad3e00b14c/transformed/jetified-editor-0.10.11/res/drawable/round_select_all_20.xml" value="0.1305" />
</map>
</option>
</component>
+1
View File
@@ -0,0 +1 @@
/build
+37
View File
@@ -0,0 +1,37 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
}
buildFeatures.viewBinding true
compileOptions {
sourceCompatibility rootProject.ext.javaSourceVersion
targetCompatibility rootProject.ext.javaTargetVersion
}
buildTypes {
release {
minifyEnabled true
}
}
}
dependencies {
implementation project(':common')
implementation common.editor
implementation common.utilcode
implementation "androidx.core:core-ktx:1.7.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
repositories {
mavenCentral()
}
View File
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# 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,43 @@
/*
* 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 androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext () {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation ().getTargetContext ();
assertEquals ("com.itsaky.androidide.actions.test", appContext.getPackageName ());
}
}
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<manifest package="com.itsaky.androidide.actions" />
@@ -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,82 @@
/*
* 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.drawable.Drawable
/**
* An action that can be registered using the [ActionsRegistry]
* [com.itsaky.androidide.actions.ActionsRegistry]
* @author Akash Yadav
*/
interface ActionItem {
val id: String
var label: String
var visible: Boolean
var enabled: Boolean
var icon: Drawable?
var requiresUIThread: Boolean
var location: Location
/**
* Prepare the action. Subclasses can modify the visual properties of this action here.
* @param data The data containing various information about the event.
*/
fun prepare(data: ActionData)
/**
* 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.
*/
fun execAction(data: ActionData): Boolean
/**
* 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, execResult: Boolean) {}
/** 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("editor_toolbar"),
/** Location marker for action items shown in editor's text action menu. */
EDITOR_TEXT_ACTIONS("editor_text_actions"),
/**
* Location marker for action items shown in 'Code actions' submenu in editor's text action
* menu.
*/
EDITOR_CODE_ACTIONS("editor_code_actions");
override fun toString(): String {
return id
}
fun forId(id: String): Location {
return values().first { it.id == id }
}
}
}
@@ -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
/**
* 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)
/** Action menus are not supposed to perform any action */
override fun execAction(data: ActionData): Boolean {
return false
}
}
@@ -0,0 +1,80 @@
/*
* 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
/** @author Akash Yadav */
abstract class ActionsRegistry {
companion object {
private var instance: ActionsRegistry? = null
@JvmStatic
fun getInstance(): ActionsRegistry {
if (instance == null) {
instance = DefaultActionsRegistry()
}
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?
/**
* Fill the given menu with the registered actions.
*
* Subclasses must first call [ActionItem.prepare] to update the action.
*/
abstract fun fillMenu(data: ActionData, location: ActionItem.Location, menu: Menu)
/** Clear all the registered actions. */
abstract fun clearActions(location: ActionItem.Location)
}
@@ -0,0 +1,150 @@
/*
* 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.blankj.utilcode.util.ThreadUtils
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.utils.Logger
import java.util.concurrent.CompletableFuture
/**
* Default implementation for the [ActionsRegistry]
*
* @author Akash Yadav
*/
class DefaultActionsRegistry : ActionsRegistry() {
private val log = Logger.newInstance("DefaultActionsRegistry")
private val actions: MutableMap<String, MutableMap<String, ActionItem>> = mutableMapOf()
private fun getActions(location: ActionItem.Location): MutableMap<String, ActionItem> {
if (actions[location.id] == null) {
actions[location.id] = mutableMapOf()
}
return actions[location.id]!!
}
override fun registerAction(action: ActionItem): Boolean {
val actionsAtLocation = getActions(action.location)
actionsAtLocation[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) {
return true
}
return false
}
override fun unregisterAction(id: String): Boolean {
for (locations in this.actions.values) {
val older = locations.remove(id)
if (older != null) {
return true
}
}
return false
}
override fun findAction(location: ActionItem.Location, id: String): ActionItem? =
getActions(location)[id]
override fun clearActions(location: ActionItem.Location) = getActions(location).clear()
override fun fillMenu(data: ActionData, location: ActionItem.Location, menu: Menu) {
val actions = getActions(location)
for (action in actions.values) {
action.prepare(data)
if (!action.visible) {
continue
}
addActionToMenu(menu, action, data)
}
}
private fun addActionToMenu(menu: Menu, action: ActionItem, data: ActionData) {
val item: MenuItem =
if (action is ActionMenu) {
val sub = menu.addSubMenu(action.label)
for (subItem in action.children) {
subItem.prepare(data)
if (subItem.visible) {
addActionToMenu(sub, subItem, data)
}
}
sub.item
} else {
menu.add(action.label)
}
item.isEnabled = action.enabled
item.icon = action.icon
if (item.icon != null) {
item.icon.alpha = if (action.enabled) 255 else 76
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM)
} else {
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
}
if (action !is ActionMenu) {
item.setOnMenuItemClickListener {
if (action.requiresUIThread) {
ThreadUtils.runOnUiThread {
val result = action.execAction(data)
action.postExec(data, result)
}
} else {
execInBackground(action, data, it)
}
true
}
}
}
private fun execInBackground(action: ActionItem, data: ActionData, it: MenuItem) {
val start = System.currentTimeMillis()
CompletableFuture.supplyAsync { action.execAction(data) }.whenComplete { result, error ->
if (result == null || !result || error != null) {
log.error(
"An error occurred when performing action '${it.title}'. Action failed in ${System.currentTimeMillis() - start}ms",
error)
} else {
log.info(
"Action '${it.title}' completed in ${System.currentTimeMillis() - start}ms")
}
ThreadUtils.runOnUiThread { action.postExec(data, result ?: false) }
}
}
}
@@ -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;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* Example local unit test, which will execute on the development machine (host).
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
public class ExampleUnitTest {
@Test
public void addition_isCorrect () {
assertEquals (4, 2 + 2);
}
}
+1
View File
@@ -122,6 +122,7 @@ dependencies {
implementation project(':xml-inflater')
implementation project(':lexers')
implementation project(':common')
implementation project(':actions')
implementation project(':lsp:api')
implementation project(':lsp:java')
@@ -21,6 +21,7 @@ package com.itsaky.androidide;
import android.annotation.SuppressLint;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.drawable.GradientDrawable;
@@ -58,6 +59,7 @@ import com.blankj.utilcode.util.IntentUtils;
import com.blankj.utilcode.util.KeyboardUtils;
import com.blankj.utilcode.util.ResourceUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.blankj.utilcode.util.ThrowableUtils;
import com.blankj.utilcode.util.ZipUtils;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
@@ -65,6 +67,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.navigation.NavigationView;
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
import com.itsaky.androidide.actions.ActionData;
import com.itsaky.androidide.actions.ActionsRegistry;
import com.itsaky.androidide.adapters.DiagnosticsAdapter;
import com.itsaky.androidide.adapters.EditorBottomSheetTabAdapter;
import com.itsaky.androidide.adapters.SearchListAdapter;
@@ -79,6 +83,7 @@ import com.itsaky.androidide.fragments.sheets.OptionsListFragment;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.fragments.sheets.TextSheetFragment;
import com.itsaky.androidide.handlers.BuildServiceHandler;
import com.itsaky.androidide.handlers.EditorActivityActions;
import com.itsaky.androidide.handlers.FileOptionsHandler;
import com.itsaky.androidide.handlers.IDEHandler;
import com.itsaky.androidide.interfaces.DiagnosticClickListener;
@@ -154,24 +159,6 @@ public class EditorActivity extends StudioActivity
private static final int ACTION_ID_OTHERS = 101;
private static final int ACTION_ID_ALL = 102;
private static final Logger LOG = Logger.newInstance("EditorActivity");
/**
* MenuItem(s) that are related to the build process
*
* <p>These items will be disabled once the build process starts and will be enabled after the
* execution
*/
private final int[] BUILD_IDS = {
R.id.menuEditor_quickRun,
R.id.menuEditor_runDebug,
R.id.menuEditor_runRelease,
R.id.menuEditor_runBuild,
R.id.menuEditor_runBundle,
R.id.menuEditor_runClean,
R.id.menuEditor_runCleanBuild,
R.id.menuEditor_lint,
R.id.menuEditor_lintDebug,
R.id.menuEditor_lintRelease
};
private ActivityEditorBinding mBinding;
private LayoutDiagnosticInfoBinding mDiagnosticInfoBinding;
@@ -234,6 +221,9 @@ public class EditorActivity extends StudioActivity
registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
this::onGetUIDesignerResult);
EditorActivityActions.register(this);
ensureToolbarMenu();
}
private void setupDrawerToggle() {
@@ -354,14 +344,18 @@ public class EditorActivity extends StudioActivity
@Override
protected void onPause() {
dispatchOnPauseToEditors();
super.onPause();
dispatchOnPauseToEditors();
EditorActivityActions.clear();
}
@Override
protected void onResume() {
super.onResume();
EditorActivityActions.register(this);
ensureToolbarMenu();
try {
checkForCompilerModule();
dispatchOnResumeToEditors();
@@ -383,88 +377,90 @@ public class EditorActivity extends StudioActivity
super.onDestroy();
}
@Override
@SuppressLint("AlwaysShowAction")
public boolean onPrepareOptionsMenu(Menu menu) {
for (int id : BUILD_IDS) {
MenuItem item = menu.findItem(id);
if (item != null) {
boolean enabled = getBuildService() != null && !getBuildService().isBuilding();
item.setEnabled(enabled);
item.getIcon().setAlpha(enabled ? 255 : 76);
}
}
MenuItem run1 = menu.findItem(R.id.menuEditor_quickRun);
MenuItem run2 = menu.findItem(R.id.menuEditor_run);
MenuItem undo = menu.findItem(R.id.menuEditor_undo);
MenuItem redo = menu.findItem(R.id.menuEditor_redo);
MenuItem save = menu.findItem(R.id.menuEditor_save);
MenuItem def = menu.findItem(R.id.menuEditor_gotoDefinition);
MenuItem ref = menu.findItem(R.id.menuEditor_findReferences);
MenuItem comment = menu.findItem(R.id.menuEditor_commentLine);
MenuItem uncomment = menu.findItem(R.id.menuEditor_uncommentLine);
MenuItem findFile = menu.findItem(R.id.menuEditor_findFile);
MenuItem viewLayout = menu.findItem(R.id.menuEditor_viewLayout);
if (KeyboardUtils.isSoftInputVisible(this)) {
run1.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
run2.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
undo.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
redo.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
viewLayout.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
} else {
run1.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
run2.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
undo.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
redo.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
viewLayout.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
final var editor = getCurrentEditor();
final var file = editor != null ? editor.getFile() : null;
final var notNull = editor != null && file != null;
final var isJava = notNull && file.getName().endsWith(".java");
final var isXml = notNull && file.getName().endsWith(".xml");
final var filesModified = notNull && mViewModel != null && mViewModel.areFilesModified();
final var isLayout =
isXml
&& file.getParentFile() != null
&& Pattern.compile(FileOptionsHandler.LAYOUT_RES_PATH_REGEX)
.matcher(file.getParentFile().getAbsolutePath())
.matches();
final var nullableAlpha = notNull ? 255 : 76;
final var javaFileAlpha = isJava ? 255 : 76;
final var layoutFileAlpha = isLayout ? 255 : 76;
final var saveAlpha = filesModified ? 255 : 76;
undo.setEnabled(notNull);
redo.setEnabled(notNull);
save.setEnabled(filesModified);
comment.setEnabled(notNull);
uncomment.setEnabled(notNull);
findFile.setEnabled(notNull);
def.setEnabled(isJava);
ref.setEnabled(isJava);
undo.getIcon().setAlpha(nullableAlpha);
redo.getIcon().setAlpha(nullableAlpha);
save.getIcon().setAlpha(saveAlpha);
comment.getIcon().setAlpha(nullableAlpha);
uncomment.getIcon().setAlpha(nullableAlpha);
findFile.getIcon().setAlpha(nullableAlpha);
def.getIcon().setAlpha(javaFileAlpha);
ref.getIcon().setAlpha(javaFileAlpha);
viewLayout.setEnabled(isLayout);
viewLayout.getIcon().setAlpha(layoutFileAlpha);
viewLayout.setShowAsActionFlags(
isLayout ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER);
return true;
}
// @Override
// @SuppressLint("AlwaysShowAction")
// public boolean onPrepareOptionsMenu(Menu menu) {
// for (int id : BUILD_IDS) {
// MenuItem item = menu.findItem(id);
// if (item != null) {
// boolean enabled = getBuildService() != null &&
// !getBuildService().isBuilding();
// item.setEnabled(enabled);
// item.getIcon().setAlpha(enabled ? 255 : 76);
// }
// }
//
// MenuItem run1 = menu.findItem(R.id.menuEditor_quickRun);
// MenuItem run2 = menu.findItem(R.id.menuEditor_run);
// MenuItem undo = menu.findItem(R.id.menuEditor_undo);
// MenuItem redo = menu.findItem(R.id.menuEditor_redo);
// MenuItem save = menu.findItem(R.id.menuEditor_save);
// MenuItem def = menu.findItem(R.id.menuEditor_gotoDefinition);
// MenuItem ref = menu.findItem(R.id.menuEditor_findReferences);
// MenuItem comment = menu.findItem(R.id.menuEditor_commentLine);
// MenuItem uncomment = menu.findItem(R.id.menuEditor_uncommentLine);
// MenuItem findFile = menu.findItem(R.id.menuEditor_findFile);
// MenuItem viewLayout = menu.findItem(R.id.menuEditor_viewLayout);
//
// if (KeyboardUtils.isSoftInputVisible(this)) {
// run1.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
// run2.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
// undo.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// redo.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
//
// viewLayout.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
// } else {
// run1.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// run2.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// undo.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
// redo.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
//
// viewLayout.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
// }
//
// final var editor = getCurrentEditor();
// final var file = editor != null ? editor.getFile() : null;
// final var notNull = editor != null && file != null;
// final var isJava = notNull && file.getName().endsWith(".java");
// final var isXml = notNull && file.getName().endsWith(".xml");
// final var filesModified = notNull && mViewModel != null &&
// mViewModel.areFilesModified();
// final var isLayout =
// isXml
// && file.getParentFile() != null
// && Pattern.compile(FileOptionsHandler.LAYOUT_RES_PATH_REGEX)
// .matcher(file.getParentFile().getAbsolutePath())
// .matches();
// final var nullableAlpha = notNull ? 255 : 76;
// final var javaFileAlpha = isJava ? 255 : 76;
// final var layoutFileAlpha = isLayout ? 255 : 76;
// final var saveAlpha = filesModified ? 255 : 76;
//
// undo.setEnabled(notNull);
// redo.setEnabled(notNull);
// save.setEnabled(filesModified);
// comment.setEnabled(notNull);
// uncomment.setEnabled(notNull);
// findFile.setEnabled(notNull);
// def.setEnabled(isJava);
// ref.setEnabled(isJava);
//
// undo.getIcon().setAlpha(nullableAlpha);
// redo.getIcon().setAlpha(nullableAlpha);
// save.getIcon().setAlpha(saveAlpha);
// comment.getIcon().setAlpha(nullableAlpha);
// uncomment.getIcon().setAlpha(nullableAlpha);
// findFile.getIcon().setAlpha(nullableAlpha);
// def.getIcon().setAlpha(javaFileAlpha);
// ref.getIcon().setAlpha(javaFileAlpha);
//
// viewLayout.setEnabled(isLayout);
// viewLayout.getIcon().setAlpha(layoutFileAlpha);
// viewLayout.setShowAsActionFlags(
// isLayout ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER);
//
// return true;
// }
@SuppressLint("RestrictedApi")
@Override
@@ -474,77 +470,21 @@ public class EditorActivity extends StudioActivity
builder.setOptionalIconsVisible(true);
}
getMenuInflater().inflate(R.menu.menu_editor, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (getBuildService() == null) {
LOG.error("Build service is null. Cannot perform option menu actions.");
return false;
}
public void ensureToolbarMenu() {
final var menu = mBinding.editorToolbar.getMenu();
menu.clear();
int id = item.getItemId();
if (id == R.id.menuEditor_runDebug || id == R.id.menuEditor_quickRun) {
getBuildServiceHandler().assembleDebug(true);
} else if (id == R.id.menuEditor_runRelease) {
getBuildService().assembleRelease();
} else if (id == R.id.menuEditor_runClean) {
getBuildService().clean();
} else if (id == R.id.menuEditor_runCleanBuild) {
getBuildService().cleanAndRebuild();
} else if (id == R.id.menuEditor_runStopDaemons) {
getBuildService().stopAllDaemons();
} else if (id == R.id.menuEditor_runBuild) {
getBuildService().build();
} else if (id == R.id.menuEditor_runBundle) {
getBuildService().bundle();
} else if (id == R.id.menuEditor_lint) {
getBuildService().lint();
} else if (id == R.id.menuEditor_lintDebug) {
getBuildService().lintDebug();
} else if (id == R.id.menuEditor_lintRelease) {
getBuildService().lintRelease();
} else if (id == R.id.menuEditor_save) {
saveAll(true, true);
} else if (id == R.id.menuEditor_findProject) {
AlertDialog d = getFindInProjectDialog();
if (d != null) {
d.show();
}
} else if (id == R.id.menuEditor_viewLayout) {
previewLayout();
} else if (id == R.id.menuEditor_daemonStatus) {
showDaemonStatus();
} else if (id == R.id.menuEditor_files) {
if (!mBinding.editorDrawerLayout.isDrawerOpen(GravityCompat.END)) {
mBinding.editorDrawerLayout.openDrawer(GravityCompat.END);
}
} else if (getCurrentEditor() != null) { // Should be checked at last
if (id == R.id.menuEditor_undo) {
this.getCurrentEditor().undo();
} else if (id == R.id.menuEditor_redo) {
this.getCurrentEditor().redo();
} else if (id == R.id.menuEditor_gotoDefinition) {
this.getCurrentEditor().findDefinition();
} else if (id == R.id.menuEditor_findReferences) {
this.getCurrentEditor().findReferences();
} else if (id == R.id.menuEditor_commentLine) {
this.getCurrentEditor().commentLine();
} else if (id == R.id.menuEditor_uncommentLine) {
this.getCurrentEditor().uncommentLine();
} else if (id == R.id.menuEditor_findFile) {
this.getCurrentEditor().beginSearch();
} else if (id == R.id.menuEditor_formatCode) {
if (!this.getCurrentEditor().getEditor().formatCodeAsync()) {
LOG.error("Unable to start format thread to format code...");
}
}
}
invalidateOptionsMenu();
return true;
final var data = new ActionData();
data.put(Context.class, EditorActivity.this);
data.put(CodeEditorView.class, getCurrentEditor());
ActionsRegistry.getInstance()
.fillMenu(
data,
com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR,
menu);
}
@Override
@@ -728,7 +668,7 @@ public class EditorActivity extends StudioActivity
editorView.onEditorSelected();
mViewModel.setCurrentFile(position, editorView.getFile());
refreshSymbolInput(editorView);
invalidateOptionsMenu();
ensureToolbarMenu();
}
@Override
@@ -741,6 +681,10 @@ public class EditorActivity extends StudioActivity
editor.ensureWindowsDismissed();
}
}
if (mViewModel.getOpenedFileCount() <= 0) {
ensureToolbarMenu();
}
}
@Override
@@ -928,7 +872,8 @@ public class EditorActivity extends StudioActivity
modified = modified || editor.isModified();
}
mViewModel.setFilesModified(modified);
final boolean finalModified = modified;
ThreadUtils.runOnUiThread(() -> mViewModel.setFilesModified(finalModified));
}
public void install(@NonNull File apk) {
@@ -984,7 +929,7 @@ public class EditorActivity extends StudioActivity
mBinding.editorContainer.setDisplayedChild(index);
invalidateOptionsMenu();
ensureToolbarMenu();
try {
return getEditorAtIndex(index);
} catch (Throwable th) {
@@ -1049,7 +994,7 @@ public class EditorActivity extends StudioActivity
}
mBinding.tabs.requestLayout();
invalidateOptionsMenu();
ensureToolbarMenu();
}
public void closeAll() {
@@ -1086,7 +1031,7 @@ public class EditorActivity extends StudioActivity
mBinding.tabs.requestLayout();
mBinding.editorContainer.removeAllViews();
invalidateOptionsMenu();
ensureToolbarMenu();
if (onSaved != null) {
onSaved.run();
@@ -1097,6 +1042,14 @@ public class EditorActivity extends StudioActivity
}
}
public boolean areFilesModified() {
return mViewModel.areFilesModified();
}
public EditorViewModel getViewModel() {
return mViewModel;
}
private void notifyFilesUnsaved(List<CodeEditorView> unsavedEditors, Runnable invokeAfter) {
//noinspection ConstantConditions
final var mapped =
@@ -1198,6 +1151,29 @@ public class EditorActivity extends StudioActivity
return -1;
}
public void previewLayout() {
try {
if (getCurrentEditor() == null || getCurrentEditor().getFile() == null) {
LOG.error("No file is opened. Cannot preview layout.");
return;
}
saveAll(false);
final Intent intent = new Intent(this, DesignerActivity.class);
intent.putExtra(
DesignerActivity.KEY_LAYOUT_PATH,
getCurrentEditor().getFile().getAbsolutePath());
intent.putStringArrayListExtra(DesignerActivity.KEY_RES_DIRS, getResourceDirectories());
LOG.info("Launching UI Designer...");
mUIDesignerLauncher.launch(intent);
} catch (Throwable th) {
LOG.error(getString(R.string.err_cannot_preview_layout), th);
getApp().toast(R.string.msg_cannot_preview_layout, Toaster.Type.ERROR);
}
}
/////////////////////////////////////////////////
////////////// PRIVATE APIS /////////////////////
/////////////////////////////////////////////////
@@ -1330,29 +1306,6 @@ public class EditorActivity extends StudioActivity
}
}
private void previewLayout() {
try {
if (getCurrentEditor() == null || getCurrentEditor().getFile() == null) {
LOG.error("No file is opened. Cannot preview layout.");
return;
}
saveAll(false);
final Intent intent = new Intent(this, DesignerActivity.class);
intent.putExtra(
DesignerActivity.KEY_LAYOUT_PATH,
getCurrentEditor().getFile().getAbsolutePath());
intent.putStringArrayListExtra(DesignerActivity.KEY_RES_DIRS, getResourceDirectories());
LOG.info("Launching UI Designer...");
mUIDesignerLauncher.launch(intent);
} catch (Throwable th) {
LOG.error(getString(R.string.err_cannot_preview_layout), th);
getApp().toast(R.string.msg_cannot_preview_layout, Toaster.Type.ERROR);
}
}
@NonNull
private ArrayList<String> getResourceDirectories() {
final var dirs = new ArrayList<String>();
@@ -1504,7 +1457,7 @@ public class EditorActivity extends StudioActivity
}
private void onSoftInputChanged() {
invalidateOptionsMenu();
ensureToolbarMenu();
if (KeyboardUtils.isSoftInputVisible(this)) {
TransitionManager.beginDelayedTransition(mBinding.getRoot(), new Slide(Gravity.TOP));
symbolInput.setVisibility(View.VISIBLE);
@@ -1563,7 +1516,7 @@ public class EditorActivity extends StudioActivity
builder.show();
}
private AlertDialog getFindInProjectDialog() {
public AlertDialog getFindInProjectDialog() {
return mFindInProjectDialog == null ? createFindInProjectDialog() : mFindInProjectDialog;
}
@@ -0,0 +1,55 @@
/*
* 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.EditorActivity
/**
* Marker class for actions that execute build related tasks.
*
* @author Akash Yadav
*/
abstract class BaseBuildAction : EditorActivityAction() {
override fun prepare(data: ActionData) {
val context = getActivity(data)
if (context == null) {
visible = false
return
} else {
visible = true
}
if (isBuildInProgress(context)) {
enabled = false
return
} else {
enabled = true
}
}
override fun postExec(data: ActionData, execResult: Boolean) {
val context = getActivity(data) ?: return
context.ensureToolbarMenu()
}
fun shouldPrepare() = visible && enabled
private fun isBuildInProgress(activity: EditorActivity): Boolean =
activity.buildServiceHandler.isBuilding
}
@@ -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 androidx.core.content.ContextCompat
import com.itsaky.androidide.views.editor.IDEEditor
/** @author Akash Yadav */
abstract class BaseEditorAction : ActionItem {
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) {
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.R.color.primaryIconColor))
return drawable
}
}
@@ -0,0 +1,39 @@
/*
* 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.EditorActivity
/** @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
fun getActivity(data: ActionData): EditorActivity? {
val context = data.get(Context::class.java) ?: return null
return context as EditorActivity
}
}
@@ -0,0 +1,23 @@
/*
* 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
/** @author Akash Yadav */
abstract class EditorActivityActionMenu : EditorActivityAction(), ActionMenu {
override val children: MutableSet<ActionItem> = mutableSetOf()
}
@@ -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
import com.itsaky.androidide.views.editor.CodeEditorView
import java.io.File
/** @author Akash Yadav */
abstract class EditorRelatedAction : EditorActivityAction() {
override fun prepare(data: ActionData) {
val editor = getEditor(data)
val file = data.get(File::class.java)
visible = editor != null && file != null
enabled = visible
}
fun getEditor(data: ActionData): CodeEditorView? = data.get(CodeEditorView::class.java)
}
@@ -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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
/** @author Akash Yadav */
class CopyAction() : BaseEditorAction() {
constructor(context: Context) : this() {
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 = "ideEditor_copy"
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.copyText()
return true
}
}
@@ -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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
/** @author Akash Yadav */
class CutAction() : BaseEditorAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.cut)
val arr = context.obtainStyledAttributes(intArrayOf(android.R.attr.actionModeCutDrawable))
icon = arr.getDrawable(0)?.let { tintDrawable(context, it) }
arr.recycle()
}
override val id: String = "ideEditor_cut"
override 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 com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
/** @author Akash Yadav */
class PasteAction() : BaseEditorAction() {
constructor(context: Context) : this() {
label = context.getString(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
get() = "ideEditor_paste"
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.pasteText()
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.editor
import android.content.Context
import android.content.res.TypedArray
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseEditorAction
/** @author Akash Yadav */
class SelectAllAction() : BaseEditorAction() {
constructor(context: Context) : this() {
label = context.getString(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 = "ideEditor_selectAll"
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.selectAll()
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.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
/** @author Akash Yadav */
class DaemonStatusAction() : EditorActivityAction() {
override val id: String = "editor_daemonStatus"
override var requiresUIThread: Boolean = true
constructor(context: Context) : this() {
label = context.getString(R.string.gradle_daemon_status)
icon = ContextCompat.getDrawable(context, R.drawable.ic_info)
}
override fun prepare(data: ActionData) {
visible = true
enabled = true
}
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.showDaemonStatus()
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.etc
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
/** @author Akash Yadav */
class FileTreeAction() : EditorActivityAction() {
override val id: String = "editor_fileTree"
constructor(context: Context) : this() {
label = context.getString(R.string.msg_file_tree)
icon = ContextCompat.getDrawable(context, R.drawable.ic_folder)
}
override fun prepare(data: ActionData) {
visible = true
enabled = true
}
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
if (!context.binding.root.isDrawerOpen(GravityCompat.END)) {
context.binding.root.openDrawer(GravityCompat.END)
return true
}
return false
}
}
@@ -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.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.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.EditorActivityAction
/** @author Akash Yadav */
class FindActionMenu() : EditorActivityAction(), ActionMenu {
constructor(context: Context) : this() {
label = context.getString(R.string.menu_find)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search)
addAction(FindInFileAction(context))
addAction(FindInProjectAction(context))
}
override val children: MutableSet<ActionItem> = mutableSetOf()
override val id: String = "editor_findActions"
override fun prepare(data: ActionData) {
visible = true
enabled = children.size > 0
}
}
@@ -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.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.EditorRelatedAction
/** @author Akash Yadav */
class FindInFileAction() : EditorRelatedAction() {
override val id: String = "editor_findInFile"
override var requiresUIThread: Boolean = true
constructor(context: Context) : this() {
label = context.getString(R.string.menu_find_file)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_file)
}
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.beginSearch()
return true
}
}
@@ -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.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
/** @author Akash Yadav */
class FindInProjectAction() : EditorActivityAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.menu_find_project)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_project)
}
override val id: String = "editor_findInProject"
override fun prepare(data: ActionData) {
getActivity(data)
?: run {
visible = false
enabled = false
return
}
visible = true
enabled = true
}
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
val dialog = context.findInProjectDialog
return if (dialog != null) {
dialog.show()
true
} else {
false
}
}
}
@@ -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.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.actions.EditorRelatedAction
import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.utils.Logger
import com.itsaky.toaster.Toaster
/** @author Akash Yadav */
class SaveFileAction() : EditorRelatedAction() {
private val log = Logger.newInstance("SaveFileAction")
override val id: String = "editor_saveFile"
constructor(context: Context) : this() {
label = context.getString(R.string.save)
icon = ContextCompat.getDrawable(context, R.drawable.ic_save)
}
override fun prepare(data: ActionData) {
val context =
getActivity(data)
?: run {
visible = false
enabled = false
return
}
visible = context.viewModel.openedFiles.isNotEmpty()
enabled = context.areFilesModified()
}
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
return try {
// Cannot use context.saveAll() because this.execAction is called on non-UI thread
// and saveAll call will result in UI actions
context.saveAllResult()
true
} catch (error: Throwable) {
log.error("Failed to save file", error)
false
}
}
override fun postExec(data: ActionData, execResult: Boolean) {
if (execResult) {
StudioApp.getInstance().toast(R.string.all_saved, Toaster.Type.SUCCESS)
} else {
log.error("Failed to save file")
TODO("Create message in strings.xml")
}
}
}
@@ -0,0 +1,41 @@
/*
* 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.lint
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.BaseBuildAction
/** @author Akash Yadav */
class LintAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.lint)
icon = ContextCompat.getDrawable(context, R.drawable.ic_lint)
}
override val id: String = "editor_lint"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lint()
return true
}
}
@@ -0,0 +1,41 @@
/*
* 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.lint
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.BaseBuildAction
/** @author Akash Yadav */
class LintDebugAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.lint_debug)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_debug)
}
override val id: String = "editor_lintDebug"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lintDebug()
return true
}
}
@@ -0,0 +1,41 @@
/*
* 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.lint
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.BaseBuildAction
/** @author Akash Yadav */
class LintReleaseAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.lint_release)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_release)
}
override val id: String = "editor_lintRelease"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.buildServiceHandler.service.lintRelease()
return true
}
}
@@ -0,0 +1,41 @@
/*
* 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.lint
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class LintTaskMenu() : BaseBuildAction(), ActionMenu {
constructor(context: Context) : this() {
label = context.getString(R.string.lint_tasks)
icon = ContextCompat.getDrawable(context, R.drawable.ic_lint)
addAction(LintAction(context))
addAction(LintDebugAction(context))
addAction(LintReleaseAction(context))
}
override val children: MutableSet<ActionItem> = mutableSetOf()
override val id: String = "editor_lintTasks"
}
@@ -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.text
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.EditorRelatedAction
import com.itsaky.androidide.views.editor.CodeEditorView
/** @author Akash Yadav */
class RedoAction() : EditorRelatedAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.redo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_redo)
}
override val id: String = "editor_redo"
override fun execAction(data: ActionData): Boolean {
val editor = data.get(CodeEditorView::class.java)
return if (editor != null) {
editor.redo()
true
} else {
false
}
}
}
@@ -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.text
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.EditorRelatedAction
import com.itsaky.androidide.views.editor.CodeEditorView
/** @author Akash Yadav */
class UndoAction() : EditorRelatedAction() {
override val id: String = "editor_undo"
constructor(context: Context) : this() {
label = context.getString(R.string.undo)
icon = ContextCompat.getDrawable(context, R.drawable.ic_undo)
}
override fun execAction(data: ActionData): Boolean {
val editor = data.get(CodeEditorView::class.java)
return if (editor != null) {
editor.undo()
true
} else {
false
}
}
}
@@ -54,6 +54,10 @@ public class BuildServiceHandler extends IDEHandler implements BuildListener {
return service;
}
public boolean isBuilding() {
return getService().isBuilding();
}
@Override
public void start() {
Objects.requireNonNull(this.activity());
@@ -150,7 +154,7 @@ public class BuildServiceHandler extends IDEHandler implements BuildListener {
.getApp()
.getPrefManager()
.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false);
activity().invalidateOptionsMenu();
activity().ensureToolbarMenu();
activity().getBinding().buildProgressIndicator.setVisibility(View.GONE);
}
@@ -169,7 +173,7 @@ public class BuildServiceHandler extends IDEHandler implements BuildListener {
.getApp()
.getPrefManager()
.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false);
activity().invalidateOptionsMenu();
activity().ensureToolbarMenu();
activity().getBinding().buildProgressIndicator.setVisibility(View.GONE);
}
@@ -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.handlers
import android.content.Context
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.build.QuickRunAction
import com.itsaky.androidide.actions.build.RunActionMenu
import com.itsaky.androidide.actions.build.StopDaemonsAction
import com.itsaky.androidide.actions.editor.CopyAction
import com.itsaky.androidide.actions.editor.CutAction
import com.itsaky.androidide.actions.editor.PasteAction
import com.itsaky.androidide.actions.editor.SelectAllAction
import com.itsaky.androidide.actions.etc.DaemonStatusAction
import com.itsaky.androidide.actions.etc.FileTreeAction
import com.itsaky.androidide.actions.etc.FindActionMenu
import com.itsaky.androidide.actions.file.SaveFileAction
import com.itsaky.androidide.actions.text.RedoAction
import com.itsaky.androidide.actions.text.UndoAction
/**
* Takes care of registering actions to the actions registry for the editor activity.
*
* @author Akash Yadav
*/
class EditorActivityActions {
companion object {
@JvmStatic
fun register(context: Context) {
val registry = ActionsRegistry.getInstance()
// Toolbar actions
registry.registerAction(UndoAction(context))
registry.registerAction(RedoAction(context))
registry.registerAction(QuickRunAction(context))
registry.registerAction(RunActionMenu(context))
registry.registerAction(SaveFileAction(context))
registry.registerAction(FindActionMenu(context))
registry.registerAction(FileTreeAction(context))
registry.registerAction(DaemonStatusAction(context))
registry.registerAction(StopDaemonsAction(context))
// editor text actions
registry.registerAction(SelectAllAction(context))
registry.registerAction(CutAction(context))
registry.registerAction(CopyAction(context))
registry.registerAction(PasteAction(context))
}
@JvmStatic
fun clear() = ActionsRegistry.getInstance().clearActions(ActionItem.Location.EDITOR_TOOLBAR)
}
}
+1 -4
View File
@@ -9,6 +9,7 @@ import java.util.stream.Collectors
buildscript {
ext.kotlin_version = '1.6.10'
project.ext {
compileSdk = 31
buildTools = "31.0.0"
@@ -37,10 +38,6 @@ buildscript {
}
}
//dependencies {
// implementation 'com.google.googlejavaformat:google-java-format:1.15.0'
//}
subprojects {
apply from: "${rootDir}/gradle/dependencies.gradle"
+1
View File
@@ -12,3 +12,4 @@ include ':lsp:api'
include ':lsp:java'
include ':lsp:xml'
include ':lsp:testing'
include ':actions'