Resolved merge conflicts

This commit is contained in:
Akash Yadav
2022-06-05 08:23:44 +05:30
parent f95bf4839a
commit 4bab018d2c
471 changed files with 47292 additions and 47292 deletions
+23 -23
View File
@@ -1,24 +1,24 @@
name: Delete old workflow runs
on:
workflow_dispatch:
inputs:
days:
description: 'Number of days.'
required: true
default: 30
minimum_runs:
description: 'The minimum runs to keep for each workflow.'
required: true
default: 6
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: ${{ github.event.inputs.days }}
name: Delete old workflow runs
on:
workflow_dispatch:
inputs:
days:
description: 'Number of days.'
required: true
default: 30
minimum_runs:
description: 'The minimum runs to keep for each workflow.'
required: true
default: 6
jobs:
del_runs:
runs-on: ubuntu-latest
steps:
- name: Delete workflow runs
uses: Mattraks/delete-workflow-runs@v2
with:
token: ${{ github.token }}
repository: ${{ github.repository }}
retain_days: ${{ github.event.inputs.days }}
keep_minimum_runs: ${{ github.event.inputs.minimum_runs }}
+1 -1
View File
@@ -5,7 +5,7 @@
<profile name="Gradle Imported" enabled="true">
<outputRelativeToContentRoot value="true" />
<processorPath useClasspath="false">
<entry name="$PROJECT_DIR$/../../../itsaky/.gradle/caches/modules-2/files-2.1/com.google.auto.value/auto-value/1.9/fd4ec236e20ae0895c30e83260f611bcbc185086/auto-value-1.9.jar" />
<entry name="$USER_HOME$/.gradle/caches/modules-2/files-2.1/com.google.auto.value/auto-value/1.9/fd4ec236e20ae0895c30e83260f611bcbc185086/auto-value-1.9.jar" />
</processorPath>
<module name="AndroidIDE.build-tools.google-java-format.main" />
</profile>
+17
View File
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="b36e6c44" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-06-05T02:50:58.945303Z" />
</component>
</project>
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/build-tools/google-java-format/src/main/java/com/google/googlejavaformat/java/StringWrapper.java" charset="UTF-8" />
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/build-tools/google-java-format/src/main/java/com/google/googlejavaformat/java/StringWrapper.java" charset="UTF-8" />
</component>
</project>
+5 -5
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoogleJavaFormatSettings">
<option name="enabled" value="true" />
</component>
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoogleJavaFormatSettings">
<option name="enabled" value="true" />
</component>
</project>
+5
View File
@@ -61,5 +61,10 @@
<option name="name" value="maven2" />
<option name="url" value="https://repo.eclipse.org/content/repositories/lemminx-snapshots/" />
</remote-repository>
<remote-repository>
<option name="id" value="MavenLocal" />
<option name="name" value="MavenLocal" />
<option name="url" value="file:$USER_HOME$/.m2/repository" />
</remote-repository>
</component>
</project>
+6 -6
View File
@@ -1,7 +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>
<?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>
+179 -179
View File
@@ -1,179 +1,179 @@
# AndroidIDE Changelog
This is the changelog of all the releases of AndroidIDE.
## **v2.0.2-beta** (in development)
### Important notes
<!--Breaking changes and other important stuff here-->
### Additions
- Warn about empty body for the following statements :
- `if`
- `else`
- `try`
- `catch`
- `finally`
- `for`
- `while`
- `do while`
- New java code actions :
- `Generate toString()`: Overrides the `toString()` method for the current class. Always accessible.
- `Generate constructor`: Allows the user to select fields from the current class, then generates a constructor with those fields as parameters.
- `Remove unused imports`: Removes imports that are not used in the source file.
- `Import orderer`: Organizes the imports. Thanks to @MrIkso (#180).
- XML Code formatter. Thanks to @MrIkso (#180).
- New Gradle command line options: `--build-cache` and `--offline`. Thanks to @MrIkso (#180).
- Highlight IDE logs based on the log line priority.
- Added Brazilian Portuguese translations. Thanks to @Hakaisu (#165).
### Removals
<!--Things that were removed-->
### Fixes
- Crash when performing 'Replace all' action in editor (#158).
- Crash when layout inflation fails (#168).
- Always show toasts on UI thread (#160).
- Fixed: Multiline logs in IDE logs are not highlighted properly.
- Fixed: Invalid log line formatting when tag to too long.
- Fixed: Application crashes when layout inflation fails (#168).
- Fixed: Invalid syntax highlighting for text tags in XML files. Thanks to @MrIkso (#180).
- Fixed: Extra empty `new line` gets appendend to files while saving.
### Improvements
- Do not show 'Cut' & 'Paste' action if editor is not editable.
- Use [ported version of NetBeans' `nb-javac`](https://github.com/itsaky/nb-javac-android) for Java language server.
- Removed extra Maven repository URLs from project templates. Thanks to @MrIkso (#188).
- Update dependency versions in project templates and update the Kotlin project template image. Thanks to @BanDroid (#191).
- Updated Hindi translations. Thanks to @Premjit-Chowdhury (#171).
- Updated Russian translation. Thansk to @AndreyKozhev (#181).
## **v2.0.1-beta**
AndroidIDE v2.0.1-beta is here with new features, improvements and bug fixes.
### Important notes
- As 64-bit installation does not work on Android 12 (due
to [restriction by the OS](https://source.android.com/devices/tech/debug/tagged-pointers)), 32-bit
build tools need to be installed when installing AndroidIDE on devices running Android 12.
- Google services and Firebase integration has been removed.
### Additions
- Code formatter for Java source code.
- Support for
[`GestureOverlayView`](https://developer.android.com/reference/android/gesture/GestureOverlayView),
[`ToggleButton`](https://developer.android.com/reference/android/widget/ToggleButton),
[`Switch`](https://developer.android.com/reference/android/widget/Switch),
[`GridLayout`](https://developer.android.com/reference/android/widget/GridLayout),
[`ListView`](https://developer.android.com/reference/android/widget/ListView) (#145) in layout
inflater.
- Preference to disable use of `TYPE_TEXT_VARIATION_VISIBLE_PASSWORD` flag in editor.
- Automatically trigger completion when attributes are inserted from completion window.
- New java code actions :
- Add import
- Add 'throws'
- Generate constructor
- Generate setters/getters
- Field to block
- Remove unused class
- Remove unused method
- Remove unused 'throws'
- You can now override multiple superclass methods at once.
- `DocumentsProvider` for providing access to files in `HOME` directory.
### Removals
- Removed initial view attributes from view tags completion in XMLCompletionProvider.
- Vertical text action window has been removed.
- Removed Google services and Firebase integration.
- Removed the horizontal text actions menu from the editor in favour of the new actions menu.
### Fixes
- Cash in attribute editors when adding an attribute.
- Crash when selecting diagnostic item from diagnostics list.
- Some diagnostic items are not shown in diagnostic window (#96).
- Diagnostics were updated late in the editor.
- Popup windows created by editor are not dismissed even when the file is closed.
- No build output when activity is recreated (#121).
- Crash in UI Designer when activity is recreated (#124).
- Invalid syntax highlighting for application logs (#152).
- Illegal argument exception while overriding methods in Java completion provider.
### Improvements
- Do not import classes from `java.lang` package when completion items are selected.
- XML completion provider now uses 'Match completions in lower case' preference.
- Dismiss all windows shown by editor when file tab is unselected or when the bottom sheet is
expanded.
- Improved the editor actions menu.
- Sort completion items according to their kind and sort text.
- Restore file tree state when application is resumed (#150).
- Updated templates to work with Android 12 (#144).
- Updated Chinese translation. Thanks to @mikofe (#128).
- Updated French translation. Thanks to @Se-Lyan (#136).
- Updated German translation (#144).
## **v2.0.0-beta**
AndroidIDE v2.0.0-beta is here with new features and improvements.
### Important Notes
- This is a pre-release and is not so stable. There are some features that are partially
implemented.
- The minimum SDK has been raised to `26` (`Android O`).
### Additions
- XML Layout Designer with ability to preview XML drawables.
- Added packages for terminal.
- libGDx project template. Thanks to @Smooth-E (#81).
- Kotlin project template. Thanks to @BanDroid (#60).
- Preference to switch between `bash` and `/system/bin/sh` in terminal.
- Preference for matching partial names in java completion.
- Preference to choose between `floating window text actions` and `text actions in ActionMode`.
- Preference to enable/disable font ligatures.
- Show process output when build tools installation fails.
- Ability to expand selection in supported languages.
- Ask user to save unsaved files before closing project.
- Hindi translation. Thanks to @Premjit-Chowdhury (#70, #127).
- German translation. Thanks to @MarvinStelter.
- Indonesian translation. Thanks to @BanDroid (#54, #57, #60, #72, #125).
- Chinese translation. Thanks to @Rosemoe (#56, #73, #104) and @mikofe (#118, #119, #123).
- French translation. Thanks to @Se-Lyan (#79).
- Russian translation. Thanks to @Smooth-E (#97).
### Removals
- Autosave of files is removed. However, a preference has been added for the same which, when
enabled, saves the files automatically before close.
### Fixes
- File does not close and crashes if reopened and closed again.
- Flickering of whole auto completion window.
- Flickering of completion item due to API info.
- Build tools installation fails if `JAVA_HOME` does not exist.
- Invalid syntax highlight for method invocations in java.
- Package private classes not included in java completion.
- IDE crashes when `Paste` option is selected in terminal and no clip is available.
- Fixed a bug due to which the IDE was not able to open empty files.
- `LogSender` class not included in JLS workspace.
### Improvements
- Show crash report when the IDE crashes.
- The Java language server is now embedded in the IDE itself.
- The AndroidIDE Gradle plugin is now packaged with the IDE.
- Show name of opened project.
- Show signature help in window instead of using a `TextView`.
- The java completion provider now uses a custom JDK runtime image with Android framework classes
and is no more dependent on JDK installation.
- Disable `Save` option when all files are saved.
- Allow installation of files from file tree.
### New Contributors
* @BanDroid
* @Rosemoe
* @Premjit-Chowdhury
* @Se-Lyan
* @Smooth-E
* @ansh2166
* @mikofe
# AndroidIDE Changelog
This is the changelog of all the releases of AndroidIDE.
## **v2.0.2-beta** (in development)
### Important notes
<!--Breaking changes and other important stuff here-->
### Additions
- Warn about empty body for the following statements :
- `if`
- `else`
- `try`
- `catch`
- `finally`
- `for`
- `while`
- `do while`
- New java code actions :
- `Generate toString()`: Overrides the `toString()` method for the current class. Always accessible.
- `Generate constructor`: Allows the user to select fields from the current class, then generates a constructor with those fields as parameters.
- `Remove unused imports`: Removes imports that are not used in the source file.
- `Import orderer`: Organizes the imports. Thanks to @MrIkso (#180).
- XML Code formatter. Thanks to @MrIkso (#180).
- New Gradle command line options: `--build-cache` and `--offline`. Thanks to @MrIkso (#180).
- Highlight IDE logs based on the log line priority.
- Added Brazilian Portuguese translations. Thanks to @Hakaisu (#165).
### Removals
<!--Things that were removed-->
### Fixes
- Crash when performing 'Replace all' action in editor (#158).
- Crash when layout inflation fails (#168).
- Always show toasts on UI thread (#160).
- Fixed: Multiline logs in IDE logs are not highlighted properly.
- Fixed: Invalid log line formatting when tag to too long.
- Fixed: Application crashes when layout inflation fails (#168).
- Fixed: Invalid syntax highlighting for text tags in XML files. Thanks to @MrIkso (#180).
- Fixed: Extra empty `new line` gets appendend to files while saving.
### Improvements
- Do not show 'Cut' & 'Paste' action if editor is not editable.
- Use [ported version of NetBeans' `nb-javac`](https://github.com/itsaky/nb-javac-android) for Java language server.
- Removed extra Maven repository URLs from project templates. Thanks to @MrIkso (#188).
- Update dependency versions in project templates and update the Kotlin project template image. Thanks to @BanDroid (#191).
- Updated Hindi translations. Thanks to @Premjit-Chowdhury (#171).
- Updated Russian translation. Thansk to @AndreyKozhev (#181).
## **v2.0.1-beta**
AndroidIDE v2.0.1-beta is here with new features, improvements and bug fixes.
### Important notes
- As 64-bit installation does not work on Android 12 (due
to [restriction by the OS](https://source.android.com/devices/tech/debug/tagged-pointers)), 32-bit
build tools need to be installed when installing AndroidIDE on devices running Android 12.
- Google services and Firebase integration has been removed.
### Additions
- Code formatter for Java source code.
- Support for
[`GestureOverlayView`](https://developer.android.com/reference/android/gesture/GestureOverlayView),
[`ToggleButton`](https://developer.android.com/reference/android/widget/ToggleButton),
[`Switch`](https://developer.android.com/reference/android/widget/Switch),
[`GridLayout`](https://developer.android.com/reference/android/widget/GridLayout),
[`ListView`](https://developer.android.com/reference/android/widget/ListView) (#145) in layout
inflater.
- Preference to disable use of `TYPE_TEXT_VARIATION_VISIBLE_PASSWORD` flag in editor.
- Automatically trigger completion when attributes are inserted from completion window.
- New java code actions :
- Add import
- Add 'throws'
- Generate constructor
- Generate setters/getters
- Field to block
- Remove unused class
- Remove unused method
- Remove unused 'throws'
- You can now override multiple superclass methods at once.
- `DocumentsProvider` for providing access to files in `HOME` directory.
### Removals
- Removed initial view attributes from view tags completion in XMLCompletionProvider.
- Vertical text action window has been removed.
- Removed Google services and Firebase integration.
- Removed the horizontal text actions menu from the editor in favour of the new actions menu.
### Fixes
- Cash in attribute editors when adding an attribute.
- Crash when selecting diagnostic item from diagnostics list.
- Some diagnostic items are not shown in diagnostic window (#96).
- Diagnostics were updated late in the editor.
- Popup windows created by editor are not dismissed even when the file is closed.
- No build output when activity is recreated (#121).
- Crash in UI Designer when activity is recreated (#124).
- Invalid syntax highlighting for application logs (#152).
- Illegal argument exception while overriding methods in Java completion provider.
### Improvements
- Do not import classes from `java.lang` package when completion items are selected.
- XML completion provider now uses 'Match completions in lower case' preference.
- Dismiss all windows shown by editor when file tab is unselected or when the bottom sheet is
expanded.
- Improved the editor actions menu.
- Sort completion items according to their kind and sort text.
- Restore file tree state when application is resumed (#150).
- Updated templates to work with Android 12 (#144).
- Updated Chinese translation. Thanks to @mikofe (#128).
- Updated French translation. Thanks to @Se-Lyan (#136).
- Updated German translation (#144).
## **v2.0.0-beta**
AndroidIDE v2.0.0-beta is here with new features and improvements.
### Important Notes
- This is a pre-release and is not so stable. There are some features that are partially
implemented.
- The minimum SDK has been raised to `26` (`Android O`).
### Additions
- XML Layout Designer with ability to preview XML drawables.
- Added packages for terminal.
- libGDx project template. Thanks to @Smooth-E (#81).
- Kotlin project template. Thanks to @BanDroid (#60).
- Preference to switch between `bash` and `/system/bin/sh` in terminal.
- Preference for matching partial names in java completion.
- Preference to choose between `floating window text actions` and `text actions in ActionMode`.
- Preference to enable/disable font ligatures.
- Show process output when build tools installation fails.
- Ability to expand selection in supported languages.
- Ask user to save unsaved files before closing project.
- Hindi translation. Thanks to @Premjit-Chowdhury (#70, #127).
- German translation. Thanks to @MarvinStelter.
- Indonesian translation. Thanks to @BanDroid (#54, #57, #60, #72, #125).
- Chinese translation. Thanks to @Rosemoe (#56, #73, #104) and @mikofe (#118, #119, #123).
- French translation. Thanks to @Se-Lyan (#79).
- Russian translation. Thanks to @Smooth-E (#97).
### Removals
- Autosave of files is removed. However, a preference has been added for the same which, when
enabled, saves the files automatically before close.
### Fixes
- File does not close and crashes if reopened and closed again.
- Flickering of whole auto completion window.
- Flickering of completion item due to API info.
- Build tools installation fails if `JAVA_HOME` does not exist.
- Invalid syntax highlight for method invocations in java.
- Package private classes not included in java completion.
- IDE crashes when `Paste` option is selected in terminal and no clip is available.
- Fixed a bug due to which the IDE was not able to open empty files.
- `LogSender` class not included in JLS workspace.
### Improvements
- Show crash report when the IDE crashes.
- The Java language server is now embedded in the IDE itself.
- The AndroidIDE Gradle plugin is now packaged with the IDE.
- Show name of opened project.
- Show signature help in window instead of using a `TextView`.
- The java completion provider now uses a custom JDK runtime image with Android framework classes
and is no more dependent on JDK installation.
- Disable `Save` option when all files are saved.
- Allow installation of files from file tree.
### New Contributors
* @BanDroid
* @Rosemoe
* @Premjit-Chowdhury
* @Se-Lyan
* @Smooth-E
* @ansh2166
* @mikofe
+27 -27
View File
@@ -1,28 +1,28 @@
# Contributing
This document outlines how to propose a change to AndrodIDE. By contributing to this project, you agree to abide the terms specified in [CODE OF CONDUCT](./CODE_OF_CONDUCT.md).
## Propose a change
To contribute to this project,
- Fork the repo.
- Clone the forked repo to your local machine.
- Open the project in Android Studio.
- Make your changes.
- Create a pull request.
## Note
`Java 11` is required to build the project.
Building AndroidIDE in AndroidIDE itself is also possible. But that might require some modifications.
If you face `AAPT2` issue while building AndroidIDE in the IDE itself, specify the `android.aapt2FromMavenOverride` property in `gradle.properties` with location of `aapt2` binary from Android SDK. However, this is an **experimental** property.
## Source code format
The source code must have `2-space` indentation (soft tab).
Java source code is formatted using `google-java-format` with `GOOGLE` style formatting.
You can execute `./gradlew formatJavaSources` in terminal to format all Java source files using Google Java Format.
**This will commit the changes as well**.
## Issues & Pull requests
Use the issue/PR templates when possible. Provide a proper title and a clear description for the issue/PR. For bug reports, provide a step-by-step procedure to reproduce the issue. Always put crash logs, build outputs, etc in `code blocks`.
# Contributing
This document outlines how to propose a change to AndrodIDE. By contributing to this project, you agree to abide the terms specified in [CODE OF CONDUCT](./CODE_OF_CONDUCT.md).
## Propose a change
To contribute to this project,
- Fork the repo.
- Clone the forked repo to your local machine.
- Open the project in Android Studio.
- Make your changes.
- Create a pull request.
## Note
`Java 11` is required to build the project.
Building AndroidIDE in AndroidIDE itself is also possible. But that might require some modifications.
If you face `AAPT2` issue while building AndroidIDE in the IDE itself, specify the `android.aapt2FromMavenOverride` property in `gradle.properties` with location of `aapt2` binary from Android SDK. However, this is an **experimental** property.
## Source code format
The source code must have `2-space` indentation (soft tab).
Java source code is formatted using `google-java-format` with `GOOGLE` style formatting.
You can execute `./gradlew formatJavaSources` in terminal to format all Java source files using Google Java Format.
**This will commit the changes as well**.
## Issues & Pull requests
Use the issue/PR templates when possible. Provide a proper title and a clear description for the issue/PR. For bug reports, provide a step-by-step procedure to reproduce the issue. Always put crash logs, build outputs, etc in `code blocks`.
If you want to discuss anything about the project, you can discuss it in the [Telegram group](https://t.me/androidide_discussions).
+37 -37
View File
@@ -1,37 +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()
}
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()
}
+16 -16
View File
@@ -1,17 +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/>.
-->
<?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" />
@@ -1,54 +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();
}
}
/*
* 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();
}
}
@@ -1,113 +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
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): 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
/**
* Return the show as action flags for the menu item.
*
* @return The show as action flags.
*/
fun getShowAsActionFlags(data: ActionData): Int = -1
/**
* Checks if the given [ActionData] has instances of the given [types].
*
* @param data The data to check.
* @param types The type of objects to look for.
* @return `true` if the [data] has the given [types], `false` otherwise.
*/
fun hasRequiredData(data: ActionData, vararg types: Class<*>): Boolean {
for (type in types) {
data.get(type) ?: return false
}
return true
}
/** Marks this action item as invisible. */
fun markInvisible() {
visible = false
enabled = false
}
/** 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.activity.toolbar"),
/** Location marker for action items shown in editor's text action menu. */
EDITOR_TEXT_ACTIONS("editor.textActions"),
/**
* Location marker for action items shown in 'Code actions' submenu in editor's text action
* menu.
*/
EDITOR_CODE_ACTIONS("editor.codeActions"),
/** Location marker for action items show in 'Find & Replace' action mode menu in editor. */
EDITOR_SEARCH_ACTION_MODE("editor.searchActionMode");
override fun toString(): String {
return id
}
fun forId(id: String): Location {
return values().first { it.id == id }
}
}
}
/*
* 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): 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
/**
* Return the show as action flags for the menu item.
*
* @return The show as action flags.
*/
fun getShowAsActionFlags(data: ActionData): Int = -1
/**
* Checks if the given [ActionData] has instances of the given [types].
*
* @param data The data to check.
* @param types The type of objects to look for.
* @return `true` if the [data] has the given [types], `false` otherwise.
*/
fun hasRequiredData(data: ActionData, vararg types: Class<*>): Boolean {
for (type in types) {
data.get(type) ?: return false
}
return true
}
/** Marks this action item as invisible. */
fun markInvisible() {
visible = false
enabled = false
}
/** 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.activity.toolbar"),
/** Location marker for action items shown in editor's text action menu. */
EDITOR_TEXT_ACTIONS("editor.textActions"),
/**
* Location marker for action items shown in 'Code actions' submenu in editor's text action
* menu.
*/
EDITOR_CODE_ACTIONS("editor.codeActions"),
/** Location marker for action items show in 'Find & Replace' action mode menu in editor. */
EDITOR_SEARCH_ACTION_MODE("editor.searchActionMode");
override fun toString(): String {
return id
}
fun forId(id: String): Location {
return values().first { it.id == id }
}
}
}
@@ -1,36 +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
}
}
/*
* 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
}
}
@@ -1,108 +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.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)
/** 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)
}
}
/*
* 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)
/** 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)
}
}
@@ -1,174 +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.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.ILogger
import java.util.concurrent.*
/**
* Default implementation for the [ActionsRegistry]
*
* @author Akash Yadav
*/
class DefaultActionsRegistry : ActionsRegistry() {
private val log = ILogger.newInstance("DefaultActionsRegistry")
private val actions: MutableMap<String, MutableMap<String, ActionItem>> = mutableMapOf()
private val listeners = mutableSetOf<ActionExecListener>()
override 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 registerActionExecListener(listener: ActionExecListener) {
listeners.add(listener)
}
override fun unregisterActionExecListener(listener: ActionExecListener) {
listeners.remove(listener)
}
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.getShowAsActionFlags(data) != -1) {
item.setShowAsAction(action.getShowAsActionFlags(data))
}
if (action !is ActionMenu) {
item.setOnMenuItemClickListener {
if (action.requiresUIThread) {
ThreadUtils.runOnUiThread {
val result = action.execAction(data)
action.postExec(data, result)
notifyActionExec(action, 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 is Boolean && !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)
notifyActionExec(action, result ?: false)
}
}
}
private fun notifyActionExec(action: ActionItem, result: Any) {
for (listener in listeners) {
listener.onExec(action, result)
}
}
}
/*
* 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.ILogger
import java.util.concurrent.*
/**
* Default implementation for the [ActionsRegistry]
*
* @author Akash Yadav
*/
class DefaultActionsRegistry : ActionsRegistry() {
private val log = ILogger.newInstance("DefaultActionsRegistry")
private val actions: MutableMap<String, MutableMap<String, ActionItem>> = mutableMapOf()
private val listeners = mutableSetOf<ActionExecListener>()
override 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 registerActionExecListener(listener: ActionExecListener) {
listeners.add(listener)
}
override fun unregisterActionExecListener(listener: ActionExecListener) {
listeners.remove(listener)
}
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.getShowAsActionFlags(data) != -1) {
item.setShowAsAction(action.getShowAsActionFlags(data))
}
if (action !is ActionMenu) {
item.setOnMenuItemClickListener {
if (action.requiresUIThread) {
ThreadUtils.runOnUiThread {
val result = action.execAction(data)
action.postExec(data, result)
notifyActionExec(action, 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 is Boolean && !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)
notifyActionExec(action, result ?: false)
}
}
}
private fun notifyActionExec(action: ActionItem, result: Any) {
for (listener in listeners) {
listener.onExec(action, result)
}
}
}
+143 -143
View File
@@ -1,144 +1,144 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
applicationId rootProject.ext.packageName
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility rootProject.ext.javaSourceVersion
targetCompatibility rootProject.ext.javaTargetVersion
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
viewBinding true
}
signingConfigs {
debug {
storeFile file ("dev.keystore")
keyAlias "androidide"
storePassword "ed68424fb109e5aa8146e4b86caa72e3"
keyPassword "ed68424fb109e5aa8146e4b86caa72e3"
}
release {
storeFile file ("dev.keystore")
keyAlias "androidide"
storePassword "ed68424fb109e5aa8146e4b86caa72e3"
keyPassword "ed68424fb109e5aa8146e4b86caa72e3"
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
packagingOptions {
resources {
excludes += ['META-INF/eclipse.inf',
'META-INF/CHANGES',
'META-INF/README.md',
'about_files/LICENSE-2.0.txt',
'plugin.xml',
'plugin.properties',
'about.mappings',
'about.properties',
'about.ini',
'modeling32.png']
}
}
lint {
abortOnError false
disable 'VectorPath', 'NestedWeights', 'ContentDescription', 'SmallSp'
}
}
dependencies {
// Annotation processors
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.1'
implementation common.editor
implementation common.utilcode
implementation common.file_picker
implementation common.glide
implementation common.quickaction
implementation common.bll
implementation common.xml2json
implementation google.gson
implementation google.guava
implementation common.jsoup
// AndroidX
implementation androidx.annotation
implementation androidx.appcompat
implementation androidx.cardview
implementation androidx.constraintlayout
implementation androidx.coordinatorlayout
implementation androidx.drawer
implementation androidx.grid
implementation androidx.preference
implementation androidx.recyclerview
implementation androidx.vectors
implementation androidx.animated_vectors
implementation google.material
// Kotlin
implementation "androidx.core:core-ktx:1.7.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Local projects here
implementation project (':build-tools:tooling-api')
implementation project (':treeview')
implementation project (':emulatorview')
implementation project (':xml-inflater')
implementation project (':lexers')
implementation project (':common')
implementation project (':actions')
implementation project (':lsp:api')
implementation project (':lsp:java')
implementation project (':lsp:xml')
implementation fileTree (dir: 'libs', include: ['*.jar'])
// LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// 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 project (path: ':build-tools:tooling-api-impl')
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
compileSdkVersion rootProject.ext.compileSdk
buildToolsVersion rootProject.ext.buildTools
defaultConfig {
applicationId rootProject.ext.packageName
minSdkVersion rootProject.ext.minSdk
targetSdkVersion rootProject.ext.targetSdk
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
}
compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility rootProject.ext.javaSourceVersion
targetCompatibility rootProject.ext.javaTargetVersion
}
kotlinOptions {
jvmTarget = "11"
}
buildFeatures {
viewBinding true
}
signingConfigs {
debug {
storeFile file ("dev.keystore")
keyAlias "androidide"
storePassword "ed68424fb109e5aa8146e4b86caa72e3"
keyPassword "ed68424fb109e5aa8146e4b86caa72e3"
}
release {
storeFile file ("dev.keystore")
keyAlias "androidide"
storePassword "ed68424fb109e5aa8146e4b86caa72e3"
keyPassword "ed68424fb109e5aa8146e4b86caa72e3"
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile ('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
packagingOptions {
resources {
excludes += ['META-INF/eclipse.inf',
'META-INF/CHANGES',
'META-INF/README.md',
'about_files/LICENSE-2.0.txt',
'plugin.xml',
'plugin.properties',
'about.mappings',
'about.properties',
'about.ini',
'modeling32.png']
}
}
lint {
abortOnError false
disable 'VectorPath', 'NestedWeights', 'ContentDescription', 'SmallSp'
}
}
dependencies {
// Annotation processors
annotationProcessor 'com.github.bumptech.glide:compiler:4.13.1'
implementation common.editor
implementation common.utilcode
implementation common.file_picker
implementation common.glide
implementation common.quickaction
implementation common.bll
implementation common.xml2json
implementation google.gson
implementation google.guava
implementation common.jsoup
// AndroidX
implementation androidx.annotation
implementation androidx.appcompat
implementation androidx.cardview
implementation androidx.constraintlayout
implementation androidx.coordinatorlayout
implementation androidx.drawer
implementation androidx.grid
implementation androidx.preference
implementation androidx.recyclerview
implementation androidx.vectors
implementation androidx.animated_vectors
implementation google.material
// Kotlin
implementation "androidx.core:core-ktx:1.7.0"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// Local projects here
implementation project (':build-tools:tooling-api')
implementation project (':treeview')
implementation project (':emulatorview')
implementation project (':xml-inflater')
implementation project (':lexers')
implementation project (':common')
implementation project (':actions')
implementation project (':lsp:api')
implementation project (':lsp:java')
implementation project (':lsp:xml')
implementation fileTree (dir: 'libs', include: ['*.jar'])
// LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
// 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 project (path: ':build-tools:tooling-api-impl')
}
Binary file not shown.
@@ -1,187 +1,187 @@
/*
* 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;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.View;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.util.Pair;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityAboutBinding;
import com.itsaky.androidide.databinding.LayoutAboutItemsBinding;
import com.itsaky.androidide.models.License;
import java.util.ArrayList;
import java.util.List;
public class AboutActivity extends StudioActivity {
public static final String JDK_SOURCE = "https://github.com/itsaky/OpenJDK-Android";
private ActivityAboutBinding binding;
@Override
protected View bindLayout() {
binding = ActivityAboutBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.items.footerText.setText(getFooter());
setupTranslations();
setupLicenses();
LayoutAboutItemsBinding items = binding.items;
TooltipCompat.setTooltipText(items.discuss, getString(R.string.discussions_on_telegram));
TooltipCompat.setTooltipText(items.github, getString(R.string.title_github));
TooltipCompat.setTooltipText(items.email, getString(R.string.about_option_email));
TooltipCompat.setTooltipText(items.website, getString(R.string.about_option_website));
items.github.setOnClickListener(v -> getApp().openGitHub());
items.discuss.setOnClickListener(v -> getApp().openTelegramGroup());
items.email.setOnClickListener(v -> getApp().emailUs());
items.website.setOnClickListener(v -> getApp().openWebsite());
}
private void setupTranslations() {
final var translations = createTranslationsList();
final var sb = new StringBuilder();
for (var translation : translations) {
final var author = translation.first;
final var language = translation.second;
sb.append(author);
sb.append(" - ");
sb.append(language);
sb.append("\n");
}
binding.items.translations.setText(sb);
}
/**
* Create the list of translations. The list items are pairs. The first item of the pair is the
* name of the language. The second item is the name of the person who made the translations.
*
* <p>If you're adding translations, make sure you add an item to this list. You can choose to
* display the language in a localized version. For example : Language: हिन्दी - < name of author
* >
*
* <p>Prefer using unicode escapes for language names. For example, हिन्दी can be written as :
* Language: \u0939\u093f\u0928\u094d\u0926\u0940 - < name of author >
*
* @return The translations list.
*/
private List<Pair<String, String>> createTranslationsList() {
final var list = new ArrayList<Pair<String, String>>();
list.add(Pair.create("Bahasa Indonesia", "Fitrah Nuno Syahbani"));
list.add(Pair.create("\u4e2d\u6587", "Rosemoe and @mikofe")); // 中文
list.add(Pair.create("Deutsch", "Marvin Stelter"));
list.add(Pair.create("\u0939\u093f\u0928\u094d\u0926\u0940", "Premjit Chowdhury")); // हिन्दी
list.add(Pair.create("Russian", "Smooth-E"));
list.add(Pair.create("French", "Se-Lyan"));
list.add(Pair.create("Portuguese-Brazilian", "Frederick'XS"));
return list;
}
private void setupLicenses() {
final var licenses = new ArrayList<License>();
licenses.add(
new License(
"AndroidX Libraries", "Apache License 2.0", "https://github.com/androidx/androidx"));
licenses.add(
new License("Gradle Build Tool", "Apache License 2.0", "https://github.com/gradle/gradle"));
licenses.add(new License("Gson", "Apache License 2.0", "https://github.com/google/gson"));
licenses.add(
new License(
"Material Components for Android",
"Apache License 2.0",
"https://github.com/material-components/material-components-android"));
licenses.add(
new License("ANTLR4 Runtime", "BSD 3-clause License", "https://github.com/antlr/antlr4"));
licenses.add(
new License("Glide", "BSD, part MIT and Apache 2.0", "https://github.com/bumptech/glide"));
licenses.add(
new License(
"Android UtilCode", "Apache License 2.0", "https://github.com/Blankj/AndroidUtilCode"));
licenses.add(
new License(
"UnicornFilePicker",
"Apache License 2.0",
"https://github.com/abhishekti7/UnicornFilePicker"));
licenses.add(
new License("QuickAction", "Apache License 2.0", "https://github.com/piruin/quickaction"));
licenses.add(
new License("XmlToJson", "Apache License 2.0", "https://github.com/smart-fun/XmlToJson"));
licenses.add(
new License(
"AndroidTreeView",
"Apache License 2.0",
"https://github.com/bmelnychuk/AndroidTreeView"));
licenses.add(
new License(
"CodeEditor v0.5.2", "Apache License 2.0", "https://github.com/Rosemoe/CodeEditor"));
licenses.add(
new License("Guava-Android", "Apache License 2.0", "https://github.com/google/guava"));
licenses.add(new License("Jsoup", "MIT License", "https://github.com/jhy/jsoup"));
licenses.add(
new License("Termux [Terminal Emulator]", "GPL 3", "https://github.com/termux/termux-app"));
licenses.add(
new License("JavaPoet", "Apache License 2.0", "https://github.com/square/JavaPoet"));
licenses.add(
new License("Zip4j", "Apache License 2.0", "https://github.com/srikanth-lingala/zip4j"));
StringBuilder sb = new StringBuilder();
for (License license : licenses) {
sb.append("\u2022 ");
sb.append(license.name);
sb.append("<br>");
sb.append(getString(R.string.msg_about_licensed_under));
sb.append(license.license);
sb.append("<br><a href=\"");
sb.append(license.url);
sb.append("\">");
sb.append(license.url);
sb.append("</a>");
sb.append("<br><br>");
}
sb.append(getString(R.string.license_jdk));
sb.append(String.format("<br><a href=\"%1$s\">%1$s</a>", JDK_SOURCE));
sb.append("<br><br>");
binding.items.licenses.setText(Html.fromHtml(sb.toString(), Html.FROM_HTML_MODE_LEGACY));
binding.items.licenses.setMovementMethod(LinkMovementMethod.getInstance());
}
private String getFooter() {
final String arch = android.os.Build.SUPPORTED_ABIS[0];
try {
final String version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
return getString(R.string.about_footer, version, arch);
} catch (Throwable th) {
return getString(R.string.about_footer_alternate, arch);
}
}
}
/*
* 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;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.View;
import androidx.appcompat.widget.TooltipCompat;
import androidx.core.util.Pair;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityAboutBinding;
import com.itsaky.androidide.databinding.LayoutAboutItemsBinding;
import com.itsaky.androidide.models.License;
import java.util.ArrayList;
import java.util.List;
public class AboutActivity extends StudioActivity {
public static final String JDK_SOURCE = "https://github.com/itsaky/OpenJDK-Android";
private ActivityAboutBinding binding;
@Override
protected View bindLayout() {
binding = ActivityAboutBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.items.footerText.setText(getFooter());
setupTranslations();
setupLicenses();
LayoutAboutItemsBinding items = binding.items;
TooltipCompat.setTooltipText(items.discuss, getString(R.string.discussions_on_telegram));
TooltipCompat.setTooltipText(items.github, getString(R.string.title_github));
TooltipCompat.setTooltipText(items.email, getString(R.string.about_option_email));
TooltipCompat.setTooltipText(items.website, getString(R.string.about_option_website));
items.github.setOnClickListener(v -> getApp().openGitHub());
items.discuss.setOnClickListener(v -> getApp().openTelegramGroup());
items.email.setOnClickListener(v -> getApp().emailUs());
items.website.setOnClickListener(v -> getApp().openWebsite());
}
private void setupTranslations() {
final var translations = createTranslationsList();
final var sb = new StringBuilder();
for (var translation : translations) {
final var author = translation.first;
final var language = translation.second;
sb.append(author);
sb.append(" - ");
sb.append(language);
sb.append("\n");
}
binding.items.translations.setText(sb);
}
/**
* Create the list of translations. The list items are pairs. The first item of the pair is the
* name of the language. The second item is the name of the person who made the translations.
*
* <p>If you're adding translations, make sure you add an item to this list. You can choose to
* display the language in a localized version. For example : Language: हिन्दी - < name of author
* >
*
* <p>Prefer using unicode escapes for language names. For example, हिन्दी can be written as :
* Language: \u0939\u093f\u0928\u094d\u0926\u0940 - < name of author >
*
* @return The translations list.
*/
private List<Pair<String, String>> createTranslationsList() {
final var list = new ArrayList<Pair<String, String>>();
list.add(Pair.create("Bahasa Indonesia", "Fitrah Nuno Syahbani"));
list.add(Pair.create("\u4e2d\u6587", "Rosemoe and @mikofe")); // 中文
list.add(Pair.create("Deutsch", "Marvin Stelter"));
list.add(Pair.create("\u0939\u093f\u0928\u094d\u0926\u0940", "Premjit Chowdhury")); // हिन्दी
list.add(Pair.create("Russian", "Smooth-E"));
list.add(Pair.create("French", "Se-Lyan"));
list.add(Pair.create("Portuguese-Brazilian", "Frederick'XS"));
return list;
}
private void setupLicenses() {
final var licenses = new ArrayList<License>();
licenses.add(
new License(
"AndroidX Libraries", "Apache License 2.0", "https://github.com/androidx/androidx"));
licenses.add(
new License("Gradle Build Tool", "Apache License 2.0", "https://github.com/gradle/gradle"));
licenses.add(new License("Gson", "Apache License 2.0", "https://github.com/google/gson"));
licenses.add(
new License(
"Material Components for Android",
"Apache License 2.0",
"https://github.com/material-components/material-components-android"));
licenses.add(
new License("ANTLR4 Runtime", "BSD 3-clause License", "https://github.com/antlr/antlr4"));
licenses.add(
new License("Glide", "BSD, part MIT and Apache 2.0", "https://github.com/bumptech/glide"));
licenses.add(
new License(
"Android UtilCode", "Apache License 2.0", "https://github.com/Blankj/AndroidUtilCode"));
licenses.add(
new License(
"UnicornFilePicker",
"Apache License 2.0",
"https://github.com/abhishekti7/UnicornFilePicker"));
licenses.add(
new License("QuickAction", "Apache License 2.0", "https://github.com/piruin/quickaction"));
licenses.add(
new License("XmlToJson", "Apache License 2.0", "https://github.com/smart-fun/XmlToJson"));
licenses.add(
new License(
"AndroidTreeView",
"Apache License 2.0",
"https://github.com/bmelnychuk/AndroidTreeView"));
licenses.add(
new License(
"CodeEditor v0.5.2", "Apache License 2.0", "https://github.com/Rosemoe/CodeEditor"));
licenses.add(
new License("Guava-Android", "Apache License 2.0", "https://github.com/google/guava"));
licenses.add(new License("Jsoup", "MIT License", "https://github.com/jhy/jsoup"));
licenses.add(
new License("Termux [Terminal Emulator]", "GPL 3", "https://github.com/termux/termux-app"));
licenses.add(
new License("JavaPoet", "Apache License 2.0", "https://github.com/square/JavaPoet"));
licenses.add(
new License("Zip4j", "Apache License 2.0", "https://github.com/srikanth-lingala/zip4j"));
StringBuilder sb = new StringBuilder();
for (License license : licenses) {
sb.append("\u2022 ");
sb.append(license.name);
sb.append("<br>");
sb.append(getString(R.string.msg_about_licensed_under));
sb.append(license.license);
sb.append("<br><a href=\"");
sb.append(license.url);
sb.append("\">");
sb.append(license.url);
sb.append("</a>");
sb.append("<br><br>");
}
sb.append(getString(R.string.license_jdk));
sb.append(String.format("<br><a href=\"%1$s\">%1$s</a>", JDK_SOURCE));
sb.append("<br><br>");
binding.items.licenses.setText(Html.fromHtml(sb.toString(), Html.FROM_HTML_MODE_LEGACY));
binding.items.licenses.setMovementMethod(LinkMovementMethod.getInstance());
}
private String getFooter() {
final String arch = android.os.Build.SUPPORTED_ABIS[0];
try {
final String version = getPackageManager().getPackageInfo(getPackageName(), 0).versionName;
return getString(R.string.about_footer, version, arch);
} catch (Throwable th) {
return getString(R.string.about_footer_alternate, arch);
}
}
}
@@ -1,321 +1,321 @@
/*
* 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;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import androidx.core.content.ContextCompat;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityDownloadBinding;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.shell.IProcessExecutor;
import com.itsaky.androidide.shell.ProcessExecutorFactory;
import com.itsaky.androidide.shell.ProcessStreamsHolder;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.ListDirectoryCallable;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.FileUtil;
import com.itsaky.androidide.utils.InputStreamLineReader;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import abhishekti7.unicorn.filepicker.UnicornFilePicker;
public class DownloadActivity extends StudioActivity {
private ActivityDownloadBinding binding;
private ProgressSheet progressSheet;
private File choosenDir = null;
@Override
protected View bindLayout() {
binding = ActivityDownloadBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final OnClickListener clickListener = this::handleClick;
binding.dismiss.setOnClickListener(clickListener);
binding.choose.setOnClickListener(clickListener);
binding.install.setOnClickListener(clickListener);
binding.install.setEnabled(false);
}
private void handleClick(View v) {
final int id = v.getId();
if (id == binding.dismiss.getId()) {
TransitionManager.beginDelayedTransition(binding.getRoot());
binding.card.setVisibility(View.GONE);
} else if (id == binding.choose.getId()) {
try {
binding.install.setEnabled(false);
UnicornFilePicker.from(this)
.addConfigBuilder()
.addItemDivider(false)
.selectMultipleFiles(false)
.setRootDirectory(FileUtil.getExternalStorageDir())
.showHiddenFiles(true)
.showOnlyDirectory(true)
.theme(R.style.AppTheme_FilePicker)
.build()
.forResult(abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE);
} catch (Throwable th) {
LOG.error(getString(R.string.err_cannot_start_file_picker), th);
}
} else if (id == binding.install.getId()) {
if (choosenDir == null) {
getApp().toast(R.string.msg_pick_files, Toaster.Type.ERROR);
return;
}
if (!choosenDir.exists()) {
getApp().toast(R.string.msg_file_doesnt_exist, Toaster.Type.ERROR);
return;
}
installAll();
}
}
private void installAll() {
showProgress();
getApp().getPrefManager().putBoolean(PreferenceManager.KEY_FRAMEWORK_DOWNLOADED, true);
try {
final File script = createExtractScript();
final ProcessStreamsHolder holder = new ProcessStreamsHolder();
final IProcessExecutor executor = ProcessExecutorFactory.commonExecutor();
executor.execAsync(
holder,
this::onInstallProcessExit,
true,
Environment.BUSYBOX.getAbsolutePath(),
"sh", // We use busybox's sh, because Environment.SHELL is not installed yet...
script.getAbsolutePath());
this.output = new StringBuilder();
final InputStreamLineReader reader =
new InputStreamLineReader(holder.in, this::onInstallationOutput);
new Thread(reader).start();
} catch (DownloadActivity.InstallationException e) {
LOG.error(getString(R.string.err_installation), e);
onInstallationFailed(e.exitCode);
} catch (IOException e) {
LOG.error(getString(R.string.err_installation), e);
onInstallationFailed(5); // Exit code 5 : I/O Error
}
}
private void onInstallationOutput(final String line) {
ThreadUtils.runOnUiThread(() -> this.appendOut(line));
}
private void onInstallProcessExit(final int code) {
ThreadUtils.runOnUiThread(
() -> {
if (code == 0) { // 0 = normal execution
getApp().getPrefManager().putBoolean(PreferenceManager.KEY_FRAMEWORK_INSTALLED, true);
showRestartNeeded();
if (getProgressSheet().isShowing()) {
getProgressSheet().dismiss();
}
} else {
onInstallationFailed(code);
}
});
}
private void onInstallationFailed(int code) {
// TODO Can this be improved by adding some animations?
if (getProgressSheet().isShowing()) {
getProgressSheet().dismiss();
}
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_installation_failed);
builder.setMessage(getString(R.string.msg_installation_failed, code, this.output.toString()));
builder.setPositiveButton(
android.R.string.ok,
(d, w) -> {
d.dismiss();
finishAffinity();
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private File createExtractScript() throws DownloadActivity.InstallationException {
final StringBuilder sb = new StringBuilder();
sb.append("cd");
joiner(sb);
sb.append("echo 'Installing...'");
joiner(sb);
File[] files = choosenDir.listFiles(ARCHIVE_FILTER);
if (files == null || files.length <= 0) {
throw new InstallationException(2);
}
for (File f : files) {
if (f.getName().endsWith(".tar.xz")) {
if (f.getName().startsWith("androidide-sysroot")) {
sb.append("cd $SYSROOT/..");
joiner(sb);
}
sb.append("$BUSYBOX tar xvJf '").append(f.getAbsolutePath()).append("'");
joiner(sb);
sb.append("cd $HOME");
joiner(sb);
} else if (f.getName().endsWith(".zip")) {
sb.append("$BUSYBOX unzip '").append(f.getAbsolutePath()).append("'");
joiner(sb);
}
}
sb.append("echo 'Cleaning unsupported flags in binaries...'");
joiner(sb);
String DONE = "DONE";
sb.append("echo ").append(DONE);
final File script = new File(Environment.TMP_DIR, "extract_tools.sh");
if (!FileIOUtils.writeFileFromString(script, sb.toString())) {
throw new InstallationException(2);
}
return script;
}
private void joiner(StringBuilder sb) {
sb.append(" && ");
}
private StringBuilder output = new StringBuilder();
private void appendOut(String line) {
LOG.debug("Installation:", line);
output.append(line.trim());
output.append("\n");
getProgressSheet().setSubMessage(line);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE
&& resultCode == RESULT_OK) {
ArrayList<String> files = data.getStringArrayListExtra("filePaths");
if (files != null) {
if (files.size() == 1) {
File choosenDir = new File(files.get(0));
if (choosenDir.exists() && choosenDir.isDirectory()) {
this.choosenDir = choosenDir;
setDone(binding.choose);
binding.install.setEnabled(true);
new TaskExecutor()
.executeAsync(
new ListDirectoryCallable(choosenDir),
__ -> binding.filesText.setText(getString(R.string.msg_installable_files, __)));
} else {
getApp().toast(R.string.msg_picked_isnt_dir, Toaster.Type.ERROR);
}
} else {
getApp().toast(R.string.msg_pick_single_file, Toaster.Type.ERROR);
}
}
}
}
private void setDone(MaterialButton button) {
final int dp8 = SizeUtils.dp2px(8);
final int dp4 = SizeUtils.dp2px(4);
final int dp24 = SizeUtils.dp2px(24);
button.setText("");
button.setIconResource(R.drawable.ic_ok);
button.setIconTint(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.bg_green)));
button.setBackgroundColor(android.graphics.Color.TRANSPARENT);
button.setPaddingRelative(dp8, dp4, dp8, dp4);
button.setStrokeWidth(0);
button.setGravity(Gravity.CENTER);
button.setIconPadding(0);
button.setInsetTop(0);
button.setInsetBottom(0);
button.setPaddingRelative(dp24, dp4, dp24, dp4);
}
private void showProgress() {
getProgressSheet().setCancelable(false);
getProgressSheet().setShowShadow(false);
getProgressSheet()
.setSubMessageEnabled(true)
.setWelcomeTextEnabled(true)
.show(getSupportFragmentManager(), "progress_sheet");
}
private void showRestartNeeded() {
setDone(binding.install);
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_restart);
builder.setMessage(R.string.msg_restart);
builder.setCancelable(false);
builder.setPositiveButton(android.R.string.ok, (p1, p2) -> finishAffinity());
builder.create().show();
}
private ProgressSheet getProgressSheet() {
return progressSheet == null
? progressSheet = new ProgressSheet().setMessage(getString(R.string.please_wait))
: progressSheet;
}
private final FileFilter ARCHIVE_FILTER =
p1 -> p1.isFile() && (p1.getName().endsWith(".tar.xz") || p1.getName().endsWith(".zip"));
private static class InstallationException extends Exception {
private final int exitCode;
public InstallationException(int exitCode) {
this.exitCode = exitCode;
}
}
}
/*
* 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;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import androidx.core.content.ContextCompat;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.SizeUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.button.MaterialButton;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.databinding.ActivityDownloadBinding;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.shell.IProcessExecutor;
import com.itsaky.androidide.shell.ProcessExecutorFactory;
import com.itsaky.androidide.shell.ProcessStreamsHolder;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.ListDirectoryCallable;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.FileUtil;
import com.itsaky.androidide.utils.InputStreamLineReader;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import abhishekti7.unicorn.filepicker.UnicornFilePicker;
public class DownloadActivity extends StudioActivity {
private ActivityDownloadBinding binding;
private ProgressSheet progressSheet;
private File choosenDir = null;
@Override
protected View bindLayout() {
binding = ActivityDownloadBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final OnClickListener clickListener = this::handleClick;
binding.dismiss.setOnClickListener(clickListener);
binding.choose.setOnClickListener(clickListener);
binding.install.setOnClickListener(clickListener);
binding.install.setEnabled(false);
}
private void handleClick(View v) {
final int id = v.getId();
if (id == binding.dismiss.getId()) {
TransitionManager.beginDelayedTransition(binding.getRoot());
binding.card.setVisibility(View.GONE);
} else if (id == binding.choose.getId()) {
try {
binding.install.setEnabled(false);
UnicornFilePicker.from(this)
.addConfigBuilder()
.addItemDivider(false)
.selectMultipleFiles(false)
.setRootDirectory(FileUtil.getExternalStorageDir())
.showHiddenFiles(true)
.showOnlyDirectory(true)
.theme(R.style.AppTheme_FilePicker)
.build()
.forResult(abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE);
} catch (Throwable th) {
LOG.error(getString(R.string.err_cannot_start_file_picker), th);
}
} else if (id == binding.install.getId()) {
if (choosenDir == null) {
getApp().toast(R.string.msg_pick_files, Toaster.Type.ERROR);
return;
}
if (!choosenDir.exists()) {
getApp().toast(R.string.msg_file_doesnt_exist, Toaster.Type.ERROR);
return;
}
installAll();
}
}
private void installAll() {
showProgress();
getApp().getPrefManager().putBoolean(PreferenceManager.KEY_FRAMEWORK_DOWNLOADED, true);
try {
final File script = createExtractScript();
final ProcessStreamsHolder holder = new ProcessStreamsHolder();
final IProcessExecutor executor = ProcessExecutorFactory.commonExecutor();
executor.execAsync(
holder,
this::onInstallProcessExit,
true,
Environment.BUSYBOX.getAbsolutePath(),
"sh", // We use busybox's sh, because Environment.SHELL is not installed yet...
script.getAbsolutePath());
this.output = new StringBuilder();
final InputStreamLineReader reader =
new InputStreamLineReader(holder.in, this::onInstallationOutput);
new Thread(reader).start();
} catch (DownloadActivity.InstallationException e) {
LOG.error(getString(R.string.err_installation), e);
onInstallationFailed(e.exitCode);
} catch (IOException e) {
LOG.error(getString(R.string.err_installation), e);
onInstallationFailed(5); // Exit code 5 : I/O Error
}
}
private void onInstallationOutput(final String line) {
ThreadUtils.runOnUiThread(() -> this.appendOut(line));
}
private void onInstallProcessExit(final int code) {
ThreadUtils.runOnUiThread(
() -> {
if (code == 0) { // 0 = normal execution
getApp().getPrefManager().putBoolean(PreferenceManager.KEY_FRAMEWORK_INSTALLED, true);
showRestartNeeded();
if (getProgressSheet().isShowing()) {
getProgressSheet().dismiss();
}
} else {
onInstallationFailed(code);
}
});
}
private void onInstallationFailed(int code) {
// TODO Can this be improved by adding some animations?
if (getProgressSheet().isShowing()) {
getProgressSheet().dismiss();
}
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_installation_failed);
builder.setMessage(getString(R.string.msg_installation_failed, code, this.output.toString()));
builder.setPositiveButton(
android.R.string.ok,
(d, w) -> {
d.dismiss();
finishAffinity();
});
builder.setNegativeButton(android.R.string.cancel, null);
builder.show();
}
private File createExtractScript() throws DownloadActivity.InstallationException {
final StringBuilder sb = new StringBuilder();
sb.append("cd");
joiner(sb);
sb.append("echo 'Installing...'");
joiner(sb);
File[] files = choosenDir.listFiles(ARCHIVE_FILTER);
if (files == null || files.length <= 0) {
throw new InstallationException(2);
}
for (File f : files) {
if (f.getName().endsWith(".tar.xz")) {
if (f.getName().startsWith("androidide-sysroot")) {
sb.append("cd $SYSROOT/..");
joiner(sb);
}
sb.append("$BUSYBOX tar xvJf '").append(f.getAbsolutePath()).append("'");
joiner(sb);
sb.append("cd $HOME");
joiner(sb);
} else if (f.getName().endsWith(".zip")) {
sb.append("$BUSYBOX unzip '").append(f.getAbsolutePath()).append("'");
joiner(sb);
}
}
sb.append("echo 'Cleaning unsupported flags in binaries...'");
joiner(sb);
String DONE = "DONE";
sb.append("echo ").append(DONE);
final File script = new File(Environment.TMP_DIR, "extract_tools.sh");
if (!FileIOUtils.writeFileFromString(script, sb.toString())) {
throw new InstallationException(2);
}
return script;
}
private void joiner(StringBuilder sb) {
sb.append(" && ");
}
private StringBuilder output = new StringBuilder();
private void appendOut(String line) {
LOG.debug("Installation:", line);
output.append(line.trim());
output.append("\n");
getProgressSheet().setSubMessage(line);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == abhishekti7.unicorn.filepicker.utils.Constants.REQ_UNICORN_FILE
&& resultCode == RESULT_OK) {
ArrayList<String> files = data.getStringArrayListExtra("filePaths");
if (files != null) {
if (files.size() == 1) {
File choosenDir = new File(files.get(0));
if (choosenDir.exists() && choosenDir.isDirectory()) {
this.choosenDir = choosenDir;
setDone(binding.choose);
binding.install.setEnabled(true);
new TaskExecutor()
.executeAsync(
new ListDirectoryCallable(choosenDir),
__ -> binding.filesText.setText(getString(R.string.msg_installable_files, __)));
} else {
getApp().toast(R.string.msg_picked_isnt_dir, Toaster.Type.ERROR);
}
} else {
getApp().toast(R.string.msg_pick_single_file, Toaster.Type.ERROR);
}
}
}
}
private void setDone(MaterialButton button) {
final int dp8 = SizeUtils.dp2px(8);
final int dp4 = SizeUtils.dp2px(4);
final int dp24 = SizeUtils.dp2px(24);
button.setText("");
button.setIconResource(R.drawable.ic_ok);
button.setIconTint(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.bg_green)));
button.setBackgroundColor(android.graphics.Color.TRANSPARENT);
button.setPaddingRelative(dp8, dp4, dp8, dp4);
button.setStrokeWidth(0);
button.setGravity(Gravity.CENTER);
button.setIconPadding(0);
button.setInsetTop(0);
button.setInsetBottom(0);
button.setPaddingRelative(dp24, dp4, dp24, dp4);
}
private void showProgress() {
getProgressSheet().setCancelable(false);
getProgressSheet().setShowShadow(false);
getProgressSheet()
.setSubMessageEnabled(true)
.setWelcomeTextEnabled(true)
.show(getSupportFragmentManager(), "progress_sheet");
}
private void showRestartNeeded() {
setDone(binding.install);
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_restart);
builder.setMessage(R.string.msg_restart);
builder.setCancelable(false);
builder.setPositiveButton(android.R.string.ok, (p1, p2) -> finishAffinity());
builder.create().show();
}
private ProgressSheet getProgressSheet() {
return progressSheet == null
? progressSheet = new ProgressSheet().setMessage(getString(R.string.please_wait))
: progressSheet;
}
private final FileFilter ARCHIVE_FILTER =
p1 -> p1.isFile() && (p1.getName().endsWith(".tar.xz") || p1.getName().endsWith(".zip"));
private static class InstallationException extends Exception {
private final int exitCode;
public InstallationException(int exitCode) {
this.exitCode = exitCode;
}
}
}
@@ -425,4 +425,4 @@ public class MainActivity extends StudioActivity
mProgressSheet.dismiss();
}
}
}
}
@@ -1,92 +1,92 @@
/************************************************************************************
* 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;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.ActivitySplashBinding;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.DialogUtils;
public class SplashActivity extends StudioActivity {
private static final Handler mHandler = new Handler();
private final Runnable mRunnable =
() -> {
ConstantsBridge.SPLASH_TO_MAIN = true;
startActivity(new Intent(SplashActivity.this, MainActivity.class));
finish();
};
private ActivitySplashBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.splashText.setText(getString(R.string.msg_checking_storage_permissions));
}
@Override
protected void onStorageAlreadyGranted() {
super.onStorageAlreadyGranted();
proceed();
}
@Override
protected void onStorageGranted() {
super.onStorageGranted();
proceed();
}
@Override
protected View bindLayout() {
binding = ActivitySplashBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
private void proceed() {
if (!StudioApp.isAbiSupported()) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_device_not_supported);
builder.setMessage(R.string.msg_device_not_supported);
builder.setCancelable(false);
builder.setPositiveButton(android.R.string.ok, (p1, p2) -> finishAffinity());
builder.create().show();
} else {
binding.splashText.setText(getString(R.string.msg_storage_granted));
if (getApp().isFrameworkInstalled()) {
goToMain();
} else {
startActivity(new Intent(this, DownloadActivity.class));
finish();
}
}
}
private void goToMain() {
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, 1000);
}
}
/************************************************************************************
* 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;
import android.content.Intent;
import android.os.Bundle;
import android.os.Handler;
import android.view.View;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.app.StudioActivity;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.ActivitySplashBinding;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.DialogUtils;
public class SplashActivity extends StudioActivity {
private static final Handler mHandler = new Handler();
private final Runnable mRunnable =
() -> {
ConstantsBridge.SPLASH_TO_MAIN = true;
startActivity(new Intent(SplashActivity.this, MainActivity.class));
finish();
};
private ActivitySplashBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding.splashText.setText(getString(R.string.msg_checking_storage_permissions));
}
@Override
protected void onStorageAlreadyGranted() {
super.onStorageAlreadyGranted();
proceed();
}
@Override
protected void onStorageGranted() {
super.onStorageGranted();
proceed();
}
@Override
protected View bindLayout() {
binding = ActivitySplashBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
private void proceed() {
if (!StudioApp.isAbiSupported()) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(R.string.title_device_not_supported);
builder.setMessage(R.string.msg_device_not_supported);
builder.setCancelable(false);
builder.setPositiveButton(android.R.string.ok, (p1, p2) -> finishAffinity());
builder.create().show();
} else {
binding.splashText.setText(getString(R.string.msg_storage_granted));
if (getApp().isFrameworkInstalled()) {
goToMain();
} else {
startActivity(new Intent(this, DownloadActivity.class));
finish();
}
}
}
private void goToMain() {
mHandler.removeCallbacks(mRunnable);
mHandler.postDelayed(mRunnable, 1000);
}
}
@@ -1,55 +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, result: Any) {
val context = getActivity(data) ?: return
context.invalidateOptionsMenu()
}
fun shouldPrepare() = visible && enabled
private fun isBuildInProgress(activity: EditorActivity): Boolean =
activity.buildService == null || activity.buildService.isBuildInProgress
}
/*
* 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, result: Any) {
val context = getActivity(data) ?: return
context.invalidateOptionsMenu()
}
fun shouldPrepare() = visible && enabled
private fun isBuildInProgress(activity: EditorActivity): Boolean =
activity.buildService == null || activity.buildService.isBuildInProgress
}
@@ -1,62 +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
}
}
/*
* 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
}
}
@@ -1,45 +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 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
override fun prepare(data: ActionData) {
if (!hasRequiredData(data, Context::class.java)) {
markInvisible()
}
}
fun getActivity(data: ActionData): EditorActivity? {
val context = data.get(Context::class.java) ?: return null
return context as EditorActivity
}
}
/*
* 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
override fun prepare(data: ActionData) {
if (!hasRequiredData(data, Context::class.java)) {
markInvisible()
}
}
fun getActivity(data: ActionData): EditorActivity? {
val context = data.get(Context::class.java) ?: return null
return context as EditorActivity
}
}
@@ -1,23 +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()
}
/*
* 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()
}
@@ -1,46 +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
import com.itsaky.androidide.views.editor.CodeEditorView
import com.itsaky.androidide.views.editor.IDEEditor
/** @author Akash Yadav */
abstract class EditorRelatedAction : EditorActivityAction() {
override var requiresUIThread: Boolean = true
override fun prepare(data: ActionData) {
val editor =
getEditor(data)
?: run {
visible = false
enabled = false
return
}
val file = editor.file
visible = file != null
enabled = visible
}
fun getEditor(data: ActionData): IDEEditor? = data.get(IDEEditor::class.java)
fun getEditorView(data: ActionData): CodeEditorView? = data[CodeEditorView::class.java]
}
/*
* 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 com.itsaky.androidide.views.editor.IDEEditor
/** @author Akash Yadav */
abstract class EditorRelatedAction : EditorActivityAction() {
override var requiresUIThread: Boolean = true
override fun prepare(data: ActionData) {
val editor =
getEditor(data)
?: run {
visible = false
enabled = false
return
}
val file = editor.file
visible = file != null
enabled = visible
}
fun getEditor(data: ActionData): IDEEditor? = data.get(IDEEditor::class.java)
fun getEditorView(data: ActionData): CodeEditorView? = data[CodeEditorView::class.java]
}
@@ -1,42 +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.build
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 AssembleDebugAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.build_debug)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_debug)
}
override val id: String = "editor_assembleDebug"
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.assembleDebug(true)
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class AssembleDebugAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.build_debug)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_debug)
}
override val id: String = "editor_assembleDebug"
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.assembleDebug(true)
return true
}
}
@@ -1,42 +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.build
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 AssembleReleaseAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.build_release)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_release)
}
override val id: String = "editor_assembleRelease"
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.assembleRelease()
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class AssembleReleaseAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.build_release)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_release)
}
override val id: String = "editor_assembleRelease"
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.assembleRelease()
return true
}
}
@@ -1,42 +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.build
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 BuildAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run)
}
override val id: String = "editor_build"
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.build()
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class BuildAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run)
}
override val id: String = "editor_build"
override fun execAction(data: ActionData): Boolean {
val activity = getActivity(data) ?: return false
activity.build()
return true
}
}
@@ -1,44 +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.build
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 BuildActionMenu() : BaseBuildAction(), ActionMenu {
constructor(context: Context) : this() {
label = context.getString(R.string.build_tasks)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run)
addAction(BuildAction(context))
addAction(AssembleDebugAction(context))
addAction(AssembleReleaseAction(context))
addAction(CreateAABAction(context))
addAction(CleanAction(context))
addAction(RebuildAction(context))
}
override val children: MutableSet<ActionItem> = mutableSetOf()
override val id: String = "editor_buildTasks"
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class BuildActionMenu() : BaseBuildAction(), ActionMenu {
constructor(context: Context) : this() {
label = context.getString(R.string.build_tasks)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run)
addAction(BuildAction(context))
addAction(AssembleDebugAction(context))
addAction(AssembleReleaseAction(context))
addAction(CreateAABAction(context))
addAction(CleanAction(context))
addAction(RebuildAction(context))
}
override val children: MutableSet<ActionItem> = mutableSetOf()
override val id: String = "editor_buildTasks"
}
@@ -1,66 +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.actions.build
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.utils.ILogger
/** @author Akash Yadav */
class CancelBuildAction() : EditorActivityAction() {
private val log = ILogger.newInstance(javaClass.simpleName)
constructor(context: Context) : this() {
label = context.getString(R.string.title_cancel_build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop_daemons)
}
override val id: String = "editor_stopGradleDaemons"
override fun prepare(data: ActionData) {
visible = true
enabled = true
}
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
log.info("Sending build cancellation request...")
context.buildService.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",
result.failureReason,
result.failureReason!!.message)
return@whenComplete
}
log.info("Build cancellation request was successfully enqueued...")
}
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.utils.ILogger
/** @author Akash Yadav */
class CancelBuildAction() : EditorActivityAction() {
private val log = ILogger.newInstance(javaClass.simpleName)
constructor(context: Context) : this() {
label = context.getString(R.string.title_cancel_build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop_daemons)
}
override val id: String = "editor_stopGradleDaemons"
override fun prepare(data: ActionData) {
visible = true
enabled = true
}
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
log.info("Sending build cancellation request...")
context.buildService.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",
result.failureReason,
result.failureReason!!.message)
return@whenComplete
}
log.info("Build cancellation request was successfully enqueued...")
}
return true
}
}
@@ -1,41 +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.build
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 CleanAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.clean_project)
icon = ContextCompat.getDrawable(context, R.drawable.ic_clean)
}
override val id: String = "editor_clean"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.clean()
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class CleanAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.clean_project)
icon = ContextCompat.getDrawable(context, R.drawable.ic_clean)
}
override val id: String = "editor_clean"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.clean()
return true
}
}
@@ -1,41 +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.build
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 CreateAABAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.create_aab)
icon = ContextCompat.getDrawable(context, R.drawable.ic_bundle)
}
override val id: String = "editor_createAAB"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.bundle()
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class CreateAABAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.create_aab)
icon = ContextCompat.getDrawable(context, R.drawable.ic_bundle)
}
override val id: String = "editor_createAAB"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.bundle()
return true
}
}
@@ -1,46 +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.build
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.actions.EditorActivityAction
/**
* Triggers a project sync request.
*
* @author Akash Yadav
*/
class ProjectSyncAction() : BaseBuildAction() {
override val id: String = "action_editor_syncProject"
override var requiresUIThread = true
constructor(context: Context) : this() {
label = context.getString(string.title_sync_project)
icon = ContextCompat.getDrawable(context, R.drawable.ic_sync)
}
override fun execAction(data: ActionData): Any {
return getActivity(data)!!.initializeProject()
}
}
/*
* 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.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.actions.EditorActivityAction
/**
* Triggers a project sync request.
*
* @author Akash Yadav
*/
class ProjectSyncAction() : BaseBuildAction() {
override val id: String = "action_editor_syncProject"
override var requiresUIThread = true
constructor(context: Context) : this() {
label = context.getString(string.title_sync_project)
icon = ContextCompat.getDrawable(context, R.drawable.ic_sync)
}
override fun execAction(data: ActionData): Any {
return getActivity(data)!!.initializeProject()
}
}
@@ -1,45 +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.build
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
/**
* The 'Quick Run' action in the editor activity.
*
* @author Akash Yadav
*/
class QuickRunAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.quick_run_debug)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_outline)
}
override val id: String = "editor_quick_run"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.assembleDebug(true)
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
/**
* The 'Quick Run' action in the editor activity.
*
* @author Akash Yadav
*/
class QuickRunAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.quick_run_debug)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_outline)
}
override val id: String = "editor_quick_run"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.assembleDebug(true)
return true
}
}
@@ -1,41 +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.build
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 RebuildAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.clean_amp_build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_rebuild)
}
override val id: String = "editor_cleanAndBuild"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.cleanAndRebuild()
return true
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.BaseBuildAction
/** @author Akash Yadav */
class RebuildAction() : BaseBuildAction() {
constructor(context: Context) : this() {
label = context.getString(R.string.clean_amp_build)
icon = ContextCompat.getDrawable(context, R.drawable.ic_rebuild)
}
override val id: String = "editor_cleanAndBuild"
override fun execAction(data: ActionData): Boolean {
val context = getActivity(data) ?: return false
context.cleanAndRebuild()
return true
}
}
@@ -1,42 +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.build
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
import com.itsaky.androidide.actions.lint.LintTaskMenu
/** @author Akash Yadav */
class RunActionMenu() : BaseBuildAction(), ActionMenu {
override val children: MutableSet<ActionItem> = mutableSetOf()
constructor(context: Context) : this() {
label = context.getString(R.string.run)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run)
addAction(BuildActionMenu(context))
addAction(LintTaskMenu(context))
}
override val id: String = "editor_runMenu"
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.actions.lint.LintTaskMenu
/** @author Akash Yadav */
class RunActionMenu() : BaseBuildAction(), ActionMenu {
override val children: MutableSet<ActionItem> = mutableSetOf()
constructor(context: Context) : this() {
label = context.getString(R.string.run)
icon = ContextCompat.getDrawable(context, R.drawable.ic_run)
addAction(BuildActionMenu(context))
addAction(LintTaskMenu(context))
}
override val id: String = "editor_runMenu"
}
@@ -1,42 +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
}
}
/*
* 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
}
}
@@ -1,53 +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.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 fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
visible = getEditor(data)?.isEditable ?: false
enabled = visible
}
override val id: String = "ideEditor_cut"
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.cutText()
return true
}
}
/*
* 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 fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
visible = getEditor(data)?.isEditable ?: false
enabled = visible
}
override val id: String = "ideEditor_cut"
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.cutText()
return true
}
}
@@ -1,44 +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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.EditorRelatedAction
/**
* Expands the current selection using the language server set in editor.
*
* @author Akash Yadav
*/
class ExpandSelectionAction() : EditorRelatedAction() {
override val id: String = "editor_expandSelection"
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
constructor(context: Context) : this() {
label = context.getString(R.string.action_expand_selection)
icon = ContextCompat.getDrawable(context, R.drawable.ic_expand_selection)
}
override fun execAction(data: ActionData): Any {
return getEditor(data)!!.expandSelection()
}
}
/*
* 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
/**
* Expands the current selection using the language server set in editor.
*
* @author Akash Yadav
*/
class ExpandSelectionAction() : EditorRelatedAction() {
override val id: String = "editor_expandSelection"
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
constructor(context: Context) : this() {
label = context.getString(R.string.action_expand_selection)
icon = ContextCompat.getDrawable(context, R.drawable.ic_expand_selection)
}
override fun execAction(data: ActionData): Any {
return getEditor(data)!!.expandSelection()
}
}
@@ -1,54 +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.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 = "ideEditor_paste"
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
visible = getEditor(data)?.isEditable ?: false
enabled = visible
}
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.pasteText()
return true
}
}
/*
* 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 = "ideEditor_paste"
override fun prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
visible = getEditor(data)?.isEditable ?: false
enabled = visible
}
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.pasteText()
return true
}
}
@@ -1,43 +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.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 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
}
}
/*
* 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 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
}
}
@@ -1,45 +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.find
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
/**
* 'Go to next' action in editor's search action mode.
* @author Akash Yadav
*/
class GoToNextAction() : SearchActionModeAction() {
override val id: String = "editor.find.next"
constructor(context: Context) : this() {
label = context.getString(R.string.next)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_next)
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)!!
if (!editor.searcher.hasQuery()) {
return false
}
return editor.searcher.gotoNext()
}
}
/*
* 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.find
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
/**
* 'Go to next' action in editor's search action mode.
* @author Akash Yadav
*/
class GoToNextAction() : SearchActionModeAction() {
override val id: String = "editor.find.next"
constructor(context: Context) : this() {
label = context.getString(R.string.next)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_next)
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)!!
if (!editor.searcher.hasQuery()) {
return false
}
return editor.searcher.gotoNext()
}
}
@@ -1,46 +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.editor.find
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionData
/**
* 'Go to previous' action in editor's search action mode.
* @author Akash Yadav
*/
class GoToPreviousAction() : SearchActionModeAction() {
override val id: String = "editor.find.previous"
constructor(context: Context) : this() {
label = context.getString(string.previous)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_previous)
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)!!
if (!editor.searcher.hasQuery()) {
return false
}
return editor.searcher.gotoPrevious()
}
}
/*
* 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.find
import android.content.Context
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionData
/**
* 'Go to previous' action in editor's search action mode.
* @author Akash Yadav
*/
class GoToPreviousAction() : SearchActionModeAction() {
override val id: String = "editor.find.previous"
constructor(context: Context) : this() {
label = context.getString(string.previous)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_previous)
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)!!
if (!editor.searcher.hasQuery()) {
return false
}
return editor.searcher.gotoPrevious()
}
}
@@ -1,87 +1,87 @@
/*
* 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.find
import android.content.Context
import android.view.LayoutInflater
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.databinding.LayoutEditorFindReplaceBinding
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.ILogger
/**
* Shows dialog to replace one or all the matched text in editor searcher.
* @author Akash Yadav
*/
class ReplaceAction() : SearchActionModeAction() {
override val id: String = "editor.search.replace"
private val log = ILogger.newInstance(javaClass.simpleName)
constructor(context: Context) : this() {
label = context.getString(R.string.replace)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_replace)
}
override fun execAction(data: ActionData): Any {
val context = data[Context::class.java]!!
val editor = getEditor(data)!!
if (!editor.searcher.hasQuery()) {
return false
}
val binding = LayoutEditorFindReplaceBinding.inflate(LayoutInflater.from(context))
val builder = DialogUtils.newMaterialDialogBuilder(context)
builder.setTitle(R.string.replace)
builder.setView(binding.root)
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton(R.string.replace) { dialog, _ ->
dialog.dismiss()
val input = binding.replacementInput.editText
if (input == null) {
log.error("Unable to perform replace action. Input field is null")
return@setPositiveButton
}
editor.searcher.replaceThis(input.text.toString())
}
builder.setNeutralButton(R.string.replaceAll) { dialog, _ ->
dialog.dismiss()
val input = binding.replacementInput.editText
if (input == null) {
log.error("Unable to perform replace action. Input field is null")
return@setNeutralButton
}
editor.searcher.replaceAllAsync(input.text.toString()).whenComplete { _, error ->
if (error != null) {
log.error("Unable to replace all matched text", error)
} else {
editor.post { data[ActionMode::class.java]?.finish() }
}
}
}
builder.show()
return true
}
}
/*
* 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.find
import android.content.Context
import android.view.LayoutInflater
import androidx.appcompat.view.ActionMode
import androidx.core.content.ContextCompat
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.databinding.LayoutEditorFindReplaceBinding
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.ILogger
/**
* Shows dialog to replace one or all the matched text in editor searcher.
* @author Akash Yadav
*/
class ReplaceAction() : SearchActionModeAction() {
override val id: String = "editor.search.replace"
private val log = ILogger.newInstance(javaClass.simpleName)
constructor(context: Context) : this() {
label = context.getString(R.string.replace)
icon = ContextCompat.getDrawable(context, R.drawable.ic_search_replace)
}
override fun execAction(data: ActionData): Any {
val context = data[Context::class.java]!!
val editor = getEditor(data)!!
if (!editor.searcher.hasQuery()) {
return false
}
val binding = LayoutEditorFindReplaceBinding.inflate(LayoutInflater.from(context))
val builder = DialogUtils.newMaterialDialogBuilder(context)
builder.setTitle(R.string.replace)
builder.setView(binding.root)
builder.setNegativeButton(android.R.string.cancel, null)
builder.setPositiveButton(R.string.replace) { dialog, _ ->
dialog.dismiss()
val input = binding.replacementInput.editText
if (input == null) {
log.error("Unable to perform replace action. Input field is null")
return@setPositiveButton
}
editor.searcher.replaceThis(input.text.toString())
}
builder.setNeutralButton(R.string.replaceAll) { dialog, _ ->
dialog.dismiss()
val input = binding.replacementInput.editText
if (input == null) {
log.error("Unable to perform replace action. Input field is null")
return@setNeutralButton
}
editor.searcher.replaceAllAsync(input.text.toString()).whenComplete { _, error ->
if (error != null) {
log.error("Unable to replace all matched text", error)
} else {
editor.post { data[ActionMode::class.java]?.finish() }
}
}
}
builder.show()
return true
}
}
@@ -1,27 +1,27 @@
/*
* 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.find
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionItem.Location
import com.itsaky.androidide.actions.EditorRelatedAction
/** @author Akash Yadav */
abstract class SearchActionModeAction : EditorRelatedAction() {
override var location: Location = ActionItem.Location.EDITOR_SEARCH_ACTION_MODE
}
/*
* 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.find
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionItem.Location
import com.itsaky.androidide.actions.EditorRelatedAction
/** @author Akash Yadav */
abstract class SearchActionModeAction : EditorRelatedAction() {
override var location: Location = ActionItem.Location.EDITOR_SEARCH_ACTION_MODE
}
@@ -1,46 +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
}
}
/*
* 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
}
}
@@ -1,52 +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
}
}
/*
* 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
}
}
@@ -1,46 +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
}
}
/*
* 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
}
}
@@ -1,42 +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 = getEditorView(data) ?: return false
editor.beginSearch()
return true
}
}
/*
* 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 = getEditorView(data) ?: return false
editor.beginSearch()
return true
}
}
@@ -1,61 +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.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() {
override var requiresUIThread: Boolean = true
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
}
}
}
/*
* 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() {
override var requiresUIThread: Boolean = true
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
}
}
}
@@ -1,62 +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.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
import com.itsaky.androidide.handlers.FileOptionsHandler
/** @author Akash Yadav */
class PreviewLayoutAction() : EditorRelatedAction() {
override val id: String = "editor_previewLayout"
constructor(context: Context) : this() {
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)
if (!visible) {
return
}
val editor = getEditor(data)!!
val file = editor.file!!
val isXml = file.name.endsWith(".xml")
visible =
isXml &&
file.parentFile != null &&
Regex(FileOptionsHandler.LAYOUT_RES_PATH_REGEX)
.matches(file.parentFile!!.absolutePath)
enabled = visible
}
override fun execAction(data: ActionData): Any {
val activity = getActivity(data)!!
activity.previewLayout()
return true
}
}
/*
* 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
import com.itsaky.androidide.handlers.FileOptionsHandler
/** @author Akash Yadav */
class PreviewLayoutAction() : EditorRelatedAction() {
override val id: String = "editor_previewLayout"
constructor(context: Context) : this() {
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)
if (!visible) {
return
}
val editor = getEditor(data)!!
val file = editor.file!!
val isXml = file.name.endsWith(".xml")
visible =
isXml &&
file.parentFile != null &&
Regex(FileOptionsHandler.LAYOUT_RES_PATH_REGEX)
.matches(file.parentFile!!.absolutePath)
enabled = visible
}
override fun execAction(data: ActionData): Any {
val activity = getActivity(data)!!
activity.previewLayout()
return true
}
}
@@ -1,46 +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.actions.ActionItem
import com.itsaky.androidide.actions.EditorRelatedAction
/**
* Action that formats the code in the editor.
*
* @author Akash Yadav
*/
class FormatCodeAction() : EditorRelatedAction() {
override val id: String = "editor_file_formatCode"
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
constructor(context: Context) : this() {
label = context.getString(R.string.title_format_code)
icon = ContextCompat.getDrawable(context, R.drawable.ic_format_code)
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)!!
editor.formatCodeAsync()
return true
}
}
/*
* 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.ActionItem
import com.itsaky.androidide.actions.EditorRelatedAction
/**
* Action that formats the code in the editor.
*
* @author Akash Yadav
*/
class FormatCodeAction() : EditorRelatedAction() {
override val id: String = "editor_file_formatCode"
override var location: ActionItem.Location = ActionItem.Location.EDITOR_TEXT_ACTIONS
constructor(context: Context) : this() {
label = context.getString(R.string.title_format_code)
icon = ContextCompat.getDrawable(context, R.drawable.ic_format_code)
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)!!
editor.formatCodeAsync()
return true
}
}
@@ -1,86 +1,86 @@
/*
* 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.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import com.itsaky.toaster.Toaster
/** @author Akash Yadav */
class SaveFileAction() : EditorRelatedAction() {
private val log = ILogger.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
val result = context.saveAllResult()
if (result.xmlSaved) {
ProjectManager.generateSources(context.buildService)
}
if (result.gradleSaved) {
context.notifySyncNeeded()
}
true
} catch (error: Throwable) {
log.error("Failed to save file", error)
false
}
}
override fun postExec(data: ActionData, result: Any) {
if (result is Boolean && result) {
StudioApp.getInstance().toast(R.string.all_saved, Toaster.Type.SUCCESS)
} else {
log.error("Failed to save file")
TODO("Create message in strings.xml")
}
}
}
/*
* 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.projects.ProjectManager
import com.itsaky.androidide.utils.ILogger
import com.itsaky.toaster.Toaster
/** @author Akash Yadav */
class SaveFileAction() : EditorRelatedAction() {
private val log = ILogger.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
val result = context.saveAllResult()
if (result.xmlSaved) {
ProjectManager.generateSources(context.buildService)
}
if (result.gradleSaved) {
context.notifySyncNeeded()
}
true
} catch (error: Throwable) {
log.error("Failed to save file", error)
false
}
}
override fun postExec(data: ActionData, result: Any) {
if (result is Boolean && result) {
StudioApp.getInstance().toast(R.string.all_saved, Toaster.Type.SUCCESS)
} else {
log.error("Failed to save file")
TODO("Create message in strings.xml")
}
}
}
@@ -1,41 +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.lint()
return true
}
}
/*
* 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.lint()
return true
}
}
@@ -1,41 +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.lintDebug()
return true
}
}
/*
* 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.lintDebug()
return true
}
}
@@ -1,41 +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.lintRelease()
return true
}
}
/*
* 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.lintRelease()
return true
}
}
@@ -1,41 +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"
}
/*
* 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"
}
@@ -1,63 +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.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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
/** @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 prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
val editor = getEditor(data)!!
enabled = editor.canRedo()
}
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data)!!
editor.redo()
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
}
}
}
/*
* 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.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.EditorRelatedAction
/** @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 prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
val editor = getEditor(data)!!
enabled = editor.canRedo()
}
override fun execAction(data: ActionData): Boolean {
val editor = getEditor(data)!!
editor.redo()
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
}
}
}
@@ -1,56 +1,56 @@
/*
* 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
/** @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 prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
val editor = getEditor(data)!!
enabled = editor.canUndo()
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)
return if (editor != null) {
editor.undo()
true
} else {
false
}
}
}
/*
* 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
/** @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 prepare(data: ActionData) {
super.prepare(data)
if (!visible) {
return
}
val editor = getEditor(data)!!
enabled = editor.canUndo()
}
override fun execAction(data: ActionData): Any {
val editor = getEditor(data)
return if (editor != null) {
editor.undo()
true
} else {
false
}
}
}
@@ -1,230 +1,230 @@
/*
* 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.content.res.Resources;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutCompletionItemBinding;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.TypefaceUtils;
import com.itsaky.apiinfo.ApiInfo;
import com.itsaky.apiinfo.models.ClassInfo;
import com.itsaky.apiinfo.models.FieldInfo;
import com.itsaky.apiinfo.models.Info;
import com.itsaky.apiinfo.models.MethodInfo;
import com.itsaky.lsp.models.CompletionItem;
import com.itsaky.lsp.models.CompletionItemKind;
import java.util.List;
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion;
import io.github.rosemoe.sora.widget.component.EditorCompletionAdapter;
public class CompletionListAdapter extends EditorCompletionAdapter {
private static final ILogger LOG = ILogger.newInstance("CompletionListAdapter");
@Override
public void attachValues(
EditorAutoCompletion window,
List<io.github.rosemoe.sora.lang.completion.CompletionItem> items) {
super.attachValues(window, items);
}
@Override
public int getItemHeight() {
return (int)
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 40, Resources.getSystem().getDisplayMetrics());
}
@Override
protected View getView(
int position, View convertView, ViewGroup parent, boolean isCurrentCursorPosition) {
final LayoutCompletionItemBinding binding =
LayoutCompletionItemBinding.inflate(LayoutInflater.from(getContext()), parent, false);
var item = (CompletionItem) getItem(position);
String label = item.getLabel(), desc = item.getDetail(), type = item.getKind().toString();
String header = type.length() <= 0 ? "O" : String.valueOf(type.charAt(0));
binding.completionIconText.setText(header);
binding.completionLabel.setText(label);
binding.completionType.setText(type);
binding.completionDetail.setText(desc);
binding.completionIconText.setTypeface(TypefaceUtils.jetbrainsMono(), Typeface.BOLD);
if (desc.isEmpty()) {
binding.completionDetail.setVisibility(View.GONE);
}
if (isCurrentCursorPosition) {
binding
.getRoot()
.setBackgroundColor(
ContextCompat.getColor(getContext(), R.color.completionList_backgroundSelected));
} else {
binding
.getRoot()
.setBackgroundColor(
ContextCompat.getColor(getContext(), R.color.completionList_background));
}
binding.completionApiInfo.setVisibility(View.GONE);
showApiInfoIfNeeded(item, binding.completionApiInfo);
return binding.getRoot();
}
private void showApiInfoIfNeeded(final CompletionItem item, final TextView completionApiInfo) {
new Thread(
() -> {
final ApiInfo apiInfo = StudioApp.getInstance().apiInfo();
boolean hasRead = apiInfo != null && apiInfo.hasRead();
boolean isValid = isValidForApiVersion(item);
if (hasRead && isValid) {
JsonElement element = new Gson().toJsonTree(item.getData());
if (element == null || !element.isJsonObject()) {
return;
}
JsonObject data = element.getAsJsonObject();
if (!data.has("className")) {
return;
}
final String className = data.get("className").getAsString();
CompletionItemKind kind = item.getKind();
ClassInfo clazz = apiInfo.getClassByName(className);
if (clazz == null) {
return;
}
Info info = clazz;
// If this Info is not a class info, find the right member
if (kind == CompletionItemKind.METHOD
&& data.has("erasedParameterTypes")
&& data.has("memberName")) {
JsonElement erasedParameterTypesElement = data.get("erasedParameterTypes");
if (erasedParameterTypesElement.isJsonArray()) {
String simpleName = data.get("memberName").getAsString();
JsonArray erasedParameterTypes = erasedParameterTypesElement.getAsJsonArray();
String[] paramTypes = new String[erasedParameterTypes.size()];
for (int i = 0; i < erasedParameterTypes.size(); i++) {
paramTypes[i] = erasedParameterTypes.get(i).getAsString();
}
MethodInfo method = clazz.getMethod(simpleName, paramTypes);
if (method != null) {
info = method;
}
}
} else if (kind == CompletionItemKind.FIELD && data.has("memberName")) {
String simpleName = data.get("memberName").getAsString();
FieldInfo field = clazz.getFieldByName(simpleName);
if (field != null) {
info = field;
}
}
final StringBuilder infoBuilder = new StringBuilder();
if (info.since > 1) {
infoBuilder.append(
completionApiInfo
.getContext()
.getString(R.string.msg_api_info_since, info.since));
infoBuilder.append("\n");
}
if (info.removed > 0) {
infoBuilder.append(
completionApiInfo
.getContext()
.getString(R.string.msg_api_info_removed, info.removed));
infoBuilder.append("\n");
}
if (info.deprecated > 0) {
infoBuilder.append(
completionApiInfo
.getContext()
.getString(R.string.msg_api_info_deprecated, info.deprecated));
infoBuilder.append("\n");
}
ThreadUtils.runOnUiThread(
() -> {
if (infoBuilder.toString().trim().length() > 0) {
completionApiInfo.setText(infoBuilder.toString().trim());
completionApiInfo.setVisibility(View.VISIBLE);
} else {
completionApiInfo.setVisibility(View.GONE);
}
});
}
})
.start();
}
private boolean isValidForApiVersion(CompletionItem item) {
if (item == null) {
return false;
}
final CompletionItemKind type = item.getKind();
final var data = item.getData();
if (
// These represent a class type
(type == CompletionItemKind.CLASS
|| type == CompletionItemKind.INTERFACE
|| type == CompletionItemKind.ENUM
// These represent a method type
|| type == CompletionItemKind.METHOD
|| type == CompletionItemKind.CONSTRUCTOR
// A field type
|| type == CompletionItemKind.FIELD)
&& data != null) {
return !TextUtils.isEmpty(data.getClassName());
}
return false;
}
}
/*
* 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.content.res.Resources;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.core.content.ContextCompat;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutCompletionItemBinding;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.TypefaceUtils;
import com.itsaky.apiinfo.ApiInfo;
import com.itsaky.apiinfo.models.ClassInfo;
import com.itsaky.apiinfo.models.FieldInfo;
import com.itsaky.apiinfo.models.Info;
import com.itsaky.apiinfo.models.MethodInfo;
import com.itsaky.lsp.models.CompletionItem;
import com.itsaky.lsp.models.CompletionItemKind;
import java.util.List;
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion;
import io.github.rosemoe.sora.widget.component.EditorCompletionAdapter;
public class CompletionListAdapter extends EditorCompletionAdapter {
private static final ILogger LOG = ILogger.newInstance("CompletionListAdapter");
@Override
public void attachValues(
EditorAutoCompletion window,
List<io.github.rosemoe.sora.lang.completion.CompletionItem> items) {
super.attachValues(window, items);
}
@Override
public int getItemHeight() {
return (int)
TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, 40, Resources.getSystem().getDisplayMetrics());
}
@Override
protected View getView(
int position, View convertView, ViewGroup parent, boolean isCurrentCursorPosition) {
final LayoutCompletionItemBinding binding =
LayoutCompletionItemBinding.inflate(LayoutInflater.from(getContext()), parent, false);
var item = (CompletionItem) getItem(position);
String label = item.getLabel(), desc = item.getDetail(), type = item.getKind().toString();
String header = type.length() <= 0 ? "O" : String.valueOf(type.charAt(0));
binding.completionIconText.setText(header);
binding.completionLabel.setText(label);
binding.completionType.setText(type);
binding.completionDetail.setText(desc);
binding.completionIconText.setTypeface(TypefaceUtils.jetbrainsMono(), Typeface.BOLD);
if (desc.isEmpty()) {
binding.completionDetail.setVisibility(View.GONE);
}
if (isCurrentCursorPosition) {
binding
.getRoot()
.setBackgroundColor(
ContextCompat.getColor(getContext(), R.color.completionList_backgroundSelected));
} else {
binding
.getRoot()
.setBackgroundColor(
ContextCompat.getColor(getContext(), R.color.completionList_background));
}
binding.completionApiInfo.setVisibility(View.GONE);
showApiInfoIfNeeded(item, binding.completionApiInfo);
return binding.getRoot();
}
private void showApiInfoIfNeeded(final CompletionItem item, final TextView completionApiInfo) {
new Thread(
() -> {
final ApiInfo apiInfo = StudioApp.getInstance().apiInfo();
boolean hasRead = apiInfo != null && apiInfo.hasRead();
boolean isValid = isValidForApiVersion(item);
if (hasRead && isValid) {
JsonElement element = new Gson().toJsonTree(item.getData());
if (element == null || !element.isJsonObject()) {
return;
}
JsonObject data = element.getAsJsonObject();
if (!data.has("className")) {
return;
}
final String className = data.get("className").getAsString();
CompletionItemKind kind = item.getKind();
ClassInfo clazz = apiInfo.getClassByName(className);
if (clazz == null) {
return;
}
Info info = clazz;
// If this Info is not a class info, find the right member
if (kind == CompletionItemKind.METHOD
&& data.has("erasedParameterTypes")
&& data.has("memberName")) {
JsonElement erasedParameterTypesElement = data.get("erasedParameterTypes");
if (erasedParameterTypesElement.isJsonArray()) {
String simpleName = data.get("memberName").getAsString();
JsonArray erasedParameterTypes = erasedParameterTypesElement.getAsJsonArray();
String[] paramTypes = new String[erasedParameterTypes.size()];
for (int i = 0; i < erasedParameterTypes.size(); i++) {
paramTypes[i] = erasedParameterTypes.get(i).getAsString();
}
MethodInfo method = clazz.getMethod(simpleName, paramTypes);
if (method != null) {
info = method;
}
}
} else if (kind == CompletionItemKind.FIELD && data.has("memberName")) {
String simpleName = data.get("memberName").getAsString();
FieldInfo field = clazz.getFieldByName(simpleName);
if (field != null) {
info = field;
}
}
final StringBuilder infoBuilder = new StringBuilder();
if (info.since > 1) {
infoBuilder.append(
completionApiInfo
.getContext()
.getString(R.string.msg_api_info_since, info.since));
infoBuilder.append("\n");
}
if (info.removed > 0) {
infoBuilder.append(
completionApiInfo
.getContext()
.getString(R.string.msg_api_info_removed, info.removed));
infoBuilder.append("\n");
}
if (info.deprecated > 0) {
infoBuilder.append(
completionApiInfo
.getContext()
.getString(R.string.msg_api_info_deprecated, info.deprecated));
infoBuilder.append("\n");
}
ThreadUtils.runOnUiThread(
() -> {
if (infoBuilder.toString().trim().length() > 0) {
completionApiInfo.setText(infoBuilder.toString().trim());
completionApiInfo.setVisibility(View.VISIBLE);
} else {
completionApiInfo.setVisibility(View.GONE);
}
});
}
})
.start();
}
private boolean isValidForApiVersion(CompletionItem item) {
if (item == null) {
return false;
}
final CompletionItemKind type = item.getKind();
final var data = item.getData();
if (
// These represent a class type
(type == CompletionItemKind.CLASS
|| type == CompletionItemKind.INTERFACE
|| type == CompletionItemKind.ENUM
// These represent a method type
|| type == CompletionItemKind.METHOD
|| type == CompletionItemKind.CONSTRUCTOR
// A field type
|| type == CompletionItemKind.FIELD)
&& data != null) {
return !TextUtils.isEmpty(data.getClassName());
}
return false;
}
}
@@ -1,319 +1,319 @@
/*
* 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.filepicker;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import com.itsaky.androidide.R;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ILogger;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
/**
* A document provider for the Storage Access Framework which exposes the files in the $HOME/
* directory to other apps.
*
* <p>Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
*
* <p>"A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
* support both of them simultaneously, your app will appear twice in the system picker UI, offering
* two different ways of accessing your stored data. This would be confusing for users." -
* http://developer.android.com/guide/topics/providers/document-provider.html#43
*
* @see <a
* href="https://github.com/termux/termux-app/blob/master/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java">TermuxDocumentsProvider</a>
*/
public class IDEDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final ILogger LOG = ILogger.newInstance("IDEDocumentsProvider");
private static final File BASE_DIR = getBaseDir();
// The default columns to return information about a root if no specific
// columns are requested in a query.
private static final String[] DEFAULT_ROOT_PROJECTION =
new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
// The default columns to return information about a document if no specific
// columns are requested in a query.
private static final String[] DEFAULT_DOCUMENT_PROJECTION =
new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
};
@NonNull
private static File getBaseDir() {
if (Environment.HOME != null) {
return Environment.HOME;
}
return new File(Environment.DEFAULT_HOME);
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
*
* <p>The reverse of @{link #getFileForDocId}.
*/
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
/** Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}). */
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1).toLowerCase();
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
@Override
public Cursor queryRoots(String[] projection) {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
final String applicationName = getContext().getString(R.string.app_name);
final MatrixCursor.RowBuilder row = result.newRow();
LOG.debug("queryRoots() before all add");
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
LOG.debug("queryRoots() before all add, 1");
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
LOG.debug("queryRoots() before all add, 2");
row.add(Root.COLUMN_SUMMARY, null);
LOG.debug("queryRoots() before all add, 3");
row.add(
Root.COLUMN_FLAGS,
Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
LOG.debug("queryRoots() before all add, 4");
row.add(Root.COLUMN_TITLE, applicationName);
LOG.debug("queryRoots() before all add, 5");
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
LOG.debug("queryRoots() before all add, 6");
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
LOG.debug("queryRoots() before all add, 7");
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
LOG.debug("queryRoots() before all add");
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
includeFile(result, null, file);
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(
final String documentId, String mode, CancellationSignal signal)
throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(
String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd =
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName)
throws FileNotFoundException {
File newFile = new File(parentDocumentId, displayName);
int noConflictId = 2;
while (newFile.exists()) {
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
}
try {
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
succeeded = newFile.mkdir();
} else {
succeeded = newFile.createNewFile();
}
if (!succeeded) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
return newFile.getPath();
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection)
throws FileNotFoundException {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(rootId);
// This example implementation searches file names for the query and doesn't rank search
// results, so we can stop as soon as we find a sufficient number of matches. Other
// implementations might rank results and use other data about files, rather than the file
// name, to produce a match.
final LinkedList<File> pending = new LinkedList<>();
pending.add(parent);
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g.
// search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(Environment.HOME.getAbsolutePath());
} catch (IOException e) {
isInsideHome = true;
}
if (isInsideHome) {
if (file.isDirectory()) {
Collections.addAll(pending, file.listFiles());
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
}
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
/**
* Add a representation of a file to a cursor.
*
* @param result the cursor to modify
* @param docId the document ID representing the desired file (may be null if given file)
* @param file the File object representing the desired file (may be null if given docID)
*/
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
}
}
/*
* 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.filepicker;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Point;
import android.os.CancellationSignal;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract.Document;
import android.provider.DocumentsContract.Root;
import android.provider.DocumentsProvider;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
import com.itsaky.androidide.R;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ILogger;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
/**
* A document provider for the Storage Access Framework which exposes the files in the $HOME/
* directory to other apps.
*
* <p>Note that this replaces providing an activity matching the ACTION_GET_CONTENT intent:
*
* <p>"A document provider and ACTION_GET_CONTENT should be considered mutually exclusive. If you
* support both of them simultaneously, your app will appear twice in the system picker UI, offering
* two different ways of accessing your stored data. This would be confusing for users." -
* http://developer.android.com/guide/topics/providers/document-provider.html#43
*
* @see <a
* href="https://github.com/termux/termux-app/blob/master/app/src/main/java/com/termux/filepicker/TermuxDocumentsProvider.java">TermuxDocumentsProvider</a>
*/
public class IDEDocumentsProvider extends DocumentsProvider {
private static final String ALL_MIME_TYPES = "*/*";
private static final ILogger LOG = ILogger.newInstance("IDEDocumentsProvider");
private static final File BASE_DIR = getBaseDir();
// The default columns to return information about a root if no specific
// columns are requested in a query.
private static final String[] DEFAULT_ROOT_PROJECTION =
new String[] {
Root.COLUMN_ROOT_ID,
Root.COLUMN_MIME_TYPES,
Root.COLUMN_FLAGS,
Root.COLUMN_ICON,
Root.COLUMN_TITLE,
Root.COLUMN_SUMMARY,
Root.COLUMN_DOCUMENT_ID,
Root.COLUMN_AVAILABLE_BYTES
};
// The default columns to return information about a document if no specific
// columns are requested in a query.
private static final String[] DEFAULT_DOCUMENT_PROJECTION =
new String[] {
Document.COLUMN_DOCUMENT_ID,
Document.COLUMN_MIME_TYPE,
Document.COLUMN_DISPLAY_NAME,
Document.COLUMN_LAST_MODIFIED,
Document.COLUMN_FLAGS,
Document.COLUMN_SIZE
};
@NonNull
private static File getBaseDir() {
if (Environment.HOME != null) {
return Environment.HOME;
}
return new File(Environment.DEFAULT_HOME);
}
/**
* Get the document id given a file. This document id must be consistent across time as other
* applications may save the ID and use it to reference documents later.
*
* <p>The reverse of @{link #getFileForDocId}.
*/
private static String getDocIdForFile(File file) {
return file.getAbsolutePath();
}
/** Get the file given a document id (the reverse of {@link #getDocIdForFile(File)}). */
private static File getFileForDocId(String docId) throws FileNotFoundException {
final File f = new File(docId);
if (!f.exists()) throw new FileNotFoundException(f.getAbsolutePath() + " not found");
return f;
}
private static String getMimeType(File file) {
if (file.isDirectory()) {
return Document.MIME_TYPE_DIR;
} else {
final String name = file.getName();
final int lastDot = name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = name.substring(lastDot + 1).toLowerCase();
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) return mime;
}
return "application/octet-stream";
}
}
@Override
public Cursor queryRoots(String[] projection) {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_ROOT_PROJECTION);
final String applicationName = getContext().getString(R.string.app_name);
final MatrixCursor.RowBuilder row = result.newRow();
LOG.debug("queryRoots() before all add");
row.add(Root.COLUMN_ROOT_ID, getDocIdForFile(BASE_DIR));
LOG.debug("queryRoots() before all add, 1");
row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(BASE_DIR));
LOG.debug("queryRoots() before all add, 2");
row.add(Root.COLUMN_SUMMARY, null);
LOG.debug("queryRoots() before all add, 3");
row.add(
Root.COLUMN_FLAGS,
Root.FLAG_SUPPORTS_CREATE | Root.FLAG_SUPPORTS_SEARCH | Root.FLAG_SUPPORTS_IS_CHILD);
LOG.debug("queryRoots() before all add, 4");
row.add(Root.COLUMN_TITLE, applicationName);
LOG.debug("queryRoots() before all add, 5");
row.add(Root.COLUMN_MIME_TYPES, ALL_MIME_TYPES);
LOG.debug("queryRoots() before all add, 6");
row.add(Root.COLUMN_AVAILABLE_BYTES, BASE_DIR.getFreeSpace());
LOG.debug("queryRoots() before all add, 7");
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
LOG.debug("queryRoots() before all add");
return result;
}
@Override
public Cursor queryDocument(String documentId, String[] projection) throws FileNotFoundException {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
includeFile(result, documentId, null);
return result;
}
@Override
public Cursor queryChildDocuments(String parentDocumentId, String[] projection, String sortOrder)
throws FileNotFoundException {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(parentDocumentId);
for (File file : parent.listFiles()) {
includeFile(result, null, file);
}
return result;
}
@Override
public ParcelFileDescriptor openDocument(
final String documentId, String mode, CancellationSignal signal)
throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final int accessMode = ParcelFileDescriptor.parseMode(mode);
return ParcelFileDescriptor.open(file, accessMode);
}
@Override
public AssetFileDescriptor openDocumentThumbnail(
String documentId, Point sizeHint, CancellationSignal signal) throws FileNotFoundException {
final File file = getFileForDocId(documentId);
final ParcelFileDescriptor pfd =
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
return new AssetFileDescriptor(pfd, 0, file.length());
}
@Override
public boolean onCreate() {
return true;
}
@Override
public String createDocument(String parentDocumentId, String mimeType, String displayName)
throws FileNotFoundException {
File newFile = new File(parentDocumentId, displayName);
int noConflictId = 2;
while (newFile.exists()) {
newFile = new File(parentDocumentId, displayName + " (" + noConflictId++ + ")");
}
try {
boolean succeeded;
if (Document.MIME_TYPE_DIR.equals(mimeType)) {
succeeded = newFile.mkdir();
} else {
succeeded = newFile.createNewFile();
}
if (!succeeded) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
} catch (IOException e) {
throw new FileNotFoundException("Failed to create document with id " + newFile.getPath());
}
return newFile.getPath();
}
@Override
public void deleteDocument(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
if (!file.delete()) {
throw new FileNotFoundException("Failed to delete document with id " + documentId);
}
}
@Override
public String getDocumentType(String documentId) throws FileNotFoundException {
File file = getFileForDocId(documentId);
return getMimeType(file);
}
@Override
public Cursor querySearchDocuments(String rootId, String query, String[] projection)
throws FileNotFoundException {
final MatrixCursor result =
new MatrixCursor(projection != null ? projection : DEFAULT_DOCUMENT_PROJECTION);
final File parent = getFileForDocId(rootId);
// This example implementation searches file names for the query and doesn't rank search
// results, so we can stop as soon as we find a sufficient number of matches. Other
// implementations might rank results and use other data about files, rather than the file
// name, to produce a match.
final LinkedList<File> pending = new LinkedList<>();
pending.add(parent);
final int MAX_SEARCH_RESULTS = 50;
while (!pending.isEmpty() && result.getCount() < MAX_SEARCH_RESULTS) {
final File file = pending.removeFirst();
// Avoid directories outside the $HOME directory linked with symlinks (to avoid e.g.
// search
// through the whole SD card).
boolean isInsideHome;
try {
isInsideHome = file.getCanonicalPath().startsWith(Environment.HOME.getAbsolutePath());
} catch (IOException e) {
isInsideHome = true;
}
if (isInsideHome) {
if (file.isDirectory()) {
Collections.addAll(pending, file.listFiles());
} else {
if (file.getName().toLowerCase().contains(query)) {
includeFile(result, null, file);
}
}
}
}
return result;
}
@Override
public boolean isChildDocument(String parentDocumentId, String documentId) {
return documentId.startsWith(parentDocumentId);
}
/**
* Add a representation of a file to a cursor.
*
* @param result the cursor to modify
* @param docId the document ID representing the desired file (may be null if given file)
* @param file the File object representing the desired file (may be null if given docID)
*/
private void includeFile(MatrixCursor result, String docId, File file)
throws FileNotFoundException {
if (docId == null) {
docId = getDocIdForFile(file);
} else {
file = getFileForDocId(docId);
}
int flags = 0;
if (file.isDirectory()) {
if (file.canWrite()) flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
} else if (file.canWrite()) {
flags |= Document.FLAG_SUPPORTS_WRITE;
}
if (file.getParentFile().canWrite()) flags |= Document.FLAG_SUPPORTS_DELETE;
final String displayName = file.getName();
final String mimeType = getMimeType(file);
if (mimeType.startsWith("image/")) flags |= Document.FLAG_SUPPORTS_THUMBNAIL;
final MatrixCursor.RowBuilder row = result.newRow();
row.add(Document.COLUMN_DOCUMENT_ID, docId);
row.add(Document.COLUMN_DISPLAY_NAME, displayName);
row.add(Document.COLUMN_SIZE, file.length());
row.add(Document.COLUMN_MIME_TYPE, mimeType);
row.add(Document.COLUMN_LAST_MODIFIED, file.lastModified());
row.add(Document.COLUMN_FLAGS, flags);
row.add(Document.COLUMN_ICON, R.mipmap.ic_launcher);
}
}
@@ -1,340 +1,340 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.fragments;
import static com.unnamed.b.atv.view.AndroidTreeView.NODES_PATH_SEPARATOR;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.IntentUtils;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutEditorFileTreeBinding;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.views.editor.CodeEditorView;
import com.itsaky.toaster.Toaster;
import com.unnamed.b.atv.model.TreeNode;
import com.unnamed.b.atv.view.AndroidTreeView;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class FileTreeFragment extends BottomSheetDialogFragment
implements TreeNode.TreeNodeClickListener, TreeNode.TreeNodeLongClickListener {
private static final String KEY_STORED_TREE_STATE = "fileTree_state";
private static final ILogger LOG = ILogger.newInstance("FileTreeFragment");
private LayoutEditorFileTreeBinding binding;
private AndroidTreeView mFileTreeView;
private FileActionListener mFileActionListener;
private TreeNode mRoot;
private String mTreeState;
public FileTreeFragment() {}
@NonNull
public static FileTreeFragment newInstance() {
return new FileTreeFragment();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mFileActionListener = (FileActionListener) context;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
saveTreeState();
outState.putString(KEY_STORED_TREE_STATE, mTreeState);
}
public void saveTreeState() {
if (mFileTreeView != null) {
mTreeState = mFileTreeView.getSaveState();
} else {
LOG.error("Unable to save tree state. TreeView is null.");
mTreeState = null;
}
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = LayoutEditorFileTreeBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_TREE_STATE)) {
mTreeState = savedInstanceState.getString(KEY_STORED_TREE_STATE, null);
}
listProjectFiles();
}
@Override
public void onClick(TreeNode node, Object p2) {
final File f = (File) p2;
if (!f.exists()) {
return;
}
if (f.isFile()) {
if (f.getName().endsWith(".apk")) {
final var intent = IntentUtils.getInstallAppIntent(f);
if (intent != null) {
requireContext().startActivity(intent);
} else {
StudioApp.getInstance()
.toast(getString(R.string.msg_apk_install_intent_failed), Toaster.Type.ERROR);
}
} else if (mFileActionListener != null && (FileUtils.isUtf8(f) || f.length() == 0)) {
mFileActionListener.openFile(f);
}
} else if (f.isDirectory() && f.exists()) {
if (node.isExpanded()) {
collapseNode(node);
} else if (f.getAbsolutePath().equals(Environment.GRADLE_USER_HOME.getAbsolutePath())
&& !node.isExpanded()) {
expandNode(node);
} else {
setLoading(node);
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
});
}
}
}
public void collapseNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.collapseNode(node);
}
public void expandNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.expandNode(node);
}
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading();
}
}
private void listNode(@NonNull TreeNode node, Runnable whenDone) {
if (whenDone == null) {
whenDone = () -> {};
}
node.getChildren().clear();
node.setExpanded(false);
final var finalWhenDone = whenDone;
TaskExecutor.execAsync(
() -> {
getNodeFromFiles(node.getValue().listFiles(), node);
TreeNode temp = node;
while (temp.size() == 1) {
temp = temp.childAt(0);
if (!temp.getValue().isDirectory()) {
break;
}
getNodeFromFiles(temp.getValue().listFiles(), temp);
temp.setExpanded(true);
}
return null;
},
__ -> finalWhenDone.run());
}
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded());
}
}
private void getNodeFromFiles(File[] files, TreeNode parent) {
Arrays.sort(files, new FileTreeCallable.SortFileName());
Arrays.sort(files, new FileTreeCallable.SortFolder());
for (File file : files) {
TreeNode node = new TreeNode(file);
node.setViewHolder(new FileTreeViewHolder(getContext()));
parent.addChild(node);
}
}
@Override
public boolean onLongClick(TreeNode node, Object value) {
if (mFileActionListener != null) {
mFileActionListener.showFileOptions((File) value, node);
}
return true;
}
public void listProjectFiles() {
final var projectDirPath = ProjectManager.INSTANCE.getProjectDirPath();
if (projectDirPath == null) {
return;
}
final File gradleProps = Environment.GRADLE_PROPS;
final File gradleHome = Environment.GRADLE_USER_HOME;
File projectDir = new File(projectDirPath);
mRoot = TreeNode.root(projectDir);
if (gradleHome.exists() && gradleHome.isDirectory()) {
if (!gradleProps.exists()) {
FileIOUtils.writeFileFromString(
gradleProps,
"# Specify global Gradle properties in this file\n"
+ "# These properties will be applicable for every project you build"
+ " with Gradle.");
}
TreeNode home = new TreeNode(gradleHome);
home.setViewHolder(new FileTreeViewHolder(getContext()));
TreeNode prop = new TreeNode(gradleProps);
prop.setViewHolder(new FileTreeViewHolder(getContext()));
home.addChild(prop);
mRoot.addChild(home);
}
mRoot.setViewHolder(new FileTreeViewHolder(getContext()));
getScrollView().setVisibility(View.GONE);
getLoadingProgress().setVisibility(View.VISIBLE);
new TaskExecutor()
.executeAsync(
new FileTreeCallable(getContext(), mRoot, projectDir),
(result) -> {
getScrollView().setVisibility(View.VISIBLE);
getLoadingProgress().setVisibility(View.GONE);
AndroidTreeView tree = createTreeView(mRoot);
if (tree != null) {
tree.setUseAutoToggle(false);
tree.setDefaultNodeClickListener(FileTreeFragment.this);
tree.setDefaultNodeLongClickListener(FileTreeFragment.this);
getScrollView().removeAllViews();
final var view = tree.getView();
getScrollView().addView(view);
view.post(this::tryRestoreState);
}
});
}
public AndroidTreeView createTreeView(TreeNode node) {
Context ctx = null;
if (getActivity() != null) {
ctx = getActivity();
} else if (getContext() != null) {
ctx = getContext();
}
if (ctx == null) {
return null;
}
return mFileTreeView = new AndroidTreeView(ctx, node, R.drawable.bg_ripple);
}
public HorizontalScrollView getScrollView() {
return binding.filetreeHorizontalScrollView;
}
public ProgressBar getLoadingProgress() {
return binding.fileTreeLoadingProgress;
}
private void tryRestoreState() {
tryRestoreState(mTreeState);
}
private void tryRestoreState(String state) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
LOG.debug("Restoring tree view state:", "'" + state + "'");
mFileTreeView.collapseAll();
final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
restoreNodeState(mRoot, openNodes);
} else {
LOG.error("Unable to restore tree state", "treeState=" + state, "treeView=" + mFileTreeView);
}
}
private void restoreNodeState(@NonNull TreeNode root, Set<String> openNodes) {
final var children = root.getChildren();
for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
final var node = children.get(i);
if (openNodes.contains(node.getPath())) {
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
restoreNodeState(node, openNodes);
});
}
}
}
public interface FileActionListener {
CodeEditorView openFile(File file);
void showFileOptions(File file, TreeNode node);
}
}
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.fragments;
import static com.unnamed.b.atv.view.AndroidTreeView.NODES_PATH_SEPARATOR;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionManager;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.IntentUtils;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutEditorFileTreeBinding;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.views.editor.CodeEditorView;
import com.itsaky.toaster.Toaster;
import com.unnamed.b.atv.model.TreeNode;
import com.unnamed.b.atv.view.AndroidTreeView;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class FileTreeFragment extends BottomSheetDialogFragment
implements TreeNode.TreeNodeClickListener, TreeNode.TreeNodeLongClickListener {
private static final String KEY_STORED_TREE_STATE = "fileTree_state";
private static final ILogger LOG = ILogger.newInstance("FileTreeFragment");
private LayoutEditorFileTreeBinding binding;
private AndroidTreeView mFileTreeView;
private FileActionListener mFileActionListener;
private TreeNode mRoot;
private String mTreeState;
public FileTreeFragment() {}
@NonNull
public static FileTreeFragment newInstance() {
return new FileTreeFragment();
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
mFileActionListener = (FileActionListener) context;
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
saveTreeState();
outState.putString(KEY_STORED_TREE_STATE, mTreeState);
}
public void saveTreeState() {
if (mFileTreeView != null) {
mTreeState = mFileTreeView.getSaveState();
} else {
LOG.error("Unable to save tree state. TreeView is null.");
mTreeState = null;
}
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = LayoutEditorFileTreeBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_TREE_STATE)) {
mTreeState = savedInstanceState.getString(KEY_STORED_TREE_STATE, null);
}
listProjectFiles();
}
@Override
public void onClick(TreeNode node, Object p2) {
final File f = (File) p2;
if (!f.exists()) {
return;
}
if (f.isFile()) {
if (f.getName().endsWith(".apk")) {
final var intent = IntentUtils.getInstallAppIntent(f);
if (intent != null) {
requireContext().startActivity(intent);
} else {
StudioApp.getInstance()
.toast(getString(R.string.msg_apk_install_intent_failed), Toaster.Type.ERROR);
}
} else if (mFileActionListener != null && (FileUtils.isUtf8(f) || f.length() == 0)) {
mFileActionListener.openFile(f);
}
} else if (f.isDirectory() && f.exists()) {
if (node.isExpanded()) {
collapseNode(node);
} else if (f.getAbsolutePath().equals(Environment.GRADLE_USER_HOME.getAbsolutePath())
&& !node.isExpanded()) {
expandNode(node);
} else {
setLoading(node);
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
});
}
}
}
public void collapseNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.collapseNode(node);
}
public void expandNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.expandNode(node);
}
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading();
}
}
private void listNode(@NonNull TreeNode node, Runnable whenDone) {
if (whenDone == null) {
whenDone = () -> {};
}
node.getChildren().clear();
node.setExpanded(false);
final var finalWhenDone = whenDone;
TaskExecutor.execAsync(
() -> {
getNodeFromFiles(node.getValue().listFiles(), node);
TreeNode temp = node;
while (temp.size() == 1) {
temp = temp.childAt(0);
if (!temp.getValue().isDirectory()) {
break;
}
getNodeFromFiles(temp.getValue().listFiles(), temp);
temp.setExpanded(true);
}
return null;
},
__ -> finalWhenDone.run());
}
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded());
}
}
private void getNodeFromFiles(File[] files, TreeNode parent) {
Arrays.sort(files, new FileTreeCallable.SortFileName());
Arrays.sort(files, new FileTreeCallable.SortFolder());
for (File file : files) {
TreeNode node = new TreeNode(file);
node.setViewHolder(new FileTreeViewHolder(getContext()));
parent.addChild(node);
}
}
@Override
public boolean onLongClick(TreeNode node, Object value) {
if (mFileActionListener != null) {
mFileActionListener.showFileOptions((File) value, node);
}
return true;
}
public void listProjectFiles() {
final var projectDirPath = ProjectManager.INSTANCE.getProjectDirPath();
if (projectDirPath == null) {
return;
}
final File gradleProps = Environment.GRADLE_PROPS;
final File gradleHome = Environment.GRADLE_USER_HOME;
File projectDir = new File(projectDirPath);
mRoot = TreeNode.root(projectDir);
if (gradleHome.exists() && gradleHome.isDirectory()) {
if (!gradleProps.exists()) {
FileIOUtils.writeFileFromString(
gradleProps,
"# Specify global Gradle properties in this file\n"
+ "# These properties will be applicable for every project you build"
+ " with Gradle.");
}
TreeNode home = new TreeNode(gradleHome);
home.setViewHolder(new FileTreeViewHolder(getContext()));
TreeNode prop = new TreeNode(gradleProps);
prop.setViewHolder(new FileTreeViewHolder(getContext()));
home.addChild(prop);
mRoot.addChild(home);
}
mRoot.setViewHolder(new FileTreeViewHolder(getContext()));
getScrollView().setVisibility(View.GONE);
getLoadingProgress().setVisibility(View.VISIBLE);
new TaskExecutor()
.executeAsync(
new FileTreeCallable(getContext(), mRoot, projectDir),
(result) -> {
getScrollView().setVisibility(View.VISIBLE);
getLoadingProgress().setVisibility(View.GONE);
AndroidTreeView tree = createTreeView(mRoot);
if (tree != null) {
tree.setUseAutoToggle(false);
tree.setDefaultNodeClickListener(FileTreeFragment.this);
tree.setDefaultNodeLongClickListener(FileTreeFragment.this);
getScrollView().removeAllViews();
final var view = tree.getView();
getScrollView().addView(view);
view.post(this::tryRestoreState);
}
});
}
public AndroidTreeView createTreeView(TreeNode node) {
Context ctx = null;
if (getActivity() != null) {
ctx = getActivity();
} else if (getContext() != null) {
ctx = getContext();
}
if (ctx == null) {
return null;
}
return mFileTreeView = new AndroidTreeView(ctx, node, R.drawable.bg_ripple);
}
public HorizontalScrollView getScrollView() {
return binding.filetreeHorizontalScrollView;
}
public ProgressBar getLoadingProgress() {
return binding.fileTreeLoadingProgress;
}
private void tryRestoreState() {
tryRestoreState(mTreeState);
}
private void tryRestoreState(String state) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
LOG.debug("Restoring tree view state:", "'" + state + "'");
mFileTreeView.collapseAll();
final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
restoreNodeState(mRoot, openNodes);
} else {
LOG.error("Unable to restore tree state", "treeState=" + state, "treeView=" + mFileTreeView);
}
}
private void restoreNodeState(@NonNull TreeNode root, Set<String> openNodes) {
final var children = root.getChildren();
for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
final var node = children.get(i);
if (openNodes.contains(node.getPath())) {
listNode(
node,
() -> {
updateChevron(node);
expandNode(node);
restoreNodeState(node, openNodes);
});
}
}
}
public interface FileActionListener {
CodeEditorView openFile(File file);
void showFileOptions(File file, TreeNode node);
}
}
@@ -1,103 +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.fragments.attr;
import android.text.Editable;
import android.text.TextUtils;
import android.widget.AutoCompleteTextView;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.adapters.AttrValueCompletionAdapter;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.TextWatcherAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* @author Akash Yadav
*/
public abstract class AbstractReferenceEditor extends BaseValueEditorFragment {
private static final ILogger LOG = ILogger.newInstance("ReferenceEditor");
public TextWatcherAdapter resInputWatcher;
@Override
protected void notifyValueChanged(@NonNull String newValue) {
try {
super.notifyValueChanged(newValue);
} catch (Throwable e) {
LOG.error("Unable to update resource value to '" + newValue + "'", e);
}
}
protected void setupReferenceInput(MaterialAutoCompleteTextView referenceInput) {
Objects.requireNonNull(referenceInput);
referenceInput.setText(attribute.getValue());
if (resInputWatcher != null) {
referenceInput.removeTextChangedListener(resInputWatcher);
}
resInputWatcher =
new TextWatcherAdapter() {
@Override
public void afterTextChanged(@NonNull Editable s) {
final var text = s.toString().trim();
if (TextUtils.isEmpty(text)) {
return;
}
notifyValueChanged(text);
}
};
referenceInput.addTextChangedListener(resInputWatcher);
CompletableFuture.supplyAsync(this::computeReferenceItems)
.whenComplete((list, error) -> handleAutoCompleteResult(list, error, referenceInput));
}
protected void handleAutoCompleteResult(
List<String> list, Throwable error, AutoCompleteTextView referenceInput) {
if (list == null || list.isEmpty()) {
LOG.error("No completion items found");
LOG.error("Error was:", error);
return;
}
LOG.debug("Found", list.size(), "resource items for completion");
ThreadUtils.runOnUiThread(
() -> {
final var adapter = new AttrValueCompletionAdapter(requireContext(), list);
referenceInput.setThreshold(1);
referenceInput.setAdapter(adapter);
adapter.notifyDataSetChanged();
});
}
protected List<String> computeReferenceItems() {
return new ArrayList<>();
}
}
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.text.Editable;
import android.text.TextUtils;
import android.widget.AutoCompleteTextView;
import androidx.annotation.NonNull;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.adapters.AttrValueCompletionAdapter;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.TextWatcherAdapter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
/**
* @author Akash Yadav
*/
public abstract class AbstractReferenceEditor extends BaseValueEditorFragment {
private static final ILogger LOG = ILogger.newInstance("ReferenceEditor");
public TextWatcherAdapter resInputWatcher;
@Override
protected void notifyValueChanged(@NonNull String newValue) {
try {
super.notifyValueChanged(newValue);
} catch (Throwable e) {
LOG.error("Unable to update resource value to '" + newValue + "'", e);
}
}
protected void setupReferenceInput(MaterialAutoCompleteTextView referenceInput) {
Objects.requireNonNull(referenceInput);
referenceInput.setText(attribute.getValue());
if (resInputWatcher != null) {
referenceInput.removeTextChangedListener(resInputWatcher);
}
resInputWatcher =
new TextWatcherAdapter() {
@Override
public void afterTextChanged(@NonNull Editable s) {
final var text = s.toString().trim();
if (TextUtils.isEmpty(text)) {
return;
}
notifyValueChanged(text);
}
};
referenceInput.addTextChangedListener(resInputWatcher);
CompletableFuture.supplyAsync(this::computeReferenceItems)
.whenComplete((list, error) -> handleAutoCompleteResult(list, error, referenceInput));
}
protected void handleAutoCompleteResult(
List<String> list, Throwable error, AutoCompleteTextView referenceInput) {
if (list == null || list.isEmpty()) {
LOG.error("No completion items found");
LOG.error("Error was:", error);
return;
}
LOG.debug("Found", list.size(), "resource items for completion");
ThreadUtils.runOnUiThread(
() -> {
final var adapter = new AttrValueCompletionAdapter(requireContext(), list);
referenceInput.setThreshold(1);
referenceInput.setAdapter(adapter);
adapter.notifyDataSetChanged();
});
}
protected List<String> computeReferenceItems() {
return new ArrayList<>();
}
}
@@ -1,83 +1,83 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutReferenceAttrEditorBinding;
import com.itsaky.inflater.values.FrameworkValues;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Akash Yadav
*/
public class ReferenceEditor extends AbstractReferenceEditor {
private LayoutReferenceAttrEditorBinding binding;
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
this.binding = LayoutReferenceAttrEditorBinding.inflate(inflater, container, false);
return this.binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupReferenceInput((MaterialAutoCompleteTextView) this.binding.resInput.getEditText());
}
@Override
protected List<String> computeReferenceItems() {
final var resTable = StudioApp.getInstance().getResourceTable();
final var list = new ArrayList<>(resTable.listResourceNames(null));
final var tables = ValuesTableFactory.getAllTables();
for (var entry : tables.entrySet()) {
final var resourceMap = entry.getValue().getResourceMap();
for (var resourceEntries : resourceMap.entrySet()) {
final var name = resourceEntries.getKey();
list.addAll(
resourceEntries.getValue().keySet().stream()
.map(("@android:" + name + "/")::concat)
.collect(Collectors.toList()));
}
}
list.addAll(FrameworkValues.listAllResources());
return list;
}
}
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.attr;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.textfield.MaterialAutoCompleteTextView;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.databinding.LayoutReferenceAttrEditorBinding;
import com.itsaky.inflater.values.FrameworkValues;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @author Akash Yadav
*/
public class ReferenceEditor extends AbstractReferenceEditor {
private LayoutReferenceAttrEditorBinding binding;
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
this.binding = LayoutReferenceAttrEditorBinding.inflate(inflater, container, false);
return this.binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
setupReferenceInput((MaterialAutoCompleteTextView) this.binding.resInput.getEditText());
}
@Override
protected List<String> computeReferenceItems() {
final var resTable = StudioApp.getInstance().getResourceTable();
final var list = new ArrayList<>(resTable.listResourceNames(null));
final var tables = ValuesTableFactory.getAllTables();
for (var entry : tables.entrySet()) {
final var resourceMap = entry.getValue().getResourceMap();
for (var resourceEntries : resourceMap.entrySet()) {
final var name = resourceEntries.getKey();
list.addAll(
resourceEntries.getValue().keySet().stream()
.map(("@android:" + name + "/")::concat)
.collect(Collectors.toList()));
}
}
list.addAll(FrameworkValues.listAllResources());
return list;
}
}
@@ -1,164 +1,164 @@
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.fragments.preferences;
import android.os.Bundle;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.blankj.utilcode.util.FileUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import java.io.File;
public class BuildPreferences extends BasePreferenceFragment
implements Preference.OnPreferenceClickListener {
public static final String KEY_GRADLE_COMMANDS = "idepref_build_gradleCommands";
public static final String KEY_GRADLE_CLEAR_CACHE = "idepref_build_gradleClearCache";
private ProgressSheet progressSheet;
@Override
public void onCreatePreferences(Bundle p1, String p2) {
super.onCreatePreferences(p1, p2);
if (getContext() == null) return;
final PreferenceScreen screen = getPreferenceScreen();
final PreferenceCategory categoryGradle = new PreferenceCategory(getContext());
final Preference customCommands = new Preference(getContext());
final Preference clearCache = new Preference(getContext());
screen.addPreference(categoryGradle);
customCommands.setKey(KEY_GRADLE_COMMANDS);
customCommands.setIcon(R.drawable.ic_bash_commands);
customCommands.setTitle(R.string.idepref_build_customgradlecommands_title);
customCommands.setSummary(R.string.idepref_build_customgradlecommands_summary);
clearCache.setKey(KEY_GRADLE_CLEAR_CACHE);
clearCache.setIcon(R.drawable.ic_delete);
clearCache.setTitle(R.string.idepref_build_clearCache_title);
clearCache.setSummary(R.string.idepref_build_clearCache_summary);
categoryGradle.setTitle(R.string.gradle);
categoryGradle.addPreference(customCommands);
categoryGradle.addPreference(clearCache);
setPreferenceScreen(screen);
customCommands.setOnPreferenceClickListener(this);
clearCache.setOnPreferenceClickListener(this);
}
@Override
public boolean onPreferenceClick(Preference p1) {
final String key = p1.getKey();
if (key.equals(KEY_GRADLE_COMMANDS)) {
showGradleCommandsDialog();
} else if (key.equals(KEY_GRADLE_CLEAR_CACHE)) {
showClearCacheDialog();
}
return true;
}
private void showGradleCommandsDialog() {
final String[] labels = {
"--stacktrace",
"--info",
"--debug",
"--scan",
"--warning-mode all",
"--build-cache",
"--offline"
};
final boolean[] checked = {
getPrefManager().isStackTraceEnabled(),
getPrefManager().isGradleInfoEnabled(),
getPrefManager().isGradleDebugEnabled(),
getPrefManager().isGradleScanEnabled(),
getPrefManager().isGradleWarningEnabled(),
getPrefManager().isGradleBuildCacheEnabled(),
getPrefManager().isGradleOfflineModeEnabled()
};
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
builder.setTitle(R.string.idepref_build_customgradlecommands_title);
builder.setMultiChoiceItems(
labels,
checked,
(p1, p2, p3) -> {
if (p2 == 0) {
getPrefManager().setGradleStacktraceEnabled(p3);
} else if (p2 == 1) {
getPrefManager().setGradleInfoEnabled(p3);
} else if (p2 == 2) {
getPrefManager().setGradleDebugEnabled(p3);
} else if (p2 == 3) {
getPrefManager().setGradleScanEnabled(p3);
} else if (p2 == 4) {
getPrefManager().setGradleWarningEnabled(p3);
} else if (p2 == 5) {
getPrefManager().setGradleBuildCacheEnabled(p3);
} else if (p2 == 6) {
getPrefManager().setGradleOfflineModeEnabled(p3);
}
});
builder.setPositiveButton(android.R.string.ok, null);
builder.setCancelable(false);
builder.create().show();
}
private void showClearCacheDialog() {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
builder.setTitle(R.string.idepref_build_clearCache_title);
builder.setMessage(R.string.msg_clear_cache);
builder.setCancelable(false);
builder.setPositiveButton(
R.string.yes,
(p1, p2) -> {
p1.dismiss();
getProgressSheet().show(getChildFragmentManager(), "progress_sheet");
new TaskExecutor().executeAsync(this::deleteCaches, __ -> getProgressSheet().dismiss());
});
builder.setNegativeButton(R.string.no, null);
builder.create().show();
}
private Object deleteCaches() {
File file = new File(Environment.GRADLE_USER_HOME, "caches");
if (file.exists()) {
FileUtils.delete(file);
}
return null;
}
private ProgressSheet getProgressSheet() {
return progressSheet == null
? progressSheet = new ProgressSheet().setMessage(getString(R.string.please_wait))
: progressSheet;
}
}
/************************************************************************************
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.fragments.preferences;
import android.os.Bundle;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceScreen;
import com.blankj.utilcode.util.FileUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import java.io.File;
public class BuildPreferences extends BasePreferenceFragment
implements Preference.OnPreferenceClickListener {
public static final String KEY_GRADLE_COMMANDS = "idepref_build_gradleCommands";
public static final String KEY_GRADLE_CLEAR_CACHE = "idepref_build_gradleClearCache";
private ProgressSheet progressSheet;
@Override
public void onCreatePreferences(Bundle p1, String p2) {
super.onCreatePreferences(p1, p2);
if (getContext() == null) return;
final PreferenceScreen screen = getPreferenceScreen();
final PreferenceCategory categoryGradle = new PreferenceCategory(getContext());
final Preference customCommands = new Preference(getContext());
final Preference clearCache = new Preference(getContext());
screen.addPreference(categoryGradle);
customCommands.setKey(KEY_GRADLE_COMMANDS);
customCommands.setIcon(R.drawable.ic_bash_commands);
customCommands.setTitle(R.string.idepref_build_customgradlecommands_title);
customCommands.setSummary(R.string.idepref_build_customgradlecommands_summary);
clearCache.setKey(KEY_GRADLE_CLEAR_CACHE);
clearCache.setIcon(R.drawable.ic_delete);
clearCache.setTitle(R.string.idepref_build_clearCache_title);
clearCache.setSummary(R.string.idepref_build_clearCache_summary);
categoryGradle.setTitle(R.string.gradle);
categoryGradle.addPreference(customCommands);
categoryGradle.addPreference(clearCache);
setPreferenceScreen(screen);
customCommands.setOnPreferenceClickListener(this);
clearCache.setOnPreferenceClickListener(this);
}
@Override
public boolean onPreferenceClick(Preference p1) {
final String key = p1.getKey();
if (key.equals(KEY_GRADLE_COMMANDS)) {
showGradleCommandsDialog();
} else if (key.equals(KEY_GRADLE_CLEAR_CACHE)) {
showClearCacheDialog();
}
return true;
}
private void showGradleCommandsDialog() {
final String[] labels = {
"--stacktrace",
"--info",
"--debug",
"--scan",
"--warning-mode all",
"--build-cache",
"--offline"
};
final boolean[] checked = {
getPrefManager().isStackTraceEnabled(),
getPrefManager().isGradleInfoEnabled(),
getPrefManager().isGradleDebugEnabled(),
getPrefManager().isGradleScanEnabled(),
getPrefManager().isGradleWarningEnabled(),
getPrefManager().isGradleBuildCacheEnabled(),
getPrefManager().isGradleOfflineModeEnabled()
};
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
builder.setTitle(R.string.idepref_build_customgradlecommands_title);
builder.setMultiChoiceItems(
labels,
checked,
(p1, p2, p3) -> {
if (p2 == 0) {
getPrefManager().setGradleStacktraceEnabled(p3);
} else if (p2 == 1) {
getPrefManager().setGradleInfoEnabled(p3);
} else if (p2 == 2) {
getPrefManager().setGradleDebugEnabled(p3);
} else if (p2 == 3) {
getPrefManager().setGradleScanEnabled(p3);
} else if (p2 == 4) {
getPrefManager().setGradleWarningEnabled(p3);
} else if (p2 == 5) {
getPrefManager().setGradleBuildCacheEnabled(p3);
} else if (p2 == 6) {
getPrefManager().setGradleOfflineModeEnabled(p3);
}
});
builder.setPositiveButton(android.R.string.ok, null);
builder.setCancelable(false);
builder.create().show();
}
private void showClearCacheDialog() {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
builder.setTitle(R.string.idepref_build_clearCache_title);
builder.setMessage(R.string.msg_clear_cache);
builder.setCancelable(false);
builder.setPositiveButton(
R.string.yes,
(p1, p2) -> {
p1.dismiss();
getProgressSheet().show(getChildFragmentManager(), "progress_sheet");
new TaskExecutor().executeAsync(this::deleteCaches, __ -> getProgressSheet().dismiss());
});
builder.setNegativeButton(R.string.no, null);
builder.create().show();
}
private Object deleteCaches() {
File file = new File(Environment.GRADLE_USER_HOME, "caches");
if (file.exists()) {
FileUtils.delete(file);
}
return null;
}
private ProgressSheet getProgressSheet() {
return progressSheet == null
? progressSheet = new ProgressSheet().setMessage(getString(R.string.please_wait))
: progressSheet;
}
}
@@ -1,319 +1,319 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.fragments.preferences;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_AUTO_SAVE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_DRAW_HEX;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_LINE_BREAK;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_PASSWORD;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_EMPTY_LINE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_INNER;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_LEADING;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_TRAILING;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FONT_LIGATURES;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FONT_SIZE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_PRINTABLE_CHARS;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_TAB_SIZE;
import static com.itsaky.lsp.java.models.JavaServerSettings.KEY_COMPLETIONS_MATCH_LOWER;
import static com.itsaky.lsp.java.models.JavaServerSettings.KEY_JAVA_PREF_GOOGLE_CODE_STYLE;
import android.os.Bundle;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.SwitchPreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.databinding.LayoutTextSizeSliderBinding;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.DialogUtils;
public class EditorPreferences extends BasePreferenceFragment
implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener {
@Override
public void onCreatePreferences(Bundle p1, String p2) {
super.onCreatePreferences(p1, p2);
if (getContext() == null) {
return;
}
final var screen = getPreferenceScreen();
final var commonCategory = new PreferenceCategory(getContext());
final var javaCategory = new PreferenceCategory(getContext());
final var fontSize = new Preference(getContext());
final var nonPrintable = new Preference(getContext());
final var tabSize = new Preference(getContext());
final var drawHex = new SwitchPreference(getContext());
final var wordWrap = new SwitchPreference(getContext());
final var magnifier = new SwitchPreference(getContext());
final var fontLigatures = new SwitchPreference(getContext());
final var autoSave = new SwitchPreference(getContext());
final var useGoogleCodeStyle = new SwitchPreference(getContext());
final var visiblePasswordFlag = new SwitchPreference(getContext());
final var completionsMatchLower = new SwitchPreference(getContext());
screen.addPreference(commonCategory);
screen.addPreference(javaCategory);
fontSize.setIcon(R.drawable.ic_text_size);
fontSize.setKey(KEY_EDITOR_FONT_SIZE);
fontSize.setTitle(R.string.idepref_editor_fontsize_title);
fontSize.setSummary(R.string.idepref_editor_fontsize_summary);
wordWrap.setIcon(R.drawable.ic_wrap_text);
wordWrap.setKey(PreferenceManager.KEY_EDITOR_WORD_WRAP);
wordWrap.setTitle(R.string.idepref_editor_word_wrap_title);
wordWrap.setSummary(R.string.idepref_editor_word_wrap_summary);
magnifier.setIcon(R.drawable.ic_loupe);
magnifier.setKey(PreferenceManager.KEY_EDITOR_USE_MAGNIFER);
magnifier.setTitle(R.string.idepref_editor_use_magnifier_title);
magnifier.setSummary(R.string.idepref_editor_use_magnifier_summary);
nonPrintable.setIcon(R.drawable.ic_drawing);
nonPrintable.setKey(KEY_EDITOR_PRINTABLE_CHARS);
nonPrintable.setTitle(R.string.idepref_editor_paintingflags_title);
nonPrintable.setSummary(R.string.idepref_editor_paintingflags_summary);
tabSize.setIcon(R.drawable.ic_tab);
tabSize.setKey(KEY_EDITOR_TAB_SIZE);
tabSize.setTitle(R.string.title_tab_size);
tabSize.setSummary(R.string.msg_tab_size);
drawHex.setIcon(R.drawable.ic_hexadecimal);
drawHex.setKey(KEY_EDITOR_DRAW_HEX);
drawHex.setTitle(R.string.idepref_editor_drawhexcolors_title);
drawHex.setSummary(R.string.idepref_editor_drawhexcolors_summary);
fontLigatures.setIcon(R.drawable.ic_font_ligatures);
fontLigatures.setKey(KEY_EDITOR_FONT_LIGATURES);
fontLigatures.setTitle(getString(R.string.idepref_editor_ligatures_title));
fontLigatures.setSummary(getString(R.string.idepref_editor_ligatures_summary));
autoSave.setIcon(R.drawable.ic_save);
autoSave.setKey(KEY_EDITOR_AUTO_SAVE);
autoSave.setTitle(getString(R.string.idepref_editor_autoSave_title));
autoSave.setSummary(getString(R.string.idepref_editor_autoSave_summary));
completionsMatchLower.setKey(KEY_COMPLETIONS_MATCH_LOWER);
completionsMatchLower.setIcon(R.drawable.ic_text_lower);
completionsMatchLower.setTitle(getString(R.string.idepref_java_matchLower_title));
completionsMatchLower.setSummary(getString(R.string.idepref_java_matchLower_summary));
useGoogleCodeStyle.setIcon(R.drawable.ic_format_code);
useGoogleCodeStyle.setKey(KEY_JAVA_PREF_GOOGLE_CODE_STYLE);
useGoogleCodeStyle.setTitle(getString(R.string.idepref_java_useGoogleStyle_title));
useGoogleCodeStyle.setSummary(getString(R.string.idepref_java_useGoogleStyle_summary));
visiblePasswordFlag.setIcon(R.drawable.ic_password_input);
visiblePasswordFlag.setKey(KEY_EDITOR_FLAG_PASSWORD);
visiblePasswordFlag.setTitle(getString(R.string.idepref_visiblePassword_title));
visiblePasswordFlag.setSummary(getString(R.string.idepref_visiblePassword_summary));
commonCategory.setTitle(getString(R.string.idepref_editor_category_common));
commonCategory.addPreference(fontSize);
commonCategory.addPreference(fontLigatures);
commonCategory.addPreference(tabSize);
commonCategory.addPreference(wordWrap);
commonCategory.addPreference(magnifier);
commonCategory.addPreference(completionsMatchLower);
commonCategory.addPreference(visiblePasswordFlag);
commonCategory.addPreference(nonPrintable);
commonCategory.addPreference(drawHex);
commonCategory.addPreference(autoSave);
javaCategory.setTitle(getString(R.string.idepref_editor_category_java));
javaCategory.addPreference(useGoogleCodeStyle);
setPreferenceScreen(screen);
fontSize.setOnPreferenceClickListener(this);
fontLigatures.setOnPreferenceChangeListener(this);
nonPrintable.setOnPreferenceClickListener(this);
tabSize.setOnPreferenceClickListener(this);
drawHex.setOnPreferenceChangeListener(this);
autoSave.setOnPreferenceChangeListener(this);
completionsMatchLower.setOnPreferenceChangeListener(this);
useGoogleCodeStyle.setOnPreferenceChangeListener(this);
visiblePasswordFlag.setOnPreferenceChangeListener(this);
wordWrap.setOnPreferenceChangeListener(this);
magnifier.setOnPreferenceChangeListener(this);
fontLigatures.setChecked(getPrefManager().getBoolean(KEY_EDITOR_FONT_LIGATURES, true));
drawHex.setChecked(getPrefManager().getBoolean(KEY_EDITOR_DRAW_HEX, true));
autoSave.setChecked(getPrefManager().getBoolean(KEY_EDITOR_AUTO_SAVE, false));
completionsMatchLower.setChecked(
getPrefManager().getBoolean(KEY_COMPLETIONS_MATCH_LOWER, false));
useGoogleCodeStyle.setChecked(
getPrefManager().getBoolean(KEY_JAVA_PREF_GOOGLE_CODE_STYLE, false));
visiblePasswordFlag.setChecked(getPrefManager().getBoolean(KEY_EDITOR_FLAG_PASSWORD, true));
wordWrap.setChecked(getPrefManager().getBoolean(PreferenceManager.KEY_EDITOR_WORD_WRAP, false));
magnifier.setChecked(
getPrefManager().getBoolean(PreferenceManager.KEY_EDITOR_USE_MAGNIFER, true));
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
final boolean value = (boolean) newValue;
switch (preference.getKey()) {
case KEY_EDITOR_FLAG_PASSWORD:
ConstantsBridge.EDITOR_PREF_VISIBLE_PASSWORD_CHANGED = true;
break;
case KEY_EDITOR_DRAW_HEX:
ConstantsBridge.EDITOR_PREF_DRAW_HEX_CHANGED = true;
break;
case KEY_EDITOR_FONT_LIGATURES:
ConstantsBridge.EDITOR_PREF_LIGATURES_CHANGED = true;
break;
case PreferenceManager.KEY_EDITOR_WORD_WRAP:
ConstantsBridge.EDITOR_PREF_WORD_WRAP_CHANGED = true;
break;
case PreferenceManager.KEY_EDITOR_USE_MAGNIFER:
ConstantsBridge.EDITOR_PREF_USE_MAGNIFIER_CHANGED = true;
break;
}
getPrefManager().putBoolean(preference.getKey(), value);
return true;
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
final String key = preference.getKey();
switch (key) {
case KEY_EDITOR_FONT_SIZE:
showTextSizeDialog();
break;
case KEY_EDITOR_PRINTABLE_CHARS:
showPrintableCharsDialog();
break;
case KEY_EDITOR_TAB_SIZE:
showTabSizeDialog();
break;
}
return true;
}
private void showTabSizeDialog() {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
final String[] sizes = new String[] {"2", "4", "6", "8"};
int current = (getPrefManager().getEditorTabSize() / 2) - 1;
if (current < 0 || current >= sizes.length) {
current = 1;
}
builder.setTitle(R.string.title_tab_size);
builder.setSingleChoiceItems(
sizes,
current,
(d, i) -> {
d.dismiss();
int tabSize = (i + 1) * 2;
if (tabSize < 2 || tabSize > 8) {
tabSize = 4;
}
getPrefManager().putInt(KEY_EDITOR_TAB_SIZE, tabSize);
});
builder.setCancelable(true);
builder.show();
}
private void showTextSizeDialog() {
final LayoutTextSizeSliderBinding binding =
LayoutTextSizeSliderBinding.inflate(LayoutInflater.from(getContext()));
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
float size = getPrefManager().getFloat(KEY_EDITOR_FONT_SIZE, 14);
if (size < 6 || size > 32) {
size = 14;
}
changeTextSize(binding, size);
binding.slider.setLabelFormatter(p1 -> String.valueOf((int) p1));
builder.setTitle(R.string.title_change_text_size);
builder.setMessage(R.string.msg_editor_font_size);
builder.setView(binding.getRoot());
builder.setPositiveButton(
android.R.string.ok,
(p1, p2) -> {
p1.dismiss();
float s = binding.slider.getValue();
changeTextSize(binding, s);
});
builder.setNeutralButton(R.string.reset, (p1, p2) -> changeTextSize(binding, 14));
builder.setCancelable(false);
builder.show();
}
private void changeTextSize(LayoutTextSizeSliderBinding binding, float size) {
getPrefManager().putFloat(KEY_EDITOR_FONT_SIZE, size);
binding.slider.setValue(size);
ConstantsBridge.EDITOR_PREF_SIZE_CHANGED = true;
}
private void showPrintableCharsDialog() {
String[] labels = {
"Leading whitespaces",
"Trailing whitespaces",
"Inner whitespaces",
"Whitespaces in empty lines",
"Line breaks"
};
final boolean[] checked = {
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_LEADING, true),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_TRAILING, false),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_INNER, true),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_EMPTY_LINE, true),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_LINE_BREAK, true)
};
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
builder.setTitle(R.string.idepref_editor_paintingflags_title);
builder.setMultiChoiceItems(
labels,
checked,
(dialog, which, isChecked) -> {
if (which == 0) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_LEADING, isChecked);
} else if (which == 1) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_TRAILING, isChecked);
} else if (which == 2) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_INNER, isChecked);
} else if (which == 3) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_EMPTY_LINE, isChecked);
} else if (which == 4) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_LINE_BREAK, isChecked);
}
ConstantsBridge.EDITOR_PREF_FLAGS_CHANGED = true;
});
builder.setPositiveButton(android.R.string.ok, null);
builder.setCancelable(false);
builder.show();
}
}
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.fragments.preferences;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_AUTO_SAVE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_DRAW_HEX;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_LINE_BREAK;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_PASSWORD;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_EMPTY_LINE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_INNER;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_LEADING;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FLAG_WS_TRAILING;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FONT_LIGATURES;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_FONT_SIZE;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_PRINTABLE_CHARS;
import static com.itsaky.androidide.managers.PreferenceManager.KEY_EDITOR_TAB_SIZE;
import static com.itsaky.lsp.java.models.JavaServerSettings.KEY_COMPLETIONS_MATCH_LOWER;
import static com.itsaky.lsp.java.models.JavaServerSettings.KEY_JAVA_PREF_GOOGLE_CODE_STYLE;
import android.os.Bundle;
import android.view.LayoutInflater;
import androidx.annotation.NonNull;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.SwitchPreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.R;
import com.itsaky.androidide.databinding.LayoutTextSizeSliderBinding;
import com.itsaky.androidide.managers.PreferenceManager;
import com.itsaky.androidide.models.ConstantsBridge;
import com.itsaky.androidide.utils.DialogUtils;
public class EditorPreferences extends BasePreferenceFragment
implements Preference.OnPreferenceChangeListener, Preference.OnPreferenceClickListener {
@Override
public void onCreatePreferences(Bundle p1, String p2) {
super.onCreatePreferences(p1, p2);
if (getContext() == null) {
return;
}
final var screen = getPreferenceScreen();
final var commonCategory = new PreferenceCategory(getContext());
final var javaCategory = new PreferenceCategory(getContext());
final var fontSize = new Preference(getContext());
final var nonPrintable = new Preference(getContext());
final var tabSize = new Preference(getContext());
final var drawHex = new SwitchPreference(getContext());
final var wordWrap = new SwitchPreference(getContext());
final var magnifier = new SwitchPreference(getContext());
final var fontLigatures = new SwitchPreference(getContext());
final var autoSave = new SwitchPreference(getContext());
final var useGoogleCodeStyle = new SwitchPreference(getContext());
final var visiblePasswordFlag = new SwitchPreference(getContext());
final var completionsMatchLower = new SwitchPreference(getContext());
screen.addPreference(commonCategory);
screen.addPreference(javaCategory);
fontSize.setIcon(R.drawable.ic_text_size);
fontSize.setKey(KEY_EDITOR_FONT_SIZE);
fontSize.setTitle(R.string.idepref_editor_fontsize_title);
fontSize.setSummary(R.string.idepref_editor_fontsize_summary);
wordWrap.setIcon(R.drawable.ic_wrap_text);
wordWrap.setKey(PreferenceManager.KEY_EDITOR_WORD_WRAP);
wordWrap.setTitle(R.string.idepref_editor_word_wrap_title);
wordWrap.setSummary(R.string.idepref_editor_word_wrap_summary);
magnifier.setIcon(R.drawable.ic_loupe);
magnifier.setKey(PreferenceManager.KEY_EDITOR_USE_MAGNIFER);
magnifier.setTitle(R.string.idepref_editor_use_magnifier_title);
magnifier.setSummary(R.string.idepref_editor_use_magnifier_summary);
nonPrintable.setIcon(R.drawable.ic_drawing);
nonPrintable.setKey(KEY_EDITOR_PRINTABLE_CHARS);
nonPrintable.setTitle(R.string.idepref_editor_paintingflags_title);
nonPrintable.setSummary(R.string.idepref_editor_paintingflags_summary);
tabSize.setIcon(R.drawable.ic_tab);
tabSize.setKey(KEY_EDITOR_TAB_SIZE);
tabSize.setTitle(R.string.title_tab_size);
tabSize.setSummary(R.string.msg_tab_size);
drawHex.setIcon(R.drawable.ic_hexadecimal);
drawHex.setKey(KEY_EDITOR_DRAW_HEX);
drawHex.setTitle(R.string.idepref_editor_drawhexcolors_title);
drawHex.setSummary(R.string.idepref_editor_drawhexcolors_summary);
fontLigatures.setIcon(R.drawable.ic_font_ligatures);
fontLigatures.setKey(KEY_EDITOR_FONT_LIGATURES);
fontLigatures.setTitle(getString(R.string.idepref_editor_ligatures_title));
fontLigatures.setSummary(getString(R.string.idepref_editor_ligatures_summary));
autoSave.setIcon(R.drawable.ic_save);
autoSave.setKey(KEY_EDITOR_AUTO_SAVE);
autoSave.setTitle(getString(R.string.idepref_editor_autoSave_title));
autoSave.setSummary(getString(R.string.idepref_editor_autoSave_summary));
completionsMatchLower.setKey(KEY_COMPLETIONS_MATCH_LOWER);
completionsMatchLower.setIcon(R.drawable.ic_text_lower);
completionsMatchLower.setTitle(getString(R.string.idepref_java_matchLower_title));
completionsMatchLower.setSummary(getString(R.string.idepref_java_matchLower_summary));
useGoogleCodeStyle.setIcon(R.drawable.ic_format_code);
useGoogleCodeStyle.setKey(KEY_JAVA_PREF_GOOGLE_CODE_STYLE);
useGoogleCodeStyle.setTitle(getString(R.string.idepref_java_useGoogleStyle_title));
useGoogleCodeStyle.setSummary(getString(R.string.idepref_java_useGoogleStyle_summary));
visiblePasswordFlag.setIcon(R.drawable.ic_password_input);
visiblePasswordFlag.setKey(KEY_EDITOR_FLAG_PASSWORD);
visiblePasswordFlag.setTitle(getString(R.string.idepref_visiblePassword_title));
visiblePasswordFlag.setSummary(getString(R.string.idepref_visiblePassword_summary));
commonCategory.setTitle(getString(R.string.idepref_editor_category_common));
commonCategory.addPreference(fontSize);
commonCategory.addPreference(fontLigatures);
commonCategory.addPreference(tabSize);
commonCategory.addPreference(wordWrap);
commonCategory.addPreference(magnifier);
commonCategory.addPreference(completionsMatchLower);
commonCategory.addPreference(visiblePasswordFlag);
commonCategory.addPreference(nonPrintable);
commonCategory.addPreference(drawHex);
commonCategory.addPreference(autoSave);
javaCategory.setTitle(getString(R.string.idepref_editor_category_java));
javaCategory.addPreference(useGoogleCodeStyle);
setPreferenceScreen(screen);
fontSize.setOnPreferenceClickListener(this);
fontLigatures.setOnPreferenceChangeListener(this);
nonPrintable.setOnPreferenceClickListener(this);
tabSize.setOnPreferenceClickListener(this);
drawHex.setOnPreferenceChangeListener(this);
autoSave.setOnPreferenceChangeListener(this);
completionsMatchLower.setOnPreferenceChangeListener(this);
useGoogleCodeStyle.setOnPreferenceChangeListener(this);
visiblePasswordFlag.setOnPreferenceChangeListener(this);
wordWrap.setOnPreferenceChangeListener(this);
magnifier.setOnPreferenceChangeListener(this);
fontLigatures.setChecked(getPrefManager().getBoolean(KEY_EDITOR_FONT_LIGATURES, true));
drawHex.setChecked(getPrefManager().getBoolean(KEY_EDITOR_DRAW_HEX, true));
autoSave.setChecked(getPrefManager().getBoolean(KEY_EDITOR_AUTO_SAVE, false));
completionsMatchLower.setChecked(
getPrefManager().getBoolean(KEY_COMPLETIONS_MATCH_LOWER, false));
useGoogleCodeStyle.setChecked(
getPrefManager().getBoolean(KEY_JAVA_PREF_GOOGLE_CODE_STYLE, false));
visiblePasswordFlag.setChecked(getPrefManager().getBoolean(KEY_EDITOR_FLAG_PASSWORD, true));
wordWrap.setChecked(getPrefManager().getBoolean(PreferenceManager.KEY_EDITOR_WORD_WRAP, false));
magnifier.setChecked(
getPrefManager().getBoolean(PreferenceManager.KEY_EDITOR_USE_MAGNIFER, true));
}
@Override
public boolean onPreferenceChange(@NonNull Preference preference, Object newValue) {
final boolean value = (boolean) newValue;
switch (preference.getKey()) {
case KEY_EDITOR_FLAG_PASSWORD:
ConstantsBridge.EDITOR_PREF_VISIBLE_PASSWORD_CHANGED = true;
break;
case KEY_EDITOR_DRAW_HEX:
ConstantsBridge.EDITOR_PREF_DRAW_HEX_CHANGED = true;
break;
case KEY_EDITOR_FONT_LIGATURES:
ConstantsBridge.EDITOR_PREF_LIGATURES_CHANGED = true;
break;
case PreferenceManager.KEY_EDITOR_WORD_WRAP:
ConstantsBridge.EDITOR_PREF_WORD_WRAP_CHANGED = true;
break;
case PreferenceManager.KEY_EDITOR_USE_MAGNIFER:
ConstantsBridge.EDITOR_PREF_USE_MAGNIFIER_CHANGED = true;
break;
}
getPrefManager().putBoolean(preference.getKey(), value);
return true;
}
@Override
public boolean onPreferenceClick(@NonNull Preference preference) {
final String key = preference.getKey();
switch (key) {
case KEY_EDITOR_FONT_SIZE:
showTextSizeDialog();
break;
case KEY_EDITOR_PRINTABLE_CHARS:
showPrintableCharsDialog();
break;
case KEY_EDITOR_TAB_SIZE:
showTabSizeDialog();
break;
}
return true;
}
private void showTabSizeDialog() {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
final String[] sizes = new String[] {"2", "4", "6", "8"};
int current = (getPrefManager().getEditorTabSize() / 2) - 1;
if (current < 0 || current >= sizes.length) {
current = 1;
}
builder.setTitle(R.string.title_tab_size);
builder.setSingleChoiceItems(
sizes,
current,
(d, i) -> {
d.dismiss();
int tabSize = (i + 1) * 2;
if (tabSize < 2 || tabSize > 8) {
tabSize = 4;
}
getPrefManager().putInt(KEY_EDITOR_TAB_SIZE, tabSize);
});
builder.setCancelable(true);
builder.show();
}
private void showTextSizeDialog() {
final LayoutTextSizeSliderBinding binding =
LayoutTextSizeSliderBinding.inflate(LayoutInflater.from(getContext()));
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
float size = getPrefManager().getFloat(KEY_EDITOR_FONT_SIZE, 14);
if (size < 6 || size > 32) {
size = 14;
}
changeTextSize(binding, size);
binding.slider.setLabelFormatter(p1 -> String.valueOf((int) p1));
builder.setTitle(R.string.title_change_text_size);
builder.setMessage(R.string.msg_editor_font_size);
builder.setView(binding.getRoot());
builder.setPositiveButton(
android.R.string.ok,
(p1, p2) -> {
p1.dismiss();
float s = binding.slider.getValue();
changeTextSize(binding, s);
});
builder.setNeutralButton(R.string.reset, (p1, p2) -> changeTextSize(binding, 14));
builder.setCancelable(false);
builder.show();
}
private void changeTextSize(LayoutTextSizeSliderBinding binding, float size) {
getPrefManager().putFloat(KEY_EDITOR_FONT_SIZE, size);
binding.slider.setValue(size);
ConstantsBridge.EDITOR_PREF_SIZE_CHANGED = true;
}
private void showPrintableCharsDialog() {
String[] labels = {
"Leading whitespaces",
"Trailing whitespaces",
"Inner whitespaces",
"Whitespaces in empty lines",
"Line breaks"
};
final boolean[] checked = {
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_LEADING, true),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_TRAILING, false),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_INNER, true),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_WS_EMPTY_LINE, true),
getPrefManager().getBoolean(KEY_EDITOR_FLAG_LINE_BREAK, true)
};
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(getContext());
builder.setTitle(R.string.idepref_editor_paintingflags_title);
builder.setMultiChoiceItems(
labels,
checked,
(dialog, which, isChecked) -> {
if (which == 0) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_LEADING, isChecked);
} else if (which == 1) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_TRAILING, isChecked);
} else if (which == 2) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_INNER, isChecked);
} else if (which == 3) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_WS_EMPTY_LINE, isChecked);
} else if (which == 4) {
getPrefManager().putBoolean(KEY_EDITOR_FLAG_LINE_BREAK, isChecked);
}
ConstantsBridge.EDITOR_PREF_FLAGS_CHANGED = true;
});
builder.setPositiveButton(android.R.string.ok, null);
builder.setCancelable(false);
builder.show();
}
}
@@ -1,192 +1,192 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.sheets;
import static com.itsaky.attrinfo.models.Attr.FORMAT_BOOLEAN;
import static com.itsaky.attrinfo.models.Attr.FORMAT_COLOR;
import static com.itsaky.attrinfo.models.Attr.FORMAT_DIMENSION;
import static com.itsaky.attrinfo.models.Attr.FORMAT_ENUM;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FLAG;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FLOAT;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FRACTION;
import static com.itsaky.attrinfo.models.Attr.FORMAT_INTEGER;
import static com.itsaky.attrinfo.models.Attr.FORMAT_REFERENCE;
import static com.itsaky.attrinfo.models.Attr.FORMAT_STRING;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.adapters.AttrValueFormatTabAdapter;
import com.itsaky.androidide.databinding.LayoutAttrValueEditorBinding;
import com.itsaky.androidide.fragments.attr.BaseValueEditorFragment;
import com.itsaky.androidide.fragments.attr.BooleanEditor;
import com.itsaky.androidide.fragments.attr.ColorEditor;
import com.itsaky.androidide.fragments.attr.DimensionEditor;
import com.itsaky.androidide.fragments.attr.EnumEditor;
import com.itsaky.androidide.fragments.attr.FlagEditor;
import com.itsaky.androidide.fragments.attr.FloatEditor;
import com.itsaky.androidide.fragments.attr.FractionEditor;
import com.itsaky.androidide.fragments.attr.IntegerEditor;
import com.itsaky.androidide.fragments.attr.ReferenceEditor;
import com.itsaky.androidide.fragments.attr.StringEditor;
import com.itsaky.androidide.models.XMLAttribute;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import org.jetbrains.annotations.Contract;
import java.util.Objects;
/**
* Sheet for editing value of an attribute.
*
* @author Akash Yadav
*/
public class AttrValueEditorSheet extends BottomSheetDialogFragment
implements BaseValueEditorFragment.OnValueChangeListener {
private static final String KEY_ATTRIBUTE = "attrEditorSheet_attribute";
private BaseValueEditorFragment.OnValueChangeListener valueChangeListener;
private LayoutAttrValueEditorBinding binding;
private AttrValueFormatTabAdapter adapter;
private XMLAttribute attribute;
@NonNull
public static AttrValueEditorSheet newInstance(final XMLAttribute attribute) {
Objects.requireNonNull(attribute);
final var args = new Bundle();
args.putParcelable(KEY_ATTRIBUTE, attribute);
final var fragment = new AttrValueEditorSheet();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.valueChangeListener = (BaseValueEditorFragment.OnValueChangeListener) getParentFragment();
}
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
this.binding = LayoutAttrValueEditorBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (attribute == null) {
this.attribute = requireArguments().getParcelable(KEY_ATTRIBUTE);
}
Objects.requireNonNull(this.attribute);
final var namespace = this.attribute.getNamespace();
final var formatText = Attr.createFormatText(attribute.getFormat());
this.binding.attributeName.setText(
String.format("%s:%s", namespace.getPrefix(), attribute.getAttributeName()));
this.binding.attributeFormat.setText(formatText);
setupTabs(formatText);
}
@SuppressLint("NotifyDataSetChanged")
private void setupTabs(@NonNull String formatText) {
this.binding.tabs.removeAllTabs();
this.binding.pager.setSaveEnabled(false);
if (this.adapter != null) {
this.adapter.removeAll();
this.adapter.notifyDataSetChanged();
}
this.adapter = new AttrValueFormatTabAdapter(getChildFragmentManager(), attribute);
final var formats = formatText.split("\\|");
for (var formatName : formats) {
if (TextUtils.isEmpty(formatName) || formatName.length() < 2) {
continue;
}
final var frag = getFragmentClass(formatName);
if (frag != null) {
this.adapter.addFragment(frag, formatName);
}
}
binding.pager.setAdapter(this.adapter);
binding.tabs.setupWithViewPager(binding.pager);
}
@Nullable
@Contract(pure = true)
private Class<? extends BaseValueEditorFragment> getFragmentClass(@NonNull String name) {
switch (name) {
case FORMAT_REFERENCE:
return ReferenceEditor.class;
case FORMAT_COLOR:
return ColorEditor.class;
case FORMAT_BOOLEAN:
return BooleanEditor.class;
case FORMAT_DIMENSION:
return DimensionEditor.class;
case FORMAT_FLOAT:
return FloatEditor.class;
case FORMAT_INTEGER:
return IntegerEditor.class;
case FORMAT_FRACTION:
return FractionEditor.class;
case FORMAT_STRING:
return StringEditor.class;
case FORMAT_ENUM:
return EnumEditor.class;
case FORMAT_FLAG:
return FlagEditor.class;
}
return null;
}
public void setAttribute(XMLAttribute attribute) {
Objects.requireNonNull(attribute);
this.attribute = attribute;
}
@Override
public void onValueChanged(IAttribute attribute, String newValue) {
if (valueChangeListener != null) {
valueChangeListener.onValueChanged(attribute, newValue);
}
}
}
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.fragments.sheets;
import static com.itsaky.attrinfo.models.Attr.FORMAT_BOOLEAN;
import static com.itsaky.attrinfo.models.Attr.FORMAT_COLOR;
import static com.itsaky.attrinfo.models.Attr.FORMAT_DIMENSION;
import static com.itsaky.attrinfo.models.Attr.FORMAT_ENUM;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FLAG;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FLOAT;
import static com.itsaky.attrinfo.models.Attr.FORMAT_FRACTION;
import static com.itsaky.attrinfo.models.Attr.FORMAT_INTEGER;
import static com.itsaky.attrinfo.models.Attr.FORMAT_REFERENCE;
import static com.itsaky.attrinfo.models.Attr.FORMAT_STRING;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.adapters.AttrValueFormatTabAdapter;
import com.itsaky.androidide.databinding.LayoutAttrValueEditorBinding;
import com.itsaky.androidide.fragments.attr.BaseValueEditorFragment;
import com.itsaky.androidide.fragments.attr.BooleanEditor;
import com.itsaky.androidide.fragments.attr.ColorEditor;
import com.itsaky.androidide.fragments.attr.DimensionEditor;
import com.itsaky.androidide.fragments.attr.EnumEditor;
import com.itsaky.androidide.fragments.attr.FlagEditor;
import com.itsaky.androidide.fragments.attr.FloatEditor;
import com.itsaky.androidide.fragments.attr.FractionEditor;
import com.itsaky.androidide.fragments.attr.IntegerEditor;
import com.itsaky.androidide.fragments.attr.ReferenceEditor;
import com.itsaky.androidide.fragments.attr.StringEditor;
import com.itsaky.androidide.models.XMLAttribute;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import org.jetbrains.annotations.Contract;
import java.util.Objects;
/**
* Sheet for editing value of an attribute.
*
* @author Akash Yadav
*/
public class AttrValueEditorSheet extends BottomSheetDialogFragment
implements BaseValueEditorFragment.OnValueChangeListener {
private static final String KEY_ATTRIBUTE = "attrEditorSheet_attribute";
private BaseValueEditorFragment.OnValueChangeListener valueChangeListener;
private LayoutAttrValueEditorBinding binding;
private AttrValueFormatTabAdapter adapter;
private XMLAttribute attribute;
@NonNull
public static AttrValueEditorSheet newInstance(final XMLAttribute attribute) {
Objects.requireNonNull(attribute);
final var args = new Bundle();
args.putParcelable(KEY_ATTRIBUTE, attribute);
final var fragment = new AttrValueEditorSheet();
fragment.setArguments(args);
return fragment;
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
this.valueChangeListener = (BaseValueEditorFragment.OnValueChangeListener) getParentFragment();
}
@Nullable
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
this.binding = LayoutAttrValueEditorBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (attribute == null) {
this.attribute = requireArguments().getParcelable(KEY_ATTRIBUTE);
}
Objects.requireNonNull(this.attribute);
final var namespace = this.attribute.getNamespace();
final var formatText = Attr.createFormatText(attribute.getFormat());
this.binding.attributeName.setText(
String.format("%s:%s", namespace.getPrefix(), attribute.getAttributeName()));
this.binding.attributeFormat.setText(formatText);
setupTabs(formatText);
}
@SuppressLint("NotifyDataSetChanged")
private void setupTabs(@NonNull String formatText) {
this.binding.tabs.removeAllTabs();
this.binding.pager.setSaveEnabled(false);
if (this.adapter != null) {
this.adapter.removeAll();
this.adapter.notifyDataSetChanged();
}
this.adapter = new AttrValueFormatTabAdapter(getChildFragmentManager(), attribute);
final var formats = formatText.split("\\|");
for (var formatName : formats) {
if (TextUtils.isEmpty(formatName) || formatName.length() < 2) {
continue;
}
final var frag = getFragmentClass(formatName);
if (frag != null) {
this.adapter.addFragment(frag, formatName);
}
}
binding.pager.setAdapter(this.adapter);
binding.tabs.setupWithViewPager(binding.pager);
}
@Nullable
@Contract(pure = true)
private Class<? extends BaseValueEditorFragment> getFragmentClass(@NonNull String name) {
switch (name) {
case FORMAT_REFERENCE:
return ReferenceEditor.class;
case FORMAT_COLOR:
return ColorEditor.class;
case FORMAT_BOOLEAN:
return BooleanEditor.class;
case FORMAT_DIMENSION:
return DimensionEditor.class;
case FORMAT_FLOAT:
return FloatEditor.class;
case FORMAT_INTEGER:
return IntegerEditor.class;
case FORMAT_FRACTION:
return FractionEditor.class;
case FORMAT_STRING:
return StringEditor.class;
case FORMAT_ENUM:
return EnumEditor.class;
case FORMAT_FLAG:
return FlagEditor.class;
}
return null;
}
public void setAttribute(XMLAttribute attribute) {
Objects.requireNonNull(attribute);
this.attribute = attribute;
}
@Override
public void onValueChanged(IAttribute attribute, String newValue) {
if (valueChangeListener != null) {
valueChangeListener.onValueChanged(attribute, newValue);
}
}
}
@@ -1,169 +1,169 @@
/*
* 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.DialogInterface
import android.text.Layout.Alignment.ALIGN_CENTER
import android.text.Layout.Alignment.ALIGN_NORMAL
import android.text.SpannableStringBuilder
import android.text.style.AlignmentSpan
import android.view.View
import android.webkit.URLUtil
import androidx.core.view.GravityCompat
import com.itsaky.androidide.EditorActivity
import com.itsaky.androidide.R.string
import com.itsaky.androidide.managers.PreferenceManager
import com.itsaky.androidide.services.GradleBuildService
import com.itsaky.androidide.tooling.events.ProgressEvent
import com.itsaky.androidide.tooling.events.StatusEvent
import com.itsaky.androidide.tooling.events.download.FileDownloadFinishEvent
import com.itsaky.androidide.tooling.events.download.FileDownloadOperationDescriptor
import com.itsaky.androidide.tooling.events.task.TaskProgressEvent
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.StudioUtils
import java.io.File
import java.lang.ref.WeakReference
/**
* Handles events received from [GradleBuildService] updates [EditorActivity].
* @author Akash Yadav
*/
class EditorEventListener : GradleBuildService.EventListener {
private var activityReference: WeakReference<EditorActivity?> = WeakReference(null)
private val log = ILogger.newInstance(javaClass.simpleName)
fun setActivity(activity: EditorActivity) {
this.activityReference = WeakReference(activity)
}
override fun prepareBuild() {
val isFirstBuild =
activity()
.app.prefManager.getBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, true)
activity()
.setStatus(
activity()
.getString(if (isFirstBuild) string.preparing_first else string.preparing))
if (isFirstBuild) {
activity().showFirstBuildNotice()
}
activity().binding.buildProgressIndicator.visibility = View.VISIBLE
}
override fun onBuildSuccessful(tasks: MutableList<String>) {
analyzeCurrentFile()
appendOutputSeparator()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
}
override fun onProgressEvent(event: ProgressEvent) {
if (event is FileDownloadFinishEvent) {
activity().setStatus("")
} else if (event is TaskProgressEvent) {
activity()
.setStatus(activity().getString(string.msg_running_task, event.descriptor.taskPath))
} else if (event is StatusEvent && event.descriptor is FileDownloadOperationDescriptor) {
val total = StudioUtils.bytesToSizeString(event.total)
val progress = StudioUtils.bytesToSizeString(event.progress)
val fileName =
URLUtil.guessFileName(
(event.descriptor as FileDownloadOperationDescriptor).uri.path, null, null)
val status = SpannableStringBuilder()
status.append(
"[$progress/$total]",
AlignmentSpan.Standard(ALIGN_NORMAL),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
status.append(
" Download $fileName",
AlignmentSpan.Standard(ALIGN_CENTER),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
activity().setStatus(status, GravityCompat.START)
}
}
private fun appendOutputSeparator() {
activity().appendBuildOut("\n\n")
}
override fun onBuildFailed(tasks: MutableList<String>) {
analyzeCurrentFile()
appendOutputSeparator()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
}
override fun onOutput(line: String?) {
activity().appendBuildOut(line)
// TODO This can be handled better when ProgressEvents are received from Tooling API server
if (line!!.contains("BUILD SUCCESSFUL") || line.contains("BUILD FAILED")) {
activity().setStatus(line)
}
}
private fun analyzeCurrentFile() {
val editorView = activity().currentEditor
if (editorView != null) {
val editor = editorView.editor
editor?.analyze()
}
}
fun activity(): EditorActivity {
return activityReference.get()
?: throw IllegalStateException("Activity reference has been destroyed!")
}
private fun installApks(apks: Set<File>?) {
if (apks == null || apks.isEmpty()) {
IDEHandler.LOG.error("Cannot install APKs: $apks")
return
}
if (apks.size == 1) {
activity().install(apks.iterator().next())
} else {
IDEHandler.LOG.info("Multiple APKs found. Let the user select...")
val files: List<File> = ArrayList(apks)
val builder = DialogUtils.newMaterialDialogBuilder(activity())
builder.setTitle(activity().getString(string.title_install_apks))
builder.setItems(getNames(files)) { d: DialogInterface, w: Int ->
d.dismiss()
activity().install(files[w])
}
builder.show()
}
}
private fun getNames(apks: Collection<File>): Array<String?> {
val names = arrayOfNulls<String>(apks.size)
for ((i, apk) in apks.withIndex()) {
names[i] = apk.name
}
return names
}
}
/*
* 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.DialogInterface
import android.text.Layout.Alignment.ALIGN_CENTER
import android.text.Layout.Alignment.ALIGN_NORMAL
import android.text.SpannableStringBuilder
import android.text.style.AlignmentSpan
import android.view.View
import android.webkit.URLUtil
import androidx.core.view.GravityCompat
import com.itsaky.androidide.EditorActivity
import com.itsaky.androidide.R.string
import com.itsaky.androidide.managers.PreferenceManager
import com.itsaky.androidide.services.GradleBuildService
import com.itsaky.androidide.tooling.events.ProgressEvent
import com.itsaky.androidide.tooling.events.StatusEvent
import com.itsaky.androidide.tooling.events.download.FileDownloadFinishEvent
import com.itsaky.androidide.tooling.events.download.FileDownloadOperationDescriptor
import com.itsaky.androidide.tooling.events.task.TaskProgressEvent
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.StudioUtils
import java.io.File
import java.lang.ref.WeakReference
/**
* Handles events received from [GradleBuildService] updates [EditorActivity].
* @author Akash Yadav
*/
class EditorEventListener : GradleBuildService.EventListener {
private var activityReference: WeakReference<EditorActivity?> = WeakReference(null)
private val log = ILogger.newInstance(javaClass.simpleName)
fun setActivity(activity: EditorActivity) {
this.activityReference = WeakReference(activity)
}
override fun prepareBuild() {
val isFirstBuild =
activity()
.app.prefManager.getBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, true)
activity()
.setStatus(
activity()
.getString(if (isFirstBuild) string.preparing_first else string.preparing))
if (isFirstBuild) {
activity().showFirstBuildNotice()
}
activity().binding.buildProgressIndicator.visibility = View.VISIBLE
}
override fun onBuildSuccessful(tasks: MutableList<String>) {
analyzeCurrentFile()
appendOutputSeparator()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
}
override fun onProgressEvent(event: ProgressEvent) {
if (event is FileDownloadFinishEvent) {
activity().setStatus("")
} else if (event is TaskProgressEvent) {
activity()
.setStatus(activity().getString(string.msg_running_task, event.descriptor.taskPath))
} else if (event is StatusEvent && event.descriptor is FileDownloadOperationDescriptor) {
val total = StudioUtils.bytesToSizeString(event.total)
val progress = StudioUtils.bytesToSizeString(event.progress)
val fileName =
URLUtil.guessFileName(
(event.descriptor as FileDownloadOperationDescriptor).uri.path, null, null)
val status = SpannableStringBuilder()
status.append(
"[$progress/$total]",
AlignmentSpan.Standard(ALIGN_NORMAL),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
status.append(
" Download $fileName",
AlignmentSpan.Standard(ALIGN_CENTER),
SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE)
activity().setStatus(status, GravityCompat.START)
}
}
private fun appendOutputSeparator() {
activity().appendBuildOut("\n\n")
}
override fun onBuildFailed(tasks: MutableList<String>) {
analyzeCurrentFile()
appendOutputSeparator()
activity().app.prefManager.putBoolean(PreferenceManager.KEY_IS_FIRST_PROJECT_BUILD, false)
activity().binding.buildProgressIndicator.visibility = View.GONE
}
override fun onOutput(line: String?) {
activity().appendBuildOut(line)
// TODO This can be handled better when ProgressEvents are received from Tooling API server
if (line!!.contains("BUILD SUCCESSFUL") || line.contains("BUILD FAILED")) {
activity().setStatus(line)
}
}
private fun analyzeCurrentFile() {
val editorView = activity().currentEditor
if (editorView != null) {
val editor = editorView.editor
editor?.analyze()
}
}
fun activity(): EditorActivity {
return activityReference.get()
?: throw IllegalStateException("Activity reference has been destroyed!")
}
private fun installApks(apks: Set<File>?) {
if (apks == null || apks.isEmpty()) {
IDEHandler.LOG.error("Cannot install APKs: $apks")
return
}
if (apks.size == 1) {
activity().install(apks.iterator().next())
} else {
IDEHandler.LOG.info("Multiple APKs found. Let the user select...")
val files: List<File> = ArrayList(apks)
val builder = DialogUtils.newMaterialDialogBuilder(activity())
builder.setTitle(activity().getString(string.title_install_apks))
builder.setItems(getNames(files)) { d: DialogInterface, w: Int ->
d.dismiss()
activity().install(files[w])
}
builder.show()
}
}
private fun getNames(apks: Collection<File>): Array<String?> {
val names = arrayOfNulls<String>(apks.size)
for ((i, apk) in apks.withIndex()) {
names[i] = apk.name
}
return names
}
}
@@ -1,55 +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.handlers;
import com.itsaky.androidide.EditorActivity;
import com.itsaky.androidide.utils.ILogger;
/**
* A handler is an implementation that handles a specific feature, data or anything else in
* AndroidIDE
*/
public abstract class IDEHandler {
protected static final ILogger LOG = ILogger.instance();
protected Provider provider;
public IDEHandler(Provider provider) {
this.provider = provider;
}
public abstract void start();
public abstract void stop();
protected EditorActivity activity() {
return provider.provideEditorActivity();
}
/** An interface to communicate between a handler and its client */
public interface Provider {
/**
* Called by handler to get a reference to {@link EditorActivity}
*
* @throws NullPointerException is this is required
*/
EditorActivity provideEditorActivity();
}
}
/************************************************************************************
* 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 com.itsaky.androidide.EditorActivity;
import com.itsaky.androidide.utils.ILogger;
/**
* A handler is an implementation that handles a specific feature, data or anything else in
* AndroidIDE
*/
public abstract class IDEHandler {
protected static final ILogger LOG = ILogger.instance();
protected Provider provider;
public IDEHandler(Provider provider) {
this.provider = provider;
}
public abstract void start();
public abstract void stop();
protected EditorActivity activity() {
return provider.provideEditorActivity();
}
/** An interface to communicate between a handler and its client */
public interface Provider {
/**
* Called by handler to get a reference to {@link EditorActivity}
*
* @throws NullPointerException is this is required
*/
EditorActivity provideEditorActivity();
}
}
@@ -1,128 +1,128 @@
/*
* 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.language;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tooling.api.model.IdeGradleProject;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.lsp.api.ILanguageServer;
import com.itsaky.lsp.models.CompletionItem;
import com.itsaky.lsp.models.CompletionParams;
import com.itsaky.lsp.models.CompletionResult;
import com.itsaky.lsp.models.Position;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
import io.github.rosemoe.sora.lang.completion.CompletionHelper;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.util.MyCharacter;
/**
* Common implementation of completion provider which requests completions to provided language
* server.
*
* @author Akash Yadav
*/
public class CommonCompletionProvider {
private static final ILogger LOG = ILogger.newInstance("CommonCompletionProvider");
private final ILanguageServer server;
private CompletableFuture<CompletionResult> future;
public CommonCompletionProvider(ILanguageServer server) {
this.server = server;
}
public static boolean checkJavaCompletionChar(char c) {
return MyCharacter.isJavaIdentifierPart(c) || c == '.';
}
public static boolean checkXMLCompletionChar(char c) {
return MyCharacter.isJavaIdentifierPart(c) || c == '<' || c == '/';
}
/**
* Computes completion items using the provided language server instance.
*
* @param content The reference to the content of the editor.
* @param file The file to compute completions for.
* @param position The position of the cursor in the content.
* @return The computed completion items. May return an empty list if the there was an error
* computing the completion items.
*/
public List<CompletionItem> complete(
ContentReference content,
Path file,
CharPosition position,
Predicate<Character> prefixMatcher) {
if (this.future != null && !this.future.isDone()) {
try {
this.future.cancel(true);
} catch (CancellationException e) {
return new ArrayList<>();
}
}
this.future =
CompletableFuture.supplyAsync(
() -> {
final var prefix =
CompletionHelper.computePrefix(content, position, prefixMatcher::test);
final var completer = server.getCompletionProvider();
if (!completer.canComplete(file)) {
return CompletionResult.EMPTY;
}
IdeGradleProject fileModule = null;
try {
fileModule = ProjectManager.INSTANCE.findModuleForFile(file.toFile()).get();
} catch (Throwable e) {
LOG.error("Unable to find module for current file", e);
}
final var params =
new CompletionParams(
new Position(position.line, position.column, position.index), file);
params.setContent(content);
params.setPrefix(prefix);
params.setModule(fileModule);
return completer.complete(params);
});
try {
final var items = future.get ().getItems ();
return items;
} catch (Throwable e) {
// Do not log if completion was interrupted or cancelled
if (!(e instanceof InterruptedException || e instanceof CompletionCancelledException)) {
LOG.error("Unable to compute completions", e);
}
return Collections.emptyList();
}
}
}
/*
* 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.language;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tooling.api.model.IdeGradleProject;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.lsp.api.ILanguageServer;
import com.itsaky.lsp.models.CompletionItem;
import com.itsaky.lsp.models.CompletionParams;
import com.itsaky.lsp.models.CompletionResult;
import com.itsaky.lsp.models.Position;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.concurrent.CompletableFuture;
import java.util.function.Predicate;
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
import io.github.rosemoe.sora.lang.completion.CompletionHelper;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.util.MyCharacter;
/**
* Common implementation of completion provider which requests completions to provided language
* server.
*
* @author Akash Yadav
*/
public class CommonCompletionProvider {
private static final ILogger LOG = ILogger.newInstance("CommonCompletionProvider");
private final ILanguageServer server;
private CompletableFuture<CompletionResult> future;
public CommonCompletionProvider(ILanguageServer server) {
this.server = server;
}
public static boolean checkJavaCompletionChar(char c) {
return MyCharacter.isJavaIdentifierPart(c) || c == '.';
}
public static boolean checkXMLCompletionChar(char c) {
return MyCharacter.isJavaIdentifierPart(c) || c == '<' || c == '/';
}
/**
* Computes completion items using the provided language server instance.
*
* @param content The reference to the content of the editor.
* @param file The file to compute completions for.
* @param position The position of the cursor in the content.
* @return The computed completion items. May return an empty list if the there was an error
* computing the completion items.
*/
public List<CompletionItem> complete(
ContentReference content,
Path file,
CharPosition position,
Predicate<Character> prefixMatcher) {
if (this.future != null && !this.future.isDone()) {
try {
this.future.cancel(true);
} catch (CancellationException e) {
return new ArrayList<>();
}
}
this.future =
CompletableFuture.supplyAsync(
() -> {
final var prefix =
CompletionHelper.computePrefix(content, position, prefixMatcher::test);
final var completer = server.getCompletionProvider();
if (!completer.canComplete(file)) {
return CompletionResult.EMPTY;
}
IdeGradleProject fileModule = null;
try {
fileModule = ProjectManager.INSTANCE.findModuleForFile(file.toFile()).get();
} catch (Throwable e) {
LOG.error("Unable to find module for current file", e);
}
final var params =
new CompletionParams(
new Position(position.line, position.column, position.index), file);
params.setContent(content);
params.setPrefix(prefix);
params.setModule(fileModule);
return completer.complete(params);
});
try {
final var items = future.get ().getItems ();
return items;
} catch (Throwable e) {
// Do not log if completion was interrupted or cancelled
if (!(e instanceof InterruptedException || e instanceof CompletionCancelledException)) {
LOG.error("Unable to compute completions", e);
}
return Collections.emptyList();
}
}
}
@@ -1,47 +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.language;
import androidx.annotation.NonNull;
import com.itsaky.lsp.models.DiagnosticItem;
import java.util.List;
/**
* Base class for analyze managers.
*
* @author Akash Yadav
*/
public interface IAnalyzeManager {
/**
* Update the diagnostic items.
*
* @param diagnostics The new list of diagnostics.
*/
default void updateDiagnostics(@NonNull List<DiagnosticItem> diagnostics) {}
/**
* Get the diagnostic items.
*
* @return The diagnostic items. Must not be <code>null</code>.
*/
@NonNull
List<DiagnosticItem> getDiagnostics();
}
/*
* 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.language;
import androidx.annotation.NonNull;
import com.itsaky.lsp.models.DiagnosticItem;
import java.util.List;
/**
* Base class for analyze managers.
*
* @author Akash Yadav
*/
public interface IAnalyzeManager {
/**
* Update the diagnostic items.
*
* @param diagnostics The new list of diagnostics.
*/
default void updateDiagnostics(@NonNull List<DiagnosticItem> diagnostics) {}
/**
* Get the diagnostic items.
*
* @return The diagnostic items. Must not be <code>null</code>.
*/
@NonNull
List<DiagnosticItem> getDiagnostics();
}
@@ -1,154 +1,154 @@
/*
* 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.language.xml;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.get;
import static io.github.rosemoe.sora.widget.schemes.EditorColorScheme.LITERAL;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.utils.CharSequenceReader;
import com.itsaky.androidide.utils.ILogger;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
import org.antlr.v4.runtime.Token;
import java.io.IOException;
import java.util.Stack;
import io.github.rosemoe.sora.lang.analysis.SimpleAnalyzeManager;
import io.github.rosemoe.sora.lang.styling.CodeBlock;
import io.github.rosemoe.sora.lang.styling.MappedSpans;
import io.github.rosemoe.sora.lang.styling.Styles;
/**
* Simple analyzer implementation for the XML files.
*
* @author Akash Yadav
*/
public class XMLAnalyzer extends SimpleAnalyzeManager<Void> {
private static final ILogger LOG = ILogger.newInstance("XMLAnalyzer");
@Override
protected Styles analyze(StringBuilder text, Delegate<Void> delegate) {
final var styles = new Styles();
final var colors = new MappedSpans.Builder();
CodePointCharStream stream;
try {
stream = CharStreams.fromReader(new CharSequenceReader(text));
} catch (IOException e) {
LOG.error("Unable to create stream for analyze", e);
return styles;
}
XMLLexer lexer = new XMLLexer(stream);
Token token;
int previous = 0;
int line, column, lastLine = 1;
var first = true;
final var stack = new Stack<CodeBlock>();
while (!delegate.isCancelled()) {
token = lexer.nextToken();
if (token == null) {
break;
}
if (token.getType() == XMLLexer.EOF) {
lastLine = token.getLine() - 1;
break;
}
line = token.getLine() - 1;
column = token.getCharPositionInLine();
lastLine = line;
switch (token.getType()) {
case XMLLexer.S:
case XMLLexer.SEA_WS:
if (first) {
colors.addNormalIfNull();
}
break;
case XMLLexer.COMMENT:
colors.addIfNeeded(line, column, forComment());
break;
case XMLLexer.OPEN:
case XMLLexer.OPEN_SLASH:
case XMLLexer.CLOSE:
case XMLLexer.SLASH:
case XMLLexer.SPECIAL_CLOSE:
case XMLLexer.EQUALS:
case XMLLexer.COLON:
case XMLLexer.XMLDeclOpen:
colors.addIfNeeded(line, column, get(OPERATOR));
break;
case XMLLexer.SLASH_CLOSE:
colors.addIfNeeded(line, column, get(OPERATOR));
final var closeBlock = stack.pop();
closeBlock.endLine = line;
closeBlock.endColumn = column;
styles.addCodeBlock(closeBlock);
break;
case XMLLexer.STRING:
colors.addIfNeeded(line, column, LITERAL);
break;
case XMLLexer.Name:
var type = SchemeAndroidIDE.TEXT_NORMAL;
if (previous == XMLLexer.OPEN) {
type = SchemeAndroidIDE.XML_TAG;
final var block = styles.obtainNewBlock();
block.startLine = line;
block.startColumn = column;
stack.push(block);
}
if (previous == XMLLexer.OPEN_SLASH) {
type = SchemeAndroidIDE.XML_TAG;
final var block = stack.pop();
block.endLine = line;
block.endColumn = column;
styles.addCodeBlock(block);
}
colors.addIfNeeded(line, column, get(type));
break;
case XMLLexer.TEXT:
default:
colors.addIfNeeded(line, column, get(SchemeAndroidIDE.TEXT_NORMAL));
break;
}
first = false;
if (token.getType() != XMLLexer.SEA_WS) {
previous = token.getType();
}
}
colors.determine(lastLine);
styles.spans = colors.build();
return styles;
}
}
/*
* 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.language.xml;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.OPERATOR;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.forComment;
import static com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE.get;
import static io.github.rosemoe.sora.widget.schemes.EditorColorScheme.LITERAL;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE;
import com.itsaky.androidide.utils.CharSequenceReader;
import com.itsaky.androidide.utils.ILogger;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CodePointCharStream;
import org.antlr.v4.runtime.Token;
import java.io.IOException;
import java.util.Stack;
import io.github.rosemoe.sora.lang.analysis.SimpleAnalyzeManager;
import io.github.rosemoe.sora.lang.styling.CodeBlock;
import io.github.rosemoe.sora.lang.styling.MappedSpans;
import io.github.rosemoe.sora.lang.styling.Styles;
/**
* Simple analyzer implementation for the XML files.
*
* @author Akash Yadav
*/
public class XMLAnalyzer extends SimpleAnalyzeManager<Void> {
private static final ILogger LOG = ILogger.newInstance("XMLAnalyzer");
@Override
protected Styles analyze(StringBuilder text, Delegate<Void> delegate) {
final var styles = new Styles();
final var colors = new MappedSpans.Builder();
CodePointCharStream stream;
try {
stream = CharStreams.fromReader(new CharSequenceReader(text));
} catch (IOException e) {
LOG.error("Unable to create stream for analyze", e);
return styles;
}
XMLLexer lexer = new XMLLexer(stream);
Token token;
int previous = 0;
int line, column, lastLine = 1;
var first = true;
final var stack = new Stack<CodeBlock>();
while (!delegate.isCancelled()) {
token = lexer.nextToken();
if (token == null) {
break;
}
if (token.getType() == XMLLexer.EOF) {
lastLine = token.getLine() - 1;
break;
}
line = token.getLine() - 1;
column = token.getCharPositionInLine();
lastLine = line;
switch (token.getType()) {
case XMLLexer.S:
case XMLLexer.SEA_WS:
if (first) {
colors.addNormalIfNull();
}
break;
case XMLLexer.COMMENT:
colors.addIfNeeded(line, column, forComment());
break;
case XMLLexer.OPEN:
case XMLLexer.OPEN_SLASH:
case XMLLexer.CLOSE:
case XMLLexer.SLASH:
case XMLLexer.SPECIAL_CLOSE:
case XMLLexer.EQUALS:
case XMLLexer.COLON:
case XMLLexer.XMLDeclOpen:
colors.addIfNeeded(line, column, get(OPERATOR));
break;
case XMLLexer.SLASH_CLOSE:
colors.addIfNeeded(line, column, get(OPERATOR));
final var closeBlock = stack.pop();
closeBlock.endLine = line;
closeBlock.endColumn = column;
styles.addCodeBlock(closeBlock);
break;
case XMLLexer.STRING:
colors.addIfNeeded(line, column, LITERAL);
break;
case XMLLexer.Name:
var type = SchemeAndroidIDE.TEXT_NORMAL;
if (previous == XMLLexer.OPEN) {
type = SchemeAndroidIDE.XML_TAG;
final var block = styles.obtainNewBlock();
block.startLine = line;
block.startColumn = column;
stack.push(block);
}
if (previous == XMLLexer.OPEN_SLASH) {
type = SchemeAndroidIDE.XML_TAG;
final var block = stack.pop();
block.endLine = line;
block.endColumn = column;
styles.addCodeBlock(block);
}
colors.addIfNeeded(line, column, get(type));
break;
case XMLLexer.TEXT:
default:
colors.addIfNeeded(line, column, get(SchemeAndroidIDE.TEXT_NORMAL));
break;
}
first = false;
if (token.getType() != XMLLexer.SEA_WS) {
previous = token.getType();
}
}
colors.determine(lastLine);
styles.spans = colors.build();
return styles;
}
}
@@ -1,147 +1,147 @@
/*
* 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.language.xml;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.language.CommonCompletionProvider;
import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageServer;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import java.io.StringReader;
import java.nio.file.Paths;
import java.util.ArrayList;
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.widget.SymbolPairMatch;
public class XMLLanguage extends IDELanguage {
private static final ILogger LOG = ILogger.newInstance("XMLLanguage");
private final CommonCompletionProvider completer;
private final NewlineHandler[] newlineHandlers;
private XMLAnalyzer analyzer;
public XMLLanguage() {
this.completer = new CommonCompletionProvider(getLanguageServer());
this.analyzer = new XMLAnalyzer();
this.newlineHandlers = new NewlineHandler[0];
}
public boolean isAutoCompleteChar(char ch) {
return JavaCharacter.isJavaIdentifierPart(ch) || ch == '<' || ch == '/';
}
@Override
public int getIndentAdvance(@NonNull String content) {
try {
XMLLexer lexer = new XMLLexer(CharStreams.fromReader(new StringReader(content)));
Token token;
int advance = 0;
while (((token = lexer.nextToken()) != null && token.getType() != token.EOF)) {
switch (token.getType()) {
case XMLLexer.OPEN:
advance++;
break;
case XMLLexer.CLOSE:
case XMLLexer.SLASH_CLOSE:
advance--;
break;
default:
break;
}
}
advance = Math.max(0, advance);
return advance * getTabSize();
} catch (Throwable e) {
LOG.error("Failed to compute indent advance", e);
}
return 0;
}
@Override
public SymbolPairMatch getSymbolPairs() {
return new SymbolPairMatch.DefaultSymbolPairs();
}
@NonNull
@Override
public AnalyzeManager getAnalyzeManager() {
return analyzer;
}
@Override
public int getInterruptionLevel() {
return INTERRUPTION_LEVEL_STRONG;
}
@Override
public void requireAutoComplete(
@NonNull ContentReference content,
@NonNull CharPosition position,
@NonNull CompletionPublisher publisher,
@NonNull Bundle extraArguments)
throws CompletionCancelledException {
if (!extraArguments.containsKey(IDEEditor.KEY_FILE)) {
return;
}
final var file = Paths.get(extraArguments.getString(IDEEditor.KEY_FILE));
publisher.setUpdateThreshold(0);
publisher.addItems(
new ArrayList<>(
completer.complete(
content, file, position, CommonCompletionProvider::checkXMLCompletionChar)));
}
@Override
public int getIndentAdvance(@NonNull ContentReference content, int line, int column) {
final var text = content.getLine(line).substring(0, column);
return getIndentAdvance(text.trim());
}
@Override
protected ILanguageServer getLanguageServer() {
return StudioApp.getInstance().getXMLLanguageServer();
}
@Override
public NewlineHandler[] getNewlineHandlers() {
return newlineHandlers;
}
@Override
public void destroy() {
analyzer = null;
}
}
/*
* 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.language.xml;
import android.os.Bundle;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.language.CommonCompletionProvider;
import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.lexers.xml.XMLLexer;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JavaCharacter;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageServer;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.Token;
import java.io.StringReader;
import java.nio.file.Paths;
import java.util.ArrayList;
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.widget.SymbolPairMatch;
public class XMLLanguage extends IDELanguage {
private static final ILogger LOG = ILogger.newInstance("XMLLanguage");
private final CommonCompletionProvider completer;
private final NewlineHandler[] newlineHandlers;
private XMLAnalyzer analyzer;
public XMLLanguage() {
this.completer = new CommonCompletionProvider(getLanguageServer());
this.analyzer = new XMLAnalyzer();
this.newlineHandlers = new NewlineHandler[0];
}
public boolean isAutoCompleteChar(char ch) {
return JavaCharacter.isJavaIdentifierPart(ch) || ch == '<' || ch == '/';
}
@Override
public int getIndentAdvance(@NonNull String content) {
try {
XMLLexer lexer = new XMLLexer(CharStreams.fromReader(new StringReader(content)));
Token token;
int advance = 0;
while (((token = lexer.nextToken()) != null && token.getType() != token.EOF)) {
switch (token.getType()) {
case XMLLexer.OPEN:
advance++;
break;
case XMLLexer.CLOSE:
case XMLLexer.SLASH_CLOSE:
advance--;
break;
default:
break;
}
}
advance = Math.max(0, advance);
return advance * getTabSize();
} catch (Throwable e) {
LOG.error("Failed to compute indent advance", e);
}
return 0;
}
@Override
public SymbolPairMatch getSymbolPairs() {
return new SymbolPairMatch.DefaultSymbolPairs();
}
@NonNull
@Override
public AnalyzeManager getAnalyzeManager() {
return analyzer;
}
@Override
public int getInterruptionLevel() {
return INTERRUPTION_LEVEL_STRONG;
}
@Override
public void requireAutoComplete(
@NonNull ContentReference content,
@NonNull CharPosition position,
@NonNull CompletionPublisher publisher,
@NonNull Bundle extraArguments)
throws CompletionCancelledException {
if (!extraArguments.containsKey(IDEEditor.KEY_FILE)) {
return;
}
final var file = Paths.get(extraArguments.getString(IDEEditor.KEY_FILE));
publisher.setUpdateThreshold(0);
publisher.addItems(
new ArrayList<>(
completer.complete(
content, file, position, CommonCompletionProvider::checkXMLCompletionChar)));
}
@Override
public int getIndentAdvance(@NonNull ContentReference content, int line, int column) {
final var text = content.getLine(line).substring(0, column);
return getIndentAdvance(text.trim());
}
@Override
protected ILanguageServer getLanguageServer() {
return StudioApp.getInstance().getXMLLanguageServer();
}
@Override
public NewlineHandler[] getNewlineHandlers() {
return newlineHandlers;
}
@Override
public void destroy() {
analyzer = null;
}
}
@@ -1,407 +1,407 @@
/*
* 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.lsp;
import android.view.View;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.gson.Gson;
import com.itsaky.androidide.EditorActivity;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.DiagnosticsAdapter;
import com.itsaky.androidide.adapters.SearchListAdapter;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.views.editor.CodeEditorView;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageClient;
import com.itsaky.lsp.models.CodeActionItem;
import com.itsaky.lsp.models.Command;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.DiagnosticResult;
import com.itsaky.lsp.models.Location;
import com.itsaky.lsp.models.Range;
import com.itsaky.lsp.models.TextEdit;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import io.github.rosemoe.sora.text.Content;
/** AndroidIDE specific implementation of the LanguageClient */
public class IDELanguageClientImpl implements ILanguageClient {
protected static final Gson gson = new Gson();
protected static final ILogger LOG = ILogger.newInstance("AbstractLanguageClient");
private static IDELanguageClientImpl mInstance;
private final Map<File, List<DiagnosticItem>> diagnostics = new HashMap<>();
protected EditorActivityProvider activityProvider;
private IDELanguageClientImpl(EditorActivityProvider provider) {
setActivityProvider(provider);
}
public static IDELanguageClientImpl getInstance() {
if (mInstance == null) {
throw new IllegalStateException("Client not initialized");
}
return mInstance;
}
public static IDELanguageClientImpl initialize(EditorActivityProvider provider) {
if (mInstance != null) {
throw new IllegalStateException("Client is already initialized");
}
mInstance = new IDELanguageClientImpl(provider);
return getInstance();
}
public static void shutdown() {
mInstance = null;
}
public static boolean isInitialized() {
return mInstance != null;
}
public void setActivityProvider(EditorActivityProvider provider) {
this.activityProvider = provider;
}
protected EditorActivity activity() {
if (activityProvider == null) return null;
return activityProvider.provide();
}
private CodeEditorView findEditorByFile(File file) {
return activity().getEditorForFile(file);
}
public void hideDiagnostics() {
if (activity() == null || activity().getDiagnosticBinding() == null) {
return;
}
activity().getDiagnosticBinding().getRoot().setVisibility(View.GONE);
}
private void hideBottomDiagnosticView(final File file) {
if (activity() == null || file == null) {
return;
}
final var frag = findEditorByFile(file);
if (frag == null || frag.getBinding() == null) {
return;
}
frag.getBinding().diagnosticTextContainer.setVisibility(View.GONE);
frag.getBinding().diagnosticText.setVisibility(View.GONE);
frag.getBinding().diagnosticText.setClickable(false);
}
@Override
public void publishDiagnostics(DiagnosticResult result) {
boolean error = result == null;
activity().handleDiagnosticsResultVisibility(error || result.getDiagnostics().isEmpty());
if (error) {
return;
}
File file = result.getFile().toFile();
if (!file.exists() || !file.isFile()) return;
final var editorView = activity().getEditorForFile(file);
if (editorView != null) {
final var editor = editorView.getEditor();
if (editor != null) {
final var editorLanguage = editor.getEditorLanguage();
if (editorLanguage instanceof IDELanguage) {
((IDELanguage) editorLanguage).setDiagnostics(result.getDiagnostics());
}
}
}
diagnostics.put(file, result.getDiagnostics());
activity().setDiagnosticsAdapter(newDiagnosticsAdapter());
}
/** Called by {@link IDEEditor IDEEditor} to show locations in EditorActivity */
public void showLocations(List<Location> locations) {
// Cannot show anything if the activity() is null
if (activity() == null) {
return;
}
boolean error = locations == null || locations.isEmpty();
activity().handleSearchResultVisibility(error);
if (error) {
activity().setSearchResultAdapter(new SearchListAdapter(null, null, null));
return;
}
final Map<File, List<SearchResult>> results = new HashMap<>();
for (int i = 0; i < locations.size(); i++) {
try {
final Location loc = locations.get(i);
if (loc == null) {
continue;
}
final File file = loc.getFile().toFile();
if (!file.exists() || !file.isFile()) continue;
var frag = findEditorByFile(file);
Content content;
if (frag != null && frag.getEditor() != null) content = frag.getEditor().getText();
else content = new Content(FileIOUtils.readFile2String(file));
final List<SearchResult> matches =
results.containsKey(file) ? results.get(file) : new ArrayList<>();
Objects.requireNonNull(matches)
.add(
new SearchResult(
loc.getRange(),
file,
content.getLineString(loc.getRange().getStart().getLine()),
content
.subContent(
loc.getRange().getStart().getLine(),
loc.getRange().getStart().getColumn(),
loc.getRange().getEnd().getLine(),
loc.getRange().getEnd().getColumn())
.toString()));
results.put(file, matches);
} catch (Throwable th) {
LOG.error("Failed to show file location", th);
}
}
activity().handleSearchResults(results);
}
@Override
public void performCodeAction(File file, CodeActionItem actionItem) {
final var editor = activity().getEditorForFile(file);
if (editor != null) {
performCodeAction(editor.getEditor(), actionItem);
}
}
/**
* Perform the given {@link CodeActionItem}
*
* @param editor The {@link IDEEditor} that invoked the code action request. This is required to
* reduce the time finding the code action from the edits.
* @param action The action to perform
*/
public void performCodeAction(IDEEditor editor, CodeActionItem action) {
if (activity() == null || editor == null || action == null) {
LOG.error(
"Unable to perform code action",
"activity=" + activity(),
"editor=" + editor,
"action=" + action);
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
return;
}
final ProgressSheet progress = new ProgressSheet();
progress.setSubMessageEnabled(false);
progress.setWelcomeTextEnabled(false);
progress.setCancelable(false);
progress.setMessage(activity().getString(R.string.msg_performing_actions));
progress.show(activity().getSupportFragmentManager(), "quick_fix_progress");
new TaskExecutor()
.executeAsyncProvideError(
() -> performCodeActionAsync(editor, action),
(result, throwable) -> {
ThreadUtils.runOnUiThread(progress::dismiss);
if (result == null || throwable != null || !result) {
LOG.error(
"Unable to perform code action", "result=" + result, "throwable=" + throwable);
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
} else {
editor.executeCommand(action.getCommand());
}
});
}
private List<DiagnosticGroup> mapAsGroup(Map<File, List<DiagnosticItem>> diags) {
List<DiagnosticGroup> groups = new ArrayList<>();
if (diags == null || diags.size() <= 0) return groups;
for (File file : diags.keySet()) {
var fileDiags = diags.get(file);
if (fileDiags == null || fileDiags.size() <= 0) continue;
DiagnosticGroup group = new DiagnosticGroup(R.drawable.ic_language_java, file, fileDiags);
groups.add(group);
}
return groups;
}
private void showAvailableQuickfixes(IDEEditor editor, List<CodeActionItem> actions) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(activity());
builder.setTitle(R.string.msg_code_actions);
builder.setItems(
asArray(actions),
(d, w) -> {
d.dismiss();
hideDiagnostics();
hideBottomDiagnosticView(editor.getFile());
performCodeAction(editor, actions.get(w));
});
builder.show();
}
private CharSequence[] asArray(List<CodeActionItem> actions) {
final String[] arr = new String[actions.size()];
for (int i = 0; i < actions.size(); i++) {
arr[i] = actions.get(i).getTitle();
}
return arr;
}
private Boolean performCodeActionAsync(final IDEEditor editor, final CodeActionItem action) {
LOG.debug("Performing code action:", action);
final var changes = action.getChanges();
if (changes.isEmpty()) {
return Boolean.FALSE;
}
for (var change : changes) {
final var path = change.getFile();
if (path == null) {
continue;
}
final File file = path.toFile();
if (!file.exists()) {
continue;
}
for (TextEdit edit : change.getEdits()) {
final String editorFilepath =
editor.getFile() == null ? "" : editor.getFile().getAbsolutePath();
if (file.getAbsolutePath().equals(editorFilepath)) {
// Edit is in the same editor which requested the code action
editInEditor(editor, edit);
} else {
var openedFrag = findEditorByFile(file);
if (openedFrag != null && openedFrag.getEditor() != null) {
// Edit is in another 'opened' file
editInEditor(openedFrag.getEditor(), edit);
} else {
// Edit is in some other file which is not opened
// We should open that file and perform the edit
openedFrag = activity().openFile(file);
if (openedFrag != null && openedFrag.getEditor() != null) {
editInEditor(openedFrag.getEditor(), edit);
}
}
}
}
}
return Boolean.TRUE;
}
private void execCommand(IDEEditor editor, Command command) {
editor.executeCommand(command);
}
private void editInEditor(final IDEEditor editor, final TextEdit edit) {
final Range range = edit.getRange();
final int startLine = range.getStart().getLine();
final int startCol = range.getStart().getColumn();
final int endLine = range.getEnd().getLine();
final int endCol = range.getEnd().getColumn();
activity()
.runOnUiThread(
() -> {
if (startLine == endLine && startCol == endCol) {
editor.getText().insert(startLine, startCol, edit.getNewText());
} else {
editor.getText().replace(startLine, startCol, endLine, endCol, edit.getNewText());
}
});
}
public DiagnosticsAdapter newDiagnosticsAdapter() {
return new DiagnosticsAdapter(mapAsGroup(this.diagnostics), activity());
}
@Override
public com.itsaky.lsp.models.ShowDocumentResult showDocument(
com.itsaky.lsp.models.ShowDocumentParams params) {
boolean success = false;
final var result = new com.itsaky.lsp.models.ShowDocumentResult(false);
if (activity() == null) {
return result;
}
if (params != null) {
File file = params.getFile().toFile();
if (file.exists() && file.isFile() && FileUtils.isUtf8(file)) {
final var range = params.getSelection();
var frag =
activity().getEditorAtIndex(activity().getBinding().tabs.getSelectedTabPosition());
if (frag != null
&& frag.getFile() != null
&& frag.getEditor() != null
&& frag.getFile().getAbsolutePath().equals(file.getAbsolutePath())) {
if (LSPUtils.isEqual(range.getStart(), range.getEnd())) {
frag.getEditor().setSelection(range.getStart().getLine(), range.getStart().getColumn());
} else {
frag.getEditor().setSelection(range);
}
} else {
activity().openFileAndSelect(file, range);
}
success = true;
}
}
result.setSuccess(success);
return result;
}
}
/*
* 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.lsp;
import android.view.View;
import com.blankj.utilcode.util.FileIOUtils;
import com.blankj.utilcode.util.FileUtils;
import com.blankj.utilcode.util.ThreadUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.gson.Gson;
import com.itsaky.androidide.EditorActivity;
import com.itsaky.androidide.R;
import com.itsaky.androidide.adapters.DiagnosticsAdapter;
import com.itsaky.androidide.adapters.SearchListAdapter;
import com.itsaky.androidide.app.StudioApp;
import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.language.IDELanguage;
import com.itsaky.androidide.models.DiagnosticGroup;
import com.itsaky.androidide.models.SearchResult;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.LSPUtils;
import com.itsaky.androidide.views.editor.CodeEditorView;
import com.itsaky.androidide.views.editor.IDEEditor;
import com.itsaky.lsp.api.ILanguageClient;
import com.itsaky.lsp.models.CodeActionItem;
import com.itsaky.lsp.models.Command;
import com.itsaky.lsp.models.DiagnosticItem;
import com.itsaky.lsp.models.DiagnosticResult;
import com.itsaky.lsp.models.Location;
import com.itsaky.lsp.models.Range;
import com.itsaky.lsp.models.TextEdit;
import com.itsaky.toaster.Toaster;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import io.github.rosemoe.sora.text.Content;
/** AndroidIDE specific implementation of the LanguageClient */
public class IDELanguageClientImpl implements ILanguageClient {
protected static final Gson gson = new Gson();
protected static final ILogger LOG = ILogger.newInstance("AbstractLanguageClient");
private static IDELanguageClientImpl mInstance;
private final Map<File, List<DiagnosticItem>> diagnostics = new HashMap<>();
protected EditorActivityProvider activityProvider;
private IDELanguageClientImpl(EditorActivityProvider provider) {
setActivityProvider(provider);
}
public static IDELanguageClientImpl getInstance() {
if (mInstance == null) {
throw new IllegalStateException("Client not initialized");
}
return mInstance;
}
public static IDELanguageClientImpl initialize(EditorActivityProvider provider) {
if (mInstance != null) {
throw new IllegalStateException("Client is already initialized");
}
mInstance = new IDELanguageClientImpl(provider);
return getInstance();
}
public static void shutdown() {
mInstance = null;
}
public static boolean isInitialized() {
return mInstance != null;
}
public void setActivityProvider(EditorActivityProvider provider) {
this.activityProvider = provider;
}
protected EditorActivity activity() {
if (activityProvider == null) return null;
return activityProvider.provide();
}
private CodeEditorView findEditorByFile(File file) {
return activity().getEditorForFile(file);
}
public void hideDiagnostics() {
if (activity() == null || activity().getDiagnosticBinding() == null) {
return;
}
activity().getDiagnosticBinding().getRoot().setVisibility(View.GONE);
}
private void hideBottomDiagnosticView(final File file) {
if (activity() == null || file == null) {
return;
}
final var frag = findEditorByFile(file);
if (frag == null || frag.getBinding() == null) {
return;
}
frag.getBinding().diagnosticTextContainer.setVisibility(View.GONE);
frag.getBinding().diagnosticText.setVisibility(View.GONE);
frag.getBinding().diagnosticText.setClickable(false);
}
@Override
public void publishDiagnostics(DiagnosticResult result) {
boolean error = result == null;
activity().handleDiagnosticsResultVisibility(error || result.getDiagnostics().isEmpty());
if (error) {
return;
}
File file = result.getFile().toFile();
if (!file.exists() || !file.isFile()) return;
final var editorView = activity().getEditorForFile(file);
if (editorView != null) {
final var editor = editorView.getEditor();
if (editor != null) {
final var editorLanguage = editor.getEditorLanguage();
if (editorLanguage instanceof IDELanguage) {
((IDELanguage) editorLanguage).setDiagnostics(result.getDiagnostics());
}
}
}
diagnostics.put(file, result.getDiagnostics());
activity().setDiagnosticsAdapter(newDiagnosticsAdapter());
}
/** Called by {@link IDEEditor IDEEditor} to show locations in EditorActivity */
public void showLocations(List<Location> locations) {
// Cannot show anything if the activity() is null
if (activity() == null) {
return;
}
boolean error = locations == null || locations.isEmpty();
activity().handleSearchResultVisibility(error);
if (error) {
activity().setSearchResultAdapter(new SearchListAdapter(null, null, null));
return;
}
final Map<File, List<SearchResult>> results = new HashMap<>();
for (int i = 0; i < locations.size(); i++) {
try {
final Location loc = locations.get(i);
if (loc == null) {
continue;
}
final File file = loc.getFile().toFile();
if (!file.exists() || !file.isFile()) continue;
var frag = findEditorByFile(file);
Content content;
if (frag != null && frag.getEditor() != null) content = frag.getEditor().getText();
else content = new Content(FileIOUtils.readFile2String(file));
final List<SearchResult> matches =
results.containsKey(file) ? results.get(file) : new ArrayList<>();
Objects.requireNonNull(matches)
.add(
new SearchResult(
loc.getRange(),
file,
content.getLineString(loc.getRange().getStart().getLine()),
content
.subContent(
loc.getRange().getStart().getLine(),
loc.getRange().getStart().getColumn(),
loc.getRange().getEnd().getLine(),
loc.getRange().getEnd().getColumn())
.toString()));
results.put(file, matches);
} catch (Throwable th) {
LOG.error("Failed to show file location", th);
}
}
activity().handleSearchResults(results);
}
@Override
public void performCodeAction(File file, CodeActionItem actionItem) {
final var editor = activity().getEditorForFile(file);
if (editor != null) {
performCodeAction(editor.getEditor(), actionItem);
}
}
/**
* Perform the given {@link CodeActionItem}
*
* @param editor The {@link IDEEditor} that invoked the code action request. This is required to
* reduce the time finding the code action from the edits.
* @param action The action to perform
*/
public void performCodeAction(IDEEditor editor, CodeActionItem action) {
if (activity() == null || editor == null || action == null) {
LOG.error(
"Unable to perform code action",
"activity=" + activity(),
"editor=" + editor,
"action=" + action);
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
return;
}
final ProgressSheet progress = new ProgressSheet();
progress.setSubMessageEnabled(false);
progress.setWelcomeTextEnabled(false);
progress.setCancelable(false);
progress.setMessage(activity().getString(R.string.msg_performing_actions));
progress.show(activity().getSupportFragmentManager(), "quick_fix_progress");
new TaskExecutor()
.executeAsyncProvideError(
() -> performCodeActionAsync(editor, action),
(result, throwable) -> {
ThreadUtils.runOnUiThread(progress::dismiss);
if (result == null || throwable != null || !result) {
LOG.error(
"Unable to perform code action", "result=" + result, "throwable=" + throwable);
StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR);
} else {
editor.executeCommand(action.getCommand());
}
});
}
private List<DiagnosticGroup> mapAsGroup(Map<File, List<DiagnosticItem>> diags) {
List<DiagnosticGroup> groups = new ArrayList<>();
if (diags == null || diags.size() <= 0) return groups;
for (File file : diags.keySet()) {
var fileDiags = diags.get(file);
if (fileDiags == null || fileDiags.size() <= 0) continue;
DiagnosticGroup group = new DiagnosticGroup(R.drawable.ic_language_java, file, fileDiags);
groups.add(group);
}
return groups;
}
private void showAvailableQuickfixes(IDEEditor editor, List<CodeActionItem> actions) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(activity());
builder.setTitle(R.string.msg_code_actions);
builder.setItems(
asArray(actions),
(d, w) -> {
d.dismiss();
hideDiagnostics();
hideBottomDiagnosticView(editor.getFile());
performCodeAction(editor, actions.get(w));
});
builder.show();
}
private CharSequence[] asArray(List<CodeActionItem> actions) {
final String[] arr = new String[actions.size()];
for (int i = 0; i < actions.size(); i++) {
arr[i] = actions.get(i).getTitle();
}
return arr;
}
private Boolean performCodeActionAsync(final IDEEditor editor, final CodeActionItem action) {
LOG.debug("Performing code action:", action);
final var changes = action.getChanges();
if (changes.isEmpty()) {
return Boolean.FALSE;
}
for (var change : changes) {
final var path = change.getFile();
if (path == null) {
continue;
}
final File file = path.toFile();
if (!file.exists()) {
continue;
}
for (TextEdit edit : change.getEdits()) {
final String editorFilepath =
editor.getFile() == null ? "" : editor.getFile().getAbsolutePath();
if (file.getAbsolutePath().equals(editorFilepath)) {
// Edit is in the same editor which requested the code action
editInEditor(editor, edit);
} else {
var openedFrag = findEditorByFile(file);
if (openedFrag != null && openedFrag.getEditor() != null) {
// Edit is in another 'opened' file
editInEditor(openedFrag.getEditor(), edit);
} else {
// Edit is in some other file which is not opened
// We should open that file and perform the edit
openedFrag = activity().openFile(file);
if (openedFrag != null && openedFrag.getEditor() != null) {
editInEditor(openedFrag.getEditor(), edit);
}
}
}
}
}
return Boolean.TRUE;
}
private void execCommand(IDEEditor editor, Command command) {
editor.executeCommand(command);
}
private void editInEditor(final IDEEditor editor, final TextEdit edit) {
final Range range = edit.getRange();
final int startLine = range.getStart().getLine();
final int startCol = range.getStart().getColumn();
final int endLine = range.getEnd().getLine();
final int endCol = range.getEnd().getColumn();
activity()
.runOnUiThread(
() -> {
if (startLine == endLine && startCol == endCol) {
editor.getText().insert(startLine, startCol, edit.getNewText());
} else {
editor.getText().replace(startLine, startCol, endLine, endCol, edit.getNewText());
}
});
}
public DiagnosticsAdapter newDiagnosticsAdapter() {
return new DiagnosticsAdapter(mapAsGroup(this.diagnostics), activity());
}
@Override
public com.itsaky.lsp.models.ShowDocumentResult showDocument(
com.itsaky.lsp.models.ShowDocumentParams params) {
boolean success = false;
final var result = new com.itsaky.lsp.models.ShowDocumentResult(false);
if (activity() == null) {
return result;
}
if (params != null) {
File file = params.getFile().toFile();
if (file.exists() && file.isFile() && FileUtils.isUtf8(file)) {
final var range = params.getSelection();
var frag =
activity().getEditorAtIndex(activity().getBinding().tabs.getSelectedTabPosition());
if (frag != null
&& frag.getFile() != null
&& frag.getEditor() != null
&& frag.getFile().getAbsolutePath().equals(file.getAbsolutePath())) {
if (LSPUtils.isEqual(range.getStart(), range.getEnd())) {
frag.getEditor().setSelection(range.getStart().getLine(), range.getStart().getColumn());
} else {
frag.getEditor().setSelection(range);
}
} else {
activity().openFileAndSelect(file, range);
}
success = true;
}
}
result.setSuccess(success);
return result;
}
}
@@ -1,335 +1,335 @@
/*
* 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.models;
import static com.itsaky.androidide.utils.ILogger.newInstance;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JSONUtility;
import com.itsaky.androidide.utils.ListingFileRedirect;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.List;
@SuppressWarnings("unused")
public class ApkMetadata {
private static final ILogger LOG = newInstance("ApkMetadata");
@SerializedName("version")
private int version;
@SerializedName("artifactType")
private ArtifactType artifactType;
@SerializedName("applicationId")
private String applicationId;
@SerializedName("variantName")
private String variantName;
@SerializedName("elements")
private List<Element> elements;
@SerializedName("elementType")
private String elementType;
@Nullable
public static File findApkFile(@NonNull File listingFIle) {
try {
// This sometimes might be a redirect to the listing file
// So we have to handle this case too.
final var redirectedFile = ListingFileRedirect.maybeExtractRedirectedFile(listingFIle);
if (redirectedFile != null) {
listingFIle = redirectedFile;
}
final var dir = listingFIle.getParentFile();
final var metadata =
JSONUtility.gson.fromJson(new FileReader(listingFIle), ApkMetadata.class);
if (!isValid(metadata)) {
LOG.warn("Invalid APK metadata:", metadata);
return null;
}
for (Element element : metadata.getElements()) {
if (element == null || element.getOutputFile() == null) {
LOG.warn("No output file specified in APK metadata element:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
final File apk = new File(dir, element.getOutputFile());
if (apk.exists() && apk.isFile()) {
LOG.info("Found apk in metadata:", apk);
return apk;
}
}
}
return null;
} catch (FileNotFoundException e) {
LOG.error("Metadata file not found...", e);
return null;
}
}
private static boolean isValid(ApkMetadata metadata) {
// Null checks
if (metadata == null
|| metadata.getArtifactType() == null
|| metadata.getArtifactType().getType() == null
|| metadata.getElements() == null) {
LOG.warn("APK metadata null check failed. Metadata:", metadata);
return false;
}
final ArtifactType type = metadata.getArtifactType();
final List<Element> elements = metadata.getElements();
if (!type.getType().equals(ArtifactType.TYPE_APK)) {
LOG.warn("Artifact is not of type APK. Metadata:", metadata);
return false;
}
if (elements.isEmpty()) {
LOG.warn("No output elements found for metadata:", metadata);
return false;
}
boolean atLeastOneApk = false;
for (Element element : elements) {
if (element == null
|| element.getOutputFile() == null
|| !element.getOutputFile().endsWith(".apk")) {
LOG.warn("Skipping output element because file is not APK:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
atLeastOneApk = true;
break;
}
}
LOG.debug("Output metadata validation succeeded");
return atLeastOneApk;
}
public List<Element> getElements() {
return elements;
}
public ArtifactType getArtifactType() {
return artifactType;
}
public void setArtifactType(ArtifactType artifactType) {
this.artifactType = artifactType;
}
public void setElements(List<Element> elements) {
this.elements = elements;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public String getApplicationId() {
return applicationId;
}
public void setApplicationId(String applicationId) {
this.applicationId = applicationId;
}
public String getVariantName() {
return variantName;
}
public void setVariantName(String variantName) {
this.variantName = variantName;
}
public String getElementType() {
return elementType;
}
public void setElementType(String elementType) {
this.elementType = elementType;
}
@NonNull
@Override
public String toString() {
return "ApkMetadata{"
+ "version="
+ version
+ ", artifactType="
+ artifactType
+ ", applicationId='"
+ applicationId
+ '\''
+ ", variantName='"
+ variantName
+ '\''
+ ", elements="
+ elements
+ ", elementType='"
+ elementType
+ '\''
+ '}';
}
public static class ArtifactType {
public static String TYPE_APK = "APK";
@SerializedName("type")
private String type;
@SerializedName("kind")
private String kind;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getKind() {
return kind;
}
public void setKind(String kind) {
this.kind = kind;
}
@NonNull
@Override
public String toString() {
return "ArtifactType{" + "type='" + type + '\'' + ", kind='" + kind + '\'' + '}';
}
}
public static class Element {
@SerializedName("type")
private String type;
@SerializedName("filters")
private List<Object> filters;
@SerializedName("attributes")
private List<Object> attributes;
@SerializedName("versionCode")
private int versionCode;
@SerializedName("versionName")
private String versionName;
@SerializedName("outputFile")
private String outputFile;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<Object> getFilters() {
return filters;
}
public void setFilters(List<Object> filters) {
this.filters = filters;
}
public List<Object> getAttributes() {
return attributes;
}
public void setAttributes(List<Object> attributes) {
this.attributes = attributes;
}
public int getVersionCode() {
return versionCode;
}
public void setVersionCode(int versionCode) {
this.versionCode = versionCode;
}
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public String getOutputFile() {
return outputFile;
}
public void setOutputFile(String outputFile) {
this.outputFile = outputFile;
}
@NonNull
@Override
public String toString() {
return "Element{"
+ "type='"
+ type
+ '\''
+ ", filters="
+ filters
+ ", attributes="
+ attributes
+ ", versionCode="
+ versionCode
+ ", versionName='"
+ versionName
+ '\''
+ ", outputFile='"
+ outputFile
+ '\''
+ '}';
}
}
}
/*
* 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.models;
import static com.itsaky.androidide.utils.ILogger.newInstance;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.annotations.SerializedName;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.androidide.utils.JSONUtility;
import com.itsaky.androidide.utils.ListingFileRedirect;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.util.List;
@SuppressWarnings("unused")
public class ApkMetadata {
private static final ILogger LOG = newInstance("ApkMetadata");
@SerializedName("version")
private int version;
@SerializedName("artifactType")
private ArtifactType artifactType;
@SerializedName("applicationId")
private String applicationId;
@SerializedName("variantName")
private String variantName;
@SerializedName("elements")
private List<Element> elements;
@SerializedName("elementType")
private String elementType;
@Nullable
public static File findApkFile(@NonNull File listingFIle) {
try {
// This sometimes might be a redirect to the listing file
// So we have to handle this case too.
final var redirectedFile = ListingFileRedirect.maybeExtractRedirectedFile(listingFIle);
if (redirectedFile != null) {
listingFIle = redirectedFile;
}
final var dir = listingFIle.getParentFile();
final var metadata =
JSONUtility.gson.fromJson(new FileReader(listingFIle), ApkMetadata.class);
if (!isValid(metadata)) {
LOG.warn("Invalid APK metadata:", metadata);
return null;
}
for (Element element : metadata.getElements()) {
if (element == null || element.getOutputFile() == null) {
LOG.warn("No output file specified in APK metadata element:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
final File apk = new File(dir, element.getOutputFile());
if (apk.exists() && apk.isFile()) {
LOG.info("Found apk in metadata:", apk);
return apk;
}
}
}
return null;
} catch (FileNotFoundException e) {
LOG.error("Metadata file not found...", e);
return null;
}
}
private static boolean isValid(ApkMetadata metadata) {
// Null checks
if (metadata == null
|| metadata.getArtifactType() == null
|| metadata.getArtifactType().getType() == null
|| metadata.getElements() == null) {
LOG.warn("APK metadata null check failed. Metadata:", metadata);
return false;
}
final ArtifactType type = metadata.getArtifactType();
final List<Element> elements = metadata.getElements();
if (!type.getType().equals(ArtifactType.TYPE_APK)) {
LOG.warn("Artifact is not of type APK. Metadata:", metadata);
return false;
}
if (elements.isEmpty()) {
LOG.warn("No output elements found for metadata:", metadata);
return false;
}
boolean atLeastOneApk = false;
for (Element element : elements) {
if (element == null
|| element.getOutputFile() == null
|| !element.getOutputFile().endsWith(".apk")) {
LOG.warn("Skipping output element because file is not APK:", element);
continue;
}
if (element.getOutputFile().endsWith(".apk")) {
atLeastOneApk = true;
break;
}
}
LOG.debug("Output metadata validation succeeded");
return atLeastOneApk;
}
public List<Element> getElements() {
return elements;
}
public ArtifactType getArtifactType() {
return artifactType;
}
public void setArtifactType(ArtifactType artifactType) {
this.artifactType = artifactType;
}
public void setElements(List<Element> elements) {
this.elements = elements;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public String getApplicationId() {
return applicationId;
}
public void setApplicationId(String applicationId) {
this.applicationId = applicationId;
}
public String getVariantName() {
return variantName;
}
public void setVariantName(String variantName) {
this.variantName = variantName;
}
public String getElementType() {
return elementType;
}
public void setElementType(String elementType) {
this.elementType = elementType;
}
@NonNull
@Override
public String toString() {
return "ApkMetadata{"
+ "version="
+ version
+ ", artifactType="
+ artifactType
+ ", applicationId='"
+ applicationId
+ '\''
+ ", variantName='"
+ variantName
+ '\''
+ ", elements="
+ elements
+ ", elementType='"
+ elementType
+ '\''
+ '}';
}
public static class ArtifactType {
public static String TYPE_APK = "APK";
@SerializedName("type")
private String type;
@SerializedName("kind")
private String kind;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getKind() {
return kind;
}
public void setKind(String kind) {
this.kind = kind;
}
@NonNull
@Override
public String toString() {
return "ArtifactType{" + "type='" + type + '\'' + ", kind='" + kind + '\'' + '}';
}
}
public static class Element {
@SerializedName("type")
private String type;
@SerializedName("filters")
private List<Object> filters;
@SerializedName("attributes")
private List<Object> attributes;
@SerializedName("versionCode")
private int versionCode;
@SerializedName("versionName")
private String versionName;
@SerializedName("outputFile")
private String outputFile;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public List<Object> getFilters() {
return filters;
}
public void setFilters(List<Object> filters) {
this.filters = filters;
}
public List<Object> getAttributes() {
return attributes;
}
public void setAttributes(List<Object> attributes) {
this.attributes = attributes;
}
public int getVersionCode() {
return versionCode;
}
public void setVersionCode(int versionCode) {
this.versionCode = versionCode;
}
public String getVersionName() {
return versionName;
}
public void setVersionName(String versionName) {
this.versionName = versionName;
}
public String getOutputFile() {
return outputFile;
}
public void setOutputFile(String outputFile) {
this.outputFile = outputFile;
}
@NonNull
@Override
public String toString() {
return "Element{"
+ "type='"
+ type
+ '\''
+ ", filters="
+ filters
+ ", attributes="
+ attributes
+ ", versionCode="
+ versionCode
+ ", versionName='"
+ versionName
+ '\''
+ ", outputFile='"
+ outputFile
+ '\''
+ '}';
}
}
}
@@ -1,155 +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.projects
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IProject.Type
import com.itsaky.androidide.tooling.api.messages.VariantDataRequest
import com.itsaky.androidide.tooling.api.messages.result.SimpleModuleData
import com.itsaky.androidide.tooling.api.messages.result.SimpleVariantData
import com.itsaky.androidide.tooling.api.model.IdeAndroidModule
import com.itsaky.androidide.tooling.api.model.IdeGradleProject
import com.itsaky.androidide.tooling.api.model.IdeGradleTask
import com.itsaky.androidide.utils.ILogger
import java.io.File
import java.util.concurrent.*
/**
* A project which lazily caches some required properties of the given project.
*
* @author Akash Yadav
*/
class CachingProject(val project: IProject) : IProject {
private val log = ILogger.newInstance(javaClass.simpleName)
private val mName: String by lazy { this.project.name.get() }
private val mDescription: String by lazy { this.project.description.get() }
private val mProjectPath: String by lazy { this.project.projectPath.get() }
private val mProjectDir: File by lazy { this.project.projectDir.get() }
private val mBuildDir: File by lazy { this.project.buildDir.get() }
private val mBuildScript: File by lazy { this.project.buildScript.get() }
private val mProjectType: Type by lazy { this.project.type.get() }
private var mFirstAppModule: IdeAndroidModule? = null
private val mCachedVariants: MutableMap<VariantDataRequest, SimpleVariantData> = mutableMapOf()
private val mModules: MutableList<SimpleModuleData> = mutableListOf()
override fun isProjectInitialized(): CompletableFuture<Boolean> {
return CompletableFuture.completedFuture(true)
}
override fun getName(): CompletableFuture<String> {
return CompletableFuture.completedFuture(this.mName)
}
override fun getDescription(): CompletableFuture<String> {
return CompletableFuture.completedFuture(mDescription)
}
override fun getProjectPath(): CompletableFuture<String> {
return CompletableFuture.completedFuture(mProjectPath)
}
override fun getType(): CompletableFuture<Type> {
return CompletableFuture.completedFuture(mProjectType)
}
override fun getProjectDir(): CompletableFuture<File> {
return CompletableFuture.completedFuture(mProjectDir)
}
override fun getBuildDir(): CompletableFuture<File> {
return CompletableFuture.completedFuture(mBuildDir)
}
override fun getBuildScript(): CompletableFuture<File> {
return CompletableFuture.completedFuture(mBuildScript)
}
override fun getTasks(): CompletableFuture<MutableList<IdeGradleTask>> {
return this.project.tasks
}
override fun getModules(): CompletableFuture<MutableList<IdeGradleProject>> {
return this.project.modules
}
override fun listModules(): CompletableFuture<MutableList<SimpleModuleData>> {
return if (this.mModules.isNotEmpty()) {
log.debug("Using cached module data...")
CompletableFuture.completedFuture(mModules)
} else {
return this.project.listModules().whenComplete { modules, err ->
if (err != null || modules == null) {
log.debug("Unable to fetch module data from tooling server", err)
return@whenComplete
}
if (modules.isEmpty()) {
log.debug("Empty module data returned by tooling server. Ignoring...")
return@whenComplete
}
mModules.clear()
mModules.addAll(modules)
}
}
}
override fun getVariantData(request: VariantDataRequest): CompletableFuture<SimpleVariantData> {
val cached = mCachedVariants[request]
if (cached != null) {
return CompletableFuture.completedFuture(cached)
}
return this.project.getVariantData(request).whenComplete { t, u ->
if (t == null) {
log.warn(
"Unable to get variant data for " +
"variant ${request.variantName} ('${request.projectPath}') from tooling server",
u)
return@whenComplete
}
mCachedVariants[request] = t
}
}
override fun findByPath(path: String): CompletableFuture<IdeGradleProject> {
return this.project.findByPath(path)
}
override fun findAndroidModules(): CompletableFuture<MutableList<IdeAndroidModule>> {
return this.project.findAndroidModules()
}
override fun findFirstAndroidModule(): CompletableFuture<IdeAndroidModule> {
return this.project.findFirstAndroidModule()
}
override fun findFirstAndroidAppModule(): CompletableFuture<IdeAndroidModule> {
if (mFirstAppModule != null) {
return CompletableFuture.completedFuture(mFirstAppModule)
}
return this.project.findFirstAndroidAppModule().whenComplete { module, _ ->
this.mFirstAppModule = module
}
}
}
/*
* 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.projects
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.IProject.Type
import com.itsaky.androidide.tooling.api.messages.VariantDataRequest
import com.itsaky.androidide.tooling.api.messages.result.SimpleModuleData
import com.itsaky.androidide.tooling.api.messages.result.SimpleVariantData
import com.itsaky.androidide.tooling.api.model.IdeAndroidModule
import com.itsaky.androidide.tooling.api.model.IdeGradleProject
import com.itsaky.androidide.tooling.api.model.IdeGradleTask
import com.itsaky.androidide.utils.ILogger
import java.io.File
import java.util.concurrent.*
/**
* A project which lazily caches some required properties of the given project.
*
* @author Akash Yadav
*/
class CachingProject(val project: IProject) : IProject {
private val log = ILogger.newInstance(javaClass.simpleName)
private val mName: String by lazy { this.project.name.get() }
private val mDescription: String by lazy { this.project.description.get() }
private val mProjectPath: String by lazy { this.project.projectPath.get() }
private val mProjectDir: File by lazy { this.project.projectDir.get() }
private val mBuildDir: File by lazy { this.project.buildDir.get() }
private val mBuildScript: File by lazy { this.project.buildScript.get() }
private val mProjectType: Type by lazy { this.project.type.get() }
private var mFirstAppModule: IdeAndroidModule? = null
private val mCachedVariants: MutableMap<VariantDataRequest, SimpleVariantData> = mutableMapOf()
private val mModules: MutableList<SimpleModuleData> = mutableListOf()
override fun isProjectInitialized(): CompletableFuture<Boolean> {
return CompletableFuture.completedFuture(true)
}
override fun getName(): CompletableFuture<String> {
return CompletableFuture.completedFuture(this.mName)
}
override fun getDescription(): CompletableFuture<String> {
return CompletableFuture.completedFuture(mDescription)
}
override fun getProjectPath(): CompletableFuture<String> {
return CompletableFuture.completedFuture(mProjectPath)
}
override fun getType(): CompletableFuture<Type> {
return CompletableFuture.completedFuture(mProjectType)
}
override fun getProjectDir(): CompletableFuture<File> {
return CompletableFuture.completedFuture(mProjectDir)
}
override fun getBuildDir(): CompletableFuture<File> {
return CompletableFuture.completedFuture(mBuildDir)
}
override fun getBuildScript(): CompletableFuture<File> {
return CompletableFuture.completedFuture(mBuildScript)
}
override fun getTasks(): CompletableFuture<MutableList<IdeGradleTask>> {
return this.project.tasks
}
override fun getModules(): CompletableFuture<MutableList<IdeGradleProject>> {
return this.project.modules
}
override fun listModules(): CompletableFuture<MutableList<SimpleModuleData>> {
return if (this.mModules.isNotEmpty()) {
log.debug("Using cached module data...")
CompletableFuture.completedFuture(mModules)
} else {
return this.project.listModules().whenComplete { modules, err ->
if (err != null || modules == null) {
log.debug("Unable to fetch module data from tooling server", err)
return@whenComplete
}
if (modules.isEmpty()) {
log.debug("Empty module data returned by tooling server. Ignoring...")
return@whenComplete
}
mModules.clear()
mModules.addAll(modules)
}
}
}
override fun getVariantData(request: VariantDataRequest): CompletableFuture<SimpleVariantData> {
val cached = mCachedVariants[request]
if (cached != null) {
return CompletableFuture.completedFuture(cached)
}
return this.project.getVariantData(request).whenComplete { t, u ->
if (t == null) {
log.warn(
"Unable to get variant data for " +
"variant ${request.variantName} ('${request.projectPath}') from tooling server",
u)
return@whenComplete
}
mCachedVariants[request] = t
}
}
override fun findByPath(path: String): CompletableFuture<IdeGradleProject> {
return this.project.findByPath(path)
}
override fun findAndroidModules(): CompletableFuture<MutableList<IdeAndroidModule>> {
return this.project.findAndroidModules()
}
override fun findFirstAndroidModule(): CompletableFuture<IdeAndroidModule> {
return this.project.findFirstAndroidModule()
}
override fun findFirstAndroidAppModule(): CompletableFuture<IdeAndroidModule> {
if (mFirstAppModule != null) {
return CompletableFuture.completedFuture(mFirstAppModule)
}
return this.project.findFirstAndroidAppModule().whenComplete { module, _ ->
this.mFirstAppModule = module
}
}
}
@@ -1,207 +1,207 @@
/*
* 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.projects
import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.services.BuildService
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.model.IdeAndroidModule
import com.itsaky.androidide.tooling.api.model.IdeGradleProject
import com.itsaky.androidide.tooling.api.model.IdeJavaModule
import com.itsaky.androidide.tooling.api.model.IdeModule
import com.itsaky.androidide.tooling.api.util.ProjectDataCollector
import com.itsaky.androidide.utils.ILogger
import com.itsaky.lsp.java.models.JavaServerConfiguration
import java.io.File
import java.nio.file.Path
import java.util.concurrent.*
/**
* Manages projects in AndroidIDE.
*
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object ProjectManager {
private val log = ILogger.newInstance(javaClass.simpleName)
var rootProject: IProject? = null
set(value) {
field = if (value == null) null else CachingProject(value)
// Cache the module data in advance for future use
if (field != null) {
field!!.listModules()
}
}
var projectPath: String? = null
fun isInitialized() = this.rootProject != null && this.rootProject!!.isProjectInitialized.get()
fun checkInit(): Boolean {
if (isInitialized()) {
return true
}
log.warn("Project is not initialized yet!")
return false
}
fun getProjectDir(): File? = if (!checkInit()) null else File(getProjectDirPath()!!)
fun getProjectDirPath(): String? {
return projectPath
}
fun generateSources(builder: BuildService?) {
if (builder == null) {
log.warn("Cannot generate sources. BuildService is null.")
return
}
getApplicationModule().whenCompleteAsync { app, _ ->
if (app == null) {
log.warn(
"Cannot run resource and source generation task. No application module found.")
return@whenCompleteAsync
}
val debug = app.simpleVariants.firstOrNull { it.name == "debug" }
if (debug == null) {
log.warn("No debug variant found in application project ${app.name}")
return@whenCompleteAsync
}
val mainArtifact = app.simpleVariants.first { it.name == "debug" }.mainArtifact
val genResourcesTask = mainArtifact.resGenTaskName
val genSourcesTask = mainArtifact.sourceGenTaskName
builder
.executeProjectTasks(
app.projectPath,
genResourcesTask ?: "",
genSourcesTask,
// If view binding is enabled, generate the view binding classes too
if (app.viewBindingOptions != null && app.viewBindingOptions!!.isEnabled)
"dataBindingGenBaseClassesDebug"
else "")
.whenComplete { result, taskErr ->
if (!result.isSuccessful || taskErr != null) {
log.warn(
"Execution for tasks '$genResourcesTask' and '$genSourcesTask' failed.",
taskErr ?: "")
} else {
notifyProjectUpdate()
}
}
}
}
fun getApplicationModule(): CompletableFuture<IdeAndroidModule?> {
if (rootProject == null) {
log.error(
"No root project instance is set. Is the project initialization process finished?")
return CompletableFuture.completedFuture(null)
}
return rootProject!!.findFirstAndroidAppModule().whenComplete { app, err ->
if (app == null || err != null) {
log.debug("An error occurred while fetching model for application module.")
return@whenComplete
}
}
}
fun getApplicationResDirectories(): CompletableFuture<List<File>?> {
return getApplicationModule().thenApplyAsync {
if (it == null) null else collectResDirectories(it)
}
}
fun notifyProjectUpdate() {
log.debug("Updating class paths in language servers...")
getApplicationModule().whenCompleteAsync { it, _ ->
if (it == null) {
log.debug("Cannot find application module. Unable to update class paths to LSP.")
return@whenCompleteAsync
}
val server = StudioApp.getInstance().javaLanguageServer
val classPaths = collectClassPaths(it)
val sourceDirs = collectSourceDirs(it)
val configuration = JavaServerConfiguration(classPaths, sourceDirs)
server.configurationChanged(configuration)
}
}
fun collectResDirectories(android: IdeAndroidModule) =
ProjectDataCollector.collectResDirectories(this.rootProject!!, android)
fun collectClassPaths(app: IdeAndroidModule) =
ProjectDataCollector.collectClassPaths(this.rootProject!!, app)
fun collectSourceDirs(app: IdeAndroidModule): Set<Path> =
ProjectDataCollector.collectSourceDirs(this.rootProject!!, app)
@Suppress("unused")
fun collectProjectDependencies(project: IProject, app: IdeAndroidModule) =
ProjectDataCollector.collectProjectDependencies(project, app)
@Suppress("unused")
fun collectSourceDirs(projects: List<IdeModule>) =
ProjectDataCollector.collectSourceDirs(projects)
@Suppress("unused")
fun collectSources(java: IdeJavaModule) = ProjectDataCollector.collectSources(java)
@Suppress("unused")
fun collectSources(android: IdeAndroidModule) = ProjectDataCollector.collectSources(android)
fun findModuleForFile(file: File): CompletableFuture<IdeGradleProject?> {
if (!checkInit()) {
return CompletableFuture.completedFuture(null)
}
val notAvailable = ":::::"
return rootProject!!
.listModules()
.thenApplyAsync { modules ->
val path = file.canonicalPath
var longestPath = ""
for (module in modules) {
val modulePath = module.projectDir.canonicalPath
if (path.startsWith(modulePath)) {
if (longestPath.length < modulePath.length) {
longestPath = modulePath
}
}
}
if (longestPath.isEmpty()) {
return@thenApplyAsync notAvailable
}
return@thenApplyAsync longestPath
}
.thenApplyAsync {
if (it == notAvailable) {
return@thenApplyAsync null
} else return@thenApplyAsync rootProject!!.findByPath(it).get()
}
}
}
/*
* 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.projects
import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.services.BuildService
import com.itsaky.androidide.tooling.api.IProject
import com.itsaky.androidide.tooling.api.model.IdeAndroidModule
import com.itsaky.androidide.tooling.api.model.IdeGradleProject
import com.itsaky.androidide.tooling.api.model.IdeJavaModule
import com.itsaky.androidide.tooling.api.model.IdeModule
import com.itsaky.androidide.tooling.api.util.ProjectDataCollector
import com.itsaky.androidide.utils.ILogger
import com.itsaky.lsp.java.models.JavaServerConfiguration
import java.io.File
import java.nio.file.Path
import java.util.concurrent.*
/**
* Manages projects in AndroidIDE.
*
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
object ProjectManager {
private val log = ILogger.newInstance(javaClass.simpleName)
var rootProject: IProject? = null
set(value) {
field = if (value == null) null else CachingProject(value)
// Cache the module data in advance for future use
if (field != null) {
field!!.listModules()
}
}
var projectPath: String? = null
fun isInitialized() = this.rootProject != null && this.rootProject!!.isProjectInitialized.get()
fun checkInit(): Boolean {
if (isInitialized()) {
return true
}
log.warn("Project is not initialized yet!")
return false
}
fun getProjectDir(): File? = if (!checkInit()) null else File(getProjectDirPath()!!)
fun getProjectDirPath(): String? {
return projectPath
}
fun generateSources(builder: BuildService?) {
if (builder == null) {
log.warn("Cannot generate sources. BuildService is null.")
return
}
getApplicationModule().whenCompleteAsync { app, _ ->
if (app == null) {
log.warn(
"Cannot run resource and source generation task. No application module found.")
return@whenCompleteAsync
}
val debug = app.simpleVariants.firstOrNull { it.name == "debug" }
if (debug == null) {
log.warn("No debug variant found in application project ${app.name}")
return@whenCompleteAsync
}
val mainArtifact = app.simpleVariants.first { it.name == "debug" }.mainArtifact
val genResourcesTask = mainArtifact.resGenTaskName
val genSourcesTask = mainArtifact.sourceGenTaskName
builder
.executeProjectTasks(
app.projectPath,
genResourcesTask ?: "",
genSourcesTask,
// If view binding is enabled, generate the view binding classes too
if (app.viewBindingOptions != null && app.viewBindingOptions!!.isEnabled)
"dataBindingGenBaseClassesDebug"
else "")
.whenComplete { result, taskErr ->
if (!result.isSuccessful || taskErr != null) {
log.warn(
"Execution for tasks '$genResourcesTask' and '$genSourcesTask' failed.",
taskErr ?: "")
} else {
notifyProjectUpdate()
}
}
}
}
fun getApplicationModule(): CompletableFuture<IdeAndroidModule?> {
if (rootProject == null) {
log.error(
"No root project instance is set. Is the project initialization process finished?")
return CompletableFuture.completedFuture(null)
}
return rootProject!!.findFirstAndroidAppModule().whenComplete { app, err ->
if (app == null || err != null) {
log.debug("An error occurred while fetching model for application module.")
return@whenComplete
}
}
}
fun getApplicationResDirectories(): CompletableFuture<List<File>?> {
return getApplicationModule().thenApplyAsync {
if (it == null) null else collectResDirectories(it)
}
}
fun notifyProjectUpdate() {
log.debug("Updating class paths in language servers...")
getApplicationModule().whenCompleteAsync { it, _ ->
if (it == null) {
log.debug("Cannot find application module. Unable to update class paths to LSP.")
return@whenCompleteAsync
}
val server = StudioApp.getInstance().javaLanguageServer
val classPaths = collectClassPaths(it)
val sourceDirs = collectSourceDirs(it)
val configuration = JavaServerConfiguration(classPaths, sourceDirs)
server.configurationChanged(configuration)
}
}
fun collectResDirectories(android: IdeAndroidModule) =
ProjectDataCollector.collectResDirectories(this.rootProject!!, android)
fun collectClassPaths(app: IdeAndroidModule) =
ProjectDataCollector.collectClassPaths(this.rootProject!!, app)
fun collectSourceDirs(app: IdeAndroidModule): Set<Path> =
ProjectDataCollector.collectSourceDirs(this.rootProject!!, app)
@Suppress("unused")
fun collectProjectDependencies(project: IProject, app: IdeAndroidModule) =
ProjectDataCollector.collectProjectDependencies(project, app)
@Suppress("unused")
fun collectSourceDirs(projects: List<IdeModule>) =
ProjectDataCollector.collectSourceDirs(projects)
@Suppress("unused")
fun collectSources(java: IdeJavaModule) = ProjectDataCollector.collectSources(java)
@Suppress("unused")
fun collectSources(android: IdeAndroidModule) = ProjectDataCollector.collectSources(android)
fun findModuleForFile(file: File): CompletableFuture<IdeGradleProject?> {
if (!checkInit()) {
return CompletableFuture.completedFuture(null)
}
val notAvailable = ":::::"
return rootProject!!
.listModules()
.thenApplyAsync { modules ->
val path = file.canonicalPath
var longestPath = ""
for (module in modules) {
val modulePath = module.projectDir.canonicalPath
if (path.startsWith(modulePath)) {
if (longestPath.length < modulePath.length) {
longestPath = modulePath
}
}
}
if (longestPath.isEmpty()) {
return@thenApplyAsync notAvailable
}
return@thenApplyAsync longestPath
}
.thenApplyAsync {
if (it == notAvailable) {
return@thenApplyAsync null
} else return@thenApplyAsync rootProject!!.findByPath(it).get()
}
}
}
@@ -1,312 +1,312 @@
/*
* 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.projects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.io.File;
import java.io.FileFilter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
public class ProjectResourceTable implements IResourceTable {
private static final ILogger LOG = ILogger.newInstance("ProjectResourceFinder");
private File resDir;
private File color;
private File layout;
private File[] drawables;
private File[] mipmaps;
@Override
public File findDrawable(@NonNull String name) {
for (File drawable : drawables) {
final File file = findFileWithName(drawable.listFiles(), name);
if (file != null) {
return file;
}
}
return null;
}
@Override
public File findLayout(@NonNull String name) {
return findFileWithName(this.layout.listFiles(), name);
}
@Override
public String findString(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findString(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@string/")) {
var ref = value.substring("@string/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@Override
public String findBoolean(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findBoolean(name);
if (resource == null) {
return name;
}
final var val = resource.getValue();
if (val.startsWith("@bool/")) {
final var ref = val.substring("@bool/".length());
if (ref.equals(name)) {
// recursive reference
return name;
}
}
return val;
}
@Override
public String findInteger(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findInteger(name);
if (resource == null) {
return name;
}
final var val = resource.getValue();
if (val.startsWith("@integer/")) {
final var ref = val.substring("@integer/".length());
if (ref.equals(name)) {
// recursive reference
return name;
}
}
return val;
}
@Override
public String findColor(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findColor(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@color/")) {
var ref = value.substring("@color/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@Override
public String[] findArray(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return new String[] {name};
}
final var resource = table.findArray(name);
if (resource == null) {
return new String[] {name};
}
return resource.getValues();
}
@Override
public String findDimension(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findDimension(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@dimen/")) {
var ref = value.substring("@dimen/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@NonNull
@Override
public Collection<String> listResourceNames(@Nullable String type) {
if (type == null) {
return listAllResourceNames();
} else {
final var set = new HashSet<String>();
switch (type) {
case "drawable":
for (var folder : this.drawables) {
listAndAddTo(set, folder, type);
}
break;
case "layout":
listAndAddTo(set, this.layout, type);
break;
case "color":
listAndAddTo(set, this.color, type);
break;
case "mipmap":
for (var folder : this.mipmaps) {
listAndAddTo(set, folder, type);
}
break;
}
return set;
}
}
private void listAndAddTo(HashSet<String> set, @NonNull File dir, String type) {
final var files = dir.listFiles();
if (files == null) {
return;
}
for (var file : files) {
var name = file.getName();
if (!name.contains(".")) {
continue;
}
name = name.substring(0, name.lastIndexOf('.'));
name = String.format(Locale.ROOT, "@%s/%s", type, name);
set.add(name);
}
}
@NonNull
private Collection<String> listAllResourceNames() {
final var set = new HashSet<>(listResourceNames("color"));
set.addAll(listResourceNames("drawable"));
set.addAll(listResourceNames("mipmap"));
set.addAll(listResourceNames("layout"));
return set;
}
@Override
public void setInflatingFile(@NonNull File file) {
if (file.getParentFile() == null) {
throw new IllegalArgumentException("Invalid inflating file");
}
setupDirectories(
file // layout file
.getParentFile() // layout dir
.getParentFile()); // res dir
}
private void setupDirectories(File resDir) {
if (resDir == null) {
LOG.error("Null resource directory passed to resource finder. Ignoring.");
return;
}
this.resDir = resDir;
this.color = new File(resDir, "color");
this.layout = new File(resDir, "layout");
final File[] drawables = resDir.listFiles(new NameStartsWith("drawable"));
this.drawables = drawables == null ? new File[0] : drawables;
final File[] mipmaps = resDir.listFiles(new NameStartsWith("mipmap"));
this.mipmaps = mipmaps == null ? new File[0] : mipmaps;
}
private File findFileWithName(File[] files, String name) {
if (files == null || name == null) {
return null;
}
for (File file : files) {
var simpleName = file.getName();
if (simpleName.startsWith(name)) {
return file;
}
}
return null;
}
private static class NameStartsWith implements FileFilter {
private final String name;
public NameStartsWith(String name) {
this.name = name;
}
@Override
public boolean accept(@NonNull File file) {
return file.getName().startsWith(name);
}
}
}
/*
* 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.projects;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.itsaky.androidide.utils.ILogger;
import com.itsaky.inflater.IResourceTable;
import com.itsaky.inflater.values.ValuesTableFactory;
import java.io.File;
import java.io.FileFilter;
import java.util.Collection;
import java.util.HashSet;
import java.util.Locale;
public class ProjectResourceTable implements IResourceTable {
private static final ILogger LOG = ILogger.newInstance("ProjectResourceFinder");
private File resDir;
private File color;
private File layout;
private File[] drawables;
private File[] mipmaps;
@Override
public File findDrawable(@NonNull String name) {
for (File drawable : drawables) {
final File file = findFileWithName(drawable.listFiles(), name);
if (file != null) {
return file;
}
}
return null;
}
@Override
public File findLayout(@NonNull String name) {
return findFileWithName(this.layout.listFiles(), name);
}
@Override
public String findString(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findString(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@string/")) {
var ref = value.substring("@string/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@Override
public String findBoolean(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findBoolean(name);
if (resource == null) {
return name;
}
final var val = resource.getValue();
if (val.startsWith("@bool/")) {
final var ref = val.substring("@bool/".length());
if (ref.equals(name)) {
// recursive reference
return name;
}
}
return val;
}
@Override
public String findInteger(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findInteger(name);
if (resource == null) {
return name;
}
final var val = resource.getValue();
if (val.startsWith("@integer/")) {
final var ref = val.substring("@integer/".length());
if (ref.equals(name)) {
// recursive reference
return name;
}
}
return val;
}
@Override
public String findColor(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findColor(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@color/")) {
var ref = value.substring("@color/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@Override
public String[] findArray(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return new String[] {name};
}
final var resource = table.findArray(name);
if (resource == null) {
return new String[] {name};
}
return resource.getValues();
}
@Override
public String findDimension(@NonNull String name) {
final var table = ValuesTableFactory.getTable(resDir);
if (table == null) {
return name;
}
final var resource = table.findDimension(name);
if (resource == null) {
return name;
}
final var value = resource.getValue();
if (value.startsWith("@dimen/")) {
var ref = value.substring("@dimen/".length());
if (name.equals(ref)) {
return name;
}
}
return value;
}
@NonNull
@Override
public Collection<String> listResourceNames(@Nullable String type) {
if (type == null) {
return listAllResourceNames();
} else {
final var set = new HashSet<String>();
switch (type) {
case "drawable":
for (var folder : this.drawables) {
listAndAddTo(set, folder, type);
}
break;
case "layout":
listAndAddTo(set, this.layout, type);
break;
case "color":
listAndAddTo(set, this.color, type);
break;
case "mipmap":
for (var folder : this.mipmaps) {
listAndAddTo(set, folder, type);
}
break;
}
return set;
}
}
private void listAndAddTo(HashSet<String> set, @NonNull File dir, String type) {
final var files = dir.listFiles();
if (files == null) {
return;
}
for (var file : files) {
var name = file.getName();
if (!name.contains(".")) {
continue;
}
name = name.substring(0, name.lastIndexOf('.'));
name = String.format(Locale.ROOT, "@%s/%s", type, name);
set.add(name);
}
}
@NonNull
private Collection<String> listAllResourceNames() {
final var set = new HashSet<>(listResourceNames("color"));
set.addAll(listResourceNames("drawable"));
set.addAll(listResourceNames("mipmap"));
set.addAll(listResourceNames("layout"));
return set;
}
@Override
public void setInflatingFile(@NonNull File file) {
if (file.getParentFile() == null) {
throw new IllegalArgumentException("Invalid inflating file");
}
setupDirectories(
file // layout file
.getParentFile() // layout dir
.getParentFile()); // res dir
}
private void setupDirectories(File resDir) {
if (resDir == null) {
LOG.error("Null resource directory passed to resource finder. Ignoring.");
return;
}
this.resDir = resDir;
this.color = new File(resDir, "color");
this.layout = new File(resDir, "layout");
final File[] drawables = resDir.listFiles(new NameStartsWith("drawable"));
this.drawables = drawables == null ? new File[0] : drawables;
final File[] mipmaps = resDir.listFiles(new NameStartsWith("mipmap"));
this.mipmaps = mipmaps == null ? new File[0] : mipmaps;
}
private File findFileWithName(File[] files, String name) {
if (files == null || name == null) {
return null;
}
for (File file : files) {
var simpleName = file.getName();
if (simpleName.startsWith(name)) {
return file;
}
}
return null;
}
private static class NameStartsWith implements FileFilter {
private final String name;
public NameStartsWith(String name) {
this.name = name;
}
@Override
public boolean accept(@NonNull File file) {
return file.getName().startsWith(name);
}
}
}
@@ -1,71 +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.services
import com.itsaky.androidide.tooling.api.messages.result.BuildCancellationRequestResult
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import java.util.concurrent.*
/**
* A build service provides API to initialize project, execute builds, query a build, cancel running
* builds, etc.
*
* @author Akash Yadav
*/
interface BuildService {
/**
* Initialize the project.
*
* @param rootDir The root directory of the project to initialize.
* @return A [CompletableFuture] which returns an [InitializeResult] when the project
* initialization process finishes.
*/
fun initializeProject(rootDir: String): CompletableFuture<InitializeResult>
/**
* Execute the given tasks.
*
* @param tasks The tasks to execute. If the fully qualified path of the task is not specified,
* then it will be executed in the root project directory.
* @return A [CompletableFuture] which returns a list of [TaskExecutionResult]. The result
* contains a list of tasks that were executed and the result of the whole execution.
* @see BuildService.executeProjectTasks
*/
fun executeTasks(vararg tasks: String): CompletableFuture<TaskExecutionResult>
/**
* Execute the given tasks of the given project.
*
* @param projectPath The path of the project. All the tasks will be executed in this project.
* @param tasks The tasks to execute. These may or may not be fully qualified names of the
* project. If the task name is not fully qualified, then it will be executed in the given
* project path. For example, if the project path is ':app' and the task is 'assembleDebug',
* then, ':app:assembleDebug' task will be executed.
* @return A [CompletableFuture] which returns a list of [TaskExecutionResult]. The result
* contains a list of tasks that were executed and the result of the whole execution.
*/
fun executeProjectTasks(
projectPath: String,
vararg tasks: String
): CompletableFuture<TaskExecutionResult>
/** Cancel any running build. */
fun cancelCurrentBuild(): CompletableFuture<BuildCancellationRequestResult>
}
/*
* 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.services
import com.itsaky.androidide.tooling.api.messages.result.BuildCancellationRequestResult
import com.itsaky.androidide.tooling.api.messages.result.InitializeResult
import com.itsaky.androidide.tooling.api.messages.result.TaskExecutionResult
import java.util.concurrent.*
/**
* A build service provides API to initialize project, execute builds, query a build, cancel running
* builds, etc.
*
* @author Akash Yadav
*/
interface BuildService {
/**
* Initialize the project.
*
* @param rootDir The root directory of the project to initialize.
* @return A [CompletableFuture] which returns an [InitializeResult] when the project
* initialization process finishes.
*/
fun initializeProject(rootDir: String): CompletableFuture<InitializeResult>
/**
* Execute the given tasks.
*
* @param tasks The tasks to execute. If the fully qualified path of the task is not specified,
* then it will be executed in the root project directory.
* @return A [CompletableFuture] which returns a list of [TaskExecutionResult]. The result
* contains a list of tasks that were executed and the result of the whole execution.
* @see BuildService.executeProjectTasks
*/
fun executeTasks(vararg tasks: String): CompletableFuture<TaskExecutionResult>
/**
* Execute the given tasks of the given project.
*
* @param projectPath The path of the project. All the tasks will be executed in this project.
* @param tasks The tasks to execute. These may or may not be fully qualified names of the
* project. If the task name is not fully qualified, then it will be executed in the given
* project path. For example, if the project path is ':app' and the task is 'assembleDebug',
* then, ':app:assembleDebug' task will be executed.
* @return A [CompletableFuture] which returns a list of [TaskExecutionResult]. The result
* contains a list of tasks that were executed and the result of the whole execution.
*/
fun executeProjectTasks(
projectPath: String,
vararg tasks: String
): CompletableFuture<TaskExecutionResult>
/** Cancel any running build. */
fun cancelCurrentBuild(): CompletableFuture<BuildCancellationRequestResult>
}
File diff suppressed because it is too large Load Diff
@@ -1,88 +1,88 @@
/*
* 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.ui.edit;
import androidx.annotation.NonNull;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import com.itsaky.lsp.util.StringUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Handles editing dimension attributes.
*
* @author Akash Yadav
*/
public class DimensionSuggestionProvider extends BaseSuggestionProvider {
/** The dimension units that will be suggested. */
private final List<String> dimensions = new ArrayList<>();
public DimensionSuggestionProvider(File layout) {
super(layout);
this.dimensions.add("dp");
this.dimensions.add("sp");
this.dimensions.add("px");
this.dimensions.add("pt");
this.dimensions.add("in");
this.dimensions.add("mm");
}
@Override
public boolean checkFormat(int format) {
return (format & Attr.DIMENSION) != 0;
}
@Override
public @NonNull List<String> suggest(IAttribute attribute, @NonNull String prefix) {
final var list = new ArrayList<String>();
if (prefix.length() > 0 && Character.isDigit(prefix.charAt(0))) {
// Fixed dimension values
// e.g. 10dp
var val = extractDimensionVal(prefix);
for (var unit : this.dimensions) {
final var dimension = val + unit;
if (StringUtils.matchesFuzzy(dimension, prefix, true)) {
list.add(dimension);
}
}
}
return list;
}
private int extractDimensionVal(@NonNull String prefix) {
StringBuilder s = new StringBuilder();
var i = 0;
while (i < prefix.length()) {
if (Character.isDigit(prefix.charAt(i))) {
s.append(prefix.charAt(i));
} else {
break;
}
i++;
}
return Integer.parseInt(s.toString());
}
}
/*
* 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.ui.edit;
import androidx.annotation.NonNull;
import com.itsaky.attrinfo.models.Attr;
import com.itsaky.inflater.IAttribute;
import com.itsaky.lsp.util.StringUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
/**
* Handles editing dimension attributes.
*
* @author Akash Yadav
*/
public class DimensionSuggestionProvider extends BaseSuggestionProvider {
/** The dimension units that will be suggested. */
private final List<String> dimensions = new ArrayList<>();
public DimensionSuggestionProvider(File layout) {
super(layout);
this.dimensions.add("dp");
this.dimensions.add("sp");
this.dimensions.add("px");
this.dimensions.add("pt");
this.dimensions.add("in");
this.dimensions.add("mm");
}
@Override
public boolean checkFormat(int format) {
return (format & Attr.DIMENSION) != 0;
}
@Override
public @NonNull List<String> suggest(IAttribute attribute, @NonNull String prefix) {
final var list = new ArrayList<String>();
if (prefix.length() > 0 && Character.isDigit(prefix.charAt(0))) {
// Fixed dimension values
// e.g. 10dp
var val = extractDimensionVal(prefix);
for (var unit : this.dimensions) {
final var dimension = val + unit;
if (StringUtils.matchesFuzzy(dimension, prefix, true)) {
list.add(dimension);
}
}
}
return list;
}
private int extractDimensionVal(@NonNull String prefix) {
StringBuilder s = new StringBuilder();
var i = 0;
while (i < prefix.length()) {
if (Character.isDigit(prefix.charAt(i))) {
s.append(prefix.charAt(i));
} else {
break;
}
i++;
}
return Integer.parseInt(s.toString());
}
}
@@ -1,84 +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.utils
import android.content.Context
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.build.CancelBuildAction
import com.itsaky.androidide.actions.build.ProjectSyncAction
import com.itsaky.androidide.actions.build.QuickRunAction
import com.itsaky.androidide.actions.build.RunActionMenu
import com.itsaky.androidide.actions.editor.CopyAction
import com.itsaky.androidide.actions.editor.CutAction
import com.itsaky.androidide.actions.editor.ExpandSelectionAction
import com.itsaky.androidide.actions.editor.PasteAction
import com.itsaky.androidide.actions.editor.SelectAllAction
import com.itsaky.androidide.actions.editor.find.GoToNextAction
import com.itsaky.androidide.actions.editor.find.GoToPreviousAction
import com.itsaky.androidide.actions.editor.find.ReplaceAction
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.etc.PreviewLayoutAction
import com.itsaky.androidide.actions.file.FormatCodeAction
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(PreviewLayoutAction(context))
registry.registerAction(FindActionMenu(context))
registry.registerAction(FileTreeAction(context))
registry.registerAction(DaemonStatusAction(context))
registry.registerAction(CancelBuildAction(context))
registry.registerAction(ProjectSyncAction(context))
// editor text actions
registry.registerAction(ExpandSelectionAction(context))
registry.registerAction(SelectAllAction(context))
registry.registerAction(CutAction(context))
registry.registerAction(CopyAction(context))
registry.registerAction(PasteAction(context))
registry.registerAction(FormatCodeAction(context))
// editor search actions
registry.registerAction(GoToNextAction(context))
registry.registerAction(GoToPreviousAction(context))
registry.registerAction(ReplaceAction(context))
}
@JvmStatic
fun clear() = ActionsRegistry.getInstance().clearActions(ActionItem.Location.EDITOR_TOOLBAR)
}
}
/*
* 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.utils
import android.content.Context
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.build.CancelBuildAction
import com.itsaky.androidide.actions.build.ProjectSyncAction
import com.itsaky.androidide.actions.build.QuickRunAction
import com.itsaky.androidide.actions.build.RunActionMenu
import com.itsaky.androidide.actions.editor.CopyAction
import com.itsaky.androidide.actions.editor.CutAction
import com.itsaky.androidide.actions.editor.ExpandSelectionAction
import com.itsaky.androidide.actions.editor.PasteAction
import com.itsaky.androidide.actions.editor.SelectAllAction
import com.itsaky.androidide.actions.editor.find.GoToNextAction
import com.itsaky.androidide.actions.editor.find.GoToPreviousAction
import com.itsaky.androidide.actions.editor.find.ReplaceAction
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.etc.PreviewLayoutAction
import com.itsaky.androidide.actions.file.FormatCodeAction
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(PreviewLayoutAction(context))
registry.registerAction(FindActionMenu(context))
registry.registerAction(FileTreeAction(context))
registry.registerAction(DaemonStatusAction(context))
registry.registerAction(CancelBuildAction(context))
registry.registerAction(ProjectSyncAction(context))
// editor text actions
registry.registerAction(ExpandSelectionAction(context))
registry.registerAction(SelectAllAction(context))
registry.registerAction(CutAction(context))
registry.registerAction(CopyAction(context))
registry.registerAction(PasteAction(context))
registry.registerAction(FormatCodeAction(context))
// editor search actions
registry.registerAction(GoToNextAction(context))
registry.registerAction(GoToPreviousAction(context))
registry.registerAction(ReplaceAction(context))
}
@JvmStatic
fun clear() = ActionsRegistry.getInstance().clearActions(ActionItem.Location.EDITOR_TOOLBAR)
}
}
@@ -1,34 +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.utils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.itsaky.androidide.tooling.api.util.ToolingApiLauncher;
public class JSONUtility {
public static final Gson gson = new Gson();
public static final Gson toolingGson;
public static final Gson prettyPrinter = new GsonBuilder().setPrettyPrinting().create();
static {
final var builder = new GsonBuilder();
ToolingApiLauncher.configureGson(builder);
toolingGson = builder.create();
}
}
/*
* 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.utils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.itsaky.androidide.tooling.api.util.ToolingApiLauncher;
public class JSONUtility {
public static final Gson gson = new Gson();
public static final Gson toolingGson;
public static final Gson prettyPrinter = new GsonBuilder().setPrettyPrinting().create();
static {
final var builder = new GsonBuilder();
ToolingApiLauncher.configureGson(builder);
toolingGson = builder.create();
}
}
@@ -1,43 +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.utils
import java.io.File
import java.io.StringReader
import java.util.*
class ListingFileRedirect {
companion object {
/** Redirect file will have this marker as the first line as comment. */
private const val REDIRECT_MARKER = "#- File Locator -"
/** Property name in a [Properties] for the metadata file location. */
private const val REDIRECT_PROPERTY_NAME = "listingFile"
@JvmStatic
fun maybeExtractRedirectedFile(redirectFile: File): File? {
val fileContent = redirectFile.readText()
return if (fileContent.startsWith(REDIRECT_MARKER)) {
val fileLocator = Properties().also { it.load(StringReader(fileContent)) }
val file = File(fileLocator.getProperty(REDIRECT_PROPERTY_NAME))
if (!file.isAbsolute) redirectFile.parentFile!!.resolve(file) else file
} else null
}
}
}
/*
* 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.utils
import java.io.File
import java.io.StringReader
import java.util.*
class ListingFileRedirect {
companion object {
/** Redirect file will have this marker as the first line as comment. */
private const val REDIRECT_MARKER = "#- File Locator -"
/** Property name in a [Properties] for the metadata file location. */
private const val REDIRECT_PROPERTY_NAME = "listingFile"
@JvmStatic
fun maybeExtractRedirectedFile(redirectFile: File): File? {
val fileContent = redirectFile.readText()
return if (fileContent.startsWith(REDIRECT_MARKER)) {
val fileLocator = Properties().also { it.load(StringReader(fileContent)) }
val file = File(fileLocator.getProperty(REDIRECT_PROPERTY_NAME))
if (!file.isAbsolute) redirectFile.parentFile!!.resolve(file) else file
} else null
}
}
}
@@ -1,20 +1,3 @@
/*
* 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/>.
*/
/*
* This file is part of AndroidIDE.
*
@@ -473,4 +456,4 @@ public class CodeEditorView extends FrameLayout {
isModified = false;
notifySaved();
}
}
}
@@ -1,391 +1,391 @@
/*
* 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.views.editor
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.view.menu.SubMenuBuilder
import androidx.core.content.ContextCompat
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.textview.MaterialTextView
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.ActionsRegistry.Companion.getInstance
import com.itsaky.androidide.actions.editor.SelectAllAction
import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.language.IDELanguage
import com.itsaky.androidide.utils.ILogger
import com.itsaky.lsp.java.JavaLanguageServer
import com.itsaky.lsp.models.DiagnosticItem
import com.itsaky.lsp.models.Range
import com.itsaky.lsp.util.DiagnosticUtil
import com.itsaky.lsp.xml.XMLLanguageServer
import io.github.rosemoe.sora.event.HandleStateChangeEvent
import io.github.rosemoe.sora.event.ScrollEvent
import io.github.rosemoe.sora.event.SelectionChangeEvent
import io.github.rosemoe.sora.event.SubscriptionReceipt
import io.github.rosemoe.sora.widget.CodeEditor
import io.github.rosemoe.sora.widget.EditorTouchEventHandler
import io.github.rosemoe.sora.widget.base.EditorPopupWindow
import java.io.File
import kotlin.math.max
import kotlin.math.min
/**
* PopupMenu for showing editor's text and code actions.
*
* @author Akash Yadav
*/
@SuppressLint("RestrictedApi")
open class EditorActionsMenu(val editor: IDEEditor) :
EditorPopupWindow(editor, FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED),
ActionsRegistry.ActionExecListener,
MenuBuilder.Callback {
companion object {
const val DELAY: Long = 200
}
private val touchHandler: EditorTouchEventHandler = editor.eventHandler
private val receipts: MutableList<SubscriptionReceipt<*>> = mutableListOf()
private val log = ILogger.newInstance(javaClass.simpleName)
private val list: ListView = ListView(editor.context)
private var mLastScroll: Long = 0
private var mLastPosition: Int = 0
private val menu: MenuBuilder = MenuBuilder(editor.context)
protected open var location = ActionItem.Location.EDITOR_TEXT_ACTIONS
open fun init() {
subscribe()
applyBackground()
list.clipChildren = true
list.clipToOutline = true
list.isVerticalFadingEdgeEnabled = true
list.isVerticalScrollBarEnabled = true
list.setFadingEdgeLength(SizeUtils.dp2px(42f))
list.divider = null
list.layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
popup.contentView = this.list
popup.animationStyle = R.style.PopupAnimation
val menu = getMenu()
if (menu is MenuBuilder) {
menu.setCallback(this)
}
}
open fun subscribe() {
receipts.add(
editor.subscribeEvent(SelectionChangeEvent::class.java) { event, _ ->
this.onSelectionChanged(event)
})
receipts.add(
editor.subscribeEvent(ScrollEvent::class.java) { _, _ -> this.onScrollEvent() })
receipts.add(
editor.subscribeEvent(HandleStateChangeEvent::class.java) { event, _ ->
this.onHandleStateChanged(event)
})
}
open fun unsubscribeEvents() {
for (receipt in receipts) {
receipt.unsubscribe()
}
}
protected open fun onSelectionChanged(event: SelectionChangeEvent) {
if (touchHandler.hasAnyHeldHandle()) {
return
}
if (event.isSelected) {
if (!isShowing) {
editor.post(::displayWindow)
}
mLastPosition = -1
} else {
var show = false
if (event.cause == SelectionChangeEvent.CAUSE_TAP &&
event.left.index == mLastPosition &&
!isShowing &&
!editor.text.isInBatchEdit) {
editor.post(::displayWindow)
show = true
} else {
dismiss()
}
mLastPosition =
if (event.cause == SelectionChangeEvent.CAUSE_TAP && !show) {
event.left.index
} else {
-1
}
}
}
protected open fun onScrollEvent() {
val last = mLastScroll
mLastScroll = System.currentTimeMillis()
if (mLastScroll - last < DELAY) {
postDisplay()
}
}
protected open fun onHandleStateChanged(
event: HandleStateChangeEvent,
) {
if (event.isHeld) {
postDisplay()
}
}
protected open fun applyBackground() {
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.RECTANGLE
drawable.cornerRadius = 16f
drawable.color =
ColorStateList.valueOf(
ContextCompat.getColor(editor.context, R.color.content_background))
drawable.setStroke(
SizeUtils.dp2px(1f), ContextCompat.getColor(editor.context, R.color.primaryLightColor))
list.background = drawable
}
private fun postDisplay() {
if (!isShowing) {
return
}
dismiss()
if (!editor.cursor.isSelected) {
return
}
editor.postDelayed(
object : Runnable {
override fun run() {
if (!touchHandler.hasAnyHeldHandle() &&
System.currentTimeMillis() - mLastScroll > DELAY &&
touchHandler.scroller.isFinished) {
displayWindow()
} else {
editor.postDelayed(this, DELAY)
}
}
},
DELAY)
}
private fun selectTop(rect: RectF): Int {
val rowHeight = editor.rowHeight
return if (rect.top - rowHeight * 3 / 2f > height) {
(rect.top - rowHeight * 3 / 2 - height).toInt()
} else {
(rect.bottom + rowHeight / 2).toInt()
}
}
open fun displayWindow() {
var top: Int
val cursor = editor.cursor
top =
if (cursor.isSelected) {
val leftRect = editor.leftHandleDescriptor.position
val rightRect = editor.rightHandleDescriptor.position
val top1 = selectTop(leftRect)
val top2 = selectTop(rightRect)
min(top1, top2)
} else {
selectTop(editor.insertHandleDescriptor.position)
}
top = max(0, min(top, editor.height - height - 5))
val handleLeftX = editor.getOffset(editor.cursor.leftLine, editor.cursor.leftColumn)
val handleRightX = editor.getOffset(editor.cursor.rightLine, editor.cursor.rightColumn)
val panelX = ((handleLeftX + handleRightX) / 2f).toInt()
setLocationAbsolutely(panelX, top)
show()
}
protected open fun fillMenu() {
getMenu().clear()
val data = onCreateActionData()
val registry = getInstance()
registry.registerActionExecListener(this)
onFillMenu(registry, data)
this.list.adapter = ActionsListAdapter(getMenu(), editor.context)
}
protected open fun onFillMenu(registry: ActionsRegistry, data: ActionData) {
registry.fillMenu(data, onGetActionLocation(), getMenu())
}
protected open fun onGetActionLocation() = location
protected open fun onCreateActionData(): ActionData {
val data = ActionData()
data.put(Context::class.java, editor.context)
data.put(IDEEditor::class.java, this.editor)
data.put(
CodeEditor::class.java,
editor) // For LSP actions, as they cannot access IDEEditor class
data.put(File::class.java, editor.file)
data.put(DiagnosticItem::class.java, getDiagnosticAtCursor())
data.put(Range::class.java, editor.cursorRange)
data.put(
JavaLanguageServer::class.java,
StudioApp.getInstance().javaLanguageServer as JavaLanguageServer)
data.put(
XMLLanguageServer::class.java,
StudioApp.getInstance().xmlLanguageServer as XMLLanguageServer)
return data
}
protected open fun getMenu(): Menu = menu
private fun getDiagnosticAtCursor(): DiagnosticItem? {
val language = editor.editorLanguage as? IDELanguage ?: return null
val diagnostics = language.diagnostics
return DiagnosticUtil.binarySearchDiagnostic(diagnostics, editor.cursorRange.start)
}
override fun onExec(action: ActionItem, result: Any) {
if (action !is SelectAllAction) {
dismiss()
}
}
override fun show() {
if (list.parent != null) {
(list.parent as ViewGroup).removeView(list)
}
fillMenu()
val dp8 = SizeUtils.dp2px(8f)
val dp16 = dp8 * 2
this.list.measure(
View.MeasureSpec.makeMeasureSpec(editor.width - dp16 * 2, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(
(260 * editor.dpUnit).toInt() - dp16 * 2, View.MeasureSpec.AT_MOST))
setSize(findWidestItem(), this.list.measuredHeight)
super.show()
}
private fun findWidestItem(): Int {
var widest = 0
val text =
LayoutInflater.from(editor.context).inflate(R.layout.layout_popup_menu_item, null) as
MaterialTextView
val dp30 = SizeUtils.dp2px(30f)
val paddingHorizontal = text.paddingStart + text.paddingEnd
val drawablePadding = text.compoundDrawablePadding
val extraWidth = dp30 * 2 // 30dp for start and end drawables both
for (i in 0 until getMenu().size()) {
val item = getMenu().getItem(i)
val title = item.title.toString()
var width = paddingHorizontal + (drawablePadding * 2)
width += text.paint.measureText(title).toInt()
width += extraWidth
if (width > widest) {
widest = width
}
}
return widest
}
private class ActionsListAdapter(val menu: Menu, val context: Context) : BaseAdapter() {
override fun getCount(): Int = menu.size()
override fun getItem(position: Int): MenuItem = menu.getItem(position)
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: MaterialTextView =
if (convertView == null || convertView !is MaterialButton) {
(context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.layout_popup_menu_item, parent, false) as
MaterialTextView
} else {
convertView as MaterialTextView
}
val item = getItem(position)
view.text = item.title
val start = item.icon ?: null
var end: Drawable? = null
if (item.hasSubMenu()) {
end = ContextCompat.getDrawable(context, R.drawable.ic_submenu_arrow)
}
view.setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null)
view.setOnClickListener { (item as MenuItemImpl).invoke() }
return view
}
}
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
// Click event of MenuItems without SubMenu is consumed by the ActionsRegistry
// So we only need to handle click event of SubMenus
if (!item.hasSubMenu()) {
return false
}
if (item.hasSubMenu() && item.subMenu is SubMenuBuilder) {
(item.subMenu as SubMenuBuilder).setCallback(this)
}
this.editor.post {
TransitionManager.beginDelayedTransition(this.list, ChangeBounds())
this.list.adapter = ActionsListAdapter(item.subMenu, editor.context)
}
return true
}
override fun onMenuModeChange(menu: MenuBuilder) {}
}
/*
* 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.views.editor
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.RectF
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ListView
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.view.menu.MenuItemImpl
import androidx.appcompat.view.menu.SubMenuBuilder
import androidx.core.content.ContextCompat
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.button.MaterialButton
import com.google.android.material.textview.MaterialTextView
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.ActionsRegistry.Companion.getInstance
import com.itsaky.androidide.actions.editor.SelectAllAction
import com.itsaky.androidide.app.StudioApp
import com.itsaky.androidide.language.IDELanguage
import com.itsaky.androidide.utils.ILogger
import com.itsaky.lsp.java.JavaLanguageServer
import com.itsaky.lsp.models.DiagnosticItem
import com.itsaky.lsp.models.Range
import com.itsaky.lsp.util.DiagnosticUtil
import com.itsaky.lsp.xml.XMLLanguageServer
import io.github.rosemoe.sora.event.HandleStateChangeEvent
import io.github.rosemoe.sora.event.ScrollEvent
import io.github.rosemoe.sora.event.SelectionChangeEvent
import io.github.rosemoe.sora.event.SubscriptionReceipt
import io.github.rosemoe.sora.widget.CodeEditor
import io.github.rosemoe.sora.widget.EditorTouchEventHandler
import io.github.rosemoe.sora.widget.base.EditorPopupWindow
import java.io.File
import kotlin.math.max
import kotlin.math.min
/**
* PopupMenu for showing editor's text and code actions.
*
* @author Akash Yadav
*/
@SuppressLint("RestrictedApi")
open class EditorActionsMenu(val editor: IDEEditor) :
EditorPopupWindow(editor, FEATURE_SHOW_OUTSIDE_VIEW_ALLOWED),
ActionsRegistry.ActionExecListener,
MenuBuilder.Callback {
companion object {
const val DELAY: Long = 200
}
private val touchHandler: EditorTouchEventHandler = editor.eventHandler
private val receipts: MutableList<SubscriptionReceipt<*>> = mutableListOf()
private val log = ILogger.newInstance(javaClass.simpleName)
private val list: ListView = ListView(editor.context)
private var mLastScroll: Long = 0
private var mLastPosition: Int = 0
private val menu: MenuBuilder = MenuBuilder(editor.context)
protected open var location = ActionItem.Location.EDITOR_TEXT_ACTIONS
open fun init() {
subscribe()
applyBackground()
list.clipChildren = true
list.clipToOutline = true
list.isVerticalFadingEdgeEnabled = true
list.isVerticalScrollBarEnabled = true
list.setFadingEdgeLength(SizeUtils.dp2px(42f))
list.divider = null
list.layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)
popup.contentView = this.list
popup.animationStyle = R.style.PopupAnimation
val menu = getMenu()
if (menu is MenuBuilder) {
menu.setCallback(this)
}
}
open fun subscribe() {
receipts.add(
editor.subscribeEvent(SelectionChangeEvent::class.java) { event, _ ->
this.onSelectionChanged(event)
})
receipts.add(
editor.subscribeEvent(ScrollEvent::class.java) { _, _ -> this.onScrollEvent() })
receipts.add(
editor.subscribeEvent(HandleStateChangeEvent::class.java) { event, _ ->
this.onHandleStateChanged(event)
})
}
open fun unsubscribeEvents() {
for (receipt in receipts) {
receipt.unsubscribe()
}
}
protected open fun onSelectionChanged(event: SelectionChangeEvent) {
if (touchHandler.hasAnyHeldHandle()) {
return
}
if (event.isSelected) {
if (!isShowing) {
editor.post(::displayWindow)
}
mLastPosition = -1
} else {
var show = false
if (event.cause == SelectionChangeEvent.CAUSE_TAP &&
event.left.index == mLastPosition &&
!isShowing &&
!editor.text.isInBatchEdit) {
editor.post(::displayWindow)
show = true
} else {
dismiss()
}
mLastPosition =
if (event.cause == SelectionChangeEvent.CAUSE_TAP && !show) {
event.left.index
} else {
-1
}
}
}
protected open fun onScrollEvent() {
val last = mLastScroll
mLastScroll = System.currentTimeMillis()
if (mLastScroll - last < DELAY) {
postDisplay()
}
}
protected open fun onHandleStateChanged(
event: HandleStateChangeEvent,
) {
if (event.isHeld) {
postDisplay()
}
}
protected open fun applyBackground() {
val drawable = GradientDrawable()
drawable.shape = GradientDrawable.RECTANGLE
drawable.cornerRadius = 16f
drawable.color =
ColorStateList.valueOf(
ContextCompat.getColor(editor.context, R.color.content_background))
drawable.setStroke(
SizeUtils.dp2px(1f), ContextCompat.getColor(editor.context, R.color.primaryLightColor))
list.background = drawable
}
private fun postDisplay() {
if (!isShowing) {
return
}
dismiss()
if (!editor.cursor.isSelected) {
return
}
editor.postDelayed(
object : Runnable {
override fun run() {
if (!touchHandler.hasAnyHeldHandle() &&
System.currentTimeMillis() - mLastScroll > DELAY &&
touchHandler.scroller.isFinished) {
displayWindow()
} else {
editor.postDelayed(this, DELAY)
}
}
},
DELAY)
}
private fun selectTop(rect: RectF): Int {
val rowHeight = editor.rowHeight
return if (rect.top - rowHeight * 3 / 2f > height) {
(rect.top - rowHeight * 3 / 2 - height).toInt()
} else {
(rect.bottom + rowHeight / 2).toInt()
}
}
open fun displayWindow() {
var top: Int
val cursor = editor.cursor
top =
if (cursor.isSelected) {
val leftRect = editor.leftHandleDescriptor.position
val rightRect = editor.rightHandleDescriptor.position
val top1 = selectTop(leftRect)
val top2 = selectTop(rightRect)
min(top1, top2)
} else {
selectTop(editor.insertHandleDescriptor.position)
}
top = max(0, min(top, editor.height - height - 5))
val handleLeftX = editor.getOffset(editor.cursor.leftLine, editor.cursor.leftColumn)
val handleRightX = editor.getOffset(editor.cursor.rightLine, editor.cursor.rightColumn)
val panelX = ((handleLeftX + handleRightX) / 2f).toInt()
setLocationAbsolutely(panelX, top)
show()
}
protected open fun fillMenu() {
getMenu().clear()
val data = onCreateActionData()
val registry = getInstance()
registry.registerActionExecListener(this)
onFillMenu(registry, data)
this.list.adapter = ActionsListAdapter(getMenu(), editor.context)
}
protected open fun onFillMenu(registry: ActionsRegistry, data: ActionData) {
registry.fillMenu(data, onGetActionLocation(), getMenu())
}
protected open fun onGetActionLocation() = location
protected open fun onCreateActionData(): ActionData {
val data = ActionData()
data.put(Context::class.java, editor.context)
data.put(IDEEditor::class.java, this.editor)
data.put(
CodeEditor::class.java,
editor) // For LSP actions, as they cannot access IDEEditor class
data.put(File::class.java, editor.file)
data.put(DiagnosticItem::class.java, getDiagnosticAtCursor())
data.put(Range::class.java, editor.cursorRange)
data.put(
JavaLanguageServer::class.java,
StudioApp.getInstance().javaLanguageServer as JavaLanguageServer)
data.put(
XMLLanguageServer::class.java,
StudioApp.getInstance().xmlLanguageServer as XMLLanguageServer)
return data
}
protected open fun getMenu(): Menu = menu
private fun getDiagnosticAtCursor(): DiagnosticItem? {
val language = editor.editorLanguage as? IDELanguage ?: return null
val diagnostics = language.diagnostics
return DiagnosticUtil.binarySearchDiagnostic(diagnostics, editor.cursorRange.start)
}
override fun onExec(action: ActionItem, result: Any) {
if (action !is SelectAllAction) {
dismiss()
}
}
override fun show() {
if (list.parent != null) {
(list.parent as ViewGroup).removeView(list)
}
fillMenu()
val dp8 = SizeUtils.dp2px(8f)
val dp16 = dp8 * 2
this.list.measure(
View.MeasureSpec.makeMeasureSpec(editor.width - dp16 * 2, View.MeasureSpec.AT_MOST),
View.MeasureSpec.makeMeasureSpec(
(260 * editor.dpUnit).toInt() - dp16 * 2, View.MeasureSpec.AT_MOST))
setSize(findWidestItem(), this.list.measuredHeight)
super.show()
}
private fun findWidestItem(): Int {
var widest = 0
val text =
LayoutInflater.from(editor.context).inflate(R.layout.layout_popup_menu_item, null) as
MaterialTextView
val dp30 = SizeUtils.dp2px(30f)
val paddingHorizontal = text.paddingStart + text.paddingEnd
val drawablePadding = text.compoundDrawablePadding
val extraWidth = dp30 * 2 // 30dp for start and end drawables both
for (i in 0 until getMenu().size()) {
val item = getMenu().getItem(i)
val title = item.title.toString()
var width = paddingHorizontal + (drawablePadding * 2)
width += text.paint.measureText(title).toInt()
width += extraWidth
if (width > widest) {
widest = width
}
}
return widest
}
private class ActionsListAdapter(val menu: Menu, val context: Context) : BaseAdapter() {
override fun getCount(): Int = menu.size()
override fun getItem(position: Int): MenuItem = menu.getItem(position)
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
val view: MaterialTextView =
if (convertView == null || convertView !is MaterialButton) {
(context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater)
.inflate(R.layout.layout_popup_menu_item, parent, false) as
MaterialTextView
} else {
convertView as MaterialTextView
}
val item = getItem(position)
view.text = item.title
val start = item.icon ?: null
var end: Drawable? = null
if (item.hasSubMenu()) {
end = ContextCompat.getDrawable(context, R.drawable.ic_submenu_arrow)
}
view.setCompoundDrawablesRelativeWithIntrinsicBounds(start, null, end, null)
view.setOnClickListener { (item as MenuItemImpl).invoke() }
return view
}
}
override fun onMenuItemSelected(menu: MenuBuilder, item: MenuItem): Boolean {
// Click event of MenuItems without SubMenu is consumed by the ActionsRegistry
// So we only need to handle click event of SubMenus
if (!item.hasSubMenu()) {
return false
}
if (item.hasSubMenu() && item.subMenu is SubMenuBuilder) {
(item.subMenu as SubMenuBuilder).setCallback(this)
}
this.editor.post {
TransitionManager.beginDelayedTransition(this.list, ChangeBounds())
this.list.adapter = ActionsListAdapter(item.subMenu, editor.context)
}
return true
}
override fun onMenuModeChange(menu: MenuBuilder) {}
}
File diff suppressed because it is too large Load Diff
@@ -1,107 +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.views.editor
import android.annotation.SuppressLint
import android.content.Context
import android.text.TextUtils
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.SearchView
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import io.github.rosemoe.sora.R
import io.github.rosemoe.sora.widget.CodeEditor
import io.github.rosemoe.sora.widget.EditorSearcher.SearchOptions
/**
* Action mode for find and replace functions in editor.
* @author Akash Yadav
*/
class SearchActionMode(val editor: IDEEditor) : ActionMode.Callback {
@SuppressLint("RestrictedApi")
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
if (mode == null) {
return false
}
if (menu is MenuBuilder) {
menu.setOptionalIconsVisible(true)
}
val search = SearchView(editor.context)
search.setOnQueryTextListener(OnQueryListener(editor))
search.isIconified = false
search.queryHint = editor.context.getString(R.string.text_to_search)
search.performClick()
search.setIconifiedByDefault(false)
mode.customView = search
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
if (mode == null || menu == null) {
return false
}
menu.clear()
val registry = ActionsRegistry.getInstance()
registry.fillMenu(createData(mode), ActionItem.Location.EDITOR_SEARCH_ACTION_MODE, menu)
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
editor.searcher.stopSearch()
}
private fun createData(mode: ActionMode): ActionData {
return ActionData().apply {
put(Context::class.java, editor.context)
put(IDEEditor::class.java, editor)
put(CodeEditor::class.java, editor)
put(ActionMode::class.java, mode)
}
}
private class OnQueryListener(val editor: IDEEditor) : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
editor.searcher.gotoNext()
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
if (TextUtils.isEmpty(newText)) {
editor.searcher.stopSearch()
return false
}
editor.searcher.search(newText!!, SearchOptions(false, false))
return false
}
}
}
/*
* 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.views.editor
import android.annotation.SuppressLint
import android.content.Context
import android.text.TextUtils
import android.view.ActionMode
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.view.menu.MenuBuilder
import androidx.appcompat.widget.SearchView
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.ActionsRegistry
import io.github.rosemoe.sora.R
import io.github.rosemoe.sora.widget.CodeEditor
import io.github.rosemoe.sora.widget.EditorSearcher.SearchOptions
/**
* Action mode for find and replace functions in editor.
* @author Akash Yadav
*/
class SearchActionMode(val editor: IDEEditor) : ActionMode.Callback {
@SuppressLint("RestrictedApi")
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
if (mode == null) {
return false
}
if (menu is MenuBuilder) {
menu.setOptionalIconsVisible(true)
}
val search = SearchView(editor.context)
search.setOnQueryTextListener(OnQueryListener(editor))
search.isIconified = false
search.queryHint = editor.context.getString(R.string.text_to_search)
search.performClick()
search.setIconifiedByDefault(false)
mode.customView = search
return true
}
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
if (mode == null || menu == null) {
return false
}
menu.clear()
val registry = ActionsRegistry.getInstance()
registry.fillMenu(createData(mode), ActionItem.Location.EDITOR_SEARCH_ACTION_MODE, menu)
return true
}
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
editor.searcher.stopSearch()
}
private fun createData(mode: ActionMode): ActionData {
return ActionData().apply {
put(Context::class.java, editor.context)
put(IDEEditor::class.java, editor)
put(CodeEditor::class.java, editor)
put(ActionMode::class.java, mode)
}
}
private class OnQueryListener(val editor: IDEEditor) : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String?): Boolean {
editor.searcher.gotoNext()
return false
}
override fun onQueryTextChange(newText: String?): Boolean {
if (TextUtils.isEmpty(newText)) {
editor.searcher.stopSearch()
return false
}
editor.searcher.search(newText!!, SearchOptions(false, false))
return false
}
}
}
@@ -1,117 +1,117 @@
/*
* 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/>.
*/
@file:Suppress("DEPRECATION")
package io.github.rosemoe.sora.widget
import android.app.ProgressDialog
import android.widget.Toast
import com.itsaky.androidide.R.string
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.views.editor.IDEEditor
import io.github.rosemoe.sora.text.TextUtils
import io.github.rosemoe.sora.util.IntPair
import java.util.concurrent.*
/**
* Search text in editor. As the constructor of [EditorSearcher] is package private, we cannot
* extend it in another package. So we put this class in the same package.
*
* @author Akash Yadav
*/
open class IDEEditorSearcher(editor: IDEEditor) : EditorSearcher(editor) {
private val log = ILogger.newInstance(javaClass.simpleName)
protected fun getEditor(): CodeEditor {
try {
val field = EditorSearcher::class.java.getDeclaredField("mEditor")
field.isAccessible = true
return field.get(this) as CodeEditor
} catch (error: Throwable) {
throw RuntimeException("Unable get instance of editor", error)
}
}
fun replaceAllAsync(replacement: String): CompletableFuture<Void> {
val dialog =
ProgressDialog.show(
getEditor().context,
getEditor().context.getString(string.replaceAll),
getEditor().context.getString(string.msg_replacing),
true,
false)
return CompletableFuture.runAsync { replaceAll(replacement) }.thenAccept {
getEditor().post { dialog.dismiss() }
}
}
override fun replaceAll(replacement: String) {
checkState()
if (!isResultValid) {
Toast.makeText(getEditor().context, "Editor is still preparing", Toast.LENGTH_SHORT)
.show()
return
}
val res = mLastResults
try {
val sb = getEditor().text.toStringBuilder()
val newLength = replacement.length
if (mOptions.useRegex) {
var delta = 0
for (i in 0 until res.size()) {
val region = res[i]
val start = IntPair.getFirst(region)
val end = IntPair.getSecond(region)
val oldLength = end - start
sb.replace(start + delta, end + delta, replacement)
delta += newLength - oldLength
}
} else {
var fromIndex = 0
var foundIndex: Int
while (TextUtils.indexOf(sb, mPattern, mOptions.ignoreCase, fromIndex).also {
foundIndex = it
} != -1) {
sb.replace(foundIndex, foundIndex + mPattern.length, replacement)
fromIndex = foundIndex + newLength
}
}
getEditor().post {
val pos = getEditor().cursor.left()
// stopSearch();
getEditor()
.text
.replace(
0,
0,
getEditor().lineCount - 1,
getEditor().text.getColumnCount(getEditor().lineCount - 1),
sb)
getEditor().setSelectionAround(pos.line, pos.column)
}
} catch (e: Exception) {
log.error("Failed to replace", e)
}
}
private fun checkState() {
check(hasQuery()) { "pattern not set" }
}
}
/*
* 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/>.
*/
@file:Suppress("DEPRECATION")
package io.github.rosemoe.sora.widget
import android.app.ProgressDialog
import android.widget.Toast
import com.itsaky.androidide.R.string
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.views.editor.IDEEditor
import io.github.rosemoe.sora.text.TextUtils
import io.github.rosemoe.sora.util.IntPair
import java.util.concurrent.*
/**
* Search text in editor. As the constructor of [EditorSearcher] is package private, we cannot
* extend it in another package. So we put this class in the same package.
*
* @author Akash Yadav
*/
open class IDEEditorSearcher(editor: IDEEditor) : EditorSearcher(editor) {
private val log = ILogger.newInstance(javaClass.simpleName)
protected fun getEditor(): CodeEditor {
try {
val field = EditorSearcher::class.java.getDeclaredField("mEditor")
field.isAccessible = true
return field.get(this) as CodeEditor
} catch (error: Throwable) {
throw RuntimeException("Unable get instance of editor", error)
}
}
fun replaceAllAsync(replacement: String): CompletableFuture<Void> {
val dialog =
ProgressDialog.show(
getEditor().context,
getEditor().context.getString(string.replaceAll),
getEditor().context.getString(string.msg_replacing),
true,
false)
return CompletableFuture.runAsync { replaceAll(replacement) }.thenAccept {
getEditor().post { dialog.dismiss() }
}
}
override fun replaceAll(replacement: String) {
checkState()
if (!isResultValid) {
Toast.makeText(getEditor().context, "Editor is still preparing", Toast.LENGTH_SHORT)
.show()
return
}
val res = mLastResults
try {
val sb = getEditor().text.toStringBuilder()
val newLength = replacement.length
if (mOptions.useRegex) {
var delta = 0
for (i in 0 until res.size()) {
val region = res[i]
val start = IntPair.getFirst(region)
val end = IntPair.getSecond(region)
val oldLength = end - start
sb.replace(start + delta, end + delta, replacement)
delta += newLength - oldLength
}
} else {
var fromIndex = 0
var foundIndex: Int
while (TextUtils.indexOf(sb, mPattern, mOptions.ignoreCase, fromIndex).also {
foundIndex = it
} != -1) {
sb.replace(foundIndex, foundIndex + mPattern.length, replacement)
fromIndex = foundIndex + newLength
}
}
getEditor().post {
val pos = getEditor().cursor.left()
// stopSearch();
getEditor()
.text
.replace(
0,
0,
getEditor().lineCount - 1,
getEditor().text.getColumnCount(getEditor().lineCount - 1),
sb)
getEditor().setSelectionAround(pos.line, pos.column)
}
} catch (e: Exception) {
log.error("Failed to replace", e)
}
}
private fun checkState() {
check(hasQuery()) { "pattern not set" }
}
}
+9 -9
View File
@@ -1,9 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M13,7h-2v4L7,11v2h4v4h2v-4h4v-2h-4L13,7zM12,2C6.49,2 2,6.49 2,12s4.49,10 10,10h8c1.1,0 2,-0.9 2,-2v-8c0,-5.51 -4.49,-10 -10,-10zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M13,7h-2v4L7,11v2h4v4h2v-4h4v-2h-4L13,7zM12,2C6.49,2 2,6.49 2,12s4.49,10 10,10h8c1.1,0 2,-0.9 2,-2v-8c0,-5.51 -4.49,-10 -10,-10zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/>
</vector>
@@ -1,8 +1,8 @@
<!-- drawable/form_textbox_password.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M17,7H22V17H17V19A1,1 0 0,0 18,20H20V22H17.5C16.95,22 16,21.55 16,21C16,21.55 15.05,22 14.5,22H12V20H14A1,1 0 0,0 15,19V5A1,1 0 0,0 14,4H12V2H14.5C15.05,2 16,2.45 16,3C16,2.45 16.95,2 17.5,2H20V4H18A1,1 0 0,0 17,5V7M2,7H13V9H4V15H13V17H2V7M20,15V9H17V15H20M8.5,12A1.5,1.5 0 0,0 7,10.5A1.5,1.5 0 0,0 5.5,12A1.5,1.5 0 0,0 7,13.5A1.5,1.5 0 0,0 8.5,12M13,10.89C12.39,10.33 11.44,10.38 10.88,11C10.32,11.6 10.37,12.55 11,13.11C11.55,13.63 12.43,13.63 13,13.11V10.89Z" />
<!-- drawable/form_textbox_password.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M17,7H22V17H17V19A1,1 0 0,0 18,20H20V22H17.5C16.95,22 16,21.55 16,21C16,21.55 15.05,22 14.5,22H12V20H14A1,1 0 0,0 15,19V5A1,1 0 0,0 14,4H12V2H14.5C15.05,2 16,2.45 16,3C16,2.45 16.95,2 17.5,2H20V4H18A1,1 0 0,0 17,5V7M2,7H13V9H4V15H13V17H2V7M20,15V9H17V15H20M8.5,12A1.5,1.5 0 0,0 7,10.5A1.5,1.5 0 0,0 5.5,12A1.5,1.5 0 0,0 7,13.5A1.5,1.5 0 0,0 8.5,12M13,10.89C12.39,10.33 11.44,10.38 10.88,11C10.32,11.6 10.37,12.55 11,13.11C11.55,13.63 12.43,13.63 13,13.11V10.89Z" />
</vector>
+7 -7
View File
@@ -1,8 +1,8 @@
<!-- drawable/eyedropper.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M19.35,11.72L17.22,13.85L15.81,12.43L8.1,20.14L3.5,22L2,20.5L3.86,15.9L11.57,8.19L10.15,6.78L12.28,4.65L19.35,11.72M16.76,3C17.93,1.83 19.83,1.83 21,3C22.17,4.17 22.17,6.07 21,7.24L19.08,9.16L14.84,4.92L16.76,3M5.56,17.03L4.5,19.5L6.97,18.44L14.4,11L13,9.6L5.56,17.03Z" />
<!-- drawable/eyedropper.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M19.35,11.72L17.22,13.85L15.81,12.43L8.1,20.14L3.5,22L2,20.5L3.86,15.9L11.57,8.19L10.15,6.78L12.28,4.65L19.35,11.72M16.76,3C17.93,1.83 19.83,1.83 21,3C22.17,4.17 22.17,6.07 21,7.24L19.08,9.16L14.84,4.92L16.76,3M5.56,17.03L4.5,19.5L6.97,18.44L14.4,11L13,9.6L5.56,17.03Z" />
</vector>
+9 -9
View File
@@ -1,10 +1,10 @@
<!-- drawable/auto_fix.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M7.5,5.6L5,7L6.4,4.5L5,2L7.5,3.4L10,2L8.6,4.5L10,7L7.5,5.6M19.5,15.4L22,14L20.6,16.5L22,19L19.5,17.6L17,19L18.4,16.5L17,14L19.5,15.4M22,2L20.6,4.5L22,7L19.5,5.6L17,7L18.4,4.5L17,2L19.5,3.4L22,2M13.34,12.78L15.78,10.34L13.66,8.22L11.22,10.66L13.34,12.78M14.37,7.29L16.71,9.63C17.1,10 17.1,10.65 16.71,11.04L5.04,22.71C4.65,23.1 4,23.1 3.63,22.71L1.29,20.37C0.9,20 0.9,19.35 1.29,18.96L12.96,7.29C13.35,6.9 14,6.9 14.37,7.29Z" />
<!-- drawable/auto_fix.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M7.5,5.6L5,7L6.4,4.5L5,2L7.5,3.4L10,2L8.6,4.5L10,7L7.5,5.6M19.5,15.4L22,14L20.6,16.5L22,19L19.5,17.6L17,19L18.4,16.5L17,14L19.5,15.4M22,2L20.6,4.5L22,7L19.5,5.6L17,7L18.4,4.5L17,2L19.5,3.4L22,2M13.34,12.78L15.78,10.34L13.66,8.22L11.22,10.66L13.34,12.78M14.37,7.29L16.71,9.63C17.1,10 17.1,10.65 16.71,11.04L5.04,22.71C4.65,23.1 4,23.1 3.63,22.71L1.29,20.37C0.9,20 0.9,19.35 1.29,18.96L12.96,7.29C13.35,6.9 14,6.9 14.37,7.29Z" />
</vector>
@@ -1,10 +1,10 @@
<!-- drawable/menu_right.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M10,17L15,12L10,7V17Z" />
<!-- drawable/menu_right.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#fff"
android:pathData="M10,17L15,12L10,7V17Z" />
</vector>
+10 -10
View File
@@ -1,10 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M4,19h6v-2L4,17v2zM20,5L4,5v2h16L20,5zM17,11L4,11v2h13.25c1.1,0 2,0.9 2,2s-0.9,2 -2,2L15,17v-2l-3,3 3,3v-2h2c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4z"/>
</vector>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:autoMirrored="true">
<path
android:fillColor="@android:color/white"
android:pathData="M4,19h6v-2L4,17v2zM20,5L4,5v2h16L20,5zM17,11L4,11v2h13.25c1.1,0 2,0.9 2,2s-0.9,2 -2,2L15,17v-2l-3,3 3,3v-2h2c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4z"/>
</vector>
@@ -1,84 +1,84 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:padding="16dp">
<RadioGroup
android:id="@+id/choice_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<RadioButton
android:id="@+id/bool_true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:padding="2dp"
android:text="true"
tools:ignore="HardcodedText" />
<RadioButton
android:id="@+id/bool_false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:padding="2dp"
android:text="false"
tools:ignore="HardcodedText" />
</RadioGroup>
<View
android:id="@+id/divider1"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="@color/primaryLightColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/choice_group" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/boolean_res_input"
style="@style/AppTheme.TextInputLayout.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_boolean_res"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider1">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:digits="abcdefghijklmnopqrstwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_/@"
android:padding="16dp" />
</com.google.android.material.textfield.TextInputLayout>
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:padding="16dp">
<RadioGroup
android:id="@+id/choice_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<RadioButton
android:id="@+id/bool_true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:padding="2dp"
android:text="true"
tools:ignore="HardcodedText" />
<RadioButton
android:id="@+id/bool_false"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginBottom="8dp"
android:padding="2dp"
android:text="false"
tools:ignore="HardcodedText" />
</RadioGroup>
<View
android:id="@+id/divider1"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="@color/primaryLightColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/choice_group" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/boolean_res_input"
style="@style/AppTheme.TextInputLayout.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_boolean_res"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider1">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:digits="abcdefghijklmnopqrstwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_/@"
android:padding="16dp" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -1,29 +1,29 @@
<?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/>.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="36dp"
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:textSize="11sp"
tools:text="Implement abstract methods">
<?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/>.
-->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="36dp"
android:gravity="center_vertical"
android:paddingStart="8dp"
android:paddingTop="4dp"
android:paddingEnd="8dp"
android:paddingBottom="4dp"
android:textSize="11sp"
tools:text="Implement abstract methods">
</TextView>
@@ -1,78 +1,78 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:padding="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/pick_color"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_color_picker"
android:textColor="@color/primaryTextColor"
app:icon="@drawable/ic_pick_color"
app:iconTint="@color/primaryIconColor"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/secondaryColor" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/color_preview"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pick_color"
app:layout_constraintTop_toTopOf="parent"
app:strokeColor="@color/primaryDarkColor"
app:strokeWidth="1dp" />
<View
android:id="@+id/divider1"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="@color/primaryLightColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pick_color" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/color_res_input"
style="@style/AppTheme.TextInputLayout.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_color_resource"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider1">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:digits="abcdefghijklmnopqrstwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_/@"
android:padding="16dp" />
</com.google.android.material.textfield.TextInputLayout>
<?xml version="1.0" encoding="utf-8"?><!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:padding="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/pick_color"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/title_color_picker"
android:textColor="@color/primaryTextColor"
app:icon="@drawable/ic_pick_color"
app:iconTint="@color/primaryIconColor"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:rippleColor="@color/secondaryColor" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/color_preview"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/pick_color"
app:layout_constraintTop_toTopOf="parent"
app:strokeColor="@color/primaryDarkColor"
app:strokeWidth="1dp" />
<View
android:id="@+id/divider1"
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_margin="16dp"
android:background="@color/primaryLightColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/pick_color" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/color_res_input"
style="@style/AppTheme.TextInputLayout.Outlined"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:hint="@string/hint_color_resource"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/divider1">
<com.google.android.material.textfield.MaterialAutoCompleteTextView
android:layout_width="match_parent"
android:layout_height="56dp"
android:digits="abcdefghijklmnopqrstwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_/@"
android:padding="16dp" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

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