From 119eb169f1cb2c6b9215dcb498d6a55769cc8fb0 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 7 Jan 2022 20:36:18 +0530 Subject: [PATCH] Use ViewFlipper instead of ViewPager2 for editor fragments --- .github/ISSUE_TEMPLATE/bug.md | 64 +- .github/ISSUE_TEMPLATE/feature_request.md | 40 +- .github/workflows/android_workflow.yml | 84 +- .idea/codeStyles/Project.xml | 232 +- .idea/codeStyles/codeStyleConfig.xml | 8 +- .idea/misc.xml | 1 + .idea/render.experimental.xml | 10 +- README.md | 254 +- app/src/main/AndroidManifest.xml | 186 +- .../com/itsaky/androidide/EditorActivity.java | 2808 +++++++++-------- .../com/itsaky/androidide/MainActivity.java | 10 +- .../adapters/AttributeListAdapter.java | 198 +- .../adapters/EditorBottomSheetTabAdapter.java | 236 +- .../adapters/EditorPagerAdapter.java | 133 - .../androidide/adapters/MainPagerAdapter.java | 60 - .../adapters/ModuleInfoAdapter.java | 88 - .../com/itsaky/androidide/app/StudioApp.java | 315 +- .../fragments/DiagnosticsListFragment.java | 166 +- .../androidide/fragments/EditorFragment.java | 463 --- .../fragments/FileTreeFragment.java | 24 +- .../androidide/fragments/IDELogFragment.java | 66 +- .../androidide/fragments/LogViewFragment.java | 154 +- .../fragments/NonEditableEditorFragment.java | 144 +- .../fragments/SearchResultFragment.java | 162 +- .../fragments/SimpleOutputFragment.java | 116 +- .../fragments/sheets/AttrEditorSheet.java | 476 +-- .../fragments/sheets/ProgressSheet.java | 12 +- .../handlers/FileOptionsHandler.java | 689 ++-- .../androidide/handlers/IDEHandler.java | 2 +- .../language/java/JavaAnalyzer.java | 924 +++--- .../androidide/lsp/IDELanguageClientImpl.java | 141 +- .../androidide/lsp/LSPClientLauncher.java | 7 - .../androidide/lsp/LanguageServerWrapper.java | 467 --- .../itsaky/androidide/lsp/TokenModifiers.java | 56 - .../com/itsaky/androidide/lsp/TokenType.java | 66 - .../itsaky/androidide/models/OpenedFile.java | 60 - .../androidide/models/XMLAttribute.java | 328 +- .../androidide/services/IDELogService.java | 69 - .../androidide/services/LogReceiver.java | 12 +- .../itsaky/androidide/tasks/TaskExecutor.java | 9 +- .../tasks/callables/ReadFileTask.java | 40 - .../androidide/ui/WidgetTouchListener.java | 222 +- .../androidide/utils/AttributeDialogs.java | 584 ++-- .../itsaky/androidide/utils/DialogUtils.java | 186 +- .../utils/EditorBottomSheetBehavior.java | 132 +- .../utils/RuntimeTypeAdapterFactory.java | 276 -- .../com/itsaky/androidide/utils/Symbols.java | 7 +- .../androidide/viewmodel/EditorViewModel.java | 287 +- .../androidide/views/CodeEditorView.java | 378 +++ .../views/DesignerLayoutContainer.java | 176 +- .../androidide/views/TooltipLayout.java | 77 - .../androidide/views/VerticalViewPager.java | 113 - .../rosemoe/editor/widget/CodeEditor.java | 40 +- .../main/res/drawable/bg_ui_view_pressed.xml | 54 +- .../main/res/drawable/ic_widget_linear.xml | 84 +- app/src/main/res/layout/activity_editor.xml | 235 +- .../layout/fragment_non_editable_editor.xml | 42 +- .../res/layout/layout_attr_editor_sheet.xml | 160 +- .../layout/layout_attr_editor_sheet_item.xml | 90 +- ...ment_editor.xml => layout_code_editor.xml} | 0 .../layout/layout_dimension_attr_editor.xml | 118 +- .../res/layout/layout_editor_bottom_sheet.xml | 148 +- .../res/layout/layout_text_attr_editor.xml | 70 +- .../colorpicker/ColorPickerView.java | 454 +-- .../main/res/drawable-anydpi/ic_hex_icon.xml | 34 +- .../main/res/layout/layout_color_picker.xml | 268 +- .../java/com/itsaky/inflater/IAttribute.java | 222 +- .../com/itsaky/inflater/IResourceFinder.java | 172 +- .../android/widget/AbsSeekBarAttrAdapter.java | 162 +- .../android/widget/AbsSpinnerAttrAdapter.java | 142 +- .../widget/AdapterViewAttrAdapter.java | 140 +- .../android/widget/ButtonAttrAdapter.java | 106 +- .../android/widget/CheckBoxAttrAdapter.java | 96 +- .../widget/CheckedTextViewAttrAdapter.java | 160 +- .../widget/CompondButtonAttrAdapter.java | 128 +- .../android/widget/EditTextAttrAdapter.java | 92 +- .../widget/ImageButtonAttrAdapter.java | 100 +- .../android/widget/ImageViewAttrAdapter.java | 270 +- .../widget/LinearLayoutAttrAdapter.java | 196 +- .../widget/ProgressBarAttrAdapter.java | 284 +- .../widget/RadioButtonAttrAdapter.java | 102 +- .../widget/RelativeLayoutAttrAdapter.java | 156 +- .../android/widget/SeekBarAttrAdapter.java | 142 +- .../android/widget/SpinnerAttrAdapter.java | 176 +- .../android/widget/TextViewAttrAdapter.java | 478 +-- .../drawable/DrawableParserFactory.java | 306 +- .../inflater/drawable/IDrawableParser.java | 148 +- .../drawable/ShapeDrawableParser.java | 698 ++-- .../inflater/util/CommonParseUtils.java | 552 ++-- xml-inflater/src/main/res/values/attrs.xml | 44 +- 90 files changed, 8569 insertions(+), 10118 deletions(-) delete mode 100755 app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java delete mode 100755 app/src/main/java/com/itsaky/androidide/adapters/MainPagerAdapter.java delete mode 100755 app/src/main/java/com/itsaky/androidide/adapters/ModuleInfoAdapter.java delete mode 100755 app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java delete mode 100755 app/src/main/java/com/itsaky/androidide/lsp/LanguageServerWrapper.java delete mode 100755 app/src/main/java/com/itsaky/androidide/lsp/TokenModifiers.java delete mode 100755 app/src/main/java/com/itsaky/androidide/lsp/TokenType.java delete mode 100644 app/src/main/java/com/itsaky/androidide/models/OpenedFile.java delete mode 100644 app/src/main/java/com/itsaky/androidide/services/IDELogService.java delete mode 100755 app/src/main/java/com/itsaky/androidide/tasks/callables/ReadFileTask.java delete mode 100755 app/src/main/java/com/itsaky/androidide/utils/RuntimeTypeAdapterFactory.java create mode 100644 app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java delete mode 100755 app/src/main/java/com/itsaky/androidide/views/TooltipLayout.java delete mode 100755 app/src/main/java/com/itsaky/androidide/views/VerticalViewPager.java rename app/src/main/res/layout/{fragment_editor.xml => layout_code_editor.xml} (100%) mode change 100755 => 100644 diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 1ec3ef5c9..b900b2595 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,32 +1,32 @@ ---- -name: Bug -about: Create a report to help us improve -title: "[BUG]" -labels: bug -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Details (please complete the following information):** - - Device: [e.g. realme 3 Pro] - - OS: [e.g. Android 11] - - AndroidIDE Version [e.g. v1.0.3-alpha] - -**Additional context** -Add any other context about the problem here. +--- +name: Bug +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Details (please complete the following information):** + - Device: [e.g. realme 3 Pro] + - OS: [e.g. Android 11] + - AndroidIDE Version [e.g. v1.0.3-alpha] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index f814388e3..e107cb083 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -1,20 +1,20 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: "[FEATURE]: " -labels: feature -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]: " +labels: feature +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/android_workflow.yml b/.github/workflows/android_workflow.yml index 03c5d5f0d..6d60ec2a2 100644 --- a/.github/workflows/android_workflow.yml +++ b/.github/workflows/android_workflow.yml @@ -1,43 +1,43 @@ -name: Android CI - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - build: - name: Build APK - runs-on: ubuntu-latest - steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.5.0 - with: - access_token: ${{ github.token }} - - - name: Clone repo - uses: actions/checkout@v2 - - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 11 - - - name: Grant execute permission for gradlew - run: chmod +x gradlew - - - name: Build debug apk - uses: eskatos/gradle-command-action@v1 - with: - arguments: assembleDebug - distributions-cache-enabled: true - dependencies-cache-enabled: true - configuration-cache-enabled: true - - - name: Upload artifact - uses: actions/upload-artifact@v2 - if: ${{ !github.head_ref }} - with: - name: apk-debug +name: Android CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + name: Build APK + runs-on: ubuntu-latest + steps: + - name: Cancel previous runs + uses: styfle/cancel-workflow-action@0.5.0 + with: + access_token: ${{ github.token }} + + - name: Clone repo + uses: actions/checkout@v2 + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build debug apk + uses: eskatos/gradle-command-action@v1 + with: + arguments: assembleDebug + distributions-cache-enabled: true + dependencies-cache-enabled: true + configuration-cache-enabled: true + + - name: Upload artifact + uses: actions/upload-artifact@v2 + if: ${{ !github.head_ref }} + with: + name: apk-debug path: app/build/outputs/apk/debug \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 4bec4ea8a..ea4e5d030 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,117 +1,117 @@ - - - - - + + + + + \ No newline at end of file diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml index a55e7a179..df5f35dcc 100644 --- a/.idea/codeStyles/codeStyleConfig.xml +++ b/.idea/codeStyles/codeStyleConfig.xml @@ -1,5 +1,5 @@ - - - + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 7207878c3..36da88db0 100755 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -13,6 +13,7 @@ + diff --git a/.idea/render.experimental.xml b/.idea/render.experimental.xml index dde5d2b09..2bff2938a 100644 --- a/.idea/render.experimental.xml +++ b/.idea/render.experimental.xml @@ -1,6 +1,6 @@ - - - - + + + + \ No newline at end of file diff --git a/README.md b/README.md index e3f26874d..6803fefa0 100755 --- a/README.md +++ b/README.md @@ -1,127 +1,127 @@ -# AndroidIDE -AndroidIDE is an IDE for Android to develop full featured Android apps on Android smartphones. - -## Main Features -- Supports Gradle -- Comes with OpenJDK 11.0.12 -- Provides auto completion for Java and XML. It also has basic (reserved) completion for Gradle files. - -More features will be implemented as we continue to develop this app. - -## Features to be implemented -- [x] Advanced Java Auto Complete -- [x] Terminal -- [x] Custom environment variables (for Build & Terminal) -- [x] SDK Manager (Available via terminal) -- [x] Gradlew support (with some limitations) -- [x] API information for classes and their members (since, removed, deprecated). -- [ ] Implement language server for XML and Kotlin -- [ ] UI Designer - - [x] Layout inflater - - [ ] Resolve resource references - - [ ] Auto-complete resource values when user edits attributes using the attribute editor - - [ ] Drag & Drop (*) - - [ ] Visual attribute editor (*) - - [ ] Android Widgets (*) -- [ ] String Translator -- [ ] Asset Studio (Drawable & Icon Maker) -- [ ] Git - -Items marked with ```(*)``` are partially implemented. - -## Limitations -- Gradle Wrapper is supported. But, it requires that the project's application module is named 'app'. -- SDK Manager is already included in Android SDK and is accessible in AndroidIDE via its Terminal. But, you cannot use it to install some tools (like NDK) because those tools are not built for Android. -- No official NDK support because we haven't built the NDK for Android. -- No Android Gradle Plugin versions other than v7.0.2 are supported (due to AAPT2). - -The app is still being developed actively. It's in beta stage and may not be stable. if you have any issues using the app, please let us know. - -## Issues/suggestions -You should report bugs and suggest features/enhancements [here](https://github.com/itsaky/AndroidIDE/issues). -Describe the issue/request briefly. Provide steps to reproduce the issue. If you need any help, you should discuss it [here](https://t.me/androidide_discussions). - -If you open a build related issue, please provide these details: -- Device model -- Android Version -- Full build output with stacktrace. - -## Screenshots -| Main Screen | Terminal | -| ----------- | -------- | -| AndroidIDE Main Screen | AndroidIDE Terminal | - -| Build Output | Java Completion | -| ------------ | --------------- | -| AndroidIDE Build Output | AndroidIDE Java Completion | - -| XML Completion | Screenshot | -| -------------- | ---------- | -| Widgets | AndroidIDE XML Completion - Widgets -| Attributes | AndroidIDE XML Completion - Attributes -| Attribute Values | AndroidIDE XML Completion - Attribute Values - -| Gradle Support | File Tree | -| -------------- | --------- | -| AndroidIDE Gradle Support | AndroidIDE File Tree | - -## Posts to help you get started -- [Installing AndroidIDE](https://telegra.ph/How-to-install-AndroidIDE-09-11) - - Author: [Marvin Stelter](https://github.com/MarvinStelter) -- [Create your first project](https://itsaky.github.io/create-your-first-project-in-androidIDE/) - - Author: [Akash Yadav](https://github.com/itsaky) - -## Developers -This project was initially developed by [Akash Yadav](https://github.com/itsaky) and [Marvin Stelter](https://github.com/MarvinStelter). We appreciate the efforts of anyone who contributes to this project. - -## Contributing -You can contribute to AndroidIDE in one of the following ways : -- Fix bugs -- Implement new features -- Report bugs -- Suggest a change -- Request a feature -- Improve the documentation - -If you create new source files, please make sure you include the license header in every file. - -### Things to take care of -There are some things that you should take care of while writing related code. - -- Layout Inflater adapters - - [Read the docs](./layout-inflater/src/main/java/com/itsaky/layoutinflater/adapters/README.md) about layout inflater adapters. - -### Any other ways to contribute? -You can also write blog posts or create YouTube videos to help newbies get started. -If you do and want us to feature it here, open an issue. - -## Thanks to -- [Rosemoe](https://github.com/Rosemoe) for the awesome [CodeEditor](https://github.com/Rosemoe/sora-editor) -- [Termux](https://github.com/termux) for [Terminal Emulator](https://github.com/termux/termux-app) -- [Bogdan Melnychuk](https://github.com/bmelnychuk) for [AndroidTreeView](https://github.com/bmelnychuk/AndroidTreeView) -- [George Fraser](https://github.com/georgewfraser) for the [Java Language Server](https://github.com/georgewfraser/java-language-server) -- [Eclipse Foundation](https://github.com/eclipse) for [LSP4J](https://github.com/eclipse/lsp4j) - -And all the developers who have contributed to this project. - -## Contact Us -- [Website](https://androidide.com) -- [Telegram](https://t.me/androidide_discussions) - -## License -``` -AndroidIDE is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -AndroidIDE is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with AndroidIDE. If not, see . -``` - -Any violations to the license can be reported either by opening an issue or writing a mail to us directly. +# AndroidIDE +AndroidIDE is an IDE for Android to develop full featured Android apps on Android smartphones. + +## Main Features +- Supports Gradle +- Comes with OpenJDK 11.0.12 +- Provides auto completion for Java and XML. It also has basic (reserved) completion for Gradle files. + +More features will be implemented as we continue to develop this app. + +## Features to be implemented +- [x] Advanced Java Auto Complete +- [x] Terminal +- [x] Custom environment variables (for Build & Terminal) +- [x] SDK Manager (Available via terminal) +- [x] Gradlew support (with some limitations) +- [x] API information for classes and their members (since, removed, deprecated). +- [ ] Implement language server for XML and Kotlin +- [ ] UI Designer + - [x] Layout inflater + - [ ] Resolve resource references + - [ ] Auto-complete resource values when user edits attributes using the attribute editor + - [ ] Drag & Drop (*) + - [ ] Visual attribute editor (*) + - [ ] Android Widgets (*) +- [ ] String Translator +- [ ] Asset Studio (Drawable & Icon Maker) +- [ ] Git + +Items marked with ```(*)``` are partially implemented. + +## Limitations +- Gradle Wrapper is supported. But, it requires that the project's application module is named 'app'. +- SDK Manager is already included in Android SDK and is accessible in AndroidIDE via its Terminal. But, you cannot use it to install some tools (like NDK) because those tools are not built for Android. +- No official NDK support because we haven't built the NDK for Android. +- No Android Gradle Plugin versions other than v7.0.2 are supported (due to AAPT2). + +The app is still being developed actively. It's in beta stage and may not be stable. if you have any issues using the app, please let us know. + +## Issues/suggestions +You should report bugs and suggest features/enhancements [here](https://github.com/itsaky/AndroidIDE/issues). +Describe the issue/request briefly. Provide steps to reproduce the issue. If you need any help, you should discuss it [here](https://t.me/androidide_discussions). + +If you open a build related issue, please provide these details: +- Device model +- Android Version +- Full build output with stacktrace. + +## Screenshots +| Main Screen | Terminal | +| ----------- | -------- | +| AndroidIDE Main Screen | AndroidIDE Terminal | + +| Build Output | Java Completion | +| ------------ | --------------- | +| AndroidIDE Build Output | AndroidIDE Java Completion | + +| XML Completion | Screenshot | +| -------------- | ---------- | +| Widgets | AndroidIDE XML Completion - Widgets +| Attributes | AndroidIDE XML Completion - Attributes +| Attribute Values | AndroidIDE XML Completion - Attribute Values + +| Gradle Support | File Tree | +| -------------- | --------- | +| AndroidIDE Gradle Support | AndroidIDE File Tree | + +## Posts to help you get started +- [Installing AndroidIDE](https://telegra.ph/How-to-install-AndroidIDE-09-11) + - Author: [Marvin Stelter](https://github.com/MarvinStelter) +- [Create your first project](https://itsaky.github.io/create-your-first-project-in-androidIDE/) + - Author: [Akash Yadav](https://github.com/itsaky) + +## Developers +This project was initially developed by [Akash Yadav](https://github.com/itsaky) and [Marvin Stelter](https://github.com/MarvinStelter). We appreciate the efforts of anyone who contributes to this project. + +## Contributing +You can contribute to AndroidIDE in one of the following ways : +- Fix bugs +- Implement new features +- Report bugs +- Suggest a change +- Request a feature +- Improve the documentation + +If you create new source files, please make sure you include the license header in every file. + +### Things to take care of +There are some things that you should take care of while writing related code. + +- Layout Inflater adapters + - [Read the docs](./layout-inflater/src/main/java/com/itsaky/layoutinflater/adapters/README.md) about layout inflater adapters. + +### Any other ways to contribute? +You can also write blog posts or create YouTube videos to help newbies get started. +If you do and want us to feature it here, open an issue. + +## Thanks to +- [Rosemoe](https://github.com/Rosemoe) for the awesome [CodeEditor](https://github.com/Rosemoe/sora-editor) +- [Termux](https://github.com/termux) for [Terminal Emulator](https://github.com/termux/termux-app) +- [Bogdan Melnychuk](https://github.com/bmelnychuk) for [AndroidTreeView](https://github.com/bmelnychuk/AndroidTreeView) +- [George Fraser](https://github.com/georgewfraser) for the [Java Language Server](https://github.com/georgewfraser/java-language-server) +- [Eclipse Foundation](https://github.com/eclipse) for [LSP4J](https://github.com/eclipse/lsp4j) + +And all the developers who have contributed to this project. + +## Contact Us +- [Website](https://androidide.com) +- [Telegram](https://t.me/androidide_discussions) + +## License +``` +AndroidIDE is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +AndroidIDE is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with AndroidIDE. If not, see . +``` + +Any violations to the license can be reported either by opening an issue or writing a mail to us directly. diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c95afbbd6..cb46d6283 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,94 +1,94 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/EditorActivity.java b/app/src/main/java/com/itsaky/androidide/EditorActivity.java index f7cd82588..1c32bc0a3 100755 --- a/app/src/main/java/com/itsaky/androidide/EditorActivity.java +++ b/app/src/main/java/com/itsaky/androidide/EditorActivity.java @@ -1,1389 +1,1419 @@ -/* - * This file is part of AndroidIDE. - * - * - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - * - */ -package com.itsaky.androidide; - -import android.annotation.SuppressLint; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Rect; -import android.graphics.drawable.GradientDrawable; -import android.os.Bundle; -import android.os.Handler; -import android.os.Looper; -import android.view.Gravity; -import android.view.Menu; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.LinearLayout; - -import androidx.activity.result.ActivityResult; -import androidx.activity.result.ActivityResultLauncher; -import androidx.activity.result.contract.ActivityResultContracts; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.appcompat.app.ActionBarDrawerToggle; -import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.view.menu.MenuBuilder; -import androidx.core.content.ContextCompat; -import androidx.core.view.GravityCompat; -import androidx.fragment.app.Fragment; -import androidx.lifecycle.ViewModelProvider; -import androidx.transition.Slide; -import androidx.transition.TransitionManager; - -import com.blankj.utilcode.util.FileUtils; -import com.blankj.utilcode.util.IntentUtils; -import com.blankj.utilcode.util.KeyboardUtils; -import com.blankj.utilcode.util.SizeUtils; -import com.blankj.utilcode.util.ThreadUtils; -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.google.android.material.navigation.NavigationView; -import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; -import com.itsaky.androidide.adapters.DiagnosticsAdapter; -import com.itsaky.androidide.adapters.EditorBottomSheetTabAdapter; -import com.itsaky.androidide.adapters.EditorPagerAdapter; -import com.itsaky.androidide.adapters.SearchListAdapter; -import com.itsaky.androidide.app.StudioActivity; -import com.itsaky.androidide.databinding.ActivityEditorBinding; -import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding; -import com.itsaky.androidide.databinding.LayoutSearchProjectBinding; -import com.itsaky.androidide.fragments.EditorFragment; -import com.itsaky.androidide.fragments.FileTreeFragment; -import com.itsaky.androidide.fragments.SearchResultFragment; -import com.itsaky.androidide.fragments.sheets.OptionsListFragment; -import com.itsaky.androidide.fragments.sheets.ProgressSheet; -import com.itsaky.androidide.fragments.sheets.TextSheetFragment; -import com.itsaky.androidide.handlers.BuildServiceHandler; -import com.itsaky.androidide.handlers.FileOptionsHandler; -import com.itsaky.androidide.handlers.IDEHandler; -import com.itsaky.androidide.interfaces.DiagnosticClickListener; -import com.itsaky.androidide.interfaces.EditorActivityProvider; -import com.itsaky.androidide.lsp.IDELanguageClientImpl; -import com.itsaky.androidide.lsp.LSP; -import com.itsaky.androidide.lsp.LSPProvider; -import com.itsaky.androidide.managers.PreferenceManager; -import com.itsaky.androidide.models.DiagnosticGroup; -import com.itsaky.androidide.models.LogLine; -import com.itsaky.androidide.models.OpenedFile; -import com.itsaky.androidide.models.SaveResult; -import com.itsaky.androidide.models.SearchResult; -import com.itsaky.androidide.models.SheetOption; -import com.itsaky.androidide.project.AndroidProject; -import com.itsaky.androidide.project.IDEProject; -import com.itsaky.androidide.services.LogReceiver; -import com.itsaky.androidide.services.builder.IDEService; -import com.itsaky.androidide.shell.ShellServer; -import com.itsaky.androidide.tasks.TaskExecutor; -import com.itsaky.androidide.utils.DialogUtils; -import com.itsaky.androidide.utils.EditorBottomSheetBehavior; -import com.itsaky.androidide.utils.LSPUtils; -import com.itsaky.androidide.utils.Logger; -import com.itsaky.androidide.utils.RecursiveFileSearcher; -import com.itsaky.androidide.utils.Symbols; -import com.itsaky.androidide.viewmodel.EditorViewModel; -import com.itsaky.androidide.views.MaterialBanner; -import com.itsaky.androidide.views.SymbolInputView; -import com.itsaky.inflater.ILayoutInflater; -import com.itsaky.lsp.services.IDELanguageServer; -import com.itsaky.toaster.Toaster; -import com.unnamed.b.atv.model.TreeNode; - -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.DidChangeConfigurationParams; -import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; -import org.jetbrains.annotations.Contract; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Pattern; - -import io.github.rosemoe.editor.widget.CodeEditor; -import me.piruin.quickaction.ActionItem; -import me.piruin.quickaction.QuickAction; - -public class EditorActivity extends StudioActivity implements FileTreeFragment.FileActionListener, - TabLayout.OnTabSelectedListener, - NavigationView.OnNavigationItemSelectedListener, - DiagnosticClickListener, - EditorFragment.FileOpenListener, - IDEHandler.Provider, - EditorActivityProvider { - - private ActivityEditorBinding mBinding; - private LayoutDiagnosticInfoBinding mDiagnosticInfoBinding; - private EditorPagerAdapter mPagerAdapter; - private EditorBottomSheetTabAdapter bottomSheetTabAdapter; - private FileTreeFragment mFileTreeFragment; - private TreeNode mLastHeld; - private SymbolInputView symbolInput; - private BuildServiceHandler mBuildServiceHandler; - private FileOptionsHandler mFileOptionsHandler; - private QuickAction mTabCloseAction; - private TextSheetFragment mDaemonStatusFragment; - private OptionsListFragment mFileOptionsFragment; - private ProgressSheet mSearchingProgress; - private AlertDialog mFindInProjectDialog; - private ActivityResultLauncher mUIDesignerLauncher; - private EditorFragment mCurrentFragment; - public static File mCurrentFile; - @SuppressWarnings("rawtypes") - private EditorBottomSheetBehavior mEditorBottomSheet; - - private EditorViewModel mViewModel; - - private static final String TAG_FILE_OPTIONS_FRAGMENT = "file_options_fragment"; - private static final Range Range_ofZero = new Range (new Position (0, 0), new Position (0, 0)); - private static final int ACTION_ID_CLOSE = 100; - private static final int ACTION_ID_OTHERS = 101; - private static final int ACTION_ID_ALL = 102; - - public static final String EXTRA_PROJECT = "project"; - public static final String KEY_BOTTOM_SHEET_SHOWN = "editor_bottomSheetShown"; - - private final LogReceiver mLogReceiver = new LogReceiver ().setLogListener (this::appendApkLog); - - /** - * MenuItem(s) that are related to the build process - *

- * These items will be disabled once the build process starts and will be enabled after the execution - */ - private final int[] BUILD_IDS = { - R.id.menuEditor_quickRun, - R.id.menuEditor_runDebug, - R.id.menuEditor_runRelease, - R.id.menuEditor_runBuild, - R.id.menuEditor_runBundle, - R.id.menuEditor_runClean, - R.id.menuEditor_runCleanBuild, - R.id.menuEditor_lint, - R.id.menuEditor_lintDebug, - R.id.menuEditor_lintRelease - }; - - @Override - protected View bindLayout () { - mBinding = ActivityEditorBinding.inflate (getLayoutInflater ()); - mDiagnosticInfoBinding = mBinding.diagnosticInfo; - return mBinding.getRoot (); - } - - @Override - protected void onCreate (Bundle savedInstanceState) { - super.onCreate (savedInstanceState); - setSupportActionBar (mBinding.editorToolbar); - - mViewModel = new ViewModelProvider (this).get (EditorViewModel.class); - getProjectFromIntent (); - - mPagerAdapter = new EditorPagerAdapter (this, this); - mFileTreeFragment = FileTreeFragment.newInstance (this.getAndroidProject ()).setFileActionListener (this); - mDaemonStatusFragment = new TextSheetFragment ().setTextSelectable (true); - - setupDrawerToggle (); - loadFragment (mFileTreeFragment); - - mViewModel.observeFiles (this, files -> { - mPagerAdapter.setFiles (files); - }); - - symbolInput = new SymbolInputView (this); - mBinding.bottomSheet.textContainer.addView (symbolInput, 0, new ViewGroup.LayoutParams (-1, -2)); - - final var mediator = new TabLayoutMediator (mBinding.tabs, - mBinding.editorViewPager, - true, - false, // Do NOT enable smooth scrolls. Doing so results in error. Any workaround or fix will be appreciated. - (tab, position) -> tab.setText (mPagerAdapter.getEditorTitle (position))); - mBinding.editorViewPager.setUserInputEnabled (false); - mBinding.editorViewPager.setOffscreenPageLimit (9); - mBinding.editorViewPager.setAdapter (mPagerAdapter); - mBinding.tabs.addOnTabSelectedListener (this); - mediator.attach (); - - setupEditorBottomSheet (); - - createQuickActions (); - - mBuildServiceHandler = new BuildServiceHandler (this); - mFileOptionsHandler = new FileOptionsHandler (this); - - startServices (); - - KeyboardUtils.registerSoftInputChangedListener (this, __ -> onSoftInputChanged ()); - registerLogReceiver (); - setupContainers (); - setupSignatureText (); - setupDiagnosticInfo (); - - startLanguageServers (); - - mUIDesignerLauncher = registerForActivityResult (new ActivityResultContracts.StartActivityForResult (), this::onGetUIDesignerResult); - } - - private void setupDrawerToggle () { - ActionBarDrawerToggle toggle = new ActionBarDrawerToggle (this, mBinding.editorDrawerLayout, mBinding.editorToolbar, R.string.app_name, R.string.app_name); - mBinding.editorDrawerLayout.addDrawerListener (toggle); - mBinding.startNav.setNavigationItemSelectedListener (this); - toggle.syncState (); - } - - private void setupEditorBottomSheet () { - bottomSheetTabAdapter = new EditorBottomSheetTabAdapter (this); - mBinding.bottomSheet.pager.setAdapter (bottomSheetTabAdapter); - - final var mediator = new TabLayoutMediator (mBinding.bottomSheet.tabs, - mBinding.bottomSheet.pager, - true, - true, - (tab, position) -> tab.setText (bottomSheetTabAdapter.getTitle (position))); - - mediator.attach (); - mBinding.bottomSheet.pager.setUserInputEnabled (false); - mBinding.bottomSheet.pager.setOffscreenPageLimit (bottomSheetTabAdapter.getItemCount () - 1); // DO not remove any views - - //noinspection rawtypes - mEditorBottomSheet = (EditorBottomSheetBehavior) EditorBottomSheetBehavior.from (mBinding.bottomSheet.getRoot ()); - mEditorBottomSheet.setBinding (mBinding.bottomSheet); - mEditorBottomSheet.addBottomSheetCallback (new BottomSheetBehavior.BottomSheetCallback () { - @Override - public void onStateChanged (@NonNull View bottomSheet, int newState) { - mBinding.bottomSheet.textContainer.setVisibility (newState == BottomSheetBehavior.STATE_EXPANDED ? View.INVISIBLE : View.VISIBLE); - } - - @Override - public void onSlide (@NonNull View bottomSheet, float slideOffset) { - mBinding.bottomSheet.textContainer.setAlpha (1f - slideOffset); - } - }); - - if (!getApp ().getPrefManager ().getBoolean (KEY_BOTTOM_SHEET_SHOWN) && mEditorBottomSheet.getState () != BottomSheetBehavior.STATE_EXPANDED) { - mEditorBottomSheet.setState (BottomSheetBehavior.STATE_EXPANDED); - - new Handler (Looper.getMainLooper ()).postDelayed (() -> { - mEditorBottomSheet.setState (BottomSheetBehavior.STATE_COLLAPSED); - getApp ().getPrefManager ().putBoolean (KEY_BOTTOM_SHEET_SHOWN, true); - }, 1500); - } - } - - @Override - public void onBackPressed () { - if (mBinding.getRoot ().isDrawerOpen (GravityCompat.END)) { - mBinding.getRoot ().closeDrawer (GravityCompat.END); - } else if (mBinding.getRoot ().isDrawerOpen (GravityCompat.START)) { - mBinding.getRoot ().closeDrawer (GravityCompat.START); - } else if (getDaemonStatusFragment ().isShowing ()) { - getDaemonStatusFragment ().dismiss (); - } else if (mFileOptionsFragment != null && mFileOptionsFragment.isShowing ()) { - mFileOptionsFragment.dismiss (); - } else if (mEditorBottomSheet.getState () == BottomSheetBehavior.STATE_EXPANDED) { - mEditorBottomSheet.setState (BottomSheetBehavior.STATE_COLLAPSED); - } else { - confirmProjectClose (); - } - } - - @Override - protected void onPause () { - new Thread (() -> { - - boolean saved; - try { - saveAll (false /* No notification */); - saved = true; - } catch (Throwable th) { - LOG.error (getString (R.string.err_cannot_save_files), th); - saved = false; - } - - if (!saved) { - ThreadUtils.runOnUiThread (() -> getApp ().toast (R.string.msg_failed_save, Toaster.Type.ERROR)); - } - }, "AndroidIDE FileSaver").start (); - super.onPause (); - } - - @Override - protected void onResume () { - super.onResume (); - try { - mFileTreeFragment.listProjectFiles (); - } catch (Throwable th) { - getApp ().toast (R.string.msg_failed_list_files, Toaster.Type.ERROR); - } - } - - @Override - protected void onDestroy () { - closeProject (false); - try { - unregisterReceiver (mLogReceiver); - } catch (Throwable th) { - LOG.error ("Failed to unregister LogReceiver", th); - } - super.onDestroy (); - } - - @Override - @SuppressLint("AlwaysShowAction") - public boolean onPrepareOptionsMenu (Menu menu) { - for (int id : BUILD_IDS) { - MenuItem item = menu.findItem (id); - if (item != null) { - boolean enabled = getBuildService () != null && !getBuildService ().isBuilding (); - item.setEnabled (enabled); - item.getIcon ().setAlpha (enabled ? 255 : 76); - } - } - - MenuItem run1 = menu.findItem (R.id.menuEditor_quickRun); - MenuItem run2 = menu.findItem (R.id.menuEditor_run); - MenuItem undo = menu.findItem (R.id.menuEditor_undo); - MenuItem redo = menu.findItem (R.id.menuEditor_redo); - MenuItem save = menu.findItem (R.id.menuEditor_save); - MenuItem def = menu.findItem (R.id.menuEditor_gotoDefinition); - MenuItem ref = menu.findItem (R.id.menuEditor_findReferences); - MenuItem comment = menu.findItem (R.id.menuEditor_commentLine); - MenuItem uncomment = menu.findItem (R.id.menuEditor_uncommentLine); - MenuItem findFile = menu.findItem (R.id.menuEditor_findFile); - MenuItem viewLayout = menu.findItem (R.id.menuEditor_viewLayout); - - if (KeyboardUtils.isSoftInputVisible (this)) { - run1.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); - run2.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); - undo.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); - redo.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); - - viewLayout.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); - } else { - run1.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); - run2.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); - undo.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); - redo.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); - - viewLayout.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); - } - - final boolean notNull = mCurrentFile != null; - final boolean isJava = notNull && mCurrentFile.getName ().endsWith (".java"); - final boolean isXml = notNull && mCurrentFile.getName ().endsWith (".xml"); - final boolean isLayout = isXml && mCurrentFile.getParentFile () != null && Pattern.compile (FileOptionsHandler.LAYOUTRES_PATH_REGEX).matcher (mCurrentFile.getParentFile ().getAbsolutePath ()).matches (); - final int nullableAlpha = notNull ? 255 : 76; - final int javaFileAlpha = isJava ? 255 : 76; - final int layoutFileAlpha = isLayout ? 255 : 76; - - undo.setEnabled (notNull); - redo.setEnabled (notNull); - save.setEnabled (notNull); - comment.setEnabled (notNull); - uncomment.setEnabled (notNull); - findFile.setEnabled (notNull); - def.setEnabled (isJava); - ref.setEnabled (isJava); - - undo.getIcon ().setAlpha (nullableAlpha); - redo.getIcon ().setAlpha (nullableAlpha); - save.getIcon ().setAlpha (nullableAlpha); - comment.getIcon ().setAlpha (nullableAlpha); - uncomment.getIcon ().setAlpha (nullableAlpha); - findFile.getIcon ().setAlpha (nullableAlpha); - def.getIcon ().setAlpha (javaFileAlpha); - ref.getIcon ().setAlpha (javaFileAlpha); - - viewLayout.setEnabled (isLayout); - viewLayout.getIcon ().setAlpha (layoutFileAlpha); - viewLayout.setShowAsActionFlags (isLayout ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER); - - return true; - } - - @SuppressLint("RestrictedApi") - @Override - public boolean onCreateOptionsMenu (Menu menu) { - if (menu instanceof MenuBuilder) { - MenuBuilder builder = (MenuBuilder) menu; - builder.setOptionalIconsVisible (true); - } - getMenuInflater ().inflate (R.menu.menu_editor, menu); - return true; - } - - @Override - public boolean onOptionsItemSelected (MenuItem item) { - if (getBuildService () == null) { - return false; - } - int id = item.getItemId (); - if (id == R.id.menuEditor_runDebug || id == R.id.menuEditor_quickRun) { - getBuildServiceHandler ().assembleDebug (true); - } else if (id == R.id.menuEditor_runRelease) { - getBuildService ().assembleRelease (); - } else if (id == R.id.menuEditor_runClean) { - getBuildService ().clean (); - } else if (id == R.id.menuEditor_runCleanBuild) { - getBuildService ().cleanAndRebuild (); - } else if (id == R.id.menuEditor_runStopDaemons) { - getBuildService ().stopAllDaemons (); - } else if (id == R.id.menuEditor_runBuild) { - getBuildService ().build (); - } else if (id == R.id.menuEditor_runBundle) { - getBuildService ().bundle (); - } else if (id == R.id.menuEditor_lint) { - getBuildService ().lint (); - } else if (id == R.id.menuEditor_lintDebug) { - getBuildService ().lintDebug (); - } else if (id == R.id.menuEditor_lintRelease) { - getBuildService ().lintRelease (); - } else if (id == R.id.menuEditor_save) { - // * 1. Notify that all files are saved - // * 2. If there were any XML files modified, call ':app:processDebugResources' task - // * - // * This will make sure that we generate view bindings and R.jar at proper time - // * This will further result in updated code completion - saveAll (true, true); - } else if (id == R.id.menuEditor_undo && this.mCurrentFragment != null && this.mCurrentFragment.isVisible ()) { - this.mCurrentFragment.undo (); - } else if (id == R.id.menuEditor_redo && this.mCurrentFragment != null && this.mCurrentFragment.isVisible ()) { - this.mCurrentFragment.redo (); - } else if (id == R.id.menuEditor_gotoDefinition && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) { - this.mCurrentFragment.findDefinition (); - } else if (id == R.id.menuEditor_findReferences && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) { - this.mCurrentFragment.findReferences (); - } else if (id == R.id.menuEditor_commentLine && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) { - this.mCurrentFragment.commentLine (); - } else if (id == R.id.menuEditor_uncommentLine && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) { - this.mCurrentFragment.uncommentLine (); - } else if (id == R.id.menuEditor_findFile && mCurrentFragment != null && this.mCurrentFragment.isVisible ()) { - this.mCurrentFragment.beginSearch (); - } else if (id == R.id.menuEditor_findProject) { - AlertDialog d = getFindInProjectDialog (); - if (d != null) { - d.show (); - } - } else if (id == R.id.menuEditor_viewLayout && mCurrentFile != null) { - previewLayout (); - } - invalidateOptionsMenu (); - return true; - } - - @Override - public EditorActivity provide () { - return this; - } - - @Override - public EditorActivity provideEditorActivity () { - return this; - } - - @Override - public AndroidProject provideAndroidProject () { - return getAndroidProject (); - } - - @Override - public IDEProject provideIDEProject () { - return getIDEProject (); - } - - public void handleSearchResults (Map> results) { - setSearchResultAdapter (new com.itsaky.androidide.adapters.SearchListAdapter (results, file -> { - openFile (file); - hideViewOptions (); - }, match -> { - openFileAndSelect (match.file, match); - hideViewOptions (); - })); - - showSearchResults (); - - if (mSearchingProgress != null && mSearchingProgress.isShowing ()) { - mSearchingProgress.dismiss (); - } - } - - public void appendApkLog (LogLine line) { - bottomSheetTabAdapter.getLogFragment ().appendLog (line); - } - - public void setIDEProject (IDEProject project) { - if (mViewModel != null) { - mViewModel.setIDEProject (project); - } - } - - public void appendBuildOut (final String str) { - bottomSheetTabAdapter.getBuildOutputFragment ().appendOutput (str); - } - - public void showDaemonStatus () { - ShellServer shell = getApp ().newShell (t -> getDaemonStatusFragment ().append (t)); - shell.bgAppend (String.format ("echo '%s'", getString (R.string.msg_getting_daemom_status))); - shell.bgAppend (String.format ("cd '%s' && sh gradlew --status", Objects.requireNonNull (getAndroidProject ()).getProjectPath ())); - if (!getDaemonStatusFragment ().isShowing ()) { - getDaemonStatusFragment ().show (getSupportFragmentManager (), "daemon_status"); - } - } - - public void hideViewOptions () { - if (mEditorBottomSheet.getState () != BottomSheetBehavior.STATE_COLLAPSED) { - mEditorBottomSheet.setState (BottomSheetBehavior.STATE_COLLAPSED); - } - } - - public void showSearchResults () { - if (mEditorBottomSheet.getState () != BottomSheetBehavior.STATE_EXPANDED) { - mEditorBottomSheet.setState (BottomSheetBehavior.STATE_EXPANDED); - } - - final int index = bottomSheetTabAdapter.findIndexOfFragmentByClass (SearchResultFragment.class); - if (index >= 0 && index < mBinding.bottomSheet.tabs.getTabCount ()) { - final var tab = mBinding.bottomSheet.tabs.getTabAt (index); - if (tab != null) { - tab.select (); - } - } - } - - public void handleDiagnosticsResultVisibility (boolean errorVisible) { - bottomSheetTabAdapter.getDiagnosticsFragment ().handleResultVisibility (errorVisible); - } - - public void handleSearchResultVisibility (boolean errorVisible) { - bottomSheetTabAdapter.getSearchResultFragment ().handleResultVisibility (errorVisible); - } - - public void setStatus (final CharSequence text) { - try { - runOnUiThread (() -> mBinding.bottomSheet.statusText.setText (text)); - } catch (Throwable th) { - LOG.error ("Failed to update status text", th); - } - } - - public void showFirstBuildNotice () { - DialogUtils.newMaterialDialogBuilder (this) - .setPositiveButton (android.R.string.ok, null) - .setTitle (R.string.title_first_build) - .setMessage (R.string.msg_first_build) - .setCancelable (false) - .create ().show (); - } - - @Override - public void onGroupClick (DiagnosticGroup group) { - if (group != null - && group.file != null - && group.file.exists () - && FileUtils.isUtf8 (group.file)) { - openFile (group.file); - hideViewOptions (); - } - } - - @Override - public void onDiagnosticClick (File file, @NonNull Diagnostic diagnostic) { - openFileAndSelect (file, diagnostic.getRange ()); - hideViewOptions (); - } - - public EditorPagerAdapter getPagerAdapter () { - return mPagerAdapter; - } - - public ActivityEditorBinding getBinding () { - return mBinding; - } - - public LayoutDiagnosticInfoBinding getDiagnosticBinding () { - return mDiagnosticInfoBinding; - } - - /** - * Positions the view to the provided coordinates and within screen - * - * @param view View to position - * @param initialX Initial X coordinate of view - * @param initialY Initial Y coordinate of view - */ - public void positionViewWithinScreen (@NonNull final View view, final float initialX, final float initialY) { - view.setX (initialX); - view.setY (initialY); - - final Rect r = new Rect (); - final int width = view.getWidth (); - view.getWindowVisibleDisplayFrame (r); - if (r.width () != width) { - - // Will be true when the view is going out of screen to left - if (initialX < r.left) { - view.setX (SizeUtils.dp2px (8)); // an offset of 8dp from the left edge of screen - } - - // Will be true when the view is going out of screen to right - if (initialX + width > r.right) { - view.setX (r.right - SizeUtils.dp2px (8) - width); // position to the right but leaving 8dp space from the right edge of screen - } - } - } - - public void openFileAndSelect (File file, org.eclipse.lsp4j.Range range) { - openFile (file, range); - EditorFragment opened = getEditorForFile (file); - if (opened != null && opened.getEditor () != null) { - CodeEditor editor = opened.getEditor (); - editor.post (() -> { - if (LSPUtils.isEqual (range.getStart (), range.getEnd ())) { - editor.setSelection (range.getStart ().getLine (), range.getEnd ().getCharacter ()); - } else { - editor.setSelectionRegion (range.getStart ().getLine (), range.getStart ().getCharacter (), range.getEnd ().getLine (), range.getEnd ().getCharacter ()); - } - }); - } - } - - @Override - public void onTabSelected (@NonNull TabLayout.Tab tab) { - final var opened = mPagerAdapter.getOpenedFile (tab.getPosition ()); - final var fragment = getEditorAtIndex (tab.getPosition ()); - if (opened != null && opened.getFile () != null && fragment != null) { - mCurrentFragment = fragment; - mCurrentFile = opened.getFile (); - refreshSymbolInput (mCurrentFragment); - } - - invalidateOptionsMenu (); - } - - @Override - public void onTabUnselected (@NonNull TabLayout.Tab tab) { - final var opened = mPagerAdapter.getOpenedFile (tab.getPosition ()); - if (opened == null) { - return; - } - - final var frag = getEditorForFile (opened.getFile ()); - if (frag == null) { - return; - } - - boolean isGradle = frag.isModified () && opened.getFile ().getName ().endsWith (EditorFragment.EXT_GRADLE); - frag.save (); - if (isGradle) { - notifySyncNeeded (); - } - } - - @Override - public void onTabReselected (TabLayout.Tab p1) { - mTabCloseAction.show (mBinding.tabs); - } - - @Override - public boolean onNavigationItemSelected (@NonNull MenuItem p1) { - final int id = p1.getItemId (); - if (id == R.id.editornav_discuss) { - getApp ().openTelegramGroup (); - } else if (id == R.id.editornav_suggest) { - getApp ().openGitHub (); - } else if (id == R.id.editornav_needHelp) { - showNeedHelpDialog (); - } else if (id == R.id.editornav_settings) { - startActivity (new Intent (this, PreferencesActivity.class)); - } else if (id == R.id.editornav_share) { - startActivity (IntentUtils.getShareTextIntent (getString (R.string.msg_share_app))); - } else if (id == R.id.editornav_close_project) { - confirmProjectClose (); - } else if (id == R.id.editornav_terminal) { - openTerminal (); - } - mBinding.getRoot ().closeDrawer (GravityCompat.START); - return false; - } - - public void loadFragment (Fragment fragment) { - super.loadFragment (fragment, mBinding.editorFrameLayout.getId ()); - } - - public FileTreeFragment getFileTreeFragment () { - return mFileTreeFragment; - } - - public TreeNode getLastHoldTreeNode () { - return mLastHeld; - } - - public OptionsListFragment getFileOptionsFragment (File file) { - mFileOptionsFragment = new OptionsListFragment (); - mFileOptionsFragment.addOption (new SheetOption (0, R.drawable.ic_file_copy_path, R.string.copy_path, file)); - mFileOptionsFragment.addOption (new SheetOption (1, R.drawable.ic_file_rename, R.string.rename_file, file)); - mFileOptionsFragment.addOption (new SheetOption (2, R.drawable.ic_delete, R.string.delete_file, file)); - if (file.isDirectory ()) { - mFileOptionsFragment.addOption (new SheetOption (3, R.drawable.ic_new_file, R.string.new_file, file)); - mFileOptionsFragment.addOption (new SheetOption (4, R.drawable.ic_new_folder, R.string.new_folder, file)); - } - mFileOptionsFragment.setOnOptionsClickListener (mFileOptionsHandler); - return mFileOptionsFragment; - } - - public ProgressSheet getProgressSheet (int msg) { - if (mSearchingProgress != null && mSearchingProgress.isShowing ()) { - mSearchingProgress.dismiss (); - } - mSearchingProgress = new ProgressSheet (); - mSearchingProgress.setCancelable (false); - mSearchingProgress.setWelcomeTextEnabled (false); - mSearchingProgress.setMessage (getString (msg)); - mSearchingProgress.setSubMessageEnabled (false); - return mSearchingProgress; - } - - public MaterialBanner getSyncBanner () { - return mBinding.syncBanner.setContentTextColor (ContextCompat.getColor (this, R.color.primaryTextColor)) - .setBannerBackgroundColor (ContextCompat.getColor (this, R.color.primaryLightColor)) - .setButtonTextColor (ContextCompat.getColor (this, R.color.secondaryColor)) - .setIcon (R.drawable.ic_sync) - .setContentText (R.string.msg_sync_needed); - } - - public TextSheetFragment getDaemonStatusFragment () { - return mDaemonStatusFragment == null ? mDaemonStatusFragment = new TextSheetFragment ().setTextSelectable (true).setTitleText (R.string.gradle_daemon_status) : mDaemonStatusFragment; - } - - public void setDiagnosticsAdapter (@NonNull final DiagnosticsAdapter adapter) { - bottomSheetTabAdapter.getDiagnosticsFragment ().setAdapter (adapter); - } - - public void setSearchResultAdapter (@NonNull final SearchListAdapter adapter) { - bottomSheetTabAdapter.getSearchResultFragment ().setAdapter (adapter); - } - - @Override - public void onOpenSuccessful (File file, String text) { - // textDocument/didOpen is now handled by CodeEditor - } - - @Override - public void showFileOptions (File thisFile, TreeNode node) { - mLastHeld = node; - getFileOptionsFragment (thisFile).show (getSupportFragmentManager (), TAG_FILE_OPTIONS_FRAGMENT); - } - - public boolean saveAll () { - return saveAll (true); - } - - public boolean saveAll (boolean notify) { - return saveAll (notify, false); - } - - public boolean saveAll (boolean notify, boolean canProcessResources) { - SaveResult result = saveAllResult (); - - if (notify) { - getApp ().toast (R.string.all_saved, Toaster.Type.SUCCESS); - } - - if (result.gradleSaved) { - notifySyncNeeded (); - } - - if (result.xmlSaved && canProcessResources && getBuildService () != null) { - getBuildService ().updateResourceClasses (); - } - - return result.gradleSaved; - } - - public SaveResult saveAllResult () { - SaveResult result = new SaveResult (); - for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { - saveResult (i, result); - } - - return result; - } - - public void saveResult (int index, SaveResult result) { - if (index >= 0 && index < mViewModel.getOpenedFileCount ()) { - EditorFragment frag = getEditorAtIndex (index); - if (frag == null || frag.getFile () == null) { - return; - } - - // Must be called before frag.save() - // Otherwise, it'll always return false - final boolean modified = frag.isModified (); - - frag.save (); - - final boolean isGradle = frag.getFile ().getName ().endsWith (EditorFragment.EXT_GRADLE); - final boolean isXml = frag.getFile ().getName ().endsWith (EditorFragment.EXT_XML); - - if (!result.gradleSaved) { - result.gradleSaved = modified && isGradle; - } - - if (!result.xmlSaved) { - result.xmlSaved = modified && isXml; - } - } - } - - public void install (@NonNull File apk) { - if (apk.exists ()) { - Intent i = IntentUtils.getInstallAppIntent (apk); - if (i != null) { - startActivity (i); - } - } - } - - public void createServices () { - new TaskExecutor ().executeAsync (() -> { - IDELanguageServer javaServer = LSPProvider.getServerForLanguage (LSPProvider.LANGUAGE_JAVA); - if (javaServer == null) { - return null; - } - - List cps = Objects.requireNonNull (getAndroidProject ()).getClassPaths (); - JsonObject settings = new JsonObject (); - JsonObject java = new JsonObject (); - JsonArray classPath = new JsonArray (); - - for (int i = 0; i < cps.size (); i++) { - classPath.add (cps.get (i)); - } - - java.add ("classPath", classPath); - settings.add ("java", java); - - DidChangeConfigurationParams params = new DidChangeConfigurationParams (); - params.setSettings (settings); - - javaServer.getWorkspaceService ().didChangeConfiguration (params); - - return null; - }, __ -> setStatus (getString (getApp ().isXmlServiceStarted () ? R.string.msg_service_started : R.string.msg_starting_completion_failed))); - } - - @Override - public EditorFragment openFile (File file) { - return openFile (file, null); - } - - public EditorFragment openFile (File file, Range selection) { - if (selection == null) { - selection = Range_ofZero; - } - - int index = openFileAndGetIndex (file, selection); - final var tab = mBinding.tabs.getTabAt (index); - if (tab != null && index >= 0 && !tab.isSelected ()) { - tab.select (); - } - - if (mBinding.editorDrawerLayout.isDrawerOpen (GravityCompat.END)) { - mBinding.editorDrawerLayout.closeDrawer (GravityCompat.END); - } - - mBinding.editorViewPager.setCurrentItem (index); - - invalidateOptionsMenu (); - try { - return getEditorAtIndex (index); - } catch (Throwable th) { - LOG.error ("Unable to get editor fragment at opened file index (" + index + ").", th); - return null; - } - } - - @SuppressLint("NotifyDataSetChanged") - public int openFileAndGetIndex (File file, Range selection) { - final var openedFileIndex = findIndexOfEditorByFile (file); - - if (openedFileIndex != -1) { - LOG.error ("File is already opened. File: " + file); - return openedFileIndex; - } - - final var position = mViewModel.getOpenedFileCount (); - - LOG.info ("Opening file at index:", position, "file: ", file); - mViewModel.addFile (new OpenedFile (file, selection)); - return position; - } - - public void closeFile (int index) { - mBinding.tabs.removeOnTabSelectedListener (this); - if (index >= 0 && index < mPagerAdapter.getItemCount ()) {// Save the file before closing - var opened = mViewModel.getOpenedFile (index); - LOG.info ("Saving file before close. File: " + opened.getFile ()); - final var fragment = getEditorAtIndex (index); - if (fragment != null) { - fragment.save (); - LOG.info ("Language server will be notified if available"); - fragment.getEditor ().close (); // Send 'textDocument/didClose' to language servers. - } - - opened = null; - - LOG.debug ("Removing file from file list..."); - mViewModel.removeFile (index); - } else { - LOG.error ("Invalid file index. Cannot close any files"); - mBinding.tabs.addOnTabSelectedListener (this); - return; - } - - mBinding.tabs.addOnTabSelectedListener (this); - - if (mPagerAdapter.getItemCount () <= 0) { - mCurrentFragment = null; - mCurrentFile = null; - } - - invalidateOptionsMenu (); - - if (mPagerAdapter.getItemCount () == 0 && - (mBinding.editorViewPager.getChildCount () != 0 || mBinding.tabs.getChildCount () != 0)) { - // TODO Find out why this happens - // Mostly for java files... - mBinding.editorViewPager.removeAllViews (); - mBinding.tabs.removeAllViews (); - } - } - - public void closeAll () { - mBinding.tabs.removeOnTabSelectedListener (this); - // Close all files one by one - // This will make sure that we save all the files and send 'textDocument/didClose' to language servers. - for (int i = 0; i < mPagerAdapter. getItemCount (); i++) { - closeFile (i); - } - mBinding.tabs.addOnTabSelectedListener (this); - } - - public void closeOthers () { - final var index = mBinding.tabs.getSelectedTabPosition (); - for (int i = 0; i < mPagerAdapter. getItemCount (); i++) { - if (i != index) { - closeFile (i); - } - } - } - - public EditorFragment getEditorAtIndex (final int index) { - return getEditorForFile (mViewModel.getOpenedFile (index).getFile ()); - } - - @Nullable - public EditorFragment getEditorForFile (@NonNull final File file) { - final var manager = getSupportFragmentManager (); - - final var tag = "f" + file.getAbsolutePath ().hashCode (); - final var fragment = manager.findFragmentByTag (tag); - - if (fragment == null) { - LOG.error ("No editor found for file: " + file); - return null; - } - - return (EditorFragment) fragment; - } - - public int findIndexOfEditorByFile (File file) { - if (file == null) { - LOG.error ("Cannot find index of a null file."); - return -1; - } - - for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { - final var opened = mViewModel.getOpenedFile (i); - if (opened.getFile ().equals (file)) { - return i; - } - } - - return -1; - } - - ///////////////////////////////////////////////// - ////////////// PRIVATE APIS ///////////////////// - ///////////////////////////////////////////////// - - @Contract(pure = true) - private void onGetUIDesignerResult (@NonNull ActivityResult result) { - if (mCurrentFragment != null && mCurrentFragment.getEditor () != null && result.getResultCode () == RESULT_OK) { - final var data = result.getData (); - if (data != null && data.hasExtra (DesignerActivity.KEY_GENERATED_CODE)) { - final var code = data.getStringExtra (DesignerActivity.KEY_GENERATED_CODE); - mCurrentFragment.getEditor ().setText (code); - saveAll (); - } else { - final var msg = getString (R.string.msg_invalid_designer_result); - getApp ().toast (msg, Toaster.Type.ERROR); - LOG.error (msg, "Data returned by UI Designer is null or is invalid."); - } - } else { - LOG.error ("UI Designer returned an invalid result code.", "Result code: " + result.getResultCode ()); - } - } - - - private void previewLayout () { - try { - saveAll (false); - if (getApp ().getLayoutInflater () == null) { - LOG.info ("Creating layout inflater instance..."); - getApp ().createInflater (getApp ().createInflaterConfig (getContextProvider (), getResourceDirectories ())); - } - - final Intent intent = new Intent (this, DesignerActivity.class); - intent.putExtra (DesignerActivity.KEY_LAYOUT_PATH, mCurrentFile.getAbsolutePath ()); - - LOG.info ("Launching UI Designer..."); - mUIDesignerLauncher.launch (intent); - } catch (Throwable th) { - LOG.error (getString (R.string.err_cannot_preview_layout), th); - getApp ().toast (R.string.msg_cannot_preview_layout, Toaster.Type.ERROR); - } - } - - @NonNull - @Contract(" -> new") - private ILayoutInflater.ContextProvider getContextProvider () { - return this::provide; - } - - @NonNull - private Set getResourceDirectories () { - final Set dirs = new HashSet<> (); - if (getAndroidProject () != null && getAndroidProject ().getModulePaths () != null && !getAndroidProject ().getModulePaths ().isEmpty ()) { - for (String path : getAndroidProject ().getModulePaths ()) { - if (path != null && new File (path).exists ()) { - File res = new File (path, "src/main/res"); - if (res.exists ()) { - dirs.add (res); - } - } - } - } - return dirs; - } - - private void openTerminal () { - final Intent intent = new Intent (this, TerminalActivity.class); - intent.putExtra (TerminalActivity.KEY_WORKING_DIRECTORY, Objects.requireNonNull (getAndroidProject ()).getProjectPath ()); - startActivity (intent); - } - - private void startLanguageServers () { - LSP.setActivityProvider (this); - LSP.Java.start (() -> { - Optional result = LSP.Java.init (Objects.requireNonNull (getAndroidProject ()).getProjectPath ()); - if (result.isPresent ()) { - LSP.Java.initialized (); - } - }); - } - - private void getProjectFromIntent () { - final var project = (AndroidProject) getIntent ().getParcelableExtra (EXTRA_PROJECT); - if (mViewModel != null) { - mViewModel.setAndroidProject (project); - } else { - LOG.error ("ViewModel is null. Cannot set project."); - } - - getApp ().getPrefManager ().setOpenedProject (Objects.requireNonNull (this.getAndroidProject ()).getProjectPath ()); - } - - @Nullable - private AndroidProject getAndroidProject () { - if (mViewModel == null) { - return null; - } - - return mViewModel.getAndroidProject (); - } - - @Nullable - private IDEProject getIDEProject () { - if (mViewModel == null) { - return null; - } - - return mViewModel.getIDEProject (); - } - - private void setupSignatureText () { - GradientDrawable gd = new GradientDrawable (); - gd.setShape (GradientDrawable.RECTANGLE); - gd.setColor (0xff212121); - gd.setStroke (1, 0xffffffff); - gd.setCornerRadius (8); - mBinding.symbolText.setBackground (gd); - mBinding.symbolText.setVisibility (View.GONE); - } - - private void setupDiagnosticInfo () { - GradientDrawable gd = new GradientDrawable (); - gd.setShape (GradientDrawable.RECTANGLE); - gd.setColor (0xff212121); - gd.setStroke (1, 0xffffffff); - gd.setCornerRadius (8); - mDiagnosticInfoBinding.getRoot ().setBackground (gd); - mDiagnosticInfoBinding.getRoot ().setVisibility (View.GONE); - } - - private void setupContainers () { - handleDiagnosticsResultVisibility (true); - handleSearchResultVisibility (true); - } - - private void startServices () { - - if (!IDELanguageClientImpl.isInitialized ()) { - IDELanguageClientImpl.initialize (this); - } - - // Actually, we don't need to start FileOptionsHandler - // Because it would work anyway - // But still we do... - mFileOptionsHandler.start (); - - mBuildServiceHandler.start (); - getBuildServiceHandler ().assembleDebug (false); - } - - private void onSoftInputChanged () { - invalidateOptionsMenu (); - if (KeyboardUtils.isSoftInputVisible (this)) { - TransitionManager.beginDelayedTransition (mBinding.getRoot (), new Slide (Gravity.TOP)); - symbolInput.setVisibility (View.VISIBLE); - mBinding.bottomSheet.statusText.setVisibility (View.GONE); - mBinding.bottomSheet.swipeHint.setVisibility (View.GONE); - } else { - TransitionManager.beginDelayedTransition (mBinding.getRoot (), new Slide (Gravity.BOTTOM)); - symbolInput.setVisibility (View.GONE); - mBinding.bottomSheet.statusText.setVisibility (View.VISIBLE); - mBinding.bottomSheet.swipeHint.setVisibility (View.VISIBLE); - } - } - - private void refreshSymbolInput (@NonNull EditorFragment frag) { - if (frag.getEditor () == null || frag.getFile () == null) { - return; - } - - symbolInput.bindEditor (frag.getEditor ()); - symbolInput.setSymbols (Symbols.forFile (frag.getFile ())); - } - - private void notifySyncNeeded () { - if (getBuildService () != null && !getBuildService ().isBuilding ()) { - getSyncBanner () - .setNegative (android.R.string.cancel, null) - .setPositive (android.R.string.ok, v -> getBuildServiceHandler ().assembleDebug (false)) - .show (); - } - } - - private void closeProject (boolean manualFinish) { - if (getBuildService () != null) { - getBuildService ().setListener (null); - getBuildService ().exit (); - } - - // Make sure we close files - // This fill further make sure that file contents are not erased. - closeAll (); - - getApp ().getPrefManager ().setOpenedProject (PreferenceManager.NO_OPENED_PROJECT); - - LSP.shutdownAll (); - - getApp ().stopAllDaemons (); - - if (manualFinish) { - finish (); - } - } - - private void confirmProjectClose () { - final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (this); - builder.setTitle (R.string.title_confirm_project_close); - builder.setMessage (R.string.msg_confirm_project_close); - builder.setNegativeButton (android.R.string.no, null); - builder.setPositiveButton (android.R.string.yes, (d, w) -> { - d.dismiss (); - closeProject (true); - }); - builder.show (); - } - - private AlertDialog getFindInProjectDialog () { - return mFindInProjectDialog == null ? createFindInProjectDialog () : mFindInProjectDialog; - } - - @Nullable - private AlertDialog createFindInProjectDialog () { - if (getAndroidProject () == null - || getAndroidProject ().getModulePaths () == null - || getAndroidProject ().getModulePaths ().size () <= 0) { - getApp ().toast (R.string.msg_no_modules, Toaster.Type.ERROR); - return null; - } - final List modules = getAndroidProject ().getModulePaths (); - final List srcDirs = new ArrayList<> (); - final LayoutSearchProjectBinding binding = LayoutSearchProjectBinding.inflate (getLayoutInflater ()); - binding.modulesContainer.removeAllViews (); - for (int i = 0; i < modules.size (); i++) { - final File file = new File (modules.get (i)); - final File src = new File (file, "src"); - if (!file.exists () || !file.isDirectory () || !src.exists () || !src.isDirectory ()) { - continue; - } - - CheckBox check = new CheckBox (this); - check.setText (file.getName ()); - check.setChecked (true); - - LinearLayout.MarginLayoutParams params = new LinearLayout.MarginLayoutParams (-2, -2); - params.bottomMargin = SizeUtils.dp2px (4); - binding.modulesContainer.addView (check, params); - - srcDirs.add (src); - } - final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (this); - builder.setTitle (R.string.menu_find_project); - builder.setView (binding.getRoot ()); - builder.setCancelable (false); - builder.setPositiveButton (R.string.menu_find, (dialog, which) -> { - final String text = Objects.requireNonNull (binding.input.getEditText ()).getText ().toString ().trim (); - if (text.isEmpty ()) { - getApp ().toast (R.string.msg_empty_search_query, Toaster.Type.ERROR); - return; - } - - final List searchDirs = new ArrayList<> (); - for (int i = 0; i < binding.modulesContainer.getChildCount (); i++) { - CheckBox check = (CheckBox) binding.modulesContainer.getChildAt (i); - if (check.isChecked ()) { - searchDirs.add (srcDirs.get (i)); - } - } - - final String extensions = Objects.requireNonNull (binding.filter.getEditText ()).getText ().toString ().trim (); - final List extensionList = new ArrayList<> (); - if (!extensions.isEmpty ()) { - if (extensions.contains ("|")) { - for (String str : extensions.split (Pattern.quote ("|"))) { - if (str == null || str.trim ().isEmpty ()) { - continue; - } - - extensionList.add (str); - } - } else { - extensionList.add (extensions); - } - } - - if (searchDirs.isEmpty ()) { - getApp ().toast (R.string.msg_select_search_modules, Toaster.Type.ERROR); - } else { - dialog.dismiss (); - getProgressSheet (R.string.msg_searching_project).show (getSupportFragmentManager (), "search_in_project_progress"); - RecursiveFileSearcher.searchRecursiveAsync (text, extensionList, searchDirs, this::handleSearchResults); - } - }); - builder.setNegativeButton (android.R.string.cancel, (__, ___) -> __.dismiss ()); - mFindInProjectDialog = builder.create (); - return mFindInProjectDialog; - } - - private void registerLogReceiver () { - IntentFilter filter = new IntentFilter (); - filter.addAction (LogReceiver.APPEND_LOG); - registerReceiver (mLogReceiver, filter); - } - - private IDEService getBuildService () { - return mBuildServiceHandler.getService (); - } - - public BuildServiceHandler getBuildServiceHandler () { - return mBuildServiceHandler; - } - - private void showNeedHelpDialog () { - MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (this); - builder.setTitle (R.string.need_help); - builder.setMessage (R.string.msg_need_help); - builder.setPositiveButton (android.R.string.ok, null); - builder.create ().show (); - } - - private void createQuickActions () { - ActionItem closeThis = new ActionItem (ACTION_ID_CLOSE, getString (R.string.action_closeThis), R.drawable.ic_close_this); - ActionItem closeOthers = new ActionItem (ACTION_ID_OTHERS, getString (R.string.action_closeOthers), R.drawable.ic_close_others); - ActionItem closeAll = new ActionItem (ACTION_ID_ALL, getString (R.string.action_closeAll), R.drawable.ic_close_all); - mTabCloseAction = new QuickAction (this, QuickAction.HORIZONTAL); - mTabCloseAction.addActionItem (closeThis, closeOthers, closeAll); - mTabCloseAction.setColorRes (R.color.tabAction_background); - mTabCloseAction.setTextColorRes (R.color.tabAction_text); - - mTabCloseAction.setOnActionItemClickListener ((item) -> { - final int id = item.getActionId (); - saveAll (); - if (id == ACTION_ID_CLOSE) { - closeFile (mBinding.tabs.getSelectedTabPosition ()); - } - - if (id == ACTION_ID_OTHERS) { - closeOthers (); - closeOthers (); - } - - if (id == ACTION_ID_ALL) { - closeAll (); - } - }); - } - - private static final Logger LOG = Logger.instance ("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 . + * + */ +package com.itsaky.androidide; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Rect; +import android.graphics.drawable.GradientDrawable; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.Gravity; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.LinearLayout; + +import androidx.activity.result.ActivityResult; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.view.menu.MenuBuilder; +import androidx.core.content.ContextCompat; +import androidx.core.view.GravityCompat; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.transition.Slide; +import androidx.transition.TransitionManager; + +import com.blankj.utilcode.util.FileUtils; +import com.blankj.utilcode.util.IntentUtils; +import com.blankj.utilcode.util.KeyboardUtils; +import com.blankj.utilcode.util.SizeUtils; +import com.blankj.utilcode.util.ThreadUtils; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.navigation.NavigationView; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.itsaky.androidide.adapters.DiagnosticsAdapter; +import com.itsaky.androidide.adapters.EditorBottomSheetTabAdapter; +import com.itsaky.androidide.adapters.SearchListAdapter; +import com.itsaky.androidide.app.StudioActivity; +import com.itsaky.androidide.databinding.ActivityEditorBinding; +import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding; +import com.itsaky.androidide.databinding.LayoutSearchProjectBinding; +import com.itsaky.androidide.fragments.FileTreeFragment; +import com.itsaky.androidide.fragments.SearchResultFragment; +import com.itsaky.androidide.fragments.sheets.OptionsListFragment; +import com.itsaky.androidide.fragments.sheets.ProgressSheet; +import com.itsaky.androidide.fragments.sheets.TextSheetFragment; +import com.itsaky.androidide.handlers.BuildServiceHandler; +import com.itsaky.androidide.handlers.FileOptionsHandler; +import com.itsaky.androidide.handlers.IDEHandler; +import com.itsaky.androidide.interfaces.DiagnosticClickListener; +import com.itsaky.androidide.interfaces.EditorActivityProvider; +import com.itsaky.androidide.lsp.IDELanguageClientImpl; +import com.itsaky.androidide.lsp.LSP; +import com.itsaky.androidide.lsp.LSPProvider; +import com.itsaky.androidide.managers.PreferenceManager; +import com.itsaky.androidide.models.DiagnosticGroup; +import com.itsaky.androidide.models.LogLine; +import com.itsaky.androidide.models.SaveResult; +import com.itsaky.androidide.models.SearchResult; +import com.itsaky.androidide.models.SheetOption; +import com.itsaky.androidide.project.AndroidProject; +import com.itsaky.androidide.project.IDEProject; +import com.itsaky.androidide.services.LogReceiver; +import com.itsaky.androidide.services.builder.IDEService; +import com.itsaky.androidide.shell.ShellServer; +import com.itsaky.androidide.tasks.TaskExecutor; +import com.itsaky.androidide.utils.DialogUtils; +import com.itsaky.androidide.utils.EditorBottomSheetBehavior; +import com.itsaky.androidide.utils.LSPUtils; +import com.itsaky.androidide.utils.Logger; +import com.itsaky.androidide.utils.RecursiveFileSearcher; +import com.itsaky.androidide.utils.Symbols; +import com.itsaky.androidide.viewmodel.EditorViewModel; +import com.itsaky.androidide.views.CodeEditorView; +import com.itsaky.androidide.views.MaterialBanner; +import com.itsaky.androidide.views.SymbolInputView; +import com.itsaky.inflater.ILayoutInflater; +import com.itsaky.lsp.services.IDELanguageServer; +import com.itsaky.toaster.Toaster; +import com.unnamed.b.atv.model.TreeNode; + +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.DidChangeConfigurationParams; +import org.eclipse.lsp4j.InitializeResult; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; +import org.jetbrains.annotations.Contract; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; + +import io.github.rosemoe.editor.widget.CodeEditor; +import me.piruin.quickaction.ActionItem; +import me.piruin.quickaction.QuickAction; + +public class EditorActivity extends StudioActivity implements FileTreeFragment.FileActionListener, + TabLayout.OnTabSelectedListener, + NavigationView.OnNavigationItemSelectedListener, + DiagnosticClickListener, + IDEHandler.Provider, + EditorActivityProvider { + + private ActivityEditorBinding mBinding; + private LayoutDiagnosticInfoBinding mDiagnosticInfoBinding; + private EditorBottomSheetTabAdapter bottomSheetTabAdapter; + private FileTreeFragment mFileTreeFragment; + private TreeNode mLastHeld; + private SymbolInputView symbolInput; + private BuildServiceHandler mBuildServiceHandler; + private FileOptionsHandler mFileOptionsHandler; + private QuickAction mTabCloseAction; + private TextSheetFragment mDaemonStatusFragment; + private OptionsListFragment mFileOptionsFragment; + private ProgressSheet mSearchingProgress; + private AlertDialog mFindInProjectDialog; + private ActivityResultLauncher mUIDesignerLauncher; + private EditorBottomSheetBehavior mEditorBottomSheet; + + private EditorViewModel mViewModel; + + private static final String TAG_FILE_OPTIONS_FRAGMENT = "file_options_fragment"; + private static final Range Range_ofZero = new Range (new Position (0, 0), new Position (0, 0)); + private static final int ACTION_ID_CLOSE = 100; + private static final int ACTION_ID_OTHERS = 101; + private static final int ACTION_ID_ALL = 102; + + public static final String EXTRA_PROJECT = "project"; + public static final String KEY_BOTTOM_SHEET_SHOWN = "editor_bottomSheetShown"; + + private final LogReceiver mLogReceiver = new LogReceiver ().setLogListener (this::appendApkLog); + + /** + * MenuItem(s) that are related to the build process + *

+ * These items will be disabled once the build process starts and will be enabled after the execution + */ + private final int[] BUILD_IDS = { + R.id.menuEditor_quickRun, + R.id.menuEditor_runDebug, + R.id.menuEditor_runRelease, + R.id.menuEditor_runBuild, + R.id.menuEditor_runBundle, + R.id.menuEditor_runClean, + R.id.menuEditor_runCleanBuild, + R.id.menuEditor_lint, + R.id.menuEditor_lintDebug, + R.id.menuEditor_lintRelease + }; + + @Override + protected View bindLayout () { + mBinding = ActivityEditorBinding.inflate (getLayoutInflater ()); + mDiagnosticInfoBinding = mBinding.diagnosticInfo; + return mBinding.getRoot (); + } + + @Override + protected void onCreate (Bundle savedInstanceState) { + super.onCreate (savedInstanceState); + setSupportActionBar (mBinding.editorToolbar); + + mViewModel = new ViewModelProvider (this).get (EditorViewModel.class); + getProjectFromIntent (); + + mFileTreeFragment = FileTreeFragment.newInstance (this.getAndroidProject ()).setFileActionListener (this); + mDaemonStatusFragment = new TextSheetFragment ().setTextSelectable (true); + + setupDrawerToggle (); + loadFragment (mFileTreeFragment); + + symbolInput = new SymbolInputView (this); + mBinding.bottomSheet.textContainer.addView (symbolInput, 0, new ViewGroup.LayoutParams (-1, -2)); + mBinding.tabs.addOnTabSelectedListener (this); + + setupEditorBottomSheet (); + createQuickActions (); + + mBuildServiceHandler = new BuildServiceHandler (this); + mFileOptionsHandler = new FileOptionsHandler (this); + + startServices (); + + KeyboardUtils.registerSoftInputChangedListener (this, __ -> onSoftInputChanged ()); + registerLogReceiver (); + setupContainers (); + setupSignatureText (); + setupDiagnosticInfo (); + startLanguageServers (); + + mUIDesignerLauncher = registerForActivityResult (new ActivityResultContracts.StartActivityForResult (), this::onGetUIDesignerResult); + } + + private void setupDrawerToggle () { + ActionBarDrawerToggle toggle = new ActionBarDrawerToggle (this, mBinding.editorDrawerLayout, mBinding.editorToolbar, R.string.app_name, R.string.app_name); + mBinding.editorDrawerLayout.addDrawerListener (toggle); + mBinding.startNav.setNavigationItemSelectedListener (this); + toggle.syncState (); + } + + private void setupEditorBottomSheet () { + bottomSheetTabAdapter = new EditorBottomSheetTabAdapter (this); + mBinding.bottomSheet.pager.setAdapter (bottomSheetTabAdapter); + + final var mediator = new TabLayoutMediator (mBinding.bottomSheet.tabs, + mBinding.bottomSheet.pager, + true, + true, + (tab, position) -> tab.setText (bottomSheetTabAdapter.getTitle (position))); + + mediator.attach (); + mBinding.bottomSheet.pager.setUserInputEnabled (false); + mBinding.bottomSheet.pager.setOffscreenPageLimit (bottomSheetTabAdapter.getItemCount () - 1); // DO not remove any views + + mEditorBottomSheet = (EditorBottomSheetBehavior) EditorBottomSheetBehavior.from (mBinding.bottomSheet.getRoot ()); + mEditorBottomSheet.setBinding (mBinding.bottomSheet); + mEditorBottomSheet.addBottomSheetCallback (new BottomSheetBehavior.BottomSheetCallback () { + @Override + public void onStateChanged (@NonNull View bottomSheet, int newState) { + mBinding.bottomSheet.textContainer.setVisibility (newState == BottomSheetBehavior.STATE_EXPANDED ? View.INVISIBLE : View.VISIBLE); + } + + @Override + public void onSlide (@NonNull View bottomSheet, float slideOffset) { + mBinding.bottomSheet.textContainer.setAlpha (1f - slideOffset); + } + }); + + if (!getApp ().getPrefManager ().getBoolean (KEY_BOTTOM_SHEET_SHOWN) && mEditorBottomSheet.getState () != BottomSheetBehavior.STATE_EXPANDED) { + mEditorBottomSheet.setState (BottomSheetBehavior.STATE_EXPANDED); + + new Handler (Looper.getMainLooper ()).postDelayed (() -> { + mEditorBottomSheet.setState (BottomSheetBehavior.STATE_COLLAPSED); + getApp ().getPrefManager ().putBoolean (KEY_BOTTOM_SHEET_SHOWN, true); + }, 1500); + } + } + + @Override + public void onBackPressed () { + if (mBinding.getRoot ().isDrawerOpen (GravityCompat.END)) { + mBinding.getRoot ().closeDrawer (GravityCompat.END); + } else if (mBinding.getRoot ().isDrawerOpen (GravityCompat.START)) { + mBinding.getRoot ().closeDrawer (GravityCompat.START); + } else if (getDaemonStatusFragment ().isShowing ()) { + getDaemonStatusFragment ().dismiss (); + } else if (mFileOptionsFragment != null && mFileOptionsFragment.isShowing ()) { + mFileOptionsFragment.dismiss (); + } else if (mEditorBottomSheet.getState () == BottomSheetBehavior.STATE_EXPANDED) { + mEditorBottomSheet.setState (BottomSheetBehavior.STATE_COLLAPSED); + } else { + confirmProjectClose (); + } + } + + @Override + protected void onPause () { + dispatchOnPauseToEditors (); + super.onPause (); + } + + @Override + protected void onResume () { + super.onResume (); + + try { + dispatchOnResumeToEditors (); + mFileTreeFragment.listProjectFiles (); + } catch (Throwable th) { + LOG.error ("Failed to update files list", th); + getApp ().toast (R.string.msg_failed_list_files, Toaster.Type.ERROR); + } + } + + @Override + protected void onDestroy () { + closeProject (false); + try { + unregisterReceiver (mLogReceiver); + } catch (Throwable th) { + LOG.error ("Failed to unregister LogReceiver", th); + } + super.onDestroy (); + } + + @Override + @SuppressLint("AlwaysShowAction") + public boolean onPrepareOptionsMenu (Menu menu) { + for (int id : BUILD_IDS) { + MenuItem item = menu.findItem (id); + if (item != null) { + boolean enabled = getBuildService () != null && !getBuildService ().isBuilding (); + item.setEnabled (enabled); + item.getIcon ().setAlpha (enabled ? 255 : 76); + } + } + + MenuItem run1 = menu.findItem (R.id.menuEditor_quickRun); + MenuItem run2 = menu.findItem (R.id.menuEditor_run); + MenuItem undo = menu.findItem (R.id.menuEditor_undo); + MenuItem redo = menu.findItem (R.id.menuEditor_redo); + MenuItem save = menu.findItem (R.id.menuEditor_save); + MenuItem def = menu.findItem (R.id.menuEditor_gotoDefinition); + MenuItem ref = menu.findItem (R.id.menuEditor_findReferences); + MenuItem comment = menu.findItem (R.id.menuEditor_commentLine); + MenuItem uncomment = menu.findItem (R.id.menuEditor_uncommentLine); + MenuItem findFile = menu.findItem (R.id.menuEditor_findFile); + MenuItem viewLayout = menu.findItem (R.id.menuEditor_viewLayout); + + if (KeyboardUtils.isSoftInputVisible (this)) { + run1.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); + run2.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); + undo.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); + redo.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); + + viewLayout.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); + } else { + run1.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); + run2.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); + undo.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); + redo.setShowAsAction (MenuItem.SHOW_AS_ACTION_NEVER); + + viewLayout.setShowAsAction (MenuItem.SHOW_AS_ACTION_ALWAYS); + } + + final var editor = getCurrentEditor (); + final var file = editor != null ? editor.getFile () : null; + final var notNull = editor != null && file != null; + final boolean isJava = notNull && file.getName ().endsWith (".java"); + final boolean isXml = notNull && file.getName ().endsWith (".xml"); + final boolean isLayout = isXml && file.getParentFile () != null && + Pattern.compile (FileOptionsHandler.LAYOUTRES_PATH_REGEX) + .matcher (file.getParentFile ().getAbsolutePath ()) + .matches (); + final int nullableAlpha = notNull ? 255 : 76; + final int javaFileAlpha = isJava ? 255 : 76; + final int layoutFileAlpha = isLayout ? 255 : 76; + + undo.setEnabled (notNull); + redo.setEnabled (notNull); + save.setEnabled (notNull); + comment.setEnabled (notNull); + uncomment.setEnabled (notNull); + findFile.setEnabled (notNull); + def.setEnabled (isJava); + ref.setEnabled (isJava); + + undo.getIcon ().setAlpha (nullableAlpha); + redo.getIcon ().setAlpha (nullableAlpha); + save.getIcon ().setAlpha (nullableAlpha); + comment.getIcon ().setAlpha (nullableAlpha); + uncomment.getIcon ().setAlpha (nullableAlpha); + findFile.getIcon ().setAlpha (nullableAlpha); + def.getIcon ().setAlpha (javaFileAlpha); + ref.getIcon ().setAlpha (javaFileAlpha); + + viewLayout.setEnabled (isLayout); + viewLayout.getIcon ().setAlpha (layoutFileAlpha); + viewLayout.setShowAsActionFlags (isLayout ? MenuItem.SHOW_AS_ACTION_ALWAYS : MenuItem.SHOW_AS_ACTION_NEVER); + + return true; + } + + @SuppressLint("RestrictedApi") + @Override + public boolean onCreateOptionsMenu (Menu menu) { + if (menu instanceof MenuBuilder) { + MenuBuilder builder = (MenuBuilder) menu; + builder.setOptionalIconsVisible (true); + } + + getMenuInflater ().inflate (R.menu.menu_editor, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected (MenuItem item) { + if (getBuildService () == null) { + LOG.error ("Build service is null. Cannot perform option menu actions."); + return false; + } + + int id = item.getItemId (); + if (id == R.id.menuEditor_runDebug || id == R.id.menuEditor_quickRun) { + getBuildServiceHandler ().assembleDebug (true); + } else if (id == R.id.menuEditor_runRelease) { + getBuildService ().assembleRelease (); + } else if (id == R.id.menuEditor_runClean) { + getBuildService ().clean (); + } else if (id == R.id.menuEditor_runCleanBuild) { + getBuildService ().cleanAndRebuild (); + } else if (id == R.id.menuEditor_runStopDaemons) { + getBuildService ().stopAllDaemons (); + } else if (id == R.id.menuEditor_runBuild) { + getBuildService ().build (); + } else if (id == R.id.menuEditor_runBundle) { + getBuildService ().bundle (); + } else if (id == R.id.menuEditor_lint) { + getBuildService ().lint (); + } else if (id == R.id.menuEditor_lintDebug) { + getBuildService ().lintDebug (); + } else if (id == R.id.menuEditor_lintRelease) { + getBuildService ().lintRelease (); + } else if (id == R.id.menuEditor_save) { + saveAll (true, true); + } else if (id == R.id.menuEditor_findProject) { + AlertDialog d = getFindInProjectDialog (); + if (d != null) { + d.show (); + } + } else if (id == R.id.menuEditor_viewLayout) { + previewLayout (); + } else if (getCurrentEditor () != null) { // Should be checked at last + if (id == R.id.menuEditor_undo) { + this.getCurrentEditor ().undo (); + } else if (id == R.id.menuEditor_redo) { + this.getCurrentEditor ().redo (); + } else if (id == R.id.menuEditor_gotoDefinition) { + this.getCurrentEditor ().findDefinition (); + } else if (id == R.id.menuEditor_findReferences) { + this.getCurrentEditor ().findReferences (); + } else if (id == R.id.menuEditor_commentLine) { + this.getCurrentEditor ().commentLine (); + } else if (id == R.id.menuEditor_uncommentLine) { + this.getCurrentEditor ().uncommentLine (); + } else if (id == R.id.menuEditor_findFile) { + this.getCurrentEditor ().beginSearch (); + } + } + + invalidateOptionsMenu (); + return true; + } + + @Override + public EditorActivity provide () { + return this; + } + + @Override + public EditorActivity provideEditorActivity () { + return this; + } + + @Override + public AndroidProject provideAndroidProject () { + return getAndroidProject (); + } + + @Override + public IDEProject provideIDEProject () { + return getIDEProject (); + } + + public void handleSearchResults (Map> results) { + setSearchResultAdapter (new com.itsaky.androidide.adapters.SearchListAdapter (results, file -> { + openFile (file); + hideViewOptions (); + }, match -> { + openFileAndSelect (match.file, match); + hideViewOptions (); + })); + + showSearchResults (); + + if (mSearchingProgress != null && mSearchingProgress.isShowing ()) { + mSearchingProgress.dismiss (); + } + } + + public void appendApkLog (LogLine line) { + bottomSheetTabAdapter.getLogFragment ().appendLog (line); + } + + public void setIDEProject (IDEProject project) { + if (mViewModel != null) { + mViewModel.setIDEProject (project); + } + } + + public void appendBuildOut (final String str) { + bottomSheetTabAdapter.getBuildOutputFragment ().appendOutput (str); + } + + public void showDaemonStatus () { + ShellServer shell = getApp ().newShell (t -> getDaemonStatusFragment ().append (t)); + shell.bgAppend (String.format ("echo '%s'", getString (R.string.msg_getting_daemom_status))); + shell.bgAppend (String.format ("cd '%s' && sh gradlew --status", Objects.requireNonNull (getAndroidProject ()).getProjectPath ())); + if (!getDaemonStatusFragment ().isShowing ()) { + getDaemonStatusFragment ().show (getSupportFragmentManager (), "daemon_status"); + } + } + + public void hideViewOptions () { + if (mEditorBottomSheet.getState () != BottomSheetBehavior.STATE_COLLAPSED) { + mEditorBottomSheet.setState (BottomSheetBehavior.STATE_COLLAPSED); + } + } + + public void showSearchResults () { + if (mEditorBottomSheet.getState () != BottomSheetBehavior.STATE_EXPANDED) { + mEditorBottomSheet.setState (BottomSheetBehavior.STATE_EXPANDED); + } + + final int index = bottomSheetTabAdapter.findIndexOfFragmentByClass (SearchResultFragment.class); + if (index >= 0 && index < mBinding.bottomSheet.tabs.getTabCount ()) { + final var tab = mBinding.bottomSheet.tabs.getTabAt (index); + if (tab != null) { + tab.select (); + } + } + } + + public void handleDiagnosticsResultVisibility (boolean errorVisible) { + runOnUiThread (() -> bottomSheetTabAdapter.getDiagnosticsFragment ().handleResultVisibility (errorVisible)); + } + + public void handleSearchResultVisibility (boolean errorVisible) { + runOnUiThread (() -> bottomSheetTabAdapter.getSearchResultFragment ().handleResultVisibility (errorVisible)); + } + + public void setStatus (final CharSequence text) { + try { + runOnUiThread (() -> mBinding.bottomSheet.statusText.setText (text)); + } catch (Throwable th) { + LOG.error ("Failed to update status text", th); + } + } + + public void showFirstBuildNotice () { + DialogUtils.newMaterialDialogBuilder (this) + .setPositiveButton (android.R.string.ok, null) + .setTitle (R.string.title_first_build) + .setMessage (R.string.msg_first_build) + .setCancelable (false) + .create ().show (); + } + + @Override + public void onGroupClick (DiagnosticGroup group) { + if (group != null + && group.file != null + && group.file.exists () + && FileUtils.isUtf8 (group.file)) { + openFile (group.file); + hideViewOptions (); + } + } + + @Override + public void onDiagnosticClick (File file, @NonNull Diagnostic diagnostic) { + openFileAndSelect (file, diagnostic.getRange ()); + hideViewOptions (); + } + + public ActivityEditorBinding getBinding () { + return mBinding; + } + + public LayoutDiagnosticInfoBinding getDiagnosticBinding () { + return mDiagnosticInfoBinding; + } + + /** + * Positions the view to the provided coordinates and within screen + * + * @param view View to position + * @param initialX Initial X coordinate of view + * @param initialY Initial Y coordinate of view + */ + public void positionViewWithinScreen (@NonNull final View view, final float initialX, final float initialY) { + view.setX (initialX); + view.setY (initialY); + + final Rect r = new Rect (); + final int width = view.getWidth (); + view.getWindowVisibleDisplayFrame (r); + if (r.width () != width) { + + // Will be true when the view is going out of screen to left + if (initialX < r.left) { + view.setX (SizeUtils.dp2px (8)); // an offset of 8dp from the left edge of screen + } + + // Will be true when the view is going out of screen to right + if (initialX + width > r.right) { + view.setX (r.right - SizeUtils.dp2px (8) - width); // position to the right but leaving 8dp space from the right edge of screen + } + } + } + + public void openFileAndSelect (File file, org.eclipse.lsp4j.Range range) { + openFile (file, range); + final var opened = getEditorForFile (file); + if (opened != null && opened.getEditor () != null) { + CodeEditor editor = opened.getEditor (); + editor.post (() -> { + if (LSPUtils.isEqual (range.getStart (), range.getEnd ())) { + editor.setSelection (range.getStart ().getLine (), range.getEnd ().getCharacter ()); + } else { + editor.setSelectionRegion (range.getStart ().getLine (), range.getStart ().getCharacter (), range.getEnd ().getLine (), range.getEnd ().getCharacter ()); + } + }); + } + } + + @Override + public void onTabSelected (@NonNull TabLayout.Tab tab) { + final var position = tab.getPosition (); + mBinding.editorContainer.setDisplayedChild (position); + + final var editorView = getEditorAtIndex (position); + mViewModel.setCurrentFile (position, Objects.requireNonNull (editorView).getFile ()); + refreshSymbolInput (editorView); + invalidateOptionsMenu (); + } + + @Override + public void onTabUnselected (@NonNull TabLayout.Tab tab) { + final var position = mViewModel.getCurrentFileIndex (); + if (position < 0 || position >= mViewModel.getOpenedFileCount ()) { + // This might happen when the file is being closed. + // In this case, the file has already been saved, so we don't need to save it again. + // see #close(int) + return; + } + + final var opened = mViewModel.getOpenedFile (position); + if (opened == null) { + LOG.error ("No opened file at index", position); + return; + } + + final var frag = getEditorAtIndex (position); + if (frag == null) { + LOG.error ("Cannot save unselected editor. Editor is null."); + return; + } + + boolean isGradle = frag.isModified () && opened.getName ().endsWith (".gradle"); + frag.save (); + + if (isGradle) { + LOG.info ("Gradle files have been modified. Sync needed."); + notifySyncNeeded (); + } + } + + @Override + public void onTabReselected (@NonNull TabLayout.Tab tab) { + mTabCloseAction.show (tab.view); + } + + @Override + public boolean onNavigationItemSelected (@NonNull MenuItem p1) { + final int id = p1.getItemId (); + if (id == R.id.editornav_discuss) { + getApp ().openTelegramGroup (); + } else if (id == R.id.editornav_suggest) { + getApp ().openGitHub (); + } else if (id == R.id.editornav_needHelp) { + showNeedHelpDialog (); + } else if (id == R.id.editornav_settings) { + startActivity (new Intent (this, PreferencesActivity.class)); + } else if (id == R.id.editornav_share) { + startActivity (IntentUtils.getShareTextIntent (getString (R.string.msg_share_app))); + } else if (id == R.id.editornav_close_project) { + confirmProjectClose (); + } else if (id == R.id.editornav_terminal) { + openTerminal (); + } + mBinding.getRoot ().closeDrawer (GravityCompat.START); + return false; + } + + public void loadFragment (Fragment fragment) { + super.loadFragment (fragment, mBinding.editorFrameLayout.getId ()); + } + + public FileTreeFragment getFileTreeFragment () { + return mFileTreeFragment; + } + + public TreeNode getLastHoldTreeNode () { + return mLastHeld; + } + + public OptionsListFragment getFileOptionsFragment (File file) { + mFileOptionsFragment = new OptionsListFragment (); + mFileOptionsFragment.addOption (new SheetOption (0, R.drawable.ic_file_copy_path, R.string.copy_path, file)); + mFileOptionsFragment.addOption (new SheetOption (1, R.drawable.ic_file_rename, R.string.rename_file, file)); + mFileOptionsFragment.addOption (new SheetOption (2, R.drawable.ic_delete, R.string.delete_file, file)); + if (file.isDirectory ()) { + mFileOptionsFragment.addOption (new SheetOption (3, R.drawable.ic_new_file, R.string.new_file, file)); + mFileOptionsFragment.addOption (new SheetOption (4, R.drawable.ic_new_folder, R.string.new_folder, file)); + } + mFileOptionsFragment.setOnOptionsClickListener (mFileOptionsHandler); + return mFileOptionsFragment; + } + + public ProgressSheet getProgressSheet (int msg) { + if (mSearchingProgress != null && mSearchingProgress.isShowing ()) { + mSearchingProgress.dismiss (); + } + mSearchingProgress = new ProgressSheet (); + mSearchingProgress.setCancelable (false); + mSearchingProgress.setWelcomeTextEnabled (false); + mSearchingProgress.setMessage (getString (msg)); + mSearchingProgress.setSubMessageEnabled (false); + return mSearchingProgress; + } + + public MaterialBanner getSyncBanner () { + return mBinding.syncBanner.setContentTextColor (ContextCompat.getColor (this, R.color.primaryTextColor)) + .setBannerBackgroundColor (ContextCompat.getColor (this, R.color.primaryLightColor)) + .setButtonTextColor (ContextCompat.getColor (this, R.color.secondaryColor)) + .setIcon (R.drawable.ic_sync) + .setContentText (R.string.msg_sync_needed); + } + + public TextSheetFragment getDaemonStatusFragment () { + return mDaemonStatusFragment == null ? mDaemonStatusFragment = new TextSheetFragment ().setTextSelectable (true).setTitleText (R.string.gradle_daemon_status) : mDaemonStatusFragment; + } + + public void setDiagnosticsAdapter (@NonNull final DiagnosticsAdapter adapter) { + runOnUiThread (() -> bottomSheetTabAdapter.getDiagnosticsFragment ().setAdapter (adapter)); + } + + public void setSearchResultAdapter (@NonNull final SearchListAdapter adapter) { + runOnUiThread (() -> bottomSheetTabAdapter.getSearchResultFragment ().setAdapter (adapter)); + } + + @Override + public void showFileOptions (File thisFile, TreeNode node) { + mLastHeld = node; + getFileOptionsFragment (thisFile).show (getSupportFragmentManager (), TAG_FILE_OPTIONS_FRAGMENT); + } + + public boolean saveAll () { + return saveAll (true); + } + + public boolean saveAll (boolean notify) { + return saveAll (notify, false); + } + + public boolean saveAll (boolean notify, boolean canProcessResources) { + SaveResult result = saveAllResult (); + + if (notify) { + getApp ().toast (R.string.all_saved, Toaster.Type.SUCCESS); + } + + if (result.gradleSaved) { + notifySyncNeeded (); + } + + if (result.xmlSaved && canProcessResources && getBuildService () != null) { + getBuildService ().updateResourceClasses (); + } + + return result.gradleSaved; + } + + public SaveResult saveAllResult () { + SaveResult result = new SaveResult (); + for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { + saveResult (i, result); + } + + return result; + } + + public void saveResult (int index, SaveResult result) { + if (index >= 0 && index < mViewModel.getOpenedFileCount ()) { + var frag = getEditorAtIndex (index); + if (frag == null) { + return; + } + + // Must be called before frag.save() + // Otherwise, it'll always return false + final boolean modified = frag.isModified (); + + frag.save (); + + final boolean isGradle = frag.getFile ().getName ().endsWith (".gradle"); + final boolean isXml = frag.getFile ().getName ().endsWith (".xml"); + + if (!result.gradleSaved) { + result.gradleSaved = modified && isGradle; + } + + if (!result.xmlSaved) { + result.xmlSaved = modified && isXml; + } + } + } + + public void install (@NonNull File apk) { + if (apk.exists ()) { + Intent i = IntentUtils.getInstallAppIntent (apk); + if (i != null) { + startActivity (i); + } + } + } + + public void createServices () { + new TaskExecutor ().executeAsync (() -> { + IDELanguageServer javaServer = LSPProvider.getServerForLanguage (LSPProvider.LANGUAGE_JAVA); + if (javaServer == null) { + LOG.error ("Cannot create services as java language server instance is null"); + return null; + } + + List cps = Objects.requireNonNull (getAndroidProject ()).getClassPaths (); + JsonObject settings = new JsonObject (); + JsonObject java = new JsonObject (); + JsonArray classPath = new JsonArray (); + + for (int i = 0; i < cps.size (); i++) { + classPath.add (cps.get (i)); + } + + java.add ("classPath", classPath); + settings.add ("java", java); + + DidChangeConfigurationParams params = new DidChangeConfigurationParams (); + params.setSettings (settings); + + javaServer.getWorkspaceService ().didChangeConfiguration (params); + + return null; + }, __ -> setStatus (getString (getApp ().isXmlServiceStarted () ? R.string.msg_service_started : R.string.msg_starting_completion_failed))); + } + + @Override + public CodeEditorView openFile (File file) { + return openFile (file, null); + } + + public CodeEditorView openFile (File file, Range selection) { + if (selection == null) { + selection = Range_ofZero; + } + + int index = openFileAndGetIndex (file, selection); + final var tab = mBinding.tabs.getTabAt (index); + if (tab != null && index >= 0 && !tab.isSelected ()) { + tab.select (); + } + + if (mBinding.editorDrawerLayout.isDrawerOpen (GravityCompat.END)) { + mBinding.editorDrawerLayout.closeDrawer (GravityCompat.END); + } + + mBinding.editorContainer.setDisplayedChild (index); + + invalidateOptionsMenu (); + try { + return getEditorAtIndex (index); + } catch (Throwable th) { + LOG.error ("Unable to get editor fragment at opened file index", index); + LOG.error (th); + return null; + } + } + + @SuppressLint("NotifyDataSetChanged") + public int openFileAndGetIndex (File file, Range selection) { + final var openedFileIndex = findIndexOfEditorByFile (file); + + if (openedFileIndex != -1) { + LOG.error ("File is already opened. File: " + file); + return openedFileIndex; + } + + final var position = mViewModel.getOpenedFileCount (); + + LOG.info ("Opening file at index:", position, "file: ", file); + + final var editor = new CodeEditorView (this, file, selection); + editor.setLayoutParams (new ViewGroup.LayoutParams ( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + )); + + mBinding.editorContainer.addView (editor); + mBinding.editorContainer.setDisplayedChild (position); + mBinding.tabs.addTab (mBinding.tabs.newTab ().setText (file.getName ())); + + mViewModel.addFile (file); + mViewModel.setCurrentFile (position, file); + + return position; + } + + public void closeFile (int index) { + if (index >= 0 && index < mViewModel.getOpenedFileCount ()) { + var opened = mViewModel.getOpenedFile (index); + LOG.info ("Closing file:", opened); + final var editor = getEditorAtIndex (index); + if (editor != null) { + editor.save (); + editor.getEditor ().close (); + } else { + LOG.error ("Cannot save file before close. Editor instance is null"); + } + + opened = null; + + mViewModel.removeFile (index); + mBinding.tabs.removeTabAt (index); + mBinding.editorContainer.removeViewAt (index); + } else { + LOG.error ("Invalid file index. Cannot close."); + return; + } + + mBinding.tabs.requestLayout (); + invalidateOptionsMenu (); + } + + public void closeAll () { + final var count = mViewModel.getOpenedFileCount (); + + // Save and close all files one by one + for (int i = 0; i < count; i++) { + final var editor = getEditorAtIndex (i); + if (editor != null) { + editor.save (); + editor.getEditor ().close (); + } else { + LOG.error ("Unable to save file at index:", i); + } + } + + mViewModel.removeAllFiles (); + mBinding.tabs.removeAllTabs (); + mBinding.tabs.requestLayout (); + mBinding.editorContainer.removeAllViews (); + + invalidateOptionsMenu (); + } + + public void closeOthers () { + final var file = mViewModel.getCurrentFile (); + for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { + final var editor = getEditorAtIndex (i); + + // Index of files changes as we keep close files + // So we compare the files instead of index + if (editor != null) { + if (!file.equals (editor.getFile ())) { + closeFile (i); + } + } else { + LOG.error ("Unable to save file at index:", i); + } + } + } + + @Nullable + public CodeEditorView getCurrentEditor () { + if (mViewModel.getCurrentFileIndex () != -1) { + return getEditorAtIndex (mViewModel.getCurrentFileIndex ()); + } + + return null; + } + + @Nullable + public CodeEditorView getEditorAtIndex (final int index) { + return (CodeEditorView) mBinding.editorContainer.getChildAt (index); + } + + @Nullable + public CodeEditorView getEditorForFile (@NonNull final File file) { + for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { + final CodeEditorView editor = (CodeEditorView) mBinding.editorContainer.getChildAt (i); + if (editor.getFile ().equals (file)) { + return editor; + } + } + + return null; + } + + public int findIndexOfEditorByFile (File file) { + if (file == null) { + LOG.error ("Cannot find index of a null file."); + return -1; + } + + for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { + final var opened = mViewModel.getOpenedFile (i); + if (opened.equals (file)) { + return i; + } + } + + return -1; + } + + ///////////////////////////////////////////////// + ////////////// PRIVATE APIS ///////////////////// + ///////////////////////////////////////////////// + + private void dispatchOnPauseToEditors () { + CompletableFuture.runAsync (() -> { + for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { + final var editor = getEditorAtIndex (i); + if (editor != null) { + editor.onPause (); + } + } + }); + } + + private void dispatchOnResumeToEditors () { + CompletableFuture.runAsync (() -> { + for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { + final var editor = getEditorAtIndex (i); + if (editor != null) { + editor.onResume (); + } + } + }); + } + + @Contract(pure = true) + private void onGetUIDesignerResult (@NonNull ActivityResult result) { + final var index = mBinding.editorContainer.getDisplayedChild (); + final var editor = getEditorAtIndex (index); + if (editor != null && result.getResultCode () == RESULT_OK) { + final var data = result.getData (); + if (data != null && data.hasExtra (DesignerActivity.KEY_GENERATED_CODE)) { + final var code = data.getStringExtra (DesignerActivity.KEY_GENERATED_CODE); + editor.getEditor ().setText (code); + saveAll (); + } else { + final var msg = getString (R.string.msg_invalid_designer_result); + getApp ().toast (msg, Toaster.Type.ERROR); + LOG.error (msg, "Data returned by UI Designer is null or is invalid."); + } + } else { + LOG.error ("UI Designer returned an invalid result code.", "Result code: " + result.getResultCode ()); + } + } + + + private void previewLayout () { + try { + + if (getCurrentEditor () == null) { + LOG.error ("No file is opened. Cannot preview layout."); + return; + } + + saveAll (false); + if (getApp ().getLayoutInflater () == null) { + LOG.info ("Creating layout inflater instance..."); + getApp ().createInflater (getApp ().createInflaterConfig (getContextProvider (), getResourceDirectories ())); + } + + final Intent intent = new Intent (this, DesignerActivity.class); + intent.putExtra (DesignerActivity.KEY_LAYOUT_PATH, getCurrentEditor ().getFile ().getAbsolutePath ()); + + LOG.info ("Launching UI Designer..."); + mUIDesignerLauncher.launch (intent); + } catch (Throwable th) { + LOG.error (getString (R.string.err_cannot_preview_layout), th); + getApp ().toast (R.string.msg_cannot_preview_layout, Toaster.Type.ERROR); + } + } + + @NonNull + @Contract(" -> new") + private ILayoutInflater.ContextProvider getContextProvider () { + return this::provide; + } + + @NonNull + private Set getResourceDirectories () { + final Set dirs = new HashSet<> (); + if (getAndroidProject () != null && getAndroidProject ().getModulePaths () != null && !getAndroidProject ().getModulePaths ().isEmpty ()) { + for (String path : getAndroidProject ().getModulePaths ()) { + if (path != null && new File (path).exists ()) { + File res = new File (path, "src/main/res"); + if (res.exists ()) { + dirs.add (res); + } + } + } + } + return dirs; + } + + private void openTerminal () { + final Intent intent = new Intent (this, TerminalActivity.class); + intent.putExtra (TerminalActivity.KEY_WORKING_DIRECTORY, Objects.requireNonNull (getAndroidProject ()).getProjectPath ()); + startActivity (intent); + } + + private void startLanguageServers () { + LSP.setActivityProvider (this); + LSP.Java.start (() -> { + Optional result = LSP.Java.init (Objects.requireNonNull (getAndroidProject ()).getProjectPath ()); + if (result.isPresent ()) { + LSP.Java.initialized (); + } + }); + } + + private void getProjectFromIntent () { + final var project = (AndroidProject) getIntent ().getParcelableExtra (EXTRA_PROJECT); + if (mViewModel != null) { + mViewModel.setAndroidProject (project); + } else { + LOG.error ("ViewModel is null. Cannot set project."); + } + + getApp ().getPrefManager ().setOpenedProject (Objects.requireNonNull (this.getAndroidProject ()).getProjectPath ()); + } + + @Nullable + private AndroidProject getAndroidProject () { + if (mViewModel == null) { + return null; + } + + return mViewModel.getAndroidProject (); + } + + @Nullable + private IDEProject getIDEProject () { + if (mViewModel == null) { + return null; + } + + return mViewModel.getIDEProject (); + } + + private void setupSignatureText () { + GradientDrawable gd = new GradientDrawable (); + gd.setShape (GradientDrawable.RECTANGLE); + gd.setColor (0xff212121); + gd.setStroke (1, 0xffffffff); + gd.setCornerRadius (8); + mBinding.symbolText.setBackground (gd); + mBinding.symbolText.setVisibility (View.GONE); + } + + private void setupDiagnosticInfo () { + GradientDrawable gd = new GradientDrawable (); + gd.setShape (GradientDrawable.RECTANGLE); + gd.setColor (0xff212121); + gd.setStroke (1, 0xffffffff); + gd.setCornerRadius (8); + mDiagnosticInfoBinding.getRoot ().setBackground (gd); + mDiagnosticInfoBinding.getRoot ().setVisibility (View.GONE); + } + + private void setupContainers () { + handleDiagnosticsResultVisibility (true); + handleSearchResultVisibility (true); + } + + private void startServices () { + + if (!IDELanguageClientImpl.isInitialized ()) { + IDELanguageClientImpl.initialize (this); + } + + // Actually, we don't need to start FileOptionsHandler + // Because it would work anyway + // But still we do... + mFileOptionsHandler.start (); + + mBuildServiceHandler.start (); + getBuildServiceHandler ().assembleDebug (false); + } + + private void onSoftInputChanged () { + invalidateOptionsMenu (); + if (KeyboardUtils.isSoftInputVisible (this)) { + TransitionManager.beginDelayedTransition (mBinding.getRoot (), new Slide (Gravity.TOP)); + symbolInput.setVisibility (View.VISIBLE); + mBinding.bottomSheet.statusText.setVisibility (View.GONE); + mBinding.bottomSheet.swipeHint.setVisibility (View.GONE); + } else { + TransitionManager.beginDelayedTransition (mBinding.getRoot (), new Slide (Gravity.BOTTOM)); + symbolInput.setVisibility (View.GONE); + mBinding.bottomSheet.statusText.setVisibility (View.VISIBLE); + mBinding.bottomSheet.swipeHint.setVisibility (View.VISIBLE); + } + } + + private void refreshSymbolInput (@NonNull CodeEditorView frag) { + symbolInput.bindEditor (frag.getEditor ()); + symbolInput.setSymbols (Symbols.forFile (frag.getFile ())); + } + + private void notifySyncNeeded () { + if (getBuildService () != null && !getBuildService ().isBuilding ()) { + getSyncBanner () + .setNegative (android.R.string.cancel, null) + .setPositive (android.R.string.ok, v -> getBuildServiceHandler ().assembleDebug (false)) + .show (); + } + } + + private void closeProject (boolean manualFinish) { + if (getBuildService () != null) { + getBuildService ().setListener (null); + if (mBuildServiceHandler != null) { + mBuildServiceHandler.stop (); + } else { + getBuildService ().exit (); + } + } + + // Make sure we close files + // This fill further make sure that file contents are not erased. + closeAll (); + + getApp ().getPrefManager ().setOpenedProject (PreferenceManager.NO_OPENED_PROJECT); + + LSP.shutdownAll (); + + getApp ().stopAllDaemons (); + + if (manualFinish) { + finish (); + } + } + + private void confirmProjectClose () { + final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (this); + builder.setTitle (R.string.title_confirm_project_close); + builder.setMessage (R.string.msg_confirm_project_close); + builder.setNegativeButton (android.R.string.no, null); + builder.setPositiveButton (android.R.string.yes, (d, w) -> { + d.dismiss (); + closeProject (true); + }); + builder.show (); + } + + private AlertDialog getFindInProjectDialog () { + return mFindInProjectDialog == null ? createFindInProjectDialog () : mFindInProjectDialog; + } + + @Nullable + private AlertDialog createFindInProjectDialog () { + if (getAndroidProject () == null + || getAndroidProject ().getModulePaths () == null + || getAndroidProject ().getModulePaths ().size () <= 0) { + getApp ().toast (R.string.msg_no_modules, Toaster.Type.ERROR); + return null; + } + final List modules = getAndroidProject ().getModulePaths (); + final List srcDirs = new ArrayList<> (); + final LayoutSearchProjectBinding binding = LayoutSearchProjectBinding.inflate (getLayoutInflater ()); + binding.modulesContainer.removeAllViews (); + for (int i = 0; i < modules.size (); i++) { + final File file = new File (modules.get (i)); + final File src = new File (file, "src"); + if (!file.exists () || !file.isDirectory () || !src.exists () || !src.isDirectory ()) { + continue; + } + + CheckBox check = new CheckBox (this); + check.setText (file.getName ()); + check.setChecked (true); + + LinearLayout.MarginLayoutParams params = new LinearLayout.MarginLayoutParams (-2, -2); + params.bottomMargin = SizeUtils.dp2px (4); + binding.modulesContainer.addView (check, params); + + srcDirs.add (src); + } + final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (this); + builder.setTitle (R.string.menu_find_project); + builder.setView (binding.getRoot ()); + builder.setCancelable (false); + builder.setPositiveButton (R.string.menu_find, (dialog, which) -> { + final String text = Objects.requireNonNull (binding.input.getEditText ()).getText ().toString ().trim (); + if (text.isEmpty ()) { + getApp ().toast (R.string.msg_empty_search_query, Toaster.Type.ERROR); + return; + } + + final List searchDirs = new ArrayList<> (); + for (int i = 0; i < mViewModel.getOpenedFileCount (); i++) { + CheckBox check = (CheckBox) binding.modulesContainer.getChildAt (i); + if (check.isChecked ()) { + searchDirs.add (srcDirs.get (i)); + } + } + + final String extensions = Objects.requireNonNull (binding.filter.getEditText ()).getText ().toString ().trim (); + final List extensionList = new ArrayList<> (); + if (!extensions.isEmpty ()) { + if (extensions.contains ("|")) { + for (String str : extensions.split (Pattern.quote ("|"))) { + if (str == null || str.trim ().isEmpty ()) { + continue; + } + + extensionList.add (str); + } + } else { + extensionList.add (extensions); + } + } + + if (searchDirs.isEmpty ()) { + getApp ().toast (R.string.msg_select_search_modules, Toaster.Type.ERROR); + } else { + dialog.dismiss (); + getProgressSheet (R.string.msg_searching_project).show (getSupportFragmentManager (), "search_in_project_progress"); + RecursiveFileSearcher.searchRecursiveAsync (text, extensionList, searchDirs, this::handleSearchResults); + } + }); + builder.setNegativeButton (android.R.string.cancel, (__, ___) -> __.dismiss ()); + mFindInProjectDialog = builder.create (); + return mFindInProjectDialog; + } + + private void registerLogReceiver () { + IntentFilter filter = new IntentFilter (); + filter.addAction (LogReceiver.APPEND_LOG); + registerReceiver (mLogReceiver, filter); + } + + private IDEService getBuildService () { + return mBuildServiceHandler.getService (); + } + + public BuildServiceHandler getBuildServiceHandler () { + return mBuildServiceHandler; + } + + private void showNeedHelpDialog () { + MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (this); + builder.setTitle (R.string.need_help); + builder.setMessage (R.string.msg_need_help); + builder.setPositiveButton (android.R.string.ok, null); + builder.create ().show (); + } + + private void createQuickActions () { + ActionItem closeThis = new ActionItem (ACTION_ID_CLOSE, getString (R.string.action_closeThis), R.drawable.ic_close_this); + ActionItem closeOthers = new ActionItem (ACTION_ID_OTHERS, getString (R.string.action_closeOthers), R.drawable.ic_close_others); + ActionItem closeAll = new ActionItem (ACTION_ID_ALL, getString (R.string.action_closeAll), R.drawable.ic_close_all); + mTabCloseAction = new QuickAction (this, QuickAction.HORIZONTAL); + mTabCloseAction.addActionItem (closeThis, closeOthers, closeAll); + mTabCloseAction.setColorRes (R.color.tabAction_background); + mTabCloseAction.setTextColorRes (R.color.tabAction_text); + + mTabCloseAction.setOnActionItemClickListener ((item) -> { + final int id = item.getActionId (); + saveAll (); + if (id == ACTION_ID_CLOSE) { + closeFile (mBinding.tabs.getSelectedTabPosition ()); + } + + if (id == ACTION_ID_OTHERS) { + closeOthers (); + closeOthers (); + } + + if (id == ACTION_ID_ALL) { + closeAll (); + } + }); + } + + private static final Logger LOG = Logger.instance ("EditorActivity"); +} diff --git a/app/src/main/java/com/itsaky/androidide/MainActivity.java b/app/src/main/java/com/itsaky/androidide/MainActivity.java index d4e5f9ffa..84f45a911 100755 --- a/app/src/main/java/com/itsaky/androidide/MainActivity.java +++ b/app/src/main/java/com/itsaky/androidide/MainActivity.java @@ -1,4 +1,4 @@ -/************************************************************************************ +/* * This file is part of AndroidIDE. * * @@ -16,19 +16,15 @@ * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . * - **************************************************************************************/ + */ package com.itsaky.androidide; -import abhishekti7.unicorn.filepicker.UnicornFilePicker; - import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.view.View; -import androidx.annotation.NonNull; -import androidx.transition.Transition; import androidx.transition.TransitionManager; import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -56,6 +52,8 @@ import java.util.ArrayList; import java.util.Objects; import java.util.concurrent.Callable; +import abhishekti7.unicorn.filepicker.UnicornFilePicker; + public class MainActivity extends StudioActivity implements View.OnClickListener, ProjectWriterCallback { private ActivityMainBinding binding; diff --git a/app/src/main/java/com/itsaky/androidide/adapters/AttributeListAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/AttributeListAdapter.java index 0c36e19c9..0e678e8fd 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/AttributeListAdapter.java +++ b/app/src/main/java/com/itsaky/androidide/adapters/AttributeListAdapter.java @@ -1,99 +1,99 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.adapters; - -import android.graphics.Typeface; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.style.ForegroundColorSpan; -import android.text.style.StyleSpan; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import androidx.recyclerview.widget.RecyclerView; - -import com.itsaky.androidide.R; -import com.itsaky.androidide.databinding.LayoutAttrEditorSheetItemBinding; -import com.itsaky.androidide.models.XMLAttribute; - -import java.util.List; - -public class AttributeListAdapter extends RecyclerView.Adapter { - - private final List attributes; - private final OnClickListener clickListener; - - public AttributeListAdapter (List attributes, OnClickListener clickListener) { - this.attributes = attributes; - this.clickListener = clickListener; - } - - @NonNull - @Override - public VH onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { - return new VH (LayoutAttrEditorSheetItemBinding.inflate (LayoutInflater.from (parent.getContext ()), parent, false)); - } - - // hello th - // - - @Override - public void onBindViewHolder (@NonNull VH holder, int position) { - final var binding = holder.binding; - final var attr = attributes.get (position); - final var sb = new SpannableStringBuilder (); - sb.append (attr.getAttributeName ()); - if (attr.isApplied ()) { - sb.append (" = "); - sb.append (attr.getValue (), new StyleSpan (Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); - - final var length = attr.getValue ().length (); - sb.setSpan (new ForegroundColorSpan (ContextCompat.getColor (binding.text.getContext (), R.color.primaryTextColor)), - sb.length () - length, - sb.length () - 1, - SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); - } - - binding.text.setText (sb); - binding.getRoot ().setOnClickListener (v -> { - if (this.clickListener != null) { - this.clickListener.onClick (binding, attr); - } - }); - } - - @Override - public int getItemCount () { - return attributes.size (); - } - - static class VH extends RecyclerView.ViewHolder { - LayoutAttrEditorSheetItemBinding binding; - - VH (@NonNull LayoutAttrEditorSheetItemBinding binding) { - super(binding.getRoot ()); - this.binding = binding; - } - } - - public interface OnClickListener { - void onClick (LayoutAttrEditorSheetItemBinding binding, XMLAttribute attribute); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.adapters; + +import android.graphics.Typeface; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.style.ForegroundColorSpan; +import android.text.style.StyleSpan; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.itsaky.androidide.R; +import com.itsaky.androidide.databinding.LayoutAttrEditorSheetItemBinding; +import com.itsaky.androidide.models.XMLAttribute; + +import java.util.List; + +public class AttributeListAdapter extends RecyclerView.Adapter { + + private final List attributes; + private final OnClickListener clickListener; + + public AttributeListAdapter (List attributes, OnClickListener clickListener) { + this.attributes = attributes; + this.clickListener = clickListener; + } + + @NonNull + @Override + public VH onCreateViewHolder (@NonNull ViewGroup parent, int viewType) { + return new VH (LayoutAttrEditorSheetItemBinding.inflate (LayoutInflater.from (parent.getContext ()), parent, false)); + } + + // hello th + // + + @Override + public void onBindViewHolder (@NonNull VH holder, int position) { + final var binding = holder.binding; + final var attr = attributes.get (position); + final var sb = new SpannableStringBuilder (); + sb.append (attr.getAttributeName ()); + if (attr.isApplied ()) { + sb.append (" = "); + sb.append (attr.getValue (), new StyleSpan (Typeface.BOLD), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + final var length = attr.getValue ().length (); + sb.setSpan (new ForegroundColorSpan (ContextCompat.getColor (binding.text.getContext (), R.color.primaryTextColor)), + sb.length () - length, + sb.length () - 1, + SpannableStringBuilder.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + binding.text.setText (sb); + binding.getRoot ().setOnClickListener (v -> { + if (this.clickListener != null) { + this.clickListener.onClick (binding, attr); + } + }); + } + + @Override + public int getItemCount () { + return attributes.size (); + } + + static class VH extends RecyclerView.ViewHolder { + LayoutAttrEditorSheetItemBinding binding; + + VH (@NonNull LayoutAttrEditorSheetItemBinding binding) { + super(binding.getRoot ()); + this.binding = binding; + } + } + + public interface OnClickListener { + void onClick (LayoutAttrEditorSheetItemBinding binding, XMLAttribute attribute); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.java index 164f4fd3a..ee87332b9 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.java +++ b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.java @@ -1,118 +1,118 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.adapters; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; - -import com.itsaky.androidide.R; -import com.itsaky.androidide.fragments.SimpleOutputFragment; -import com.itsaky.androidide.fragments.DiagnosticsListFragment; -import com.itsaky.androidide.fragments.IDELogFragment; -import com.itsaky.androidide.fragments.LogViewFragment; -import com.itsaky.androidide.fragments.SearchResultFragment; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class EditorBottomSheetTabAdapter extends FragmentStateAdapter { - - private final List fragments; - - public EditorBottomSheetTabAdapter (@NonNull FragmentActivity fragmentActivity) { - super (fragmentActivity); - - this.fragments = new ArrayList<> (); - this.fragments.add (new Tab (fragmentActivity.getString (R.string.build_output), new SimpleOutputFragment ())); - this.fragments.add (new Tab (fragmentActivity.getString (R.string.app_logs), new LogViewFragment ())); - this.fragments.add (new Tab (fragmentActivity.getString (R.string.ide_logs), new IDELogFragment ())); - this.fragments.add (new Tab (fragmentActivity.getString (R.string.view_diags), new DiagnosticsListFragment ())); - this.fragments.add (new Tab (fragmentActivity.getString (R.string.view_search_results), new SearchResultFragment ())); - } - - @NonNull - @Override - public Fragment createFragment (int position) { - return this.fragments.get (position).fragment; - } - - @Override - public int getItemCount () { - return fragments.size (); - } - - public String getTitle (int position) { - return fragments.get (position).title; - } - - @NonNull - public SimpleOutputFragment getBuildOutputFragment () { - return Objects.requireNonNull (findFragmentByClass (SimpleOutputFragment.class)); - } - - @NonNull - public LogViewFragment getLogFragment () { - return Objects.requireNonNull (findFragmentByClass (LogViewFragment.class)); - } - - @NonNull - public DiagnosticsListFragment getDiagnosticsFragment () { - return Objects.requireNonNull (findFragmentByClass (DiagnosticsListFragment.class)); - } - - @NonNull - public SearchResultFragment getSearchResultFragment () { - return Objects.requireNonNull (findFragmentByClass (SearchResultFragment.class)); - } - - @Nullable - private T findFragmentByClass (Class clazz) { - for (final var tab : this.fragments) { - if (tab.fragment.getClass () == clazz) { - return (T) tab.fragment; - } - } - return null; - } - - public int findIndexOfFragmentByClass (Class tClass) { - for (int i = 0; i < this.fragments.size (); i++) { - final var tab = this.fragments.get (i); - if (tab.fragment.getClass () == tClass) { - return i; - } - } - - return -1; - } - - static class Tab { - - public Tab (String title, Fragment fragment) { - this.title = title; - this.fragment = fragment; - } - - String title; - Fragment fragment; - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.adapters; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import com.itsaky.androidide.R; +import com.itsaky.androidide.fragments.SimpleOutputFragment; +import com.itsaky.androidide.fragments.DiagnosticsListFragment; +import com.itsaky.androidide.fragments.IDELogFragment; +import com.itsaky.androidide.fragments.LogViewFragment; +import com.itsaky.androidide.fragments.SearchResultFragment; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class EditorBottomSheetTabAdapter extends FragmentStateAdapter { + + private final List fragments; + + public EditorBottomSheetTabAdapter (@NonNull FragmentActivity fragmentActivity) { + super (fragmentActivity); + + this.fragments = new ArrayList<> (); + this.fragments.add (new Tab (fragmentActivity.getString (R.string.build_output), new SimpleOutputFragment ())); + this.fragments.add (new Tab (fragmentActivity.getString (R.string.app_logs), new LogViewFragment ())); + this.fragments.add (new Tab (fragmentActivity.getString (R.string.ide_logs), new IDELogFragment ())); + this.fragments.add (new Tab (fragmentActivity.getString (R.string.view_diags), new DiagnosticsListFragment ())); + this.fragments.add (new Tab (fragmentActivity.getString (R.string.view_search_results), new SearchResultFragment ())); + } + + @NonNull + @Override + public Fragment createFragment (int position) { + return this.fragments.get (position).fragment; + } + + @Override + public int getItemCount () { + return fragments.size (); + } + + public String getTitle (int position) { + return fragments.get (position).title; + } + + @NonNull + public SimpleOutputFragment getBuildOutputFragment () { + return Objects.requireNonNull (findFragmentByClass (SimpleOutputFragment.class)); + } + + @NonNull + public LogViewFragment getLogFragment () { + return Objects.requireNonNull (findFragmentByClass (LogViewFragment.class)); + } + + @NonNull + public DiagnosticsListFragment getDiagnosticsFragment () { + return Objects.requireNonNull (findFragmentByClass (DiagnosticsListFragment.class)); + } + + @NonNull + public SearchResultFragment getSearchResultFragment () { + return Objects.requireNonNull (findFragmentByClass (SearchResultFragment.class)); + } + + @Nullable + private T findFragmentByClass (Class clazz) { + for (final var tab : this.fragments) { + if (tab.fragment.getClass () == clazz) { + return (T) tab.fragment; + } + } + return null; + } + + public int findIndexOfFragmentByClass (Class tClass) { + for (int i = 0; i < this.fragments.size (); i++) { + final var tab = this.fragments.get (i); + if (tab.fragment.getClass () == tClass) { + return i; + } + } + + return -1; + } + + static class Tab { + + public Tab (String title, Fragment fragment) { + this.title = title; + this.fragment = fragment; + } + + String title; + Fragment fragment; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java deleted file mode 100755 index d70207206..000000000 --- a/app/src/main/java/com/itsaky/androidide/adapters/EditorPagerAdapter.java +++ /dev/null @@ -1,133 +0,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 . - * - */ - -package com.itsaky.androidide.adapters; - -import androidx.annotation.NonNull; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.recyclerview.widget.DiffUtil; -import androidx.viewpager2.adapter.FragmentStateAdapter; - -import com.itsaky.androidide.fragments.EditorFragment; -import com.itsaky.androidide.models.OpenedFile; -import com.itsaky.androidide.utils.Logger; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class EditorPagerAdapter extends FragmentStateAdapter implements EditorFragment.ModificationStateListener { - - private final List mFiles; - private final EditorFragment.FileOpenListener mFileOpenListener; - - private static final Logger LOG = Logger.instance ("EditorPagerAdapter"); - - public EditorPagerAdapter (final FragmentActivity activity, EditorFragment.FileOpenListener fileOpenListener) { - super (activity); - this.mFileOpenListener = fileOpenListener; - this.mFiles = new ArrayList<> (); - } - - public void setFiles (List files) { - final var result = DiffUtil.calculateDiff (new DiffUtil.Callback () { - @Override - public int getOldListSize () { - return mFiles.size (); - } - - @Override - public int getNewListSize () { - return files.size (); - } - - @Override - public boolean areItemsTheSame (int oldItemPosition, int newItemPosition) { - return Objects.equals (mFiles.get (oldItemPosition), files.get (newItemPosition)); - } - - @Override - public boolean areContentsTheSame (int oldItemPosition, int newItemPosition) { - return areItemsTheSame (oldItemPosition, newItemPosition); - } - }); - mFiles.clear (); - mFiles.addAll (files); - result.dispatchUpdatesTo (this); - } - - - public String getEditorTitle (int index) { - return mFiles.get (index).getFile ().getName (); - } - - public List getFiles () { - return mFiles; - } - - public OpenedFile getOpenedFile (int index) { - return mFiles.get (index); - } - - @Override - public void onModified (EditorFragment editor) { - } - - @Override - public void onSaved (EditorFragment editor) { - } - - @NonNull - @Override - public Fragment createFragment (int position) { - return mFiles.get (position) - .createFragment (this.mFileOpenListener, this); - } - - @Override - public int getItemCount () { - return mFiles.size (); - } - - @Override - public long getItemId (int position) { - if (mFiles.isEmpty ()) { - LOG.warn ("File list is empty but getItemId(int) is being called. Returning -1"); - return -1; - } - - final var id = mFiles.get (position).hashCode (); - LOG.debug ("Item ID for editor fragment at position " + position + " is " + id); - return id; - } - - @Override - public boolean containsItem (long itemId) { - for (final var file : mFiles) { - if (itemId == file.hashCode ()) { // see getItemId(int) - return true; - } - } - - LOG.warn ("EditorPagerAdapter does not contain any file with item ID : " + itemId); - return false; - } -} diff --git a/app/src/main/java/com/itsaky/androidide/adapters/MainPagerAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/MainPagerAdapter.java deleted file mode 100755 index 315abfe2e..000000000 --- a/app/src/main/java/com/itsaky/androidide/adapters/MainPagerAdapter.java +++ /dev/null @@ -1,60 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.adapters; -import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentManager; -import androidx.fragment.app.FragmentPagerAdapter; -import java.util.ArrayList; - -public class MainPagerAdapter extends FragmentPagerAdapter -{ - private ArrayList mFragmentList; - - public MainPagerAdapter(FragmentManager manager) - { - super(manager); - mFragmentList = new ArrayList<>(); - } - - @Override - public Fragment getItem(int p1) - { - return mFragmentList != null && p1 < mFragmentList.size() ? mFragmentList.get(p1) : null; - } - - @Override - public int getCount() - { - return mFragmentList != null ? mFragmentList.size() : 0; - } - - public MainPagerAdapter addFragment(Fragment frag) - { - getFragments().add(frag); - return this; - } - - public ArrayList getFragments() - { - return mFragmentList == null ? mFragmentList = new ArrayList<>() : mFragmentList; - } -} diff --git a/app/src/main/java/com/itsaky/androidide/adapters/ModuleInfoAdapter.java b/app/src/main/java/com/itsaky/androidide/adapters/ModuleInfoAdapter.java deleted file mode 100755 index 1e4767907..000000000 --- a/app/src/main/java/com/itsaky/androidide/adapters/ModuleInfoAdapter.java +++ /dev/null @@ -1,88 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.adapters; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.text.SpannableStringBuilder; -import android.view.LayoutInflater; -import android.view.ViewGroup; -import androidx.recyclerview.widget.RecyclerView; -import com.itsaky.androidide.databinding.LayoutModuleInfoItemBinding; -import com.itsaky.androidide.project.IDEModule; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; - -public class ModuleInfoAdapter extends RecyclerView.Adapter { - - private List modules; - - public ModuleInfoAdapter(List modules) { - this.modules = modules; - if(this.modules == null) - this.modules = new ArrayList<>(); - } - - @Override - public ModuleInfoAdapter.VH onCreateViewHolder(ViewGroup p1, int p2) { - return new VH(LayoutModuleInfoItemBinding.inflate(LayoutInflater.from(p1.getContext()), p1, false)); - } - - @Override - @SuppressLint("SetTextI18n") - public void onBindViewHolder(ModuleInfoAdapter.VH p1, int p2) { - final IDEModule module = modules.get(p2); - final LayoutModuleInfoItemBinding binding = p1.binding; - final Context ctx = binding.getRoot().getContext(); - binding.moduleName.setText(module.displayName.substring(0, 1).toUpperCase(Locale.US) + module.displayName.substring(1)); - - SpannableStringBuilder summaryBuilder = new SpannableStringBuilder(); - if(!module.isLibrary) { - summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_package_name, module.applicationId)); - summaryBuilder.append("\n"); - } - summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_min_sdk, module.minSdk.apiString)); - summaryBuilder.append("\n"); - summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_target_sdk, module.targetSdk.apiString)); - summaryBuilder.append("\n"); - summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_dependency_count, module.dependencies.size())); - summaryBuilder.append("\n"); - summaryBuilder.append(ctx.getString(com.itsaky.androidide.R.string.msg_task_count, module.tasks.size())); - - binding.moduleSummary.setText(summaryBuilder); - } - - @Override - public int getItemCount() { - return modules.size(); - } - - public class VH extends RecyclerView.ViewHolder { - LayoutModuleInfoItemBinding binding; - - public VH(LayoutModuleInfoItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } -} diff --git a/app/src/main/java/com/itsaky/androidide/app/StudioApp.java b/app/src/main/java/com/itsaky/androidide/app/StudioApp.java index f6abb832d..4a7f09c77 100755 --- a/app/src/main/java/com/itsaky/androidide/app/StudioApp.java +++ b/app/src/main/java/com/itsaky/androidide/app/StudioApp.java @@ -1,159 +1,156 @@ -/************************************************************************************ - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - * -**************************************************************************************/ -package com.itsaky.androidide.app; - -import android.content.Intent; - -import com.blankj.utilcode.util.ThrowableUtils; -import com.google.firebase.messaging.FirebaseMessaging; -import com.itsaky.androidide.CrashHandlerActivity; -import com.itsaky.androidide.language.xml.completion.XMLCompletionService; -import com.itsaky.androidide.project.ProjectResourceFinder; -import com.itsaky.androidide.services.IDELogService; -import com.itsaky.androidide.services.MessagingService; -import com.itsaky.inflater.ILayoutInflater; -import com.itsaky.inflater.IResourceFinder; -import com.itsaky.inflater.LayoutInflaterConfiguration; -import com.itsaky.androidide.utils.FileUtil; -import com.itsaky.androidide.utils.Logger; -import com.itsaky.apiinfo.ApiInfo; -import com.itsaky.attrinfo.AttrInfo; -import com.itsaky.widgets.WidgetInfo; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.util.Set; - -public class StudioApp extends BaseApplication { - - private static StudioApp instance; - private static ApiInfo mApiInfo; - private static AttrInfo mAttrInfo; - private static WidgetInfo mWidgetInfo; - - private IResourceFinder mResFinder; - private XMLCompletionService mXmlCompletionService; - private ILayoutInflater mLayoutInflater; - private Thread.UncaughtExceptionHandler uncaughtExceptionHandler; - - public static final boolean DEBUG = com.itsaky.androidide.BuildConfig.DEBUG; - - private static final Logger LOG = Logger.instance("StudioApp"); - - @Override - public void onCreate() { - instance = this; - this.uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); - Thread.setDefaultUncaughtExceptionHandler(this::handleCrash); - super.onCreate(); - - FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_UPDATE); - FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_DEV_MSGS); - - initializeApiInformation(); - } - - private void handleCrash(Thread thread, Throwable th) { - writeException(th); - - try { - final var intent = new Intent (); - intent.setAction (CrashHandlerActivity.REPORT_ACTION); - intent.putExtra (CrashHandlerActivity.TRACE_KEY, ThrowableUtils.getFullStackTrace (th)); - intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity (intent); - - if(this.uncaughtExceptionHandler != null) { - this.uncaughtExceptionHandler.uncaughtException(thread, th); - } - System.exit (1); - } catch (Throwable error) { - LOG.error ("Unable to show crash handler activity", error); - } - } - - public void createInflater (LayoutInflaterConfiguration config) { - this.mLayoutInflater = ILayoutInflater.newInstance(config); - } - - public LayoutInflaterConfiguration createInflaterConfig(ILayoutInflater.ContextProvider contextProvider, Set resDirs) { - return new LayoutInflaterConfiguration.Builder () - .setAttrInfo(this.attrInfo()) - .setWidgetInfo(this.widgetInfo()) - .setResourceFinder(mResFinder == null ? mResFinder = new ProjectResourceFinder() : mResFinder) - .setResourceDirectories(resDirs) - .setContextProvider(contextProvider) - .create(); - } - - public ILayoutInflater getLayoutInflater () { - return mLayoutInflater; - } - - public IResourceFinder getResFinder () { - return this.mResFinder; - } - - /** - * Reads API version information from api-versions.xml - */ - public void initializeApiInformation() { - new Thread(() -> { - try { - mApiInfo = new ApiInfo (StudioApp.this); - mAttrInfo = new AttrInfo (StudioApp.this); - mWidgetInfo = new WidgetInfo (StudioApp.this); - - mXmlCompletionService = new XMLCompletionService (mAttrInfo, mWidgetInfo); - } catch (Throwable th) { - LOG.error (getString (com.itsaky.androidide.R.string.err_init_sdkinfo), th); - } - }, "SDK Information Loader").start(); - } - - public void stopAllDaemons() { - newShell(null).bgAppend("gradle --stop"); - } - - public XMLCompletionService getXmlCompletionService() { - return mXmlCompletionService; - } - - public ApiInfo apiInfo() { - return mApiInfo; - } - - public AttrInfo attrInfo () { - return mAttrInfo; - } - - public WidgetInfo widgetInfo () { - return mWidgetInfo; - } - - public boolean isXmlServiceStarted() { - return mXmlCompletionService != null; - } - - public void setStopGradleDaemon(boolean startGradleDaemon) { - } - - public static StudioApp getInstance() { - return instance; - } -} +/************************************************************************************ + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + * +**************************************************************************************/ +package com.itsaky.androidide.app; + +import android.content.Intent; + +import com.blankj.utilcode.util.ThrowableUtils; +import com.google.firebase.messaging.FirebaseMessaging; +import com.itsaky.androidide.CrashHandlerActivity; +import com.itsaky.androidide.language.xml.completion.XMLCompletionService; +import com.itsaky.androidide.project.ProjectResourceFinder; +import com.itsaky.androidide.services.MessagingService; +import com.itsaky.androidide.utils.Logger; +import com.itsaky.apiinfo.ApiInfo; +import com.itsaky.attrinfo.AttrInfo; +import com.itsaky.inflater.ILayoutInflater; +import com.itsaky.inflater.IResourceFinder; +import com.itsaky.inflater.LayoutInflaterConfiguration; +import com.itsaky.widgets.WidgetInfo; + +import java.io.File; +import java.util.Set; + +public class StudioApp extends BaseApplication { + + private static StudioApp instance; + private static ApiInfo mApiInfo; + private static AttrInfo mAttrInfo; + private static WidgetInfo mWidgetInfo; + + private IResourceFinder mResFinder; + private XMLCompletionService mXmlCompletionService; + private ILayoutInflater mLayoutInflater; + private Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + + public static final boolean DEBUG = com.itsaky.androidide.BuildConfig.DEBUG; + + private static final Logger LOG = Logger.instance("StudioApp"); + + @Override + public void onCreate() { + instance = this; + this.uncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); + Thread.setDefaultUncaughtExceptionHandler(this::handleCrash); + super.onCreate(); + + FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_UPDATE); + FirebaseMessaging.getInstance().subscribeToTopic(MessagingService.TOPIC_DEV_MSGS); + + initializeApiInformation(); + } + + private void handleCrash(Thread thread, Throwable th) { + writeException(th); + + try { + final var intent = new Intent (); + intent.setAction (CrashHandlerActivity.REPORT_ACTION); + intent.putExtra (CrashHandlerActivity.TRACE_KEY, ThrowableUtils.getFullStackTrace (th)); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity (intent); + + if(this.uncaughtExceptionHandler != null) { + this.uncaughtExceptionHandler.uncaughtException(thread, th); + } + System.exit (1); + } catch (Throwable error) { + LOG.error ("Unable to show crash handler activity", error); + } + } + + public void createInflater (LayoutInflaterConfiguration config) { + this.mLayoutInflater = ILayoutInflater.newInstance(config); + } + + public LayoutInflaterConfiguration createInflaterConfig(ILayoutInflater.ContextProvider contextProvider, Set resDirs) { + return new LayoutInflaterConfiguration.Builder () + .setAttrInfo(this.attrInfo()) + .setWidgetInfo(this.widgetInfo()) + .setResourceFinder(mResFinder == null ? mResFinder = new ProjectResourceFinder() : mResFinder) + .setResourceDirectories(resDirs) + .setContextProvider(contextProvider) + .create(); + } + + public ILayoutInflater getLayoutInflater () { + return mLayoutInflater; + } + + public IResourceFinder getResFinder () { + return this.mResFinder; + } + + /** + * Reads API version information from api-versions.xml + */ + public void initializeApiInformation() { + new Thread(() -> { + try { + mApiInfo = new ApiInfo (StudioApp.this); + mAttrInfo = new AttrInfo (StudioApp.this); + mWidgetInfo = new WidgetInfo (StudioApp.this); + + mXmlCompletionService = new XMLCompletionService (mAttrInfo, mWidgetInfo); + } catch (Throwable th) { + LOG.error (getString (com.itsaky.androidide.R.string.err_init_sdkinfo), th); + } + }, "SDK Information Loader").start(); + } + + public void stopAllDaemons() { + newShell(null).bgAppend("gradle --stop"); + } + + public XMLCompletionService getXmlCompletionService() { + return mXmlCompletionService; + } + + public ApiInfo apiInfo() { + return mApiInfo; + } + + public AttrInfo attrInfo () { + return mAttrInfo; + } + + public WidgetInfo widgetInfo () { + return mWidgetInfo; + } + + public boolean isXmlServiceStarted() { + return mXmlCompletionService != null; + } + + public void setStopGradleDaemon(boolean startGradleDaemon) { + } + + public static StudioApp getInstance() { + return instance; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/DiagnosticsListFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/DiagnosticsListFragment.java index 2081c7841..4380e9376 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/DiagnosticsListFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/DiagnosticsListFragment.java @@ -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 . - */ - -package com.itsaky.androidide.fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.itsaky.androidide.adapters.DiagnosticsAdapter; -import com.itsaky.androidide.views.EmptyView; - -import java.util.ArrayList; - -public class DiagnosticsListFragment extends Fragment { - - private RecyclerView list; - private EmptyView emptyView; - - private DiagnosticsAdapter unsavedAdapter; - - @Nullable - @Override - public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) { - final var container = new LinearLayout (inflater.getContext ()); - container.setOrientation (LinearLayout.VERTICAL); - container.setLayoutParams (new ViewGroup.LayoutParams (-1, -1)); - - this.list = new RecyclerView (inflater.getContext ()); - this.emptyView = new EmptyView (inflater.getContext ()); - - container.addView (list, new LinearLayout.LayoutParams (-1, -1)); - container.addView (emptyView, new LinearLayout.LayoutParams (-1, -1)); - - return container; - } - - @Override - public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated (view, savedInstanceState); - - list.setLayoutManager (new LinearLayoutManager (list.getContext ())); - list.setAdapter (unsavedAdapter != null ? unsavedAdapter : new DiagnosticsAdapter (new ArrayList<> (), null)); - unsavedAdapter = null; - } - - public void setAdapter (@NonNull DiagnosticsAdapter adapter) { - if (list != null) { - list.setAdapter (adapter); - } else { - unsavedAdapter = adapter; - } - } - - public void handleResultVisibility (boolean error) { - if (emptyView != null && list != null) { - emptyView.setVisibility (error ? View.VISIBLE : View.GONE); - list.setVisibility (error ? View.GONE : View.VISIBLE); - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.itsaky.androidide.adapters.DiagnosticsAdapter; +import com.itsaky.androidide.views.EmptyView; + +import java.util.ArrayList; + +public class DiagnosticsListFragment extends Fragment { + + private RecyclerView list; + private EmptyView emptyView; + + private DiagnosticsAdapter unsavedAdapter; + + @Nullable + @Override + public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) { + final var container = new LinearLayout (inflater.getContext ()); + container.setOrientation (LinearLayout.VERTICAL); + container.setLayoutParams (new ViewGroup.LayoutParams (-1, -1)); + + this.list = new RecyclerView (inflater.getContext ()); + this.emptyView = new EmptyView (inflater.getContext ()); + + container.addView (list, new LinearLayout.LayoutParams (-1, -1)); + container.addView (emptyView, new LinearLayout.LayoutParams (-1, -1)); + + return container; + } + + @Override + public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated (view, savedInstanceState); + + list.setLayoutManager (new LinearLayoutManager (list.getContext ())); + list.setAdapter (unsavedAdapter != null ? unsavedAdapter : new DiagnosticsAdapter (new ArrayList<> (), null)); + unsavedAdapter = null; + } + + public void setAdapter (@NonNull DiagnosticsAdapter adapter) { + if (list != null) { + list.setAdapter (adapter); + } else { + unsavedAdapter = adapter; + } + } + + public void handleResultVisibility (boolean error) { + if (emptyView != null && list != null) { + emptyView.setVisibility (error ? View.VISIBLE : View.GONE); + list.setVisibility (error ? View.GONE : View.VISIBLE); + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java deleted file mode 100755 index 1c00990f2..000000000 --- a/app/src/main/java/com/itsaky/androidide/fragments/EditorFragment.java +++ /dev/null @@ -1,463 +0,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 . - * - **************************************************************************************/ -package com.itsaky.androidide.fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.NonNull; - -import com.blankj.utilcode.util.FileIOUtils; -import com.blankj.utilcode.util.SizeUtils; -import com.itsaky.androidide.adapters.CompletionListAdapter; -import com.itsaky.androidide.databinding.FragmentEditorBinding; -import com.itsaky.androidide.language.groovy.GroovyLanguage; -import com.itsaky.androidide.language.java.JavaLanguage; -import com.itsaky.androidide.language.xml.XMLLanguage; -import com.itsaky.androidide.lexers.xml.XMLLexer; -import com.itsaky.androidide.managers.PreferenceManager; -import com.itsaky.androidide.models.ConstantsBridge; -import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE; -import com.itsaky.androidide.tasks.TaskExecutor; -import com.itsaky.androidide.tasks.callables.ReadFileTask; -import com.itsaky.androidide.utils.LSPUtils; -import com.itsaky.androidide.utils.Logger; -import com.itsaky.androidide.utils.TypefaceUtils; - -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.Token; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; - -import java.io.File; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import io.github.rosemoe.editor.interfaces.EditorEventListener; -import io.github.rosemoe.editor.langs.EmptyLanguage; -import io.github.rosemoe.editor.widget.CodeEditor; - -public class EditorFragment extends BaseFragment implements EditorEventListener { - - public FragmentEditorBinding mBinding; - private File mFile; - private boolean isModified = false; - private boolean isFirstCreate = false; - - private ModificationStateListener mModificationStateListener; - private FileOpenListener mOpenListener; - - private static final Logger LOG = Logger.instance ("EditorFragment"); - - public static final String KEY_FILE_PATH = "file_path"; - public static final String KEY_LINE_START = "line_start"; - public static final String KEY_LINE_END = "line_end"; - public static final String KEY_COLUMN_START = "col_start"; - public static final String KEY_COLUMN_END = "col_end"; - - public static final String EXT_JAVA = ".java"; - public static final String EXT_XML = ".xml"; - public static final String EXT_HTML = ".html"; - public static final String EXT_GRADLE = ".gradle"; - public static final String EXT_GROOVY = ".groovy"; - public static final String EXT_KOTLIN = ".kt"; - public static final String EXT_JSON = ".json"; - - public EditorFragment setModificationStateListener (ModificationStateListener listener) { - this.mModificationStateListener = listener; - return this; - } - - public FragmentEditorBinding getBinding () { - return mBinding; - } - - @NonNull - public static EditorFragment newInstance (@NonNull File file, @NonNull Range selection) { - Bundle bundle = new Bundle (); - bundle.putString (KEY_FILE_PATH, file.getAbsolutePath ()); - bundle.putInt (KEY_LINE_START, selection.getStart ().getLine ()); - bundle.putInt (KEY_LINE_END, selection.getEnd ().getLine ()); - bundle.putInt (KEY_COLUMN_START, selection.getStart ().getCharacter ()); - bundle.putInt (KEY_COLUMN_END, selection.getEnd ().getCharacter ()); - - EditorFragment frag = new EditorFragment (); - frag.setArguments (bundle); - return frag; - } - - public EditorFragment setFileOpenListener (FileOpenListener openListener) { - this.mOpenListener = openListener; - return this; - } - - public CodeEditor getEditor () { - if (mBinding == null) { - return null; - } - return mBinding.editor; - } - - @Override - public void onCreate (Bundle savedInstanceState) { - super.onCreate (savedInstanceState); - isFirstCreate = true; - } - - @Override - public void onSaveInstanceState (@NonNull Bundle outState) { - save (); - super.onSaveInstanceState (outState); - } - - @Override - public View onCreateView (@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - mBinding = FragmentEditorBinding.inflate (inflater, container, false); - return mBinding.getRoot (); - } - - @Override - public void onViewCreated (@NonNull View view, Bundle savedInstanceState) { - super.onViewCreated (view, savedInstanceState); - - LOG.debug ("onViewCreated() called"); - mFile = new File (requireArguments ().getString (KEY_FILE_PATH)); - - mBinding.editor.setOverScrollEnabled (false); - mBinding.editor.setTypefaceText (TypefaceUtils.jetbrainsMono ()); - mBinding.editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE); - mBinding.editor.setHighlightCurrentBlock (true); - mBinding.editor.setEventListener (this); - mBinding.editor.setAutoCompletionOnComposing (true); - mBinding.editor.setAutoCompletionItemAdapter (new CompletionListAdapter ()); - mBinding.editor.setLineColorsEnabled (true); - mBinding.editor.setDividerWidth (SizeUtils.dp2px (1)); - - mBinding.diagnosticTextContainer.setVisibility (View.GONE); - - LOG.debug ("Configuring editor from preferences..."); - configureEditorIfNeeded (); - - LOG.debug ("Reading initial selection range"); - final Range range = fromArgs (requireArguments ()); - new TaskExecutor ().executeAsync (new ReadFileTask (mFile), result -> { - mBinding.editor.setText (result); - postRead (); - mBinding.editor.post (() -> { - if (LSPUtils.isEqual (range.getStart (), range.getEnd ())) { - getEditor ().setSelection (range.getStart ().getLine (), range.getStart ().getCharacter ()); - } else { - getEditor ().setSelectionRegion (range.getStart ().getLine (), range.getStart ().getCharacter (), range.getEnd ().getLine (), range.getEnd ().getCharacter ()); - } - }); - }); - } - - private Range fromArgs (@NonNull Bundle args) { - if (!(args.containsKey (KEY_LINE_START) - && args.containsKey (KEY_COLUMN_START) - && args.containsKey (KEY_LINE_END) - && args.containsKey (KEY_COLUMN_END))) { - return LSPUtils.Range_ofZero; - } - - return new Range ( - new Position ( - args.getInt (KEY_LINE_START), - args.getInt (KEY_COLUMN_START)), - new Position ( - args.getInt (KEY_LINE_END), - args.getInt (KEY_COLUMN_END) - )); - } - - public void setDiagnostics (List diags) { - if (diags == null) { - mBinding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (new HashMap<> ()); - return; - } - Map> map = new HashMap<> (); - for (int i = 0; i < diags.size (); i++) { - final Diagnostic d = diags.get (i); - if (d == null) { - continue; - } - final Range range = d.getRange (); - final int line = range.getStart ().getLine (); - final int column = range.getStart ().getCharacter (); - Map mappedByColumn = map.get (line); - if (mappedByColumn == null) { - mappedByColumn = new HashMap<> (); - } - mappedByColumn.put (column, d); - map.put (line, mappedByColumn); - } - mBinding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (map); - } - - private void configureEditorIfNeeded () { - boolean sizeChanged = isFirstCreate || ConstantsBridge.EDITORPREF_SIZE_CHANGED; - boolean flagsChanged = isFirstCreate || ConstantsBridge.EDITORPREF_FLAGS_CHANGED; - boolean drawHexChanged = isFirstCreate || ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED; - final PreferenceManager prefs = getStudioActivity ().getApp ().getPrefManager (); - if (sizeChanged) { - float textSize = prefs.getFloat (PreferenceManager.KEY_EDITOR_FONT_SIZE); - if (textSize < 6 || textSize > 32) { - textSize = 14; - } - mBinding.editor.setTextSize (textSize); - ConstantsBridge.EDITORPREF_SIZE_CHANGED = false; - } - - if (flagsChanged) { - int flags = 0; - if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_LEADING, true)) { - flags |= CodeEditor.FLAG_DRAW_WHITESPACE_LEADING; - } - if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_TRAILING, false)) { - flags |= CodeEditor.FLAG_DRAW_WHITESPACE_TRAILING; - } - if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_INNER, true)) { - flags |= CodeEditor.FLAG_DRAW_WHITESPACE_INNER; - } - if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_EMPTY_LINE, true)) { - flags |= CodeEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE; - } - if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_LINE_BREAK, true)) { - flags |= CodeEditor.FLAG_DRAW_LINE_SEPARATOR; - } - - mBinding.editor.setNonPrintablePaintingFlags (flags); - ConstantsBridge.EDITORPREF_FLAGS_CHANGED = false; - } - - if (drawHexChanged) { - mBinding.editor.setLineColorsEnabled (prefs.getBoolean (PreferenceManager.KEY_EDITOR_DRAW_HEX, true)); - ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED = false; - } - - isFirstCreate = false; - } - - public String getText () { - return mBinding.editor.getText ().toString (); - } - - public boolean isModified () { - return isModified; - } - - public void undo () { - if (mBinding.editor.canUndo ()) { - mBinding.editor.undo (); - } - } - - public void redo () { - if (mBinding.editor.canRedo ()) { - mBinding.editor.redo (); - } - } - - public void findDefinition () { - mBinding.editor.findDefinition (); - } - - public void findReferences () { - mBinding.editor.findReferences (); - } - - public void commentLine () { - mBinding.editor.commentLine (); - } - - public void uncommentLine () { - mBinding.editor.uncommentLine (); - } - - public void beginSearch () { - mBinding.editor.beginSearchMode (); - } - - public void save () { - if (mFile == null || mBinding == null) { - return; - } - - final String text = mBinding.editor.getText ().toString (); - FileIOUtils.writeFileFromString (mFile, text); - notifySaved (); - isModified = false; - } - - public File getFile () { - return mFile; - } - - @Override - public void onResume () { - super.onResume (); - configureEditorIfNeeded (); - } - - private void postRead () { - if (mFile.isFile () && mFile.getName ().endsWith (EXT_JAVA)) { - mBinding.editor.setEditorLanguage (new JavaLanguage (getFile ())); - } else if (mFile.isFile () && mFile.getName ().endsWith (EXT_XML)) { - mBinding.editor.setEditorLanguage (new XMLLanguage (getFile ())); - } else if (mFile.isFile () && mFile.getName ().endsWith (EXT_GRADLE)) { - mBinding.editor.setEditorLanguage (new GroovyLanguage (getFile ())); - } else { - mBinding.editor.setEditorLanguage (new EmptyLanguage ()); - } - - // File must be set only after setting the language - // This will make sure that textDocument/didOpen is sent - mBinding.editor.setFile (getFile ()); - - mBinding.editor.setColorScheme (new SchemeAndroidIDE ()); - if (mOpenListener != null) { - mOpenListener.onOpenSuccessful (getFile (), getText ()); - } - } - - private void closeCurrentTag (String text, int line, int col) { - try { - XMLLexer lexer = new XMLLexer (CharStreams.fromReader (new StringReader (text))); - Token token; - boolean wasSlash = false, wasOpen = false; - ArrayList currentNames = new ArrayList<> (); - while (((token = lexer.nextToken ()) != null && token.getType () != token.EOF)) { - final int type = token.getType (); - if (type == XMLLexer.OPEN) { - wasOpen = true; - } else if (type == XMLLexer.Name) { - if (wasOpen && wasSlash && currentNames.size () > 0) { - currentNames.remove (0); - } else if (wasOpen) { - currentNames.add (0, token.getText ()); - wasOpen = false; - } - } else if (type == XMLLexer.OPEN_SLASH) { - int l = token.getLine () - 1; - int c = token.getCharPositionInLine (); - if (l == line && c == col) { - break; - } else if (currentNames.size () > 0) { - currentNames.remove (0); - } - } else if (type == XMLLexer.SLASH_CLOSE - || type == XMLLexer.SPECIAL_CLOSE) { - if (currentNames.size () > 0 && token.getText ().trim ().endsWith ("/>")) { - currentNames.remove (0); - } - } else if (type == XMLLexer.SLASH) { - wasSlash = true; - } else { - wasOpen = wasSlash = false; - } - } - if (currentNames.size () > 0) { - mBinding.editor.getText ().insert (line, col + 2, currentNames.get (0)); - } - } catch (Throwable th) { - LOG.error ("Unable to close current tag", th); - } - } - - @Override - public boolean onRequestFormat (CodeEditor editor, boolean async) { - return false; - } - - @Override - public boolean onFormatFail (CodeEditor editor, Throwable cause) { - return false; - } - - @Override - public void onFormatSucceed (CodeEditor editor) { - } - - @Override - public void onNewTextSet (CodeEditor editor) { - } - - @Override - public void onSetSelection (int startLine, int startCol, int endLine, int endCol) { - } - - @Override - public void afterDelete (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence deletedContent) { - isModified = true; - notifyModified (); - } - - @Override - public void afterInsert (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence insertedContent) { - isModified = true; - - if (getFile () != null && getFile ().getName ().endsWith (EXT_XML)) { - boolean isOpen = false; - try { - isOpen = editor.getText ().charAt (editor.getCursor ().getLeft () - 2) == '<'; - } catch (Throwable th) { - // ignored - } - if (isOpen && insertedContent.toString ().equals ("/")) { - closeCurrentTag (editor.getText ().toString (), endLine, endColumn); - } - } - - notifyModified (); - } - - @Override - public void beforeReplace (CodeEditor editor, CharSequence content) { - } - - private void notifySaved () { - mBinding.editor.didSave (); - - if (mModificationStateListener != null) { - mModificationStateListener.onSaved (this); - } - } - - private void notifyModified () { - if (mModificationStateListener != null) { - mModificationStateListener.onModified (this); - } - } - - public interface FileOpenListener { - void onOpenSuccessful (File file, String text); - } - - public interface ModificationStateListener { - void onModified (EditorFragment editor); - - void onSaved (EditorFragment editor); - } -} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java index 8f0102ce5..43791e580 100755 --- a/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/FileTreeFragment.java @@ -28,10 +28,11 @@ 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.bumptech.glide.Glide; import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.itsaky.androidide.R; import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder; @@ -41,11 +42,10 @@ import com.itsaky.androidide.project.AndroidProject; import com.itsaky.androidide.tasks.TaskExecutor; import com.itsaky.androidide.tasks.callables.FileTreeCallable; import com.itsaky.androidide.utils.Environment; -import com.itsaky.toaster.Toaster; +import com.itsaky.androidide.views.CodeEditorView; import com.unnamed.b.atv.model.TreeNode; import com.unnamed.b.atv.view.AndroidTreeView; import java.io.File; -import java.util.ArrayList; import java.util.Arrays; import com.blankj.utilcode.util.FileUtils; @@ -65,6 +65,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN return this; } + @NonNull public static FileTreeFragment newInstance(AndroidProject project) { Bundle bundle = new Bundle(); bundle.putParcelable("project", project); @@ -82,8 +83,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - if (getActivity() == null || getArguments() == null || !getArguments().containsKey("project")) return; - mProject = getArguments().getParcelable("project"); + mProject = requireArguments ().getParcelable("project"); listProjectFiles(); } @@ -102,12 +102,12 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN expandNode(node); } else { setLoading(node); - listNode(node, true); + listNode(node); } } } - private void listNode(TreeNode node, boolean wasClicked) { + private void listNode (@NonNull TreeNode node) { node.getChildren().clear(); node.setExpanded(false); new TaskExecutor().executeAsync(() -> { @@ -136,13 +136,13 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN } } - private void setLoading(TreeNode node) { + private void setLoading(@NonNull TreeNode node) { if (node.getViewHolder() instanceof FileTreeViewHolder) { ((FileTreeViewHolder) node.getViewHolder()).setLoading(); } } - private void updateChevron(TreeNode node) { + private void updateChevron(@NonNull TreeNode node) { if (node.getViewHolder() instanceof FileTreeViewHolder) { ((FileTreeViewHolder) node.getViewHolder()).updateChevron(!node.isExpanded()); } @@ -180,9 +180,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN final File gradleProps = Environment.GRADLE_PROPS; final File gradleHome = Environment.GRADLE_USER_HOME; - final File root = StudioApp.getInstance().getRootDir().getParentFile(); File projectDir = new File(mProject.getProjectPath()); -// projectDir = root; mRoot = TreeNode.root(projectDir); if(gradleHome.exists() && gradleHome.isDirectory()) { if(!gradleProps.exists()) @@ -235,7 +233,7 @@ public class FileTreeFragment extends BottomSheetDialogFragment implements TreeN } public interface FileActionListener { - public EditorFragment openFile(File file); - public void showFileOptions(File file, TreeNode node); + CodeEditorView openFile(File file); + void showFileOptions(File file, TreeNode node); } } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/IDELogFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/IDELogFragment.java index 4f366a545..1af375c11 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/IDELogFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/IDELogFragment.java @@ -1,33 +1,33 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.fragments; - -import com.itsaky.androidide.utils.Logger; - -public class IDELogFragment extends SimpleOutputFragment { - - private final Logger.LogListener listener = this::log; - - public IDELogFragment () { - Logger.addLogListener (listener); - } - - private void log (int priority, String tag, String message) { - appendOutput (String.format ("%s: %s", tag, message)); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.fragments; + +import com.itsaky.androidide.utils.Logger; + +public class IDELogFragment extends SimpleOutputFragment { + + private final Logger.LogListener listener = this::log; + + public IDELogFragment () { + Logger.addLogListener (listener); + } + + private void log (int priority, String tag, String message) { + appendOutput (String.format ("%s: %s", tag, message)); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/LogViewFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/LogViewFragment.java index fcbd7caa6..e08497127 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/LogViewFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/LogViewFragment.java @@ -1,77 +1,77 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.fragments; - -import android.os.Bundle; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.blankj.utilcode.util.ThreadUtils; -import com.itsaky.androidide.language.logs.LogLanguageImpl; -import com.itsaky.androidide.models.LogLine; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -import io.github.rosemoe.editor.interfaces.EditorLanguage; - -public class LogViewFragment extends NonEditableEditorFragment { - - private final EditorLanguage language = getLanguage (); - private final List unsavedLines = new ArrayList<> (); - - @Override - public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated (view, savedInstanceState); - Objects.requireNonNull (getEditor ()).setEditorLanguage (language); - - if (!unsavedLines.isEmpty ()) { - for (var line : unsavedLines) { - applyToLanguage (line); - getEditor ().getText ().append (line.toString ().trim () + "\n"); - } - unsavedLines.clear (); - } - } - - public void appendLog (LogLine line) { - if (getEditor () == null) { - unsavedLines.add (line); - return; - } - - applyToLanguage (line); - - final var lineString = line.toString (); - final var msg = lineString.endsWith ("\n") ? lineString : lineString + "\n"; - ThreadUtils.runOnUiThread (() -> getEditor ().getText ().append (msg)); - } - - private void applyToLanguage (LogLine line) { - if (language instanceof LogLanguageImpl) { - ((LogLanguageImpl)this.language).addLine (line); - } - } - - protected EditorLanguage getLanguage () { - return new LogLanguageImpl (); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.fragments; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.blankj.utilcode.util.ThreadUtils; +import com.itsaky.androidide.language.logs.LogLanguageImpl; +import com.itsaky.androidide.models.LogLine; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import io.github.rosemoe.editor.interfaces.EditorLanguage; + +public class LogViewFragment extends NonEditableEditorFragment { + + private final EditorLanguage language = getLanguage (); + private final List unsavedLines = new ArrayList<> (); + + @Override + public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated (view, savedInstanceState); + Objects.requireNonNull (getEditor ()).setEditorLanguage (language); + + if (!unsavedLines.isEmpty ()) { + for (var line : unsavedLines) { + applyToLanguage (line); + getEditor ().getText ().append (line.toString ().trim () + "\n"); + } + unsavedLines.clear (); + } + } + + public void appendLog (LogLine line) { + if (getEditor () == null) { + unsavedLines.add (line); + return; + } + + applyToLanguage (line); + + final var lineString = line.toString (); + final var msg = lineString.endsWith ("\n") ? lineString : lineString + "\n"; + ThreadUtils.runOnUiThread (() -> getEditor ().getText ().append (msg)); + } + + private void applyToLanguage (LogLine line) { + if (language instanceof LogLanguageImpl) { + ((LogLanguageImpl)this.language).addLine (line); + } + } + + protected EditorLanguage getLanguage () { + return new LogLanguageImpl (); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/NonEditableEditorFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/NonEditableEditorFragment.java index ec8c78435..891ddd108 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/NonEditableEditorFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/NonEditableEditorFragment.java @@ -1,72 +1,72 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.fragments; - -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 androidx.fragment.app.Fragment; - -import com.itsaky.androidide.databinding.FragmentNonEditableEditorBinding; -import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE; -import com.itsaky.androidide.utils.TypefaceUtils; - -import io.github.rosemoe.editor.langs.EmptyLanguage; -import io.github.rosemoe.editor.widget.CodeEditor; - -public abstract class NonEditableEditorFragment extends Fragment { - - private FragmentNonEditableEditorBinding binding; - - @Nullable - @Override - public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - this.binding = FragmentNonEditableEditorBinding.inflate (inflater, container, false); - return binding.getRoot (); - } - - @Override - public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated (view, savedInstanceState); - - final var editor = binding.getRoot (); - editor.setEditable (false); - editor.setDividerWidth (0); - editor.setEditorLanguage (new EmptyLanguage ()); - editor.setOverScrollEnabled (false); - editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE); - editor.setWordwrap (false); - editor.setUndoEnabled (false); - editor.setTypefaceLineNumber (TypefaceUtils.jetbrainsMono ()); - editor.setTypefaceText (TypefaceUtils.jetbrainsMono ()); - editor.setTextSize (12); - editor.setColorScheme (new SchemeAndroidIDE ()); - } - - @Nullable - public CodeEditor getEditor () { - if (binding == null) { - return null; - } - return binding.editor; - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.fragments; + +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 androidx.fragment.app.Fragment; + +import com.itsaky.androidide.databinding.FragmentNonEditableEditorBinding; +import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE; +import com.itsaky.androidide.utils.TypefaceUtils; + +import io.github.rosemoe.editor.langs.EmptyLanguage; +import io.github.rosemoe.editor.widget.CodeEditor; + +public abstract class NonEditableEditorFragment extends Fragment { + + private FragmentNonEditableEditorBinding binding; + + @Nullable + @Override + public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + this.binding = FragmentNonEditableEditorBinding.inflate (inflater, container, false); + return binding.getRoot (); + } + + @Override + public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated (view, savedInstanceState); + + final var editor = binding.getRoot (); + editor.setEditable (false); + editor.setDividerWidth (0); + editor.setEditorLanguage (new EmptyLanguage ()); + editor.setOverScrollEnabled (false); + editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE); + editor.setWordwrap (false); + editor.setUndoEnabled (false); + editor.setTypefaceLineNumber (TypefaceUtils.jetbrainsMono ()); + editor.setTypefaceText (TypefaceUtils.jetbrainsMono ()); + editor.setTextSize (12); + editor.setColorScheme (new SchemeAndroidIDE ()); + } + + @Nullable + public CodeEditor getEditor () { + if (binding == null) { + return null; + } + return binding.editor; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/SearchResultFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/SearchResultFragment.java index ac1ce194b..433e2a0a0 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/SearchResultFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/SearchResultFragment.java @@ -1,81 +1,81 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.fragments; - -import android.os.Bundle; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.itsaky.androidide.adapters.SearchListAdapter; -import com.itsaky.androidide.views.EmptyView; - -public class SearchResultFragment extends Fragment { - - private RecyclerView list; - private EmptyView emptyView; - - private SearchListAdapter unsavedAdapter; - - @Nullable - @Override - public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) { - final var container = new LinearLayout (inflater.getContext ()); - container.setOrientation (LinearLayout.VERTICAL); - container.setLayoutParams (new ViewGroup.LayoutParams (-1, -1)); - - this.list = new RecyclerView (inflater.getContext ()); - this.emptyView = new EmptyView (inflater.getContext ()); - - container.addView (list, new LinearLayout.LayoutParams (-1, -1)); - container.addView (emptyView, new LinearLayout.LayoutParams (-1, -1)); - - return container; - } - - @Override - public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated (view, savedInstanceState); - - list.setLayoutManager (new LinearLayoutManager (list.getContext ())); - list.setAdapter (unsavedAdapter != null ? unsavedAdapter : new SearchListAdapter (null, null, null)); - unsavedAdapter = null; - } - - public void setAdapter (@NonNull final SearchListAdapter adapter) { - if (list == null) { - unsavedAdapter = adapter; - return; - } - this.list.setAdapter (adapter); - } - - public void handleResultVisibility (boolean error) { - if (emptyView != null && list != null) { - emptyView.setVisibility (error ? View.VISIBLE : View.GONE); - list.setVisibility (error ? View.GONE : View.VISIBLE); - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.fragments; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.itsaky.androidide.adapters.SearchListAdapter; +import com.itsaky.androidide.views.EmptyView; + +public class SearchResultFragment extends Fragment { + + private RecyclerView list; + private EmptyView emptyView; + + private SearchListAdapter unsavedAdapter; + + @Nullable + @Override + public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup parent, @Nullable Bundle savedInstanceState) { + final var container = new LinearLayout (inflater.getContext ()); + container.setOrientation (LinearLayout.VERTICAL); + container.setLayoutParams (new ViewGroup.LayoutParams (-1, -1)); + + this.list = new RecyclerView (inflater.getContext ()); + this.emptyView = new EmptyView (inflater.getContext ()); + + container.addView (list, new LinearLayout.LayoutParams (-1, -1)); + container.addView (emptyView, new LinearLayout.LayoutParams (-1, -1)); + + return container; + } + + @Override + public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated (view, savedInstanceState); + + list.setLayoutManager (new LinearLayoutManager (list.getContext ())); + list.setAdapter (unsavedAdapter != null ? unsavedAdapter : new SearchListAdapter (null, null, null)); + unsavedAdapter = null; + } + + public void setAdapter (@NonNull final SearchListAdapter adapter) { + if (list == null) { + unsavedAdapter = adapter; + return; + } + this.list.setAdapter (adapter); + } + + public void handleResultVisibility (boolean error) { + if (emptyView != null && list != null) { + emptyView.setVisibility (error ? View.VISIBLE : View.GONE); + list.setVisibility (error ? View.GONE : View.VISIBLE); + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/SimpleOutputFragment.java b/app/src/main/java/com/itsaky/androidide/fragments/SimpleOutputFragment.java index f00eaea63..3bd91220a 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/SimpleOutputFragment.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/SimpleOutputFragment.java @@ -1,58 +1,58 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.fragments; - -import android.os.Bundle; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.blankj.utilcode.util.ThreadUtils; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class SimpleOutputFragment extends NonEditableEditorFragment { - - private final List unsavedLines = new ArrayList<> (); - - @Override - public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated (view, savedInstanceState); - - if (!unsavedLines.isEmpty ()) { - for (String line : unsavedLines) { - Objects.requireNonNull (getEditor ()).getText ().append (line.trim () + "\n"); - } - unsavedLines.clear (); - } - } - - public void appendOutput (String output) { - if (getEditor () == null) { - unsavedLines.add (output); - return; - } - ThreadUtils.runOnUiThread (() -> { - final var message = output == null || output.endsWith ("\n") ? output : output + "\n"; - getEditor ().getText ().append (message); - }); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.fragments; + +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.blankj.utilcode.util.ThreadUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class SimpleOutputFragment extends NonEditableEditorFragment { + + private final List unsavedLines = new ArrayList<> (); + + @Override + public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated (view, savedInstanceState); + + if (!unsavedLines.isEmpty ()) { + for (String line : unsavedLines) { + Objects.requireNonNull (getEditor ()).getText ().append (line.trim () + "\n"); + } + unsavedLines.clear (); + } + } + + public void appendOutput (String output) { + if (getEditor () == null) { + unsavedLines.add (output); + return; + } + ThreadUtils.runOnUiThread (() -> { + final var message = output == null || output.endsWith ("\n") ? output : output + "\n"; + getEditor ().getText ().append (message); + }); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrEditorSheet.java b/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrEditorSheet.java index 02be37ea2..46aff660e 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrEditorSheet.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/sheets/AttrEditorSheet.java @@ -1,239 +1,239 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.fragments.sheets; - -import static com.itsaky.attrinfo.models.Attr.BOOLEAN; -import static com.itsaky.attrinfo.models.Attr.COLOR; -import static com.itsaky.attrinfo.models.Attr.DIMENSION; -import static com.itsaky.attrinfo.models.Attr.ENUM; -import static com.itsaky.attrinfo.models.Attr.FLAG; -import static com.itsaky.attrinfo.models.Attr.STRING; - -import static com.itsaky.androidide.utils.AttributeDialogs.*; - -import android.content.DialogInterface; -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 androidx.appcompat.app.AlertDialog; -import androidx.transition.ChangeBounds; -import androidx.transition.TransitionManager; - -import com.google.android.material.bottomsheet.BottomSheetDialogFragment; -import com.itsaky.androidide.R; -import com.itsaky.androidide.adapters.AttributeListAdapter; -import com.itsaky.androidide.adapters.SimpleIconTextAdapter; -import com.itsaky.androidide.app.StudioApp; -import com.itsaky.androidide.colorpicker.ColorPickerView; -import com.itsaky.androidide.databinding.LayoutAttrEditorSheetBinding; -import com.itsaky.androidide.databinding.LayoutAttrEditorSheetItemBinding; -import com.itsaky.androidide.models.IconTextListItem; -import com.itsaky.androidide.models.XMLAttribute; -import com.itsaky.androidide.utils.AttributeDialogs; -import com.itsaky.androidide.utils.DialogUtils; -import com.itsaky.androidide.utils.Logger; -import com.itsaky.inflater.IView; -import com.itsaky.toaster.Toaster; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; - -public class AttrEditorSheet extends BottomSheetDialogFragment implements SimpleIconTextAdapter.OnBindListener { - - private static final List VIEW_ACTIONS = new ArrayList<> (); - - private IView selectedView; - private LayoutAttrEditorSheetBinding binding; - - private OnViewDeletionFailedListener mDeletionFailedListener; - - private static final Logger LOG = Logger.instance ("AttrBottomSheet"); - - @Nullable - @Override - public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - this.binding = LayoutAttrEditorSheetBinding.inflate (LayoutInflater.from (getContext ()), container, false); - return binding.getRoot (); - } - - @Override - public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { - super.onViewCreated (view, savedInstanceState); - - if (VIEW_ACTIONS.isEmpty ()) { - Collections.addAll (VIEW_ACTIONS, - IconTextListItem.create (getString (R.string.title_viewaction_delete), R.drawable.ic_delete), - IconTextListItem.create (getString (R.string.msg_viewaction_select_parent), R.drawable.ic_view_select_parent) - ); - } - - setupViewData (); - - AttributeDialogs.init (getActivity ()); - } - - @Override - public void onDismiss (@NonNull DialogInterface dialog) { - AttributeDialogs.release (); - super.onDismiss (dialog); - } - - public AttrEditorSheet setDeletionFailedListener (OnViewDeletionFailedListener listener) { - this.mDeletionFailedListener = listener; - return this; - } - - private void setupViewData () { - binding.actionsList.setAdapter (new SimpleIconTextAdapter (VIEW_ACTIONS).setOnBindListener (this)); - - if (this.selectedView == null) { - LOG.error ("Cannot edit attributes of a null view."); - return; - } - - binding.widgetName.setText (this.selectedView.getXmlTag ()); - binding.attrList.setAdapter (new AttributeListAdapter ( - this.selectedView.getAttributes () - .stream () - .map (XMLAttribute::new) - .collect (Collectors.toList ()), this::onAttrClick) - ); - } - - public void setSelectedView (IView view) { - this.selectedView = view; - } - - private void onAttrClick (LayoutAttrEditorSheetItemBinding binding, @NonNull XMLAttribute attribute) { - - if (this.selectedView == null) { - LOG.error ("Cannot edit attributes of a null view."); - return; - } - - final var format = attribute.findFormat (); - if (format == -1) { - StudioApp.getInstance ().toast (getString (R.string.msg_no_attr_format), Toaster.Type.ERROR); - LOG.error (getString (R.string.msg_no_attr_format), attribute); - return; - } - - showEditorDialog (attribute); - } - - private void showEditorDialog (@NonNull XMLAttribute attribute) { - final var attr = attribute.getAttr (); - final var values = attr.possibleValues.toArray (new String[0]); - final AttributeDialogs.OnClickListener onDone = (dialog, which, newValue) -> applyNewValue (attribute, newValue); - - final ColorPickerView.OnPickListener mPickListener = (color, hexCode) -> applyNewValue (attribute, hexCode); - - AlertDialog dialog = null; - if (attribute.hasFormat (DIMENSION)) { - dialog = AttributeDialogs.dimensionEditor (attribute.getValue (), onDone); - } else if (attribute.hasFormat (STRING)) { - dialog = AttributeDialogs.stringEditor (onDone); - } else if (attribute.hasFormat (BOOLEAN)) { - dialog = booleanEditor (onDone); - } else if (attribute.hasFormat (ENUM)) { - dialog = enumEditor (values, attribute.getValue (), onDone); - } else if (attribute.hasFormat (FLAG)) { - dialog = flagEditor (values, attribute.getValue (), onDone); - } else if (attribute.hasFormat (COLOR)) { - // TODO Implement something to allow values from colors.xml or ColorStateLists - // Make this color picker optional - dialog = colorPicker (mPickListener); - } - - if (dialog != null) { - dialog.show (); - } - } - - private void applyNewValue (@NonNull XMLAttribute attribute, String newValue) { - if (!this.selectedView.updateAttribute (attribute.getNamespace (), attribute.getAttributeName (), newValue)) { - StudioApp.getInstance ().toast ("Unable to update this attribute", Toaster.Type.ERROR); - } else { - // Update the view data - // This will make sure that the attributes list has been updated - setupViewData (); - } - } - - @Override - public void postBind (IconTextListItem item, @NonNull SimpleIconTextAdapter.VH holder, int position) { - final var binding = holder.binding; - binding.getRoot ().setOnClickListener (v -> onViewActionClick (position)); - } - - private void onViewActionClick (int position) { - if (this.selectedView == null) { - return; - } - - if (position == 0) { // Delete - DialogUtils.newYesNoDialog (getContext (), (dialog, which) -> { - var handled = selectedView.removeFromParent (); - if (!handled) { - handled = mDeletionFailedListener != null && mDeletionFailedListener.onDeletionFailed (this.selectedView); - } - if (!handled) { - StudioApp.getInstance ().toast (getString(R.string.msg_view_deletion_failed), Toaster.Type.ERROR); - } else { - dismiss (); - } - }, (dialog, which) -> dialog.dismiss ()).show (); - } else if (position == 1) { // Select parent - if (this.selectedView.getParent () == null) { - StudioApp.getInstance ().toast (getString (R.string.msg_no_view_parent), Toaster.Type.ERROR); - return; - } - - this.selectedView = this.selectedView.getParent (); - - TransitionManager.beginDelayedTransition (binding.getRoot (), new ChangeBounds ()); - setupViewData (); - } - } - - /** - * A listener can be used to get notified when we fail to remove - * a view from its parent. This is used in {@link com.itsaky.androidide.DesignerActivity DesignerActivity}. - * - * When the user tries to remove the outermost view of the inflated layout, - * AttrEditorSheet fails to remove the view from its parent ({@link IView#getParent()} is null for root XML layout). - * In this case, DesignerActivity check if the view that we were trying to delete is the root layout or not. - * If it is the root layout, then it deletes the layout from the layout container. - * - * @author Akash Yadav - */ - public interface OnViewDeletionFailedListener { - - /** - * @param view The view that was not removed from its parent. - * @return {@code true} if the listener handled the error. {@code false} otherwise. - */ - boolean onDeletionFailed (IView view); - } +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.fragments.sheets; + +import static com.itsaky.attrinfo.models.Attr.BOOLEAN; +import static com.itsaky.attrinfo.models.Attr.COLOR; +import static com.itsaky.attrinfo.models.Attr.DIMENSION; +import static com.itsaky.attrinfo.models.Attr.ENUM; +import static com.itsaky.attrinfo.models.Attr.FLAG; +import static com.itsaky.attrinfo.models.Attr.STRING; + +import static com.itsaky.androidide.utils.AttributeDialogs.*; + +import android.content.DialogInterface; +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 androidx.appcompat.app.AlertDialog; +import androidx.transition.ChangeBounds; +import androidx.transition.TransitionManager; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.itsaky.androidide.R; +import com.itsaky.androidide.adapters.AttributeListAdapter; +import com.itsaky.androidide.adapters.SimpleIconTextAdapter; +import com.itsaky.androidide.app.StudioApp; +import com.itsaky.androidide.colorpicker.ColorPickerView; +import com.itsaky.androidide.databinding.LayoutAttrEditorSheetBinding; +import com.itsaky.androidide.databinding.LayoutAttrEditorSheetItemBinding; +import com.itsaky.androidide.models.IconTextListItem; +import com.itsaky.androidide.models.XMLAttribute; +import com.itsaky.androidide.utils.AttributeDialogs; +import com.itsaky.androidide.utils.DialogUtils; +import com.itsaky.androidide.utils.Logger; +import com.itsaky.inflater.IView; +import com.itsaky.toaster.Toaster; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class AttrEditorSheet extends BottomSheetDialogFragment implements SimpleIconTextAdapter.OnBindListener { + + private static final List VIEW_ACTIONS = new ArrayList<> (); + + private IView selectedView; + private LayoutAttrEditorSheetBinding binding; + + private OnViewDeletionFailedListener mDeletionFailedListener; + + private static final Logger LOG = Logger.instance ("AttrBottomSheet"); + + @Nullable + @Override + public View onCreateView (@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + this.binding = LayoutAttrEditorSheetBinding.inflate (LayoutInflater.from (getContext ()), container, false); + return binding.getRoot (); + } + + @Override + public void onViewCreated (@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated (view, savedInstanceState); + + if (VIEW_ACTIONS.isEmpty ()) { + Collections.addAll (VIEW_ACTIONS, + IconTextListItem.create (getString (R.string.title_viewaction_delete), R.drawable.ic_delete), + IconTextListItem.create (getString (R.string.msg_viewaction_select_parent), R.drawable.ic_view_select_parent) + ); + } + + setupViewData (); + + AttributeDialogs.init (getActivity ()); + } + + @Override + public void onDismiss (@NonNull DialogInterface dialog) { + AttributeDialogs.release (); + super.onDismiss (dialog); + } + + public AttrEditorSheet setDeletionFailedListener (OnViewDeletionFailedListener listener) { + this.mDeletionFailedListener = listener; + return this; + } + + private void setupViewData () { + binding.actionsList.setAdapter (new SimpleIconTextAdapter (VIEW_ACTIONS).setOnBindListener (this)); + + if (this.selectedView == null) { + LOG.error ("Cannot edit attributes of a null view."); + return; + } + + binding.widgetName.setText (this.selectedView.getXmlTag ()); + binding.attrList.setAdapter (new AttributeListAdapter ( + this.selectedView.getAttributes () + .stream () + .map (XMLAttribute::new) + .collect (Collectors.toList ()), this::onAttrClick) + ); + } + + public void setSelectedView (IView view) { + this.selectedView = view; + } + + private void onAttrClick (LayoutAttrEditorSheetItemBinding binding, @NonNull XMLAttribute attribute) { + + if (this.selectedView == null) { + LOG.error ("Cannot edit attributes of a null view."); + return; + } + + final var format = attribute.findFormat (); + if (format == -1) { + StudioApp.getInstance ().toast (getString (R.string.msg_no_attr_format), Toaster.Type.ERROR); + LOG.error (getString (R.string.msg_no_attr_format), attribute); + return; + } + + showEditorDialog (attribute); + } + + private void showEditorDialog (@NonNull XMLAttribute attribute) { + final var attr = attribute.getAttr (); + final var values = attr.possibleValues.toArray (new String[0]); + final AttributeDialogs.OnClickListener onDone = (dialog, which, newValue) -> applyNewValue (attribute, newValue); + + final ColorPickerView.OnPickListener mPickListener = (color, hexCode) -> applyNewValue (attribute, hexCode); + + AlertDialog dialog = null; + if (attribute.hasFormat (DIMENSION)) { + dialog = AttributeDialogs.dimensionEditor (attribute.getValue (), onDone); + } else if (attribute.hasFormat (STRING)) { + dialog = AttributeDialogs.stringEditor (onDone); + } else if (attribute.hasFormat (BOOLEAN)) { + dialog = booleanEditor (onDone); + } else if (attribute.hasFormat (ENUM)) { + dialog = enumEditor (values, attribute.getValue (), onDone); + } else if (attribute.hasFormat (FLAG)) { + dialog = flagEditor (values, attribute.getValue (), onDone); + } else if (attribute.hasFormat (COLOR)) { + // TODO Implement something to allow values from colors.xml or ColorStateLists + // Make this color picker optional + dialog = colorPicker (mPickListener); + } + + if (dialog != null) { + dialog.show (); + } + } + + private void applyNewValue (@NonNull XMLAttribute attribute, String newValue) { + if (!this.selectedView.updateAttribute (attribute.getNamespace (), attribute.getAttributeName (), newValue)) { + StudioApp.getInstance ().toast ("Unable to update this attribute", Toaster.Type.ERROR); + } else { + // Update the view data + // This will make sure that the attributes list has been updated + setupViewData (); + } + } + + @Override + public void postBind (IconTextListItem item, @NonNull SimpleIconTextAdapter.VH holder, int position) { + final var binding = holder.binding; + binding.getRoot ().setOnClickListener (v -> onViewActionClick (position)); + } + + private void onViewActionClick (int position) { + if (this.selectedView == null) { + return; + } + + if (position == 0) { // Delete + DialogUtils.newYesNoDialog (getContext (), (dialog, which) -> { + var handled = selectedView.removeFromParent (); + if (!handled) { + handled = mDeletionFailedListener != null && mDeletionFailedListener.onDeletionFailed (this.selectedView); + } + if (!handled) { + StudioApp.getInstance ().toast (getString(R.string.msg_view_deletion_failed), Toaster.Type.ERROR); + } else { + dismiss (); + } + }, (dialog, which) -> dialog.dismiss ()).show (); + } else if (position == 1) { // Select parent + if (this.selectedView.getParent () == null) { + StudioApp.getInstance ().toast (getString (R.string.msg_no_view_parent), Toaster.Type.ERROR); + return; + } + + this.selectedView = this.selectedView.getParent (); + + TransitionManager.beginDelayedTransition (binding.getRoot (), new ChangeBounds ()); + setupViewData (); + } + } + + /** + * A listener can be used to get notified when we fail to remove + * a view from its parent. This is used in {@link com.itsaky.androidide.DesignerActivity DesignerActivity}. + * + * When the user tries to remove the outermost view of the inflated layout, + * AttrEditorSheet fails to remove the view from its parent ({@link IView#getParent()} is null for root XML layout). + * In this case, DesignerActivity check if the view that we were trying to delete is the root layout or not. + * If it is the root layout, then it deletes the layout from the layout container. + * + * @author Akash Yadav + */ + public interface OnViewDeletionFailedListener { + + /** + * @param view The view that was not removed from its parent. + * @return {@code true} if the listener handled the error. {@code false} otherwise. + */ + boolean onDeletionFailed (IView view); + } } \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/fragments/sheets/ProgressSheet.java b/app/src/main/java/com/itsaky/androidide/fragments/sheets/ProgressSheet.java index dba8cc2d4..ccd633726 100755 --- a/app/src/main/java/com/itsaky/androidide/fragments/sheets/ProgressSheet.java +++ b/app/src/main/java/com/itsaky/androidide/fragments/sheets/ProgressSheet.java @@ -28,6 +28,8 @@ import android.view.View; import android.widget.LinearLayout; import com.itsaky.androidide.databinding.LayoutProgressSheetBinding; import com.blankj.utilcode.util.SizeUtils; +import com.itsaky.androidide.utils.Logger; + import android.widget.RelativeLayout; public class ProgressSheet extends BaseBottomSheetFragment { @@ -39,6 +41,8 @@ public class ProgressSheet extends BaseBottomSheetFragment { private boolean subMessageEnabled = false; private boolean welcomTextEnabled = false; + private final Logger LOG = Logger.instance ("ProgressSheet"); + @Override protected void bind(LinearLayout container) { binding = LayoutProgressSheetBinding.inflate(LayoutInflater.from(getContext())); @@ -56,14 +60,18 @@ public class ProgressSheet extends BaseBottomSheetFragment { RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) binding.message.getLayoutParams(); try { p.removeRule(RelativeLayout.CENTER_VERTICAL); - } catch (Throwable th) {} + } catch (Throwable th) { + LOG.error ("Unable to remove center_vertical rule.", th); + } binding.message.setLayoutParams(p); } else { binding.subMessage.setVisibility(View.GONE); RelativeLayout.LayoutParams p = (RelativeLayout.LayoutParams) binding.message.getLayoutParams(); try { p.addRule(RelativeLayout.CENTER_VERTICAL); - } catch (Throwable th) {} + } catch (Throwable th) { + LOG.error ("Unable to remove center_vertical rule.", th); + } binding.message.setLayoutParams(p); } diff --git a/app/src/main/java/com/itsaky/androidide/handlers/FileOptionsHandler.java b/app/src/main/java/com/itsaky/androidide/handlers/FileOptionsHandler.java index fe7b81696..116a6c39a 100755 --- a/app/src/main/java/com/itsaky/androidide/handlers/FileOptionsHandler.java +++ b/app/src/main/java/com/itsaky/androidide/handlers/FileOptionsHandler.java @@ -1,345 +1,344 @@ -/************************************************************************************ - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - * -**************************************************************************************/ -package com.itsaky.androidide.handlers; - -import android.view.LayoutInflater; -import com.blankj.utilcode.util.ClipboardUtils; -import com.blankj.utilcode.util.FileIOUtils; -import com.blankj.utilcode.util.FileUtils; -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.itsaky.androidide.R; -import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder; -import com.itsaky.androidide.databinding.LayoutCreateFileJavaBinding; -import com.itsaky.androidide.databinding.LayoutDialogTextInputBinding; -import com.itsaky.androidide.fragments.EditorFragment; -import com.itsaky.androidide.fragments.sheets.OptionsListFragment; -import com.itsaky.androidide.lsp.LSP; -import com.itsaky.androidide.models.SheetOption; -import com.itsaky.androidide.utils.DialogUtils; -import com.itsaky.androidide.utils.Environment; -import com.itsaky.androidide.utils.ProjectWriter; -import com.itsaky.toaster.Toaster; -import com.unnamed.b.atv.model.TreeNode; -import java.io.File; -import java.util.Objects; -import java.util.regex.Pattern; - -public class FileOptionsHandler extends IDEHandler implements OptionsListFragment.OnOptionsClickListener { - - public static final String RES_PATH_REGEX = "/.*/src/.*/res"; - public static final String LAYOUTRES_PATH_REGEX = "/.*/src/.*/res/layout"; - public static final String MENURES_PATH_REGEX = "/.*/src/.*/res/menu"; - public static final String DRAWABLERES_PATH_REGEX = "/.*/src/.*/res/drawable"; - public static final String JAVA_PATH_REGEX = "/.*/src/.*/java"; - - public FileOptionsHandler(Provider provider) { - super(provider); - } - - @Override - public void start() { - if(activity() == null) - throwNPE(); - } - - @Override - public void stop() { - } - - @Override - public void onOptionsClick(SheetOption option) { - if(option.extra instanceof File) { - final File f = (File) option.extra; - switch(option.id) { - case 0 : - ClipboardUtils.copyText("[AndroidIDE] Copied File Path", f.getAbsolutePath()); - activity().getApp().toast(R.string.copied, Toaster.Type.SUCCESS); - break; - case 1 : - renameFile(f); - break; - case 2 : - delete(f); - break; - case 3 : - createNewFile(f); - break; - case 4 : - createNewFolder(f); - break; - } - } - } - - private void createNewFile(File f) { - createNewFile(f, false); - } - - private void createNewFile(final File f, boolean forceUnknownType) { - if(forceUnknownType) { - createNewEmptyFile(f); - } else { - final boolean isJava = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + JAVA_PATH_REGEX).matcher(f.getAbsolutePath()).find(); - final boolean isRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + RES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); - final boolean isLayoutRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + LAYOUTRES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); - final boolean isMenuRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + MENURES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); - final boolean isDrawableRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + DRAWABLERES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); - if(isJava) { - createJavaClass(f); - } else if(isLayoutRes && f.getName().equals("layout")) { - createLayoutRes(f); - } else if(isMenuRes && f.getName().equals("menu")) { - createMenuRes(f); - } else if(isDrawableRes && f.getName().equals("drawable")) { - createDrawableRes(f); - } else if(isRes && f.getName().equals("res")) { - createNewResource(f); - } else { - createNewEmptyFile(f); - } - } - } - - private void createJavaClass(final File f) { - final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); - final LayoutCreateFileJavaBinding binding = LayoutCreateFileJavaBinding.inflate(activity().getLayoutInflater()); - builder.setView(binding.getRoot()); - builder.setTitle(R.string.new_java_class); - builder.setPositiveButton(R.string.text_create, (p1, p2) -> { - p1.dismiss(); - final String name = Objects.requireNonNull (binding.name.getEditText ()).getText().toString().trim(); - final String pkgName = ProjectWriter.getPackageName(f); - if(pkgName == null || pkgName.trim().length() <= 0) { - activity().getApp().toast(R.string.msg_get_package_failed, Toaster.Type.ERROR); - } else { - final int id = binding.typeGroup.getCheckedButtonId(); - final var javaName = name.endsWith (".java") ? name : name.concat (".java"); - final var className = !name.contains (".") ? name : name.substring (0, name.lastIndexOf (".")); - if(id == binding.typeClass.getId()) { - createFile(f, javaName, ProjectWriter.createJavaClass(pkgName, className)); - } else if(id == binding.typeInterface.getId()) { - createFile(f, javaName, ProjectWriter.createJavaInterface(pkgName, className)); - } else if(id == binding.typeEnum.getId()) { - createFile(f, javaName, ProjectWriter.createJavaEnum(pkgName, className)); - } else if(id == binding.typeActivity.getId()) { - createFile(f, javaName, ProjectWriter.createActivity(pkgName, className)); - } else { - createFile(f, name, ""); - } - - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.setCancelable(false); - builder.create().show(); - } - - private void createLayoutRes(File f) { - createNewFileWithContent(Environment.mkdirIfNotExits(f), ProjectWriter.createLayout(), ".xml"); - } - - private void createMenuRes(File f) { - createNewFileWithContent(Environment.mkdirIfNotExits(f), ProjectWriter.createMenu(), ".xml"); - } - - private void createDrawableRes(File f) { - createNewFileWithContent(Environment.mkdirIfNotExits(f), ProjectWriter.createDrawable(), ".xml"); - } - - private void createNewResource(File f) { - final String[] labels = { - activity(). getString(R.string.restype_drawable), - activity(). getString(R.string.restype_layout), - activity(). getString(R.string.restype_menu), - activity(). getString(R.string.restype_other) - }; - final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); - builder.setTitle(R.string.new_xml_resource); - builder.setItems(labels, (dialogInterface, position) -> { - if(position == 0) { - createDrawableRes(new File(f, "drawable")); - } else if(position == 1) { - createLayoutRes(new File(f, "layout")); - } else if(position == 2) { - createMenuRes(new File(f, "menu")); - } else if(position == 3) { - createNewFile(f, true); - } - }); - builder.create().show(); - } - - private void createNewEmptyFile(File f) { - createNewFileWithContent(f, ""); - } - - private void createNewFileWithContent(File f, String content) { - createNewFileWithContent(f, content, null); - } - - private void createNewFileWithContent(File folder, String content, String extension) { - final LayoutDialogTextInputBinding binding = LayoutDialogTextInputBinding.inflate(activity(). getLayoutInflater()); - final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); - - Objects.requireNonNull (binding.name.getEditText ()).setHint(R.string.file_name); - - builder.setTitle(R.string.new_file); - builder.setMessage(activity(). getString(R.string.msg_can_contain_slashes).concat("\n\n").concat(activity(). getString(R.string.msg_newfile_dest, folder.getAbsolutePath()))); - builder.setView(binding.getRoot()); - builder.setCancelable(false); - builder.setPositiveButton(R.string.text_create, (p1, p2) -> { - p1.dismiss(); - String name = binding.name.getEditText().getText().toString().trim(); - if(extension != null && extension.trim().length() > 0) { - name = name.endsWith(extension) ? name : name.concat(extension); - } - createFile(folder, name, content); - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.create().show(); - } - - private void createFile(File f, String name, String content) { - if(name.length() > 0 && name.length() <= 40 && !name.startsWith("/")) { - final File file = new File(f, name); - if(file.exists()) { - activity().getApp().toast(R.string.msg_file_exists, Toaster.Type.ERROR); - } else { - if(FileIOUtils.writeFileFromString(file, content)) { - LSP.notifyFileCreated(file); - activity(). getApp().toast(R.string.msg_file_created, Toaster.Type.SUCCESS); - if(activity().getLastHoldTreeNode() != null) { - TreeNode node = new TreeNode(file); - node.setViewHolder(new FileTreeViewHolder(activity())); - activity().getLastHoldTreeNode().addChild(node); - activity().getFileTreeFragment().expandNode(activity().getLastHoldTreeNode()); - } else { - activity().getFileTreeFragment().listProjectFiles(); - } - } else { - activity(). getApp().toast(R.string.msg_file_creation_failed, Toaster.Type.ERROR); - } - } - } else { - activity(). getApp().toast(R.string.msg_invalid_name, Toaster.Type.ERROR); - } - } - - private void createNewFolder(File f) { - final LayoutDialogTextInputBinding binding = LayoutDialogTextInputBinding.inflate(activity(). getLayoutInflater()); - final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); - - Objects.requireNonNull (binding.name.getEditText ()).setHint(R.string.folder_name); - - builder.setTitle(R.string.new_folder); - builder.setMessage(R.string.msg_can_contain_slashes); - builder.setView(binding.getRoot()); - builder.setCancelable(false); - builder.setPositiveButton(R.string.text_create, (p1, p2) -> { - p1.dismiss(); - final String name = binding.name.getEditText().getText().toString().trim(); - if(name.length() > 0 && name.length() <= 40 && !name.startsWith("/")) { - final File file = new File(f, name); - if(file.exists()) { - activity(). getApp().toast(R.string.msg_folder_exists, Toaster.Type.ERROR); - } else { - if(file.mkdirs()) { - activity(). getApp().toast(R.string.msg_folder_created, Toaster.Type.SUCCESS); - if(activity().getLastHoldTreeNode() != null) { - TreeNode node = new TreeNode(file); - node.setViewHolder(new FileTreeViewHolder(activity())); - activity().getLastHoldTreeNode().addChild(node); - activity().getFileTreeFragment().expandNode(activity().getLastHoldTreeNode()); - } else { - activity().getFileTreeFragment().listProjectFiles(); - } - } else { - activity().getApp().toast(R.string.msg_folder_creation_failed, Toaster.Type.ERROR); - } - } - } else { - activity(). getApp().toast(R.string.msg_invalid_name, Toaster.Type.ERROR); - } - }); - builder.setNegativeButton(android.R.string.cancel, null); - builder.create().show(); - } - - private void delete(final File f) { - MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); - builder - .setNegativeButton(android.R.string.no, null) - .setPositiveButton(android.R.string.yes, (p1, p2) -> { - p1.dismiss(); - final boolean deleted = FileUtils.delete(f); - activity(). getApp().toast(deleted ? R.string.deleted : R.string.delete_failed, deleted ? Toaster.Type.SUCCESS : Toaster.Type.ERROR); - if(deleted) { - LSP.notifyFileDeleted(f); - if(activity().getLastHoldTreeNode() != null) { - TreeNode parent = activity().getLastHoldTreeNode().getParent(); - parent.deleteChild(activity().getLastHoldTreeNode()); - activity().getFileTreeFragment().expandNode(parent); - } else { - activity().getFileTreeFragment().listProjectFiles(); - } - - EditorFragment frag = activity().getEditorForFile (f); - if(frag != null) { - activity().closeFile(activity().findIndexOfEditorByFile (frag.getFile ())); - } - } - }) - .setTitle(R.string.title_confirm_delete) - .setMessage(activity().getString(R.string.msg_confirm_delete, String.format("%s [%s]", f.getName(), f.getAbsolutePath()))) - .setCancelable(false) - .create().show(); - } - - private void renameFile(File f) { - final LayoutDialogTextInputBinding binding = LayoutDialogTextInputBinding.inflate(LayoutInflater.from(activity())); - MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); - - Objects.requireNonNull (binding.name.getEditText ()).setHint(activity(). getString(R.string.new_name)); - binding.name.getEditText().setText(f.getName()); - - builder.setTitle(R.string.rename_file); - builder.setMessage(R.string.msg_rename_file); - builder.setView(binding.getRoot()); - builder.setNegativeButton(android.R.string.cancel, null); - builder.setPositiveButton(R.string.rename_file, (p1, p2) -> { - p1.dismiss(); - String name = binding.name.getEditText().getText().toString().trim(); - boolean renamed = name.length () > 0 && name.length () <= 40 && FileUtils.rename (f, name); - activity(). getApp().toast(renamed ? R.string.renamed : R.string.rename_failed, renamed ? Toaster.Type.SUCCESS : Toaster.Type.ERROR); - if(renamed) { - LSP.notifyFileRenamed(f, name); - if(activity().getLastHoldTreeNode() != null) { - TreeNode parent = activity().getLastHoldTreeNode().getParent(); - parent.deleteChild(activity().getLastHoldTreeNode()); - TreeNode node = new TreeNode(new File(f.getParentFile(), name)); - node.setViewHolder(new FileTreeViewHolder(activity())); - parent.addChild(node); - activity().getFileTreeFragment().expandNode(parent); - } else { - activity().getFileTreeFragment().listProjectFiles(); - } - } - }); - builder.create().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 . + * +**************************************************************************************/ +package com.itsaky.androidide.handlers; + +import android.view.LayoutInflater; +import com.blankj.utilcode.util.ClipboardUtils; +import com.blankj.utilcode.util.FileIOUtils; +import com.blankj.utilcode.util.FileUtils; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.itsaky.androidide.R; +import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder; +import com.itsaky.androidide.databinding.LayoutCreateFileJavaBinding; +import com.itsaky.androidide.databinding.LayoutDialogTextInputBinding; +import com.itsaky.androidide.fragments.sheets.OptionsListFragment; +import com.itsaky.androidide.lsp.LSP; +import com.itsaky.androidide.models.SheetOption; +import com.itsaky.androidide.utils.DialogUtils; +import com.itsaky.androidide.utils.Environment; +import com.itsaky.androidide.utils.ProjectWriter; +import com.itsaky.toaster.Toaster; +import com.unnamed.b.atv.model.TreeNode; +import java.io.File; +import java.util.Objects; +import java.util.regex.Pattern; + +public class FileOptionsHandler extends IDEHandler implements OptionsListFragment.OnOptionsClickListener { + + public static final String RES_PATH_REGEX = "/.*/src/.*/res"; + public static final String LAYOUTRES_PATH_REGEX = "/.*/src/.*/res/layout"; + public static final String MENURES_PATH_REGEX = "/.*/src/.*/res/menu"; + public static final String DRAWABLERES_PATH_REGEX = "/.*/src/.*/res/drawable"; + public static final String JAVA_PATH_REGEX = "/.*/src/.*/java"; + + public FileOptionsHandler(Provider provider) { + super(provider); + } + + @Override + public void start() { + if(activity() == null) + throwNPE(); + } + + @Override + public void stop() { + } + + @Override + public void onOptionsClick(SheetOption option) { + if(option.extra instanceof File) { + final File f = (File) option.extra; + switch(option.id) { + case 0 : + ClipboardUtils.copyText("[AndroidIDE] Copied File Path", f.getAbsolutePath()); + activity().getApp().toast(R.string.copied, Toaster.Type.SUCCESS); + break; + case 1 : + renameFile(f); + break; + case 2 : + delete(f); + break; + case 3 : + createNewFile(f); + break; + case 4 : + createNewFolder(f); + break; + } + } + } + + private void createNewFile(File f) { + createNewFile(f, false); + } + + private void createNewFile(final File f, boolean forceUnknownType) { + if(forceUnknownType) { + createNewEmptyFile(f); + } else { + final boolean isJava = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + JAVA_PATH_REGEX).matcher(f.getAbsolutePath()).find(); + final boolean isRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + RES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); + final boolean isLayoutRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + LAYOUTRES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); + final boolean isMenuRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + MENURES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); + final boolean isDrawableRes = Pattern.compile(Pattern.quote(androidProject().getProjectPath()) + DRAWABLERES_PATH_REGEX).matcher(f.getAbsolutePath()).find(); + if(isJava) { + createJavaClass(f); + } else if(isLayoutRes && f.getName().equals("layout")) { + createLayoutRes(f); + } else if(isMenuRes && f.getName().equals("menu")) { + createMenuRes(f); + } else if(isDrawableRes && f.getName().equals("drawable")) { + createDrawableRes(f); + } else if(isRes && f.getName().equals("res")) { + createNewResource(f); + } else { + createNewEmptyFile(f); + } + } + } + + private void createJavaClass(final File f) { + final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); + final LayoutCreateFileJavaBinding binding = LayoutCreateFileJavaBinding.inflate(activity().getLayoutInflater()); + builder.setView(binding.getRoot()); + builder.setTitle(R.string.new_java_class); + builder.setPositiveButton(R.string.text_create, (p1, p2) -> { + p1.dismiss(); + final String name = Objects.requireNonNull (binding.name.getEditText ()).getText().toString().trim(); + final String pkgName = ProjectWriter.getPackageName(f); + if(pkgName == null || pkgName.trim().length() <= 0) { + activity().getApp().toast(R.string.msg_get_package_failed, Toaster.Type.ERROR); + } else { + final int id = binding.typeGroup.getCheckedButtonId(); + final var javaName = name.endsWith (".java") ? name : name.concat (".java"); + final var className = !name.contains (".") ? name : name.substring (0, name.lastIndexOf (".")); + if(id == binding.typeClass.getId()) { + createFile(f, javaName, ProjectWriter.createJavaClass(pkgName, className)); + } else if(id == binding.typeInterface.getId()) { + createFile(f, javaName, ProjectWriter.createJavaInterface(pkgName, className)); + } else if(id == binding.typeEnum.getId()) { + createFile(f, javaName, ProjectWriter.createJavaEnum(pkgName, className)); + } else if(id == binding.typeActivity.getId()) { + createFile(f, javaName, ProjectWriter.createActivity(pkgName, className)); + } else { + createFile(f, name, ""); + } + + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setCancelable(false); + builder.create().show(); + } + + private void createLayoutRes(File f) { + createNewFileWithContent(Environment.mkdirIfNotExits(f), ProjectWriter.createLayout(), ".xml"); + } + + private void createMenuRes(File f) { + createNewFileWithContent(Environment.mkdirIfNotExits(f), ProjectWriter.createMenu(), ".xml"); + } + + private void createDrawableRes(File f) { + createNewFileWithContent(Environment.mkdirIfNotExits(f), ProjectWriter.createDrawable(), ".xml"); + } + + private void createNewResource(File f) { + final String[] labels = { + activity(). getString(R.string.restype_drawable), + activity(). getString(R.string.restype_layout), + activity(). getString(R.string.restype_menu), + activity(). getString(R.string.restype_other) + }; + final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); + builder.setTitle(R.string.new_xml_resource); + builder.setItems(labels, (dialogInterface, position) -> { + if(position == 0) { + createDrawableRes(new File(f, "drawable")); + } else if(position == 1) { + createLayoutRes(new File(f, "layout")); + } else if(position == 2) { + createMenuRes(new File(f, "menu")); + } else if(position == 3) { + createNewFile(f, true); + } + }); + builder.create().show(); + } + + private void createNewEmptyFile(File f) { + createNewFileWithContent(f, ""); + } + + private void createNewFileWithContent(File f, String content) { + createNewFileWithContent(f, content, null); + } + + private void createNewFileWithContent(File folder, String content, String extension) { + final LayoutDialogTextInputBinding binding = LayoutDialogTextInputBinding.inflate(activity(). getLayoutInflater()); + final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); + + Objects.requireNonNull (binding.name.getEditText ()).setHint(R.string.file_name); + + builder.setTitle(R.string.new_file); + builder.setMessage(activity(). getString(R.string.msg_can_contain_slashes).concat("\n\n").concat(activity(). getString(R.string.msg_newfile_dest, folder.getAbsolutePath()))); + builder.setView(binding.getRoot()); + builder.setCancelable(false); + builder.setPositiveButton(R.string.text_create, (p1, p2) -> { + p1.dismiss(); + String name = binding.name.getEditText().getText().toString().trim(); + if(extension != null && extension.trim().length() > 0) { + name = name.endsWith(extension) ? name : name.concat(extension); + } + createFile(folder, name, content); + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.create().show(); + } + + private void createFile(File f, String name, String content) { + if(name.length() > 0 && name.length() <= 40 && !name.startsWith("/")) { + final File file = new File(f, name); + if(file.exists()) { + activity().getApp().toast(R.string.msg_file_exists, Toaster.Type.ERROR); + } else { + if(FileIOUtils.writeFileFromString(file, content)) { + LSP.notifyFileCreated(file); + activity(). getApp().toast(R.string.msg_file_created, Toaster.Type.SUCCESS); + if(activity().getLastHoldTreeNode() != null) { + TreeNode node = new TreeNode(file); + node.setViewHolder(new FileTreeViewHolder(activity())); + activity().getLastHoldTreeNode().addChild(node); + activity().getFileTreeFragment().expandNode(activity().getLastHoldTreeNode()); + } else { + activity().getFileTreeFragment().listProjectFiles(); + } + } else { + activity(). getApp().toast(R.string.msg_file_creation_failed, Toaster.Type.ERROR); + } + } + } else { + activity(). getApp().toast(R.string.msg_invalid_name, Toaster.Type.ERROR); + } + } + + private void createNewFolder(File f) { + final LayoutDialogTextInputBinding binding = LayoutDialogTextInputBinding.inflate(activity(). getLayoutInflater()); + final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); + + Objects.requireNonNull (binding.name.getEditText ()).setHint(R.string.folder_name); + + builder.setTitle(R.string.new_folder); + builder.setMessage(R.string.msg_can_contain_slashes); + builder.setView(binding.getRoot()); + builder.setCancelable(false); + builder.setPositiveButton(R.string.text_create, (p1, p2) -> { + p1.dismiss(); + final String name = binding.name.getEditText().getText().toString().trim(); + if(name.length() > 0 && name.length() <= 40 && !name.startsWith("/")) { + final File file = new File(f, name); + if(file.exists()) { + activity(). getApp().toast(R.string.msg_folder_exists, Toaster.Type.ERROR); + } else { + if(file.mkdirs()) { + activity(). getApp().toast(R.string.msg_folder_created, Toaster.Type.SUCCESS); + if(activity().getLastHoldTreeNode() != null) { + TreeNode node = new TreeNode(file); + node.setViewHolder(new FileTreeViewHolder(activity())); + activity().getLastHoldTreeNode().addChild(node); + activity().getFileTreeFragment().expandNode(activity().getLastHoldTreeNode()); + } else { + activity().getFileTreeFragment().listProjectFiles(); + } + } else { + activity().getApp().toast(R.string.msg_folder_creation_failed, Toaster.Type.ERROR); + } + } + } else { + activity(). getApp().toast(R.string.msg_invalid_name, Toaster.Type.ERROR); + } + }); + builder.setNegativeButton(android.R.string.cancel, null); + builder.create().show(); + } + + private void delete(final File f) { + MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); + builder + .setNegativeButton(android.R.string.no, null) + .setPositiveButton(android.R.string.yes, (p1, p2) -> { + p1.dismiss(); + final boolean deleted = FileUtils.delete(f); + activity(). getApp().toast(deleted ? R.string.deleted : R.string.delete_failed, deleted ? Toaster.Type.SUCCESS : Toaster.Type.ERROR); + if(deleted) { + LSP.notifyFileDeleted(f); + if(activity().getLastHoldTreeNode() != null) { + TreeNode parent = activity().getLastHoldTreeNode().getParent(); + parent.deleteChild(activity().getLastHoldTreeNode()); + activity().getFileTreeFragment().expandNode(parent); + } else { + activity().getFileTreeFragment().listProjectFiles(); + } + + final var frag = activity().getEditorForFile (f); + if(frag != null) { + activity().closeFile(activity().findIndexOfEditorByFile (frag.getFile ())); + } + } + }) + .setTitle(R.string.title_confirm_delete) + .setMessage(activity().getString(R.string.msg_confirm_delete, String.format("%s [%s]", f.getName(), f.getAbsolutePath()))) + .setCancelable(false) + .create().show(); + } + + private void renameFile(File f) { + final LayoutDialogTextInputBinding binding = LayoutDialogTextInputBinding.inflate(LayoutInflater.from(activity())); + MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); + + Objects.requireNonNull (binding.name.getEditText ()).setHint(activity(). getString(R.string.new_name)); + binding.name.getEditText().setText(f.getName()); + + builder.setTitle(R.string.rename_file); + builder.setMessage(R.string.msg_rename_file); + builder.setView(binding.getRoot()); + builder.setNegativeButton(android.R.string.cancel, null); + builder.setPositiveButton(R.string.rename_file, (p1, p2) -> { + p1.dismiss(); + String name = binding.name.getEditText().getText().toString().trim(); + boolean renamed = name.length () > 0 && name.length () <= 40 && FileUtils.rename (f, name); + activity(). getApp().toast(renamed ? R.string.renamed : R.string.rename_failed, renamed ? Toaster.Type.SUCCESS : Toaster.Type.ERROR); + if(renamed) { + LSP.notifyFileRenamed(f, name); + if(activity().getLastHoldTreeNode() != null) { + TreeNode parent = activity().getLastHoldTreeNode().getParent(); + parent.deleteChild(activity().getLastHoldTreeNode()); + TreeNode node = new TreeNode(new File(f.getParentFile(), name)); + node.setViewHolder(new FileTreeViewHolder(activity())); + parent.addChild(node); + activity().getFileTreeFragment().expandNode(parent); + } else { + activity().getFileTreeFragment().listProjectFiles(); + } + } + }); + builder.create().show(); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/handlers/IDEHandler.java b/app/src/main/java/com/itsaky/androidide/handlers/IDEHandler.java index f74760969..1e604ae7e 100755 --- a/app/src/main/java/com/itsaky/androidide/handlers/IDEHandler.java +++ b/app/src/main/java/com/itsaky/androidide/handlers/IDEHandler.java @@ -58,7 +58,7 @@ public abstract class IDEHandler { /** * An interface to communicate between a handler and its client */ - public static interface Provider { + public interface Provider { /** * Called by handler to get a reference to {@link EditorActivity} diff --git a/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java b/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java index f7a5749b6..ef6eddbf1 100644 --- a/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java +++ b/app/src/main/java/com/itsaky/androidide/language/java/JavaAnalyzer.java @@ -1,462 +1,462 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.language.java; - -import androidx.annotation.NonNull; - -import com.itsaky.androidide.lexers.java.JavaLexer; -import com.itsaky.androidide.models.ConstantsBridge; -import com.itsaky.androidide.utils.LSPUtils; -import com.itsaky.lsp.SemanticHighlight; -import com.itsaky.lsp.services.IDELanguageServer; - -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.Token; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.Position; -import org.eclipse.lsp4j.Range; - -import java.io.File; -import java.io.StringReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Stack; -import java.util.stream.Collectors; - -import io.github.rosemoe.editor.langs.AbstractCodeAnalyzer; -import io.github.rosemoe.editor.struct.BlockLine; -import io.github.rosemoe.editor.text.CharPosition; -import io.github.rosemoe.editor.text.Content; -import io.github.rosemoe.editor.text.TextAnalyzeResult; -import io.github.rosemoe.editor.text.TextAnalyzer; -import io.github.rosemoe.editor.widget.EditorColorScheme; - -// TODO request language server for semantic highlights instead of waiting for it to send - -/** - * Code analyzer for the Java Language - * - * @author Akash Yadav - */ -public class JavaAnalyzer extends AbstractCodeAnalyzer { - - private HighlightRangeHelper helper; - private Map> diagnostics = new HashMap<> (); - - private final Map> stringMap = new HashMap<> (); - - @Override - public void analyze (IDELanguageServer languageServer, File file, @NonNull Content content, TextAnalyzeResult colors, @NonNull TextAnalyzer.AnalyzeThread.Delegate delegate) throws Exception { - final var stream = CharStreams.fromReader (new StringReader (content.toString ())); - final var lexer = new JavaLexer (stream); - final var stack = new Stack (); - - Token token; - int line, column, lastLine = 1, type, currSwitch = 0, maxSwitch = 0; - boolean isFirst = true; - - this.stringMap.clear (); - - while (delegate.shouldAnalyze ()) { - token = lexer.nextToken (); - - if (token == null) { - break; - } - - type = token.getType (); - line = token.getLine () - 1; - column = token.getCharPositionInLine (); - - if (type == JavaLexer.EOF) { - lastLine = line; - break; - } - - switch (type) { - case JavaLexer.WS: - if (isFirst) { - colors.addNormalIfNull (); - } - break; - case JavaLexer.ABSTRACT: - case JavaLexer.ASSERT: - case JavaLexer.BREAK: - case JavaLexer.CASE: - case JavaLexer.CATCH: - case JavaLexer.CLASS: - case JavaLexer.CONST: - case JavaLexer.CONTINUE: - case JavaLexer.DEFAULT: - case JavaLexer.DO: - case JavaLexer.ELSE: - case JavaLexer.EXTENDS: - case JavaLexer.FINAL: - case JavaLexer.FINALLY: - case JavaLexer.FOR: - case JavaLexer.IF: - case JavaLexer.GOTO: - case JavaLexer.IMPLEMENTS: - case JavaLexer.IMPORT: - case JavaLexer.INSTANCEOF: - case JavaLexer.INTERFACE: - case JavaLexer.NATIVE: - case JavaLexer.NEW: - case JavaLexer.PACKAGE: - case JavaLexer.PRIVATE: - case JavaLexer.PROTECTED: - case JavaLexer.PUBLIC: - case JavaLexer.RETURN: - case JavaLexer.STATIC: - case JavaLexer.STRICTFP: - case JavaLexer.SUPER: - case JavaLexer.SWITCH: - case JavaLexer.SYNCHRONIZED: - case JavaLexer.THIS: - case JavaLexer.THROW: - case JavaLexer.THROWS: - case JavaLexer.TRANSIENT: - case JavaLexer.TRY: - case JavaLexer.VOID: - case JavaLexer.VOLATILE: - case JavaLexer.WHILE: - case JavaLexer.VAR: - colors.addIfNeeded (line, column, EditorColorScheme.KEYWORD); - break; - case JavaLexer.DECIMAL_LITERAL: - case JavaLexer.HEX_LITERAL: - case JavaLexer.OCT_LITERAL: - case JavaLexer.BINARY_LITERAL: - case JavaLexer.FLOAT_LITERAL: - case JavaLexer.HEX_FLOAT_LITERAL: - case JavaLexer.BOOL_LITERAL: - case JavaLexer.CHAR_LITERAL: - case JavaLexer.NULL_LITERAL: - colors.addIfNeeded (line, column, EditorColorScheme.LITERAL); - break; - case JavaLexer.STRING_LITERAL: - checkAndAddHexString (token, EditorColorScheme.LITERAL, colors); - - Position start = new Position (line, column); - Position end = new Position (line, column + token.getText ().length ()); - colors.addStringRange (line, new Range (start, end)); - - { - List ranges = stringMap.get (line); - if (ranges == null) { - ranges = new ArrayList<> (); - } - ranges.add (new Range (start, end)); - stringMap.put (line, ranges); - } - break; - case JavaLexer.LPAREN: - case JavaLexer.RPAREN: - case JavaLexer.LBRACK: - case JavaLexer.RBRACK: - case JavaLexer.SEMI: - case JavaLexer.COMMA: - case JavaLexer.ASSIGN: - case JavaLexer.GT: - case JavaLexer.LT: - case JavaLexer.BANG: - case JavaLexer.TILDE: - case JavaLexer.QUESTION: - case JavaLexer.COLON: - case JavaLexer.EQUAL: - case JavaLexer.GE: - case JavaLexer.LE: - case JavaLexer.NOTEQUAL: - case JavaLexer.AND: - case JavaLexer.OR: - case JavaLexer.INC: - case JavaLexer.DEC: - case JavaLexer.ADD: - case JavaLexer.SUB: - case JavaLexer.MUL: - case JavaLexer.DIV: - case JavaLexer.BITAND: - case JavaLexer.BITOR: - case JavaLexer.CARET: - case JavaLexer.MOD: - case JavaLexer.ADD_ASSIGN: - case JavaLexer.SUB_ASSIGN: - case JavaLexer.MUL_ASSIGN: - case JavaLexer.DIV_ASSIGN: - case JavaLexer.AND_ASSIGN: - case JavaLexer.OR_ASSIGN: - case JavaLexer.XOR_ASSIGN: - case JavaLexer.MOD_ASSIGN: - case JavaLexer.LSHIFT_ASSIGN: - case JavaLexer.RSHIFT_ASSIGN: - case JavaLexer.URSHIFT_ASSIGN: - case JavaLexer.ARROW: - case JavaLexer.COLONCOLON: - case JavaLexer.ELLIPSIS: - case JavaLexer.DOT: - colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR); - break; - case JavaLexer.BOOLEAN: - case JavaLexer.BYTE: - case JavaLexer.CHAR: - case JavaLexer.DOUBLE: - case JavaLexer.ENUM: - case JavaLexer.FLOAT: - case JavaLexer.INT: - case JavaLexer.LONG: - case JavaLexer.SHORT: - colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME); - break; - case JavaLexer.BLOCK_COMMENT: - case JavaLexer.LINE_COMMENT: - colors.addIfNeeded (line, column, EditorColorScheme.COMMENT); - - final CharPosition a = content.getIndexer ().getCharPosition (token.getStopIndex ()); - final Position s = new Position (line, column); - final Position e = new Position (a.line, a.column); - colors.addCommentRange (line, new Range (s, e)); - - String lineComment = token.getText (); - if (lineComment != null) { - if (lineComment.startsWith ("//")) { - lineComment = lineComment.substring (2); - } - - // Trim leading whitespaces - int startsFrom = 0; - for (int i = 0; i < lineComment.length (); i++) { - if (Character.isWhitespace (lineComment.charAt (i))) { - startsFrom++; - } else { - break; - } - } - - lineComment = lineComment.substring (startsFrom); - - final String text = new String (lineComment.toCharArray ()); - lineComment = lineComment.toLowerCase (Locale.getDefault ()); - if (lineComment.startsWith ("todo ")) { - diagnostics.put (line, Collections.singletonMap (column, LSPUtils.newInfoDiagnostic (line, - column, - token.getText ().length (), - text, - token.getText ()))); - } else if (lineComment.startsWith (ConstantsBridge.CUSTOM_COMMENT_WARNING_TOKEN)) { - diagnostics.put (line, Collections.singletonMap (column, LSPUtils.newWarningDiagnostic (line, - column, - token.getText ().length (), - text, - token.getText ()))); - } - } - - break; - case JavaLexer.AT: - colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION); - break; - case JavaLexer.IDENTIFIER: - if (helper != null && helper.isPackageName (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.PACKAGE_NAME); - break; - } - - if (helper != null && helper.isEnumType (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.ENUM_TYPE); - break; - } - - if (helper != null && helper.isClassName (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME); - break; - } - - if (helper != null && helper.isAnnotationType (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION); - break; - } - - if (helper != null && helper.isInterface (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.INTERFACE); - break; - } - - if (helper != null && helper.isEnum (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.ENUM); - break; - } - - if (helper != null && helper.isStaticField (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.STATIC_FIELD); - break; - } - - if (helper != null && helper.isField (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.FIELD); - break; - } - - if (helper != null && helper.isParameter (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.PARAMETER); - break; - } - - if (helper != null && helper.isLocal (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.LOCAL_VARIABLE); - break; - } - - if (helper != null && helper.isExceptionParam (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.EXCEPTION_PARAM); - break; - } - - if (helper != null && helper.isMethodDeclaration (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.METHOD_DECLARATION); - break; - } - - if (helper != null && helper.isMethodInvocation (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.METHOD_INVOCATION); - break; - } - - if (helper != null && helper.isConstructor (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.CONSTRUCTOR); - break; - } - - if (helper != null && helper.isStaticInit (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.STATIC_INIT); - break; - } - - if (helper != null && helper.isInstanceInit (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.INSTANCE_INIT); - break; - } - - if (helper != null && helper.isTypeParam (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.TYPE_PARAM); - break; - } - - if (helper != null && helper.isResourceVariable (line, column)) { - colors.addIfNeeded (line, column, EditorColorScheme.RESOURCE_VARIABLE); - break; - } - - colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL); - break; - case JavaLexer.LBRACE: - colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR); - if (stack.isEmpty ()) { - if (currSwitch > maxSwitch) { - maxSwitch = currSwitch; - } - currSwitch = 0; - } - currSwitch++; - BlockLine block = colors.obtainNewBlock (); - block.startLine = line; - block.startColumn = column; - stack.push (block); - break; - case JavaLexer.RBRACE: - colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR); - if (!stack.isEmpty ()) { - BlockLine block2 = stack.pop (); - block2.endLine = line; - block2.endColumn = column; - if (block2.startLine != block2.endLine) { - colors.addBlockLine (block2); - } - } - break; - default: - colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL); - break; - } - - isFirst = false; - } - - if (stack.isEmpty () && currSwitch > maxSwitch) { - maxSwitch = currSwitch; - } - colors.determine (lastLine); - colors.setSuppressSwitch (maxSwitch + 10); - } - - @Override - public void setSemanticHighlights (SemanticHighlight highlights) { - this.helper = new HighlightRangeHelper (highlights); - this.helper.sort (); - } - - @Override - public void updateDiagnostics (Map> diagnostics) { - if (diagnostics == null) { - diagnostics = new HashMap<> (); - } - this.diagnostics = diagnostics; - } - - @Override - public Diagnostic findDiagnosticContaining (int line, int column) { - final Map mappedByColumn = diagnostics.get (line); - if (mappedByColumn != null) { - Diagnostic diag = mappedByColumn.get (column); - if (diag != null) { - return diag; - } else { - for (Map.Entry entry : mappedByColumn.entrySet ()) { - diag = entry.getValue (); - final int start = diag.getRange ().getStart ().getCharacter (); - final int end = diag.getRange ().getEnd ().getCharacter (); - - if (column >= start && column <= end) { - return diag; - } - } - } - } - return super.findDiagnosticContaining (line, column); - } - - @Override - public List findDiagnosticsContainingLine (int line) { - final Map diags = diagnostics.get (line); - if (diags == null) { - return super.findDiagnosticsContainingLine (line); - } - return diags.entrySet ().stream () - .filter (d -> d != null && d.getValue () != null) - .map (Map.Entry::getValue) - .collect (Collectors.toList ()); - } - - @Override - public Map getDiagnosticsAtLine (int line) { - return diagnostics.get (line); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.language.java; + +import androidx.annotation.NonNull; + +import com.itsaky.androidide.lexers.java.JavaLexer; +import com.itsaky.androidide.models.ConstantsBridge; +import com.itsaky.androidide.utils.LSPUtils; +import com.itsaky.lsp.SemanticHighlight; +import com.itsaky.lsp.services.IDELanguageServer; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.Token; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.Position; +import org.eclipse.lsp4j.Range; + +import java.io.File; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Stack; +import java.util.stream.Collectors; + +import io.github.rosemoe.editor.langs.AbstractCodeAnalyzer; +import io.github.rosemoe.editor.struct.BlockLine; +import io.github.rosemoe.editor.text.CharPosition; +import io.github.rosemoe.editor.text.Content; +import io.github.rosemoe.editor.text.TextAnalyzeResult; +import io.github.rosemoe.editor.text.TextAnalyzer; +import io.github.rosemoe.editor.widget.EditorColorScheme; + +// TODO request language server for semantic highlights instead of waiting for it to send + +/** + * Code analyzer for the Java Language + * + * @author Akash Yadav + */ +public class JavaAnalyzer extends AbstractCodeAnalyzer { + + private HighlightRangeHelper helper; + private Map> diagnostics = new HashMap<> (); + + private final Map> stringMap = new HashMap<> (); + + @Override + public void analyze (IDELanguageServer languageServer, File file, @NonNull Content content, TextAnalyzeResult colors, @NonNull TextAnalyzer.AnalyzeThread.Delegate delegate) throws Exception { + final var stream = CharStreams.fromReader (new StringReader (content.toString ())); + final var lexer = new JavaLexer (stream); + final var stack = new Stack (); + + Token token; + int line, column, lastLine = 1, type, currSwitch = 0, maxSwitch = 0; + boolean isFirst = true; + + this.stringMap.clear (); + + while (delegate.shouldAnalyze ()) { + token = lexer.nextToken (); + + if (token == null) { + break; + } + + type = token.getType (); + line = token.getLine () - 1; + column = token.getCharPositionInLine (); + + if (type == JavaLexer.EOF) { + lastLine = line; + break; + } + + switch (type) { + case JavaLexer.WS: + if (isFirst) { + colors.addNormalIfNull (); + } + break; + case JavaLexer.ABSTRACT: + case JavaLexer.ASSERT: + case JavaLexer.BREAK: + case JavaLexer.CASE: + case JavaLexer.CATCH: + case JavaLexer.CLASS: + case JavaLexer.CONST: + case JavaLexer.CONTINUE: + case JavaLexer.DEFAULT: + case JavaLexer.DO: + case JavaLexer.ELSE: + case JavaLexer.EXTENDS: + case JavaLexer.FINAL: + case JavaLexer.FINALLY: + case JavaLexer.FOR: + case JavaLexer.IF: + case JavaLexer.GOTO: + case JavaLexer.IMPLEMENTS: + case JavaLexer.IMPORT: + case JavaLexer.INSTANCEOF: + case JavaLexer.INTERFACE: + case JavaLexer.NATIVE: + case JavaLexer.NEW: + case JavaLexer.PACKAGE: + case JavaLexer.PRIVATE: + case JavaLexer.PROTECTED: + case JavaLexer.PUBLIC: + case JavaLexer.RETURN: + case JavaLexer.STATIC: + case JavaLexer.STRICTFP: + case JavaLexer.SUPER: + case JavaLexer.SWITCH: + case JavaLexer.SYNCHRONIZED: + case JavaLexer.THIS: + case JavaLexer.THROW: + case JavaLexer.THROWS: + case JavaLexer.TRANSIENT: + case JavaLexer.TRY: + case JavaLexer.VOID: + case JavaLexer.VOLATILE: + case JavaLexer.WHILE: + case JavaLexer.VAR: + colors.addIfNeeded (line, column, EditorColorScheme.KEYWORD); + break; + case JavaLexer.DECIMAL_LITERAL: + case JavaLexer.HEX_LITERAL: + case JavaLexer.OCT_LITERAL: + case JavaLexer.BINARY_LITERAL: + case JavaLexer.FLOAT_LITERAL: + case JavaLexer.HEX_FLOAT_LITERAL: + case JavaLexer.BOOL_LITERAL: + case JavaLexer.CHAR_LITERAL: + case JavaLexer.NULL_LITERAL: + colors.addIfNeeded (line, column, EditorColorScheme.LITERAL); + break; + case JavaLexer.STRING_LITERAL: + checkAndAddHexString (token, EditorColorScheme.LITERAL, colors); + + Position start = new Position (line, column); + Position end = new Position (line, column + token.getText ().length ()); + colors.addStringRange (line, new Range (start, end)); + + { + List ranges = stringMap.get (line); + if (ranges == null) { + ranges = new ArrayList<> (); + } + ranges.add (new Range (start, end)); + stringMap.put (line, ranges); + } + break; + case JavaLexer.LPAREN: + case JavaLexer.RPAREN: + case JavaLexer.LBRACK: + case JavaLexer.RBRACK: + case JavaLexer.SEMI: + case JavaLexer.COMMA: + case JavaLexer.ASSIGN: + case JavaLexer.GT: + case JavaLexer.LT: + case JavaLexer.BANG: + case JavaLexer.TILDE: + case JavaLexer.QUESTION: + case JavaLexer.COLON: + case JavaLexer.EQUAL: + case JavaLexer.GE: + case JavaLexer.LE: + case JavaLexer.NOTEQUAL: + case JavaLexer.AND: + case JavaLexer.OR: + case JavaLexer.INC: + case JavaLexer.DEC: + case JavaLexer.ADD: + case JavaLexer.SUB: + case JavaLexer.MUL: + case JavaLexer.DIV: + case JavaLexer.BITAND: + case JavaLexer.BITOR: + case JavaLexer.CARET: + case JavaLexer.MOD: + case JavaLexer.ADD_ASSIGN: + case JavaLexer.SUB_ASSIGN: + case JavaLexer.MUL_ASSIGN: + case JavaLexer.DIV_ASSIGN: + case JavaLexer.AND_ASSIGN: + case JavaLexer.OR_ASSIGN: + case JavaLexer.XOR_ASSIGN: + case JavaLexer.MOD_ASSIGN: + case JavaLexer.LSHIFT_ASSIGN: + case JavaLexer.RSHIFT_ASSIGN: + case JavaLexer.URSHIFT_ASSIGN: + case JavaLexer.ARROW: + case JavaLexer.COLONCOLON: + case JavaLexer.ELLIPSIS: + case JavaLexer.DOT: + colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR); + break; + case JavaLexer.BOOLEAN: + case JavaLexer.BYTE: + case JavaLexer.CHAR: + case JavaLexer.DOUBLE: + case JavaLexer.ENUM: + case JavaLexer.FLOAT: + case JavaLexer.INT: + case JavaLexer.LONG: + case JavaLexer.SHORT: + colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME); + break; + case JavaLexer.BLOCK_COMMENT: + case JavaLexer.LINE_COMMENT: + colors.addIfNeeded (line, column, EditorColorScheme.COMMENT); + + final CharPosition a = content.getIndexer ().getCharPosition (token.getStopIndex ()); + final Position s = new Position (line, column); + final Position e = new Position (a.line, a.column); + colors.addCommentRange (line, new Range (s, e)); + + String lineComment = token.getText (); + if (lineComment != null) { + if (lineComment.startsWith ("//")) { + lineComment = lineComment.substring (2); + } + + // Trim leading whitespaces + int startsFrom = 0; + for (int i = 0; i < lineComment.length (); i++) { + if (Character.isWhitespace (lineComment.charAt (i))) { + startsFrom++; + } else { + break; + } + } + + lineComment = lineComment.substring (startsFrom); + + final String text = new String (lineComment.toCharArray ()); + lineComment = lineComment.toLowerCase (Locale.getDefault ()); + if (lineComment.startsWith ("todo ")) { + diagnostics.put (line, Collections.singletonMap (column, LSPUtils.newInfoDiagnostic (line, + column, + token.getText ().length (), + text, + token.getText ()))); + } else if (lineComment.startsWith (ConstantsBridge.CUSTOM_COMMENT_WARNING_TOKEN)) { + diagnostics.put (line, Collections.singletonMap (column, LSPUtils.newWarningDiagnostic (line, + column, + token.getText ().length (), + text, + token.getText ()))); + } + } + + break; + case JavaLexer.AT: + colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION); + break; + case JavaLexer.IDENTIFIER: + if (helper != null && helper.isPackageName (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.PACKAGE_NAME); + break; + } + + if (helper != null && helper.isEnumType (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.ENUM_TYPE); + break; + } + + if (helper != null && helper.isClassName (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.TYPE_NAME); + break; + } + + if (helper != null && helper.isAnnotationType (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.ANNOTATION); + break; + } + + if (helper != null && helper.isInterface (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.INTERFACE); + break; + } + + if (helper != null && helper.isEnum (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.ENUM); + break; + } + + if (helper != null && helper.isStaticField (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.STATIC_FIELD); + break; + } + + if (helper != null && helper.isField (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.FIELD); + break; + } + + if (helper != null && helper.isParameter (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.PARAMETER); + break; + } + + if (helper != null && helper.isLocal (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.LOCAL_VARIABLE); + break; + } + + if (helper != null && helper.isExceptionParam (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.EXCEPTION_PARAM); + break; + } + + if (helper != null && helper.isMethodDeclaration (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.METHOD_DECLARATION); + break; + } + + if (helper != null && helper.isMethodInvocation (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.METHOD_INVOCATION); + break; + } + + if (helper != null && helper.isConstructor (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.CONSTRUCTOR); + break; + } + + if (helper != null && helper.isStaticInit (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.STATIC_INIT); + break; + } + + if (helper != null && helper.isInstanceInit (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.INSTANCE_INIT); + break; + } + + if (helper != null && helper.isTypeParam (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.TYPE_PARAM); + break; + } + + if (helper != null && helper.isResourceVariable (line, column)) { + colors.addIfNeeded (line, column, EditorColorScheme.RESOURCE_VARIABLE); + break; + } + + colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL); + break; + case JavaLexer.LBRACE: + colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR); + if (stack.isEmpty ()) { + if (currSwitch > maxSwitch) { + maxSwitch = currSwitch; + } + currSwitch = 0; + } + currSwitch++; + BlockLine block = colors.obtainNewBlock (); + block.startLine = line; + block.startColumn = column; + stack.push (block); + break; + case JavaLexer.RBRACE: + colors.addIfNeeded (line, column, EditorColorScheme.OPERATOR); + if (!stack.isEmpty ()) { + BlockLine block2 = stack.pop (); + block2.endLine = line; + block2.endColumn = column; + if (block2.startLine != block2.endLine) { + colors.addBlockLine (block2); + } + } + break; + default: + colors.addIfNeeded (line, column, EditorColorScheme.TEXT_NORMAL); + break; + } + + isFirst = false; + } + + if (stack.isEmpty () && currSwitch > maxSwitch) { + maxSwitch = currSwitch; + } + colors.determine (lastLine); + colors.setSuppressSwitch (maxSwitch + 10); + } + + @Override + public void setSemanticHighlights (SemanticHighlight highlights) { + this.helper = new HighlightRangeHelper (highlights); + this.helper.sort (); + } + + @Override + public void updateDiagnostics (Map> diagnostics) { + if (diagnostics == null) { + diagnostics = new HashMap<> (); + } + this.diagnostics = diagnostics; + } + + @Override + public Diagnostic findDiagnosticContaining (int line, int column) { + final Map mappedByColumn = diagnostics.get (line); + if (mappedByColumn != null) { + Diagnostic diag = mappedByColumn.get (column); + if (diag != null) { + return diag; + } else { + for (Map.Entry entry : mappedByColumn.entrySet ()) { + diag = entry.getValue (); + final int start = diag.getRange ().getStart ().getCharacter (); + final int end = diag.getRange ().getEnd ().getCharacter (); + + if (column >= start && column <= end) { + return diag; + } + } + } + } + return super.findDiagnosticContaining (line, column); + } + + @Override + public List findDiagnosticsContainingLine (int line) { + final Map diags = diagnostics.get (line); + if (diags == null) { + return super.findDiagnosticsContainingLine (line); + } + return diags.entrySet ().stream () + .filter (d -> d != null && d.getValue () != null) + .map (Map.Entry::getValue) + .collect (Collectors.toList ()); + } + + @Override + public Map getDiagnosticsAtLine (int line) { + return diagnostics.get (line); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java index 19ed3dcca..5be033baa 100755 --- a/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java +++ b/app/src/main/java/com/itsaky/androidide/lsp/IDELanguageClientImpl.java @@ -1,4 +1,4 @@ -/************************************************************************************ +/* * This file is part of AndroidIDE. * * @@ -16,15 +16,14 @@ * You should have received a copy of the GNU General Public License * along with AndroidIDE. If not, see . * -**************************************************************************************/ - - + */ package com.itsaky.androidide.lsp; -import android.content.DialogInterface; import android.text.SpannableStringBuilder; import android.text.style.ForegroundColorSpan; import android.view.View; + +import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import androidx.transition.TransitionManager; @@ -39,9 +38,7 @@ 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.databinding.FragmentEditorBinding; import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding; -import com.itsaky.androidide.fragments.EditorFragment; import com.itsaky.androidide.fragments.sheets.ProgressSheet; import com.itsaky.androidide.interfaces.EditorActivityProvider; import com.itsaky.androidide.models.DiagnosticGroup; @@ -50,11 +47,39 @@ import com.itsaky.androidide.tasks.TaskExecutor; import com.itsaky.androidide.utils.DialogUtils; import com.itsaky.androidide.utils.LSPUtils; import com.itsaky.androidide.utils.Logger; +import com.itsaky.androidide.views.CodeEditorView; import com.itsaky.lsp.SemanticHighlight; import com.itsaky.lsp.services.IDELanguageClient; import com.itsaky.toaster.Toaster; -import io.github.rosemoe.editor.text.Content; -import io.github.rosemoe.editor.widget.CodeEditor; + +import org.eclipse.lsp4j.ApplyWorkspaceEditParams; +import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; +import org.eclipse.lsp4j.CodeAction; +import org.eclipse.lsp4j.Command; +import org.eclipse.lsp4j.ConfigurationParams; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.Location; +import org.eclipse.lsp4j.LocationLink; +import org.eclipse.lsp4j.LogTraceParams; +import org.eclipse.lsp4j.MessageActionItem; +import org.eclipse.lsp4j.MessageParams; +import org.eclipse.lsp4j.ParameterInformation; +import org.eclipse.lsp4j.ProgressParams; +import org.eclipse.lsp4j.PublishDiagnosticsParams; +import org.eclipse.lsp4j.Range; +import org.eclipse.lsp4j.RegistrationParams; +import org.eclipse.lsp4j.SetTraceParams; +import org.eclipse.lsp4j.ShowDocumentParams; +import org.eclipse.lsp4j.ShowDocumentResult; +import org.eclipse.lsp4j.ShowMessageRequestParams; +import org.eclipse.lsp4j.SignatureHelp; +import org.eclipse.lsp4j.SignatureInformation; +import org.eclipse.lsp4j.TextEdit; +import org.eclipse.lsp4j.UnregistrationParams; +import org.eclipse.lsp4j.WorkDoneProgressCreateParams; +import org.eclipse.lsp4j.WorkspaceFolder; +import org.eclipse.lsp4j.jsonrpc.messages.Either; + import java.io.File; import java.net.URI; import java.util.ArrayList; @@ -62,35 +87,12 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; -import org.eclipse.lsp4j.CodeAction; -import org.eclipse.lsp4j.Command; -import org.eclipse.lsp4j.Diagnostic; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.LocationLink; -import org.eclipse.lsp4j.LogTraceParams; -import org.eclipse.lsp4j.MessageParams; -import org.eclipse.lsp4j.ParameterInformation; -import org.eclipse.lsp4j.PublishDiagnosticsParams; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.ShowDocumentParams; -import org.eclipse.lsp4j.ShowDocumentResult; -import org.eclipse.lsp4j.SignatureHelp; -import org.eclipse.lsp4j.SignatureInformation; -import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.lsp4j.SetTraceParams; -import org.eclipse.lsp4j.ApplyWorkspaceEditResponse; -import org.eclipse.lsp4j.WorkDoneProgressCreateParams; -import org.eclipse.lsp4j.ProgressParams; -import org.eclipse.lsp4j.ConfigurationParams; -import org.eclipse.lsp4j.RegistrationParams; -import org.eclipse.lsp4j.ApplyWorkspaceEditParams; -import org.eclipse.lsp4j.MessageActionItem; -import org.eclipse.lsp4j.WorkspaceFolder; -import org.eclipse.lsp4j.ShowMessageRequestParams; -import org.eclipse.lsp4j.UnregistrationParams; + +import io.github.rosemoe.editor.text.Content; +import io.github.rosemoe.editor.widget.CodeEditor; /** * AndroidIDE specific implementation of the LanguageClient @@ -99,13 +101,11 @@ public class IDELanguageClientImpl implements IDELanguageClient { protected static final Gson gson = new Gson(); protected static final Logger LOG = Logger.instance("AbstractLanguageClient"); - public static final int DIAGNOSTIC_TRANSITION_DURATION = 80; private final Map> diagnostics = new HashMap<>(); private static IDELanguageClientImpl mInstance; protected EditorActivityProvider activityProvider; - private boolean isConnected; private IDELanguageClientImpl (EditorActivityProvider provider) { setActivityProvider(provider); @@ -136,27 +136,20 @@ public class IDELanguageClientImpl implements IDELanguageClient { public void setActivityProvider(EditorActivityProvider provider) { this.activityProvider = provider; } - - /** - * Are we connected to the server? - */ - public boolean isConnected() { - return isConnected; - } protected EditorActivity activity() { if (activityProvider == null) return null; return activityProvider.provide(); } - private EditorFragment findEditorByFile (File file) { + private CodeEditorView findEditorByFile (File file) { return activity ().getEditorForFile (file); } @Override public void semanticHighlights(SemanticHighlight highlights) { final File file = new File(URI.create(highlights.uri)); - final EditorFragment editor = findEditorByFile(file); + final var editor = findEditorByFile(file); if(editor != null) { editor.getEditor().setSemanticHighlights(highlights); @@ -200,13 +193,13 @@ public class IDELanguageClientImpl implements IDELanguageClient { return; } - final EditorFragment frag = findEditorByFile(file); - if(frag == null || frag.getBinding() == null) { + final var frag = findEditorByFile(file); + if(frag == null) { hideBottomDiagnosticView(file); return; } - final FragmentEditorBinding binding = frag.getBinding(); + final var binding = frag.getBinding(); binding.diagnosticTextContainer.setVisibility(View.VISIBLE); binding.diagnosticText.setVisibility(View.VISIBLE); binding.diagnosticText.setClickable(false); @@ -219,13 +212,12 @@ public class IDELanguageClientImpl implements IDELanguageClient { } future.whenComplete((a, b) -> { - final Throwable error = b; if(a == null || a.isEmpty()) { hideBottomDiagnosticView(file); return; } - final List actions = a.stream().filter(e -> e.isRight()).map (e -> e.getRight()).collect(Collectors.toList()); - if(actions == null || actions.isEmpty()) { + final List actions = a.stream().filter(Either::isRight).map (Either::getRight).collect(Collectors.toList()); + if(actions.isEmpty()) { hideBottomDiagnosticView(file); return; } @@ -236,9 +228,7 @@ public class IDELanguageClientImpl implements IDELanguageClient { sb.append(diagnostic.getMessage()); binding.diagnosticText.setText(sb); binding.diagnosticText.setClickable(true); - binding.diagnosticText.setOnClickListener(v -> { - showAvailableQuickfixes(editor, actions); - }); + binding.diagnosticText.setOnClickListener(v -> showAvailableQuickfixes(editor, actions)); }); }); } @@ -256,7 +246,7 @@ public class IDELanguageClientImpl implements IDELanguageClient { return; } - final EditorFragment frag = findEditorByFile(file); + final var frag = findEditorByFile(file); if(frag == null || frag.getBinding() == null) { return; } @@ -277,7 +267,7 @@ public class IDELanguageClientImpl implements IDELanguageClient { SignatureInformation info = signatureWithMostParams(signature); if(info == null) return; activity().getBinding().symbolText.setText(formatSignature(info, signature.getActiveParameter())); - final EditorFragment frag = findEditorByFile(file); + final var frag = findEditorByFile(file); if(frag != null) { final CodeEditor editor = frag.getEditor(); final float[] cursor = editor.getCursorPosition(); @@ -325,7 +315,8 @@ public class IDELanguageClientImpl implements IDELanguageClient { * @param signature Signature information * @param paramIndex Currently active parameter index */ - private CharSequence formatSignature(SignatureInformation signature, int paramIndex) { + @NonNull + private CharSequence formatSignature(@NonNull SignatureInformation signature, int paramIndex) { String name = signature.getLabel(); name = name.substring(0, name.indexOf("(")); @@ -362,7 +353,7 @@ public class IDELanguageClientImpl implements IDELanguageClient { diagnostics.put(file, params.getDiagnostics()); activity().setDiagnosticsAdapter(newDiagnosticsAdapter()); - EditorFragment editor; + CodeEditorView editor; if((editor = findEditorByFile(file)) != null) { editor.setDiagnostics(params.getDiagnostics()); } @@ -394,13 +385,13 @@ public class IDELanguageClientImpl implements IDELanguageClient { if(loc == null || loc.getUri() == null || loc.getRange() == null) continue; final File file = new File(URI.create(loc.getUri())); if(!file.exists() || !file.isFile()) continue; - EditorFragment frag = findEditorByFile(file); + var frag = findEditorByFile(file); Content content; if(frag != null && frag.getEditor() != null) content = frag.getEditor().getText(); else content = new Content(null, FileIOUtils.readFile2String(file)); final List matches = results.containsKey(file) ? results.get(file) : new ArrayList<>(); - matches.add( + Objects.requireNonNull (matches).add( new SearchResult( loc.getRange(), file, @@ -415,7 +406,7 @@ public class IDELanguageClientImpl implements IDELanguageClient { ); results.put(file, matches); } catch (Throwable th) { - + LOG.error ("Failed to show file location", th); } } @@ -434,9 +425,9 @@ public class IDELanguageClientImpl implements IDELanguageClient { showLocations(locations .stream() - .filter(l -> l != null) - .map(l -> asLocation(l)) - .filter(l -> l != null) + .filter(Objects::nonNull) + .map(this::asLocation) + .filter(Objects::nonNull) .collect(Collectors.toList()) ); } @@ -462,14 +453,9 @@ public class IDELanguageClientImpl implements IDELanguageClient { progress.show(activity().getSupportFragmentManager(), "quick_fix_progress"); new TaskExecutor().executeAsyncProvideError(() -> performCodeActionAsync(editor, action), (a, b) -> { - final Boolean complete = a; - final Throwable error = b; - progress.dismiss(); - - if(complete == null || error != null || !complete.booleanValue()) { + if(a == null || b != null || !a) { StudioApp.getInstance().toast(R.string.msg_cannot_perform_fix, Toaster.Type.ERROR); - return; } }); } @@ -491,7 +477,7 @@ public class IDELanguageClientImpl implements IDELanguageClient { File file = new File(URI.create(params.getUri())); if(file.exists() && file.isFile() && FileUtils.isUtf8(file)) { final Range range = params.getSelection(); - EditorFragment frag = activity().getEditorAtIndex ( + var frag = activity().getEditorAtIndex ( activity().getBinding ().tabs.getSelectedTabPosition ()); if(frag != null && frag.getFile() != null @@ -554,13 +540,10 @@ public class IDELanguageClientImpl implements IDELanguageClient { final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder (activity ()); builder.setTitle(R.string.msg_code_actions); builder.setItems(asArray(actions), (d, w) -> { - final DialogInterface dialog = d; - final int which = w; - - dialog.dismiss(); + d.dismiss(); hideDiagnostics(); hideBottomDiagnosticView(editor.getFile()); - performCodeAction(editor, actions.get(which)); + performCodeAction(editor, actions.get(w)); }); builder.show(); } @@ -589,7 +572,7 @@ public class IDELanguageClientImpl implements IDELanguageClient { // Edit is in the same editor which requested the code action editInEditor(editor, edit); } else { - EditorFragment openedFrag = findEditorByFile(file); + var openedFrag = findEditorByFile(file); if(openedFrag != null && openedFrag.getEditor() != null) { // Edit is in another 'opened' file diff --git a/app/src/main/java/com/itsaky/androidide/lsp/LSPClientLauncher.java b/app/src/main/java/com/itsaky/androidide/lsp/LSPClientLauncher.java index 171d09c80..de8f536a3 100755 --- a/app/src/main/java/com/itsaky/androidide/lsp/LSPClientLauncher.java +++ b/app/src/main/java/com/itsaky/androidide/lsp/LSPClientLauncher.java @@ -66,13 +66,6 @@ public abstract class LSPClientLauncher extends Thread { public IDELanguageServer getServer() { return this.server; } - - /** - * Are we connected to the server? - */ - public boolean isConnected() { - return languageClient != null && languageClient.isConnected(); - } /** * Actual logic to start the server diff --git a/app/src/main/java/com/itsaky/androidide/lsp/LanguageServerWrapper.java b/app/src/main/java/com/itsaky/androidide/lsp/LanguageServerWrapper.java deleted file mode 100755 index 7f52aa2f6..000000000 --- a/app/src/main/java/com/itsaky/androidide/lsp/LanguageServerWrapper.java +++ /dev/null @@ -1,467 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.lsp; - -import com.itsaky.lsp.SemanticHighlight; -import com.itsaky.lsp.SemanticHighlightsParams; -import com.itsaky.lsp.services.IDELanguageServer; -import com.itsaky.lsp.services.IDETextDocumentService; -import com.itsaky.lsp.services.IDEWorkspaceService; - -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import org.eclipse.lsp4j.CallHierarchyIncomingCall; -import org.eclipse.lsp4j.CallHierarchyIncomingCallsParams; -import org.eclipse.lsp4j.CallHierarchyItem; -import org.eclipse.lsp4j.CallHierarchyOutgoingCall; -import org.eclipse.lsp4j.CallHierarchyOutgoingCallsParams; -import org.eclipse.lsp4j.CallHierarchyPrepareParams; -import org.eclipse.lsp4j.CodeAction; -import org.eclipse.lsp4j.CodeActionParams; -import org.eclipse.lsp4j.CodeLens; -import org.eclipse.lsp4j.CodeLensParams; -import org.eclipse.lsp4j.ColorInformation; -import org.eclipse.lsp4j.ColorPresentation; -import org.eclipse.lsp4j.ColorPresentationParams; -import org.eclipse.lsp4j.Command; -import org.eclipse.lsp4j.CompletionItem; -import org.eclipse.lsp4j.CompletionList; -import org.eclipse.lsp4j.CompletionParams; -import org.eclipse.lsp4j.CreateFilesParams; -import org.eclipse.lsp4j.DeclarationParams; -import org.eclipse.lsp4j.DefinitionParams; -import org.eclipse.lsp4j.DeleteFilesParams; -import org.eclipse.lsp4j.DidChangeConfigurationParams; -import org.eclipse.lsp4j.DidChangeTextDocumentParams; -import org.eclipse.lsp4j.DidChangeWatchedFilesParams; -import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams; -import org.eclipse.lsp4j.DidCloseTextDocumentParams; -import org.eclipse.lsp4j.DidOpenTextDocumentParams; -import org.eclipse.lsp4j.DidSaveTextDocumentParams; -import org.eclipse.lsp4j.DocumentColorParams; -import org.eclipse.lsp4j.DocumentFormattingParams; -import org.eclipse.lsp4j.DocumentHighlight; -import org.eclipse.lsp4j.DocumentHighlightParams; -import org.eclipse.lsp4j.DocumentLink; -import org.eclipse.lsp4j.DocumentLinkParams; -import org.eclipse.lsp4j.DocumentOnTypeFormattingParams; -import org.eclipse.lsp4j.DocumentRangeFormattingParams; -import org.eclipse.lsp4j.DocumentSymbol; -import org.eclipse.lsp4j.DocumentSymbolParams; -import org.eclipse.lsp4j.ExecuteCommandParams; -import org.eclipse.lsp4j.FoldingRange; -import org.eclipse.lsp4j.FoldingRangeRequestParams; -import org.eclipse.lsp4j.Hover; -import org.eclipse.lsp4j.HoverParams; -import org.eclipse.lsp4j.ImplementationParams; -import org.eclipse.lsp4j.InitializeParams; -import org.eclipse.lsp4j.InitializeResult; -import org.eclipse.lsp4j.InitializedParams; -import org.eclipse.lsp4j.LinkedEditingRangeParams; -import org.eclipse.lsp4j.LinkedEditingRanges; -import org.eclipse.lsp4j.Location; -import org.eclipse.lsp4j.LocationLink; -import org.eclipse.lsp4j.Moniker; -import org.eclipse.lsp4j.MonikerParams; -import org.eclipse.lsp4j.PrepareRenameParams; -import org.eclipse.lsp4j.PrepareRenameResult; -import org.eclipse.lsp4j.Range; -import org.eclipse.lsp4j.ReferenceParams; -import org.eclipse.lsp4j.RenameFilesParams; -import org.eclipse.lsp4j.RenameParams; -import org.eclipse.lsp4j.ResolveTypeHierarchyItemParams; -import org.eclipse.lsp4j.SelectionRange; -import org.eclipse.lsp4j.SelectionRangeParams; -import org.eclipse.lsp4j.SemanticTokens; -import org.eclipse.lsp4j.SemanticTokensDelta; -import org.eclipse.lsp4j.SemanticTokensDeltaParams; -import org.eclipse.lsp4j.SemanticTokensParams; -import org.eclipse.lsp4j.SemanticTokensRangeParams; -import org.eclipse.lsp4j.SetTraceParams; -import org.eclipse.lsp4j.SignatureHelp; -import org.eclipse.lsp4j.SignatureHelpParams; -import org.eclipse.lsp4j.SymbolInformation; -import org.eclipse.lsp4j.TextEdit; -import org.eclipse.lsp4j.TypeDefinitionParams; -import org.eclipse.lsp4j.TypeHierarchyItem; -import org.eclipse.lsp4j.TypeHierarchyParams; -import org.eclipse.lsp4j.WillSaveTextDocumentParams; -import org.eclipse.lsp4j.WorkDoneProgressCancelParams; -import org.eclipse.lsp4j.WorkspaceEdit; -import org.eclipse.lsp4j.WorkspaceSymbolParams; -import org.eclipse.lsp4j.jsonrpc.messages.Either; -import org.eclipse.lsp4j.services.LanguageServer; -import org.eclipse.lsp4j.services.TextDocumentService; -import org.eclipse.lsp4j.services.WorkspaceService; - -/** - * Wraps a {@link LanguageServer} to an {@link IDELanguageServer } - */ -public class LanguageServerWrapper implements IDELanguageServer { - - private final LanguageServer server; - private final IDETextDocumentService textDocumentServiceWrapper; - private final IDEWorkspaceService workspaceService; - - public LanguageServerWrapper(LanguageServer server) { - this.server = server; - this.textDocumentServiceWrapper = new TextDocumentServiceWrapper(server.getTextDocumentService()); - this.workspaceService = new WorkspaceServiceWrapper(server.getWorkspaceService()); - } - - public LanguageServer getServer() { - return this.server; - } - - @Override - public CompletableFuture initialize(InitializeParams params) { - return server.initialize(params); - } - - @Override - public void initialized(InitializedParams params) { - server.initialized(params); - } - - @Override - public void initialized() { - server.initialized(); - } - - @Override - public CompletableFuture shutdown() { - return server.shutdown(); - } - - @Override - public void exit() { - server.exit(); - } - - @Override - public IDETextDocumentService getTextDocumentService() { - return textDocumentServiceWrapper; - } - - @Override - public IDEWorkspaceService getWorkspaceService() { - return workspaceService; - } - - @Override - public void cancelProgress(WorkDoneProgressCancelParams params) { - server.cancelProgress(params); - } - - @Override - public void setTrace(SetTraceParams params) { - } - - class TextDocumentServiceWrapper implements IDETextDocumentService { - - private final TextDocumentService service; - - public TextDocumentServiceWrapper(TextDocumentService service) { - this.service = service; - } - - @Override - public CompletableFuture> callHierarchyIncomingCalls(CallHierarchyIncomingCallsParams p1) { - return service.callHierarchyIncomingCalls(p1); - } - - @Override - public CompletableFuture> callHierarchyOutgoingCalls(CallHierarchyOutgoingCallsParams p1) { - return service.callHierarchyOutgoingCalls(p1); - } - - @Override - public CompletableFuture>> codeAction(CodeActionParams p1) { - return service.codeAction(p1); - } - - @Override - public CompletableFuture> codeLens(CodeLensParams p1) { - return service.codeLens(p1); - } - - @Override - public CompletableFuture> colorPresentation(ColorPresentationParams p1) { - return service.colorPresentation(p1); - } - - @Override - public CompletableFuture, CompletionList>> completion(CompletionParams p1) { - return service.completion(p1); - } - - @Override - public CompletableFuture, List>> declaration(DeclarationParams p1) { - return service.declaration(p1); - } - - @Override - public CompletableFuture, List>> definition(DefinitionParams p1) { - return service.definition(p1); - } - - @Override - public void didChange(DidChangeTextDocumentParams p1) { - service.didChange(p1); - } - - @Override - public void didClose(DidCloseTextDocumentParams p1) { - service.didClose(p1); - } - - @Override - public void didOpen(DidOpenTextDocumentParams p1) { - service.didOpen(p1); - } - - @Override - public void didSave(DidSaveTextDocumentParams p1) { - service.didSave(p1); - } - - @Override - public CompletableFuture> documentColor(DocumentColorParams p1) { - return service.documentColor(p1); - } - - @Override - public CompletableFuture> documentHighlight(DocumentHighlightParams p1) { - return service.documentHighlight(p1); - } - - @Override - public CompletableFuture> documentLink(DocumentLinkParams p1) { - return service.documentLink(p1); - } - - @Override - public CompletableFuture documentLinkResolve(DocumentLink p1) { - return service.documentLinkResolve(p1); - } - - @Override - public CompletableFuture>> documentSymbol(DocumentSymbolParams p1) { - return service.documentSymbol(p1); - } - - @Override - public CompletableFuture> foldingRange(FoldingRangeRequestParams p1) { - return service.foldingRange(p1); - } - - @Override - public CompletableFuture> formatting(DocumentFormattingParams p1) { - return service.formatting(p1); - } - - @Override - public CompletableFuture hover(HoverParams p1) { - return service.hover(p1); - } - - @Override - public CompletableFuture, List>> implementation(ImplementationParams p1) { - return service.implementation(p1); - } - - @Override - public CompletableFuture linkedEditingRange(LinkedEditingRangeParams p1) { - return service.linkedEditingRange(p1); - } - - @Override - public CompletableFuture> moniker(MonikerParams p1) { - return service.moniker(p1); - } - - @Override - public CompletableFuture> onTypeFormatting(DocumentOnTypeFormattingParams p1) { - return service.onTypeFormatting(p1); - } - - @Override - public CompletableFuture> prepareCallHierarchy(CallHierarchyPrepareParams p1) { - return service.prepareCallHierarchy(p1); - } - - @Override - public CompletableFuture> prepareRename(PrepareRenameParams p1) { - return service.prepareRename(p1); - } - - @Override - public CompletableFuture> rangeFormatting(DocumentRangeFormattingParams p1) { - return service.rangeFormatting(p1); - } - - @Override - public CompletableFuture> references(ReferenceParams p1) { - return service.references(p1); - } - - @Override - public CompletableFuture rename(RenameParams p1) { - return service.rename(p1); - } - - @Override - public CompletableFuture resolveCodeAction(CodeAction p1) { - return service.resolveCodeAction(p1); - } - - @Override - public CompletableFuture resolveCodeLens(CodeLens p1) { - return service.resolveCodeLens(p1); - } - - @Override - public CompletableFuture resolveCompletionItem(CompletionItem p1) { - return service.resolveCompletionItem(p1); - } - - @Override - public CompletableFuture resolveTypeHierarchy(ResolveTypeHierarchyItemParams p1) { - return service.resolveTypeHierarchy(p1); - } - - @Override - public CompletableFuture> selectionRange(SelectionRangeParams p1) { - return service.selectionRange(p1); - } - - @Override - public CompletableFuture semanticTokensFull(SemanticTokensParams p1) { - return service.semanticTokensFull(p1); - } - - @Override - public CompletableFuture> semanticTokensFullDelta(SemanticTokensDeltaParams p1) { - return service.semanticTokensFullDelta(p1); - } - - @Override - public CompletableFuture semanticTokensRange(SemanticTokensRangeParams p1) { - return service.semanticTokensRange(p1); - } - - @Override - public CompletableFuture signatureHelp(SignatureHelpParams p1) { - return service.signatureHelp(p1); - } - - @Override - public CompletableFuture, List>> typeDefinition(TypeDefinitionParams p1) { - return service.typeDefinition(p1); - } - - @Override - public CompletableFuture typeHierarchy(TypeHierarchyParams p1) { - return service.typeHierarchy(p1); - } - - @Override - public void willSave(WillSaveTextDocumentParams p1) { - service.willSave(p1); - } - - @Override - public CompletableFuture> willSaveWaitUntil(WillSaveTextDocumentParams p1) { - return service.willSaveWaitUntil(p1); - } - - @Override - public CompletableFuture> semanticHighlights(SemanticHighlightsParams params) { - return CompletableFuture.completedFuture(Collections.emptyList()); - } - } - - class WorkspaceServiceWrapper implements IDEWorkspaceService { - - private final WorkspaceService service; - - public WorkspaceServiceWrapper(WorkspaceService service) { - this.service = service; - } - - @Override - public CompletableFuture executeCommand(ExecuteCommandParams params) { - return service.executeCommand(params); - } - - @Override - public CompletableFuture> symbol(WorkspaceSymbolParams params) { - return service.symbol(params); - } - - @Override - public void didChangeConfiguration(DidChangeConfigurationParams params) { - service.didChangeConfiguration(params); - } - - @Override - public void didChangeWatchedFiles(DidChangeWatchedFilesParams params) { - service.didChangeWatchedFiles(params); - } - - @Override - public void didChangeWorkspaceFolders(DidChangeWorkspaceFoldersParams params) { - service.didChangeWorkspaceFolders(params); - } - - @Override - public CompletableFuture willCreateFiles(CreateFilesParams params) { - return service.willCreateFiles(params); - } - - @Override - public void didCreateFiles(CreateFilesParams params) { - service.didCreateFiles(params); - } - - @Override - public CompletableFuture willRenameFiles(RenameFilesParams params) { - return service.willRenameFiles(params); - } - - @Override - public void didRenameFiles(RenameFilesParams params) { - service.didRenameFiles(params); - } - - @Override - public CompletableFuture willDeleteFiles(DeleteFilesParams params) { - return service.willDeleteFiles(params); - } - - @Override - public void didDeleteFiles(DeleteFilesParams params) { - service.didDeleteFiles(params); - } - } -} diff --git a/app/src/main/java/com/itsaky/androidide/lsp/TokenModifiers.java b/app/src/main/java/com/itsaky/androidide/lsp/TokenModifiers.java deleted file mode 100755 index 1e591946d..000000000 --- a/app/src/main/java/com/itsaky/androidide/lsp/TokenModifiers.java +++ /dev/null @@ -1,56 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.lsp; - -import org.eclipse.lsp4j.*; - -public enum TokenModifiers { - - PUBLIC("public"), - PRIVATE("private"), - PROTECTED("protected"), - NATIVE("native"), - DEFAULT("default"), - STRICTFP("strictp"), - SYNCHRONIZED("synchronized"), - TRANSIENT("transient"), - VOLATILE("volatile"), - ABSTRACT(SemanticTokenModifiers.Abstract), - STATIC(SemanticTokenModifiers.Static), - FINAL(SemanticTokenModifiers.Readonly), - - DEPRECATED(SemanticTokenModifiers.Deprecated); - - public final int bitmask = 1 << ordinal(); - public final int invertBitmask = ~bitmask; - - private String name; - - private TokenModifiers (String name) { - this.name = name; - } - - @Override - public String toString () { - return name; - } -} diff --git a/app/src/main/java/com/itsaky/androidide/lsp/TokenType.java b/app/src/main/java/com/itsaky/androidide/lsp/TokenType.java deleted file mode 100755 index bcfb69916..000000000 --- a/app/src/main/java/com/itsaky/androidide/lsp/TokenType.java +++ /dev/null @@ -1,66 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.lsp; - -import org.eclipse.lsp4j.SemanticTokenTypes; -import io.github.rosemoe.editor.widget.EditorColorScheme; - -public enum TokenType { - /** - * Order of the member declaration must be preseved! - */ - NAMESPACE(SemanticTokenTypes.Namespace, EditorColorScheme.PACKAGE_NAME), - CLASS(SemanticTokenTypes.Class, EditorColorScheme.TYPE_NAME), - INTERFACE(SemanticTokenTypes.Interface, EditorColorScheme.INTERFACE), - ENUM(SemanticTokenTypes.Enum, EditorColorScheme.ENUM), - ENUM_MEMBER(SemanticTokenTypes.EnumMember, EditorColorScheme.STATIC_FIELD), - TYPE(SemanticTokenTypes.Type, EditorColorScheme.TYPE_NAME), - TYPE_PARAMETER(SemanticTokenTypes.TypeParameter, EditorColorScheme.TYPE_PARAM), - METHOD(SemanticTokenTypes.Method, EditorColorScheme.METHOD_DECLARATION), - PROPERTY(SemanticTokenTypes.Property, EditorColorScheme.FIELD), - VARIABLE(SemanticTokenTypes.Variable, EditorColorScheme.LOCAL_VARIABLE), - PARAMETER(SemanticTokenTypes.Parameter, EditorColorScheme.PARAMETER), - MODIFIER(SemanticTokenTypes.Modifier, EditorColorScheme.KEYWORD), - KEYWORD(SemanticTokenTypes.Keyword, EditorColorScheme.KEYWORD), - - ANNOTATION("annotation", EditorColorScheme.ANNOTATION), - ANNOTATION_TYPE("annotationType", EditorColorScheme.TYPE_NAME), - - UNKNOWN("unknown", EditorColorScheme.TEXT_NORMAL); - - private String name; - private int editorType; - - private TokenType (String name, int editorType) { - this.name = name; - this.editorType = editorType; - } - - public int getEditorType() { - return editorType; - } - - @Override - public String toString () { - return name; - } -} diff --git a/app/src/main/java/com/itsaky/androidide/models/OpenedFile.java b/app/src/main/java/com/itsaky/androidide/models/OpenedFile.java deleted file mode 100644 index 0ce7f0e0a..000000000 --- a/app/src/main/java/com/itsaky/androidide/models/OpenedFile.java +++ /dev/null @@ -1,60 +0,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 . - */ - -package com.itsaky.androidide.models; - -import com.itsaky.androidide.fragments.EditorFragment; -import com.itsaky.androidide.project.AndroidProject; - -import org.eclipse.lsp4j.Range; - -import java.io.File; - -/** - * Represents a file that has been opened in the editor, - * - * @author Akash Yadav - */ -public class OpenedFile { - - private final File file; - private final Range selection; - - public OpenedFile (File file, Range selection) { - this.file = file; - this.selection = selection; - } - - public File getFile () { - return file; - } - - public Range getSelection () { - return selection; - } - - public int hashCode () { - return this.file.getAbsolutePath ().hashCode (); - } - - public EditorFragment createFragment (final EditorFragment.FileOpenListener fileOpenListener, - EditorFragment.ModificationStateListener modificationStateListener) { - return EditorFragment.newInstance (this.file, this.selection) - .setModificationStateListener (modificationStateListener) - .setFileOpenListener (fileOpenListener); - } -} diff --git a/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java b/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java index 0ee80dffa..89bdbfbae 100644 --- a/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java +++ b/app/src/main/java/com/itsaky/androidide/models/XMLAttribute.java @@ -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 . - */ - -package com.itsaky.androidide.models; - -import android.os.Parcel; -import android.os.Parcelable; - -import androidx.annotation.NonNull; - -import com.itsaky.androidide.app.StudioApp; -import com.itsaky.attrinfo.models.Attr; -import com.itsaky.inflater.IAttribute; -import com.itsaky.inflater.impl.UiAttribute; - -import org.jetbrains.annotations.Contract; - -/** - * A model that is used to show attributes list of a view. - * - * @author Akash Yadav - */ -public class XMLAttribute extends UiAttribute implements Parcelable { - - /** - * Is this attribute applied to any view? - */ - private boolean isApplied = false; - - /** - * Format of this attribute. May be stored here so we don't - * have to look in {@link com.itsaky.attrinfo.AttrInfo AttrInfo} - * again and again. - */ - private int format = -1; - - private Attr attr; - - public XMLAttribute (IAttribute attribute) { - this (attribute, true); - } - - public XMLAttribute (@NonNull IAttribute attribute, boolean isApplied) { - this (attribute.getNamespace (), attribute.getAttributeName (), attribute.getValue (), isApplied); - } - - public XMLAttribute (String namespace, String name, String value, boolean isApplied) { - super (namespace, name, value); - this.isApplied = isApplied; - } - - private XMLAttribute (@NonNull Parcel in) { - this (in.readString (), in.readString (), in.readString (), in.readByte () != 0); - } - - public void setFormat (int format) { - this.format = format; - } - - public int getFormat () { - return format; - } - - public int findFormat () { - - if (getFormat () > 0) { - return getFormat (); - } - - final var attrs = StudioApp.getInstance ().attrInfo (); - this.attr = attrs.getAttribute (getAttributeName ()); - if (this.attr != null) { - this.format = this.attr.format; - } - - return getFormat (); - } - - public boolean hasFormat (int format) { - return (this.format & format) != 0; - } - - public Attr getAttr () { - return attr; - } - - public static final Creator CREATOR = new Creator () { - @NonNull - @Contract("_ -> new") - @Override - public XMLAttribute createFromParcel (Parcel in) { - return new XMLAttribute (in); - } - - @NonNull - @Contract(value = "_ -> new", pure = true) - @Override - public XMLAttribute[] newArray (int size) { - return new XMLAttribute[size]; - } - }; - - @Override - public void apply (String value) { - super.apply (value); - isApplied = true; - } - - public boolean isApplied () { - return isApplied; - } - - /** - * Create a new attribute instance with the android namespace. - * {@link #isApplied()} will return true by default. - * @param name The name of the attribute. - * @param value The value of the attribute. - * @return A new instance of this class with the provided data. - */ - @NonNull - @Contract("_, _ -> new") - public static XMLAttribute newAndroidAttribute (String name, String value) { - return new XMLAttribute ("android", name, value, true); - } - - /** - * Create a new attribute instance with the android namespace. - * The {@link #isApplied()} will return {@code false} by default. - * @param name The name of the attribute. - * @return The new attribute instance. - */ - @NonNull - @Contract("_ -> new") - public static XMLAttribute newAndroidAttribute (String name) { - return new XMLAttribute ("android", name, "", false); - } - - @Override - public int describeContents () { - return 0; - } - - @Override - public void writeToParcel (@NonNull Parcel dest, int flags) { - dest.writeString (getNamespace ()); - dest.writeString (getAttributeName ()); - dest.writeString (getValue ()); - dest.writeByte ((byte) (isApplied ? 1 : 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 . + */ + +package com.itsaky.androidide.models; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import com.itsaky.androidide.app.StudioApp; +import com.itsaky.attrinfo.models.Attr; +import com.itsaky.inflater.IAttribute; +import com.itsaky.inflater.impl.UiAttribute; + +import org.jetbrains.annotations.Contract; + +/** + * A model that is used to show attributes list of a view. + * + * @author Akash Yadav + */ +public class XMLAttribute extends UiAttribute implements Parcelable { + + /** + * Is this attribute applied to any view? + */ + private boolean isApplied = false; + + /** + * Format of this attribute. May be stored here so we don't + * have to look in {@link com.itsaky.attrinfo.AttrInfo AttrInfo} + * again and again. + */ + private int format = -1; + + private Attr attr; + + public XMLAttribute (IAttribute attribute) { + this (attribute, true); + } + + public XMLAttribute (@NonNull IAttribute attribute, boolean isApplied) { + this (attribute.getNamespace (), attribute.getAttributeName (), attribute.getValue (), isApplied); + } + + public XMLAttribute (String namespace, String name, String value, boolean isApplied) { + super (namespace, name, value); + this.isApplied = isApplied; + } + + private XMLAttribute (@NonNull Parcel in) { + this (in.readString (), in.readString (), in.readString (), in.readByte () != 0); + } + + public void setFormat (int format) { + this.format = format; + } + + public int getFormat () { + return format; + } + + public int findFormat () { + + if (getFormat () > 0) { + return getFormat (); + } + + final var attrs = StudioApp.getInstance ().attrInfo (); + this.attr = attrs.getAttribute (getAttributeName ()); + if (this.attr != null) { + this.format = this.attr.format; + } + + return getFormat (); + } + + public boolean hasFormat (int format) { + return (this.format & format) != 0; + } + + public Attr getAttr () { + return attr; + } + + public static final Creator CREATOR = new Creator () { + @NonNull + @Contract("_ -> new") + @Override + public XMLAttribute createFromParcel (Parcel in) { + return new XMLAttribute (in); + } + + @NonNull + @Contract(value = "_ -> new", pure = true) + @Override + public XMLAttribute[] newArray (int size) { + return new XMLAttribute[size]; + } + }; + + @Override + public void apply (String value) { + super.apply (value); + isApplied = true; + } + + public boolean isApplied () { + return isApplied; + } + + /** + * Create a new attribute instance with the android namespace. + * {@link #isApplied()} will return true by default. + * @param name The name of the attribute. + * @param value The value of the attribute. + * @return A new instance of this class with the provided data. + */ + @NonNull + @Contract("_, _ -> new") + public static XMLAttribute newAndroidAttribute (String name, String value) { + return new XMLAttribute ("android", name, value, true); + } + + /** + * Create a new attribute instance with the android namespace. + * The {@link #isApplied()} will return {@code false} by default. + * @param name The name of the attribute. + * @return The new attribute instance. + */ + @NonNull + @Contract("_ -> new") + public static XMLAttribute newAndroidAttribute (String name) { + return new XMLAttribute ("android", name, "", false); + } + + @Override + public int describeContents () { + return 0; + } + + @Override + public void writeToParcel (@NonNull Parcel dest, int flags) { + dest.writeString (getNamespace ()); + dest.writeString (getAttributeName ()); + dest.writeString (getValue ()); + dest.writeByte ((byte) (isApplied ? 1 : 0)); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/services/IDELogService.java b/app/src/main/java/com/itsaky/androidide/services/IDELogService.java deleted file mode 100644 index 41fa57b55..000000000 --- a/app/src/main/java/com/itsaky/androidide/services/IDELogService.java +++ /dev/null @@ -1,69 +0,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 . - */ - -package com.itsaky.androidide.services; - -import androidx.annotation.NonNull; - -import com.itsaky.androidide.app.StudioApp; -import com.itsaky.androidide.models.LogLine; -import com.itsaky.androidide.shell.ShellServer; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -public class IDELogService { - - private final ShellServer shell; - private final List logListeners; - - private boolean started; - - public IDELogService () { - this.shell = StudioApp.getInstance ().newShell (this::output); - this.logListeners = new ArrayList<> (); - } - - public void start () { - if (started) { - return; - } - - this.shell.bgAppend ("logcat -v threadtime"); - started = true; - } - - public boolean isStarted () { - return started; - } - - public void addLogListener (LogReceiver.LogListener listener) { - this.logListeners.add (Objects.requireNonNull (listener)); - } - - public void removeLogListener (LogReceiver.LogListener listener) { - this.logListeners.remove (Objects.requireNonNull (listener)); - } - - private void output (@NonNull String log) { - final var line = LogLine.forLogString (log); - for (final var listener : this.logListeners) { - listener.appendLogLine (line); - } - } -} diff --git a/app/src/main/java/com/itsaky/androidide/services/LogReceiver.java b/app/src/main/java/com/itsaky/androidide/services/LogReceiver.java index f85a77b5b..c4d8a3a38 100755 --- a/app/src/main/java/com/itsaky/androidide/services/LogReceiver.java +++ b/app/src/main/java/com/itsaky/androidide/services/LogReceiver.java @@ -25,6 +25,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import com.itsaky.androidide.models.LogLine; +import com.itsaky.androidide.utils.Logger; + import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -34,6 +36,8 @@ public class LogReceiver extends BroadcastReceiver { public static final String APPEND_LOG = "com.itsaky.androidide.logs.APPEND_LOG"; public static final String EXTRA_LINE = "log_line"; + + private final Logger LOG = Logger.instance ("LogReceiver"); public LogReceiver setLogListener(LogListener listener) { this.listener = listener; @@ -47,7 +51,9 @@ public class LogReceiver extends BroadcastReceiver { if(line == null) return; try { sendLogLine(line); - } catch (Throwable th) {} + } catch (Throwable th) { + LOG.error ("Unable to parse log line from app.", th); + } } } @@ -58,7 +64,7 @@ public class LogReceiver extends BroadcastReceiver { } } - public static interface LogListener { - public void appendLogLine(LogLine line); + public interface LogListener { + void appendLogLine (LogLine line); } } diff --git a/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java b/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java index 8a53f0c2d..710193271 100755 --- a/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java +++ b/app/src/main/java/com/itsaky/androidide/tasks/TaskExecutor.java @@ -33,6 +33,8 @@ public class TaskExecutor { private final Executor executor = Executors.newSingleThreadExecutor(); private final Handler handler = new Handler(Looper.getMainLooper()); + + private final Logger LOG = Logger.instance ("TaskExecutor"); public interface Callback { void complete(R result); @@ -49,7 +51,9 @@ public class TaskExecutor { handler.post(() -> { callback.complete(result); }); - } catch (Throwable th) {} + } catch (Throwable th) { + LOG.error ("Callable task was not able to finish", th); + } }); } @@ -61,6 +65,7 @@ public class TaskExecutor { try { result = callable.call(); } catch (Throwable th) { + LOG.error ("Callable task was not able to finish", th); error = th; } final R resultCopied = result; @@ -71,4 +76,4 @@ public class TaskExecutor { }); }); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/tasks/callables/ReadFileTask.java b/app/src/main/java/com/itsaky/androidide/tasks/callables/ReadFileTask.java deleted file mode 100755 index 0a4c15e2b..000000000 --- a/app/src/main/java/com/itsaky/androidide/tasks/callables/ReadFileTask.java +++ /dev/null @@ -1,40 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.tasks.callables; - -import com.blankj.utilcode.util.FileIOUtils; -import java.io.File; -import java.util.concurrent.Callable; - -public class ReadFileTask implements Callable { - - private final File file; - - public ReadFileTask(File file) { - this.file = file; - } - - @Override - public String call() throws Exception { - return FileIOUtils.readFile2String(file); - } -} diff --git a/app/src/main/java/com/itsaky/androidide/ui/WidgetTouchListener.java b/app/src/main/java/com/itsaky/androidide/ui/WidgetTouchListener.java index a42df2044..50fffb5d4 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/WidgetTouchListener.java +++ b/app/src/main/java/com/itsaky/androidide/ui/WidgetTouchListener.java @@ -1,111 +1,111 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.ui; - -import android.annotation.SuppressLint; -import android.view.GestureDetector; -import android.view.HapticFeedbackConstants; -import android.view.MotionEvent; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import com.itsaky.inflater.IView; - -/** - * A touch listener for handling touch events of a view in the - * UI Designer. - * - * Some of the views have no effect of setting a click listener - * or a long click listener (e.g. SeekBar). In order to fix this, - * we use {@link android.view.GestureDetector} and manually handle - * click events. - * - * @author Akash Yadav - */ -public class WidgetTouchListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { - - private final GestureDetector mGestureDetector; - private final OnClickListener clickListener; - private final OnLongClickListener longClickListener; - - private final IView mView; - - public WidgetTouchListener (@NonNull IView view, @Nullable OnClickListener clickListener, @Nullable OnLongClickListener longClickListener) { - this.mView = view; - this.mGestureDetector = new GestureDetector (view.asView().getContext(), this); - this.clickListener = clickListener; - this.longClickListener = longClickListener; - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouch(View view, @NonNull MotionEvent motionEvent) { - return mGestureDetector.onTouchEvent(motionEvent); - } - - @Override - public boolean onDown(MotionEvent e) { - return true; // We would like to get notified about further events. - } - - // TODO Should we use onSingleTapConfirmed instead? - @Override - public boolean onSingleTapUp(MotionEvent e) { - if (this.clickListener != null) { - this.clickListener.onClick(this.mView); - } - - return true; - } - - @Override - public void onLongPress(MotionEvent e) { - - if (this.mView.getParent () == null) { - // This is the root layout so this should not be draggable - return; - } - - if (this.longClickListener != null && - this.longClickListener.onLongClick(this.mView)) { - this.mView.asView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); - } - } - - /** - * A listener which is invoked when we detect a single - * click event. - * - * @author Akash Yadav - */ - public interface OnClickListener { - void onClick (IView view); - } - - /** - * A listener which is invoked when we detect a long - * click event. - * - * @author Akash Yadav - */ - public interface OnLongClickListener { - boolean onLongClick (IView view); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.ui; + +import android.annotation.SuppressLint; +import android.view.GestureDetector; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.itsaky.inflater.IView; + +/** + * A touch listener for handling touch events of a view in the + * UI Designer. + * + * Some of the views have no effect of setting a click listener + * or a long click listener (e.g. SeekBar). In order to fix this, + * we use {@link android.view.GestureDetector} and manually handle + * click events. + * + * @author Akash Yadav + */ +public class WidgetTouchListener extends GestureDetector.SimpleOnGestureListener implements View.OnTouchListener { + + private final GestureDetector mGestureDetector; + private final OnClickListener clickListener; + private final OnLongClickListener longClickListener; + + private final IView mView; + + public WidgetTouchListener (@NonNull IView view, @Nullable OnClickListener clickListener, @Nullable OnLongClickListener longClickListener) { + this.mView = view; + this.mGestureDetector = new GestureDetector (view.asView().getContext(), this); + this.clickListener = clickListener; + this.longClickListener = longClickListener; + } + + @SuppressLint("ClickableViewAccessibility") + @Override + public boolean onTouch(View view, @NonNull MotionEvent motionEvent) { + return mGestureDetector.onTouchEvent(motionEvent); + } + + @Override + public boolean onDown(MotionEvent e) { + return true; // We would like to get notified about further events. + } + + // TODO Should we use onSingleTapConfirmed instead? + @Override + public boolean onSingleTapUp(MotionEvent e) { + if (this.clickListener != null) { + this.clickListener.onClick(this.mView); + } + + return true; + } + + @Override + public void onLongPress(MotionEvent e) { + + if (this.mView.getParent () == null) { + // This is the root layout so this should not be draggable + return; + } + + if (this.longClickListener != null && + this.longClickListener.onLongClick(this.mView)) { + this.mView.asView().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + } + } + + /** + * A listener which is invoked when we detect a single + * click event. + * + * @author Akash Yadav + */ + public interface OnClickListener { + void onClick (IView view); + } + + /** + * A listener which is invoked when we detect a long + * click event. + * + * @author Akash Yadav + */ + public interface OnLongClickListener { + boolean onLongClick (IView view); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/utils/AttributeDialogs.java b/app/src/main/java/com/itsaky/androidide/utils/AttributeDialogs.java index 79132530f..f556b2c16 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/AttributeDialogs.java +++ b/app/src/main/java/com/itsaky/androidide/utils/AttributeDialogs.java @@ -1,293 +1,293 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.utils; - -import static com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder; - -import android.content.Context; -import android.content.DialogInterface; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.WindowManager; - -import androidx.annotation.NonNull; -import androidx.appcompat.app.AlertDialog; - -import com.itsaky.androidide.R; -import com.itsaky.androidide.app.StudioApp; -import com.itsaky.androidide.colorpicker.ColorPickerView; -import com.itsaky.androidide.databinding.LayoutDimensionAttrEditorBinding; -import com.itsaky.androidide.databinding.LayoutTextAttrEditorBinding; -import com.itsaky.toaster.Toaster; - -import java.util.ArrayList; -import java.util.regex.Pattern; - -/** - * Helper class for creating dialogs for editing view attributes. - *
- * TODO Should we merge this into {@link DialogUtils}? - * TODO An attribute can accept multiple types of values (dimension, enum, reference, etc) - * Can we somehow combine the editors according to attribute format? - * @author Akash Yadav - */ -public class AttributeDialogs { - - private static final String DIMENSION_MATCH = "match_parent"; - private static final String DIMENSION_WRAP = "wrap_content"; - - private static final Logger LOG = Logger.instance ("AttributeDialogs"); - - /** - * Context that will be used for creating dialogs. - * This must be initialized by calling {@link #init(Context)} - * and the reference must be released by calling {@link #release()} - */ - private static Context mContext; - - /** - * Initialize this class with the provided context. - * @param ctx The context that will be used for creating dialogs. - * @see #release() - */ - public static void init (Context ctx) { - mContext = ctx; - } - - /** - * Release the context instance stored in this class. - * - * @see #init(Context) - */ - public static void release () { - mContext = null; - } - - @NonNull - public static AlertDialog booleanEditor (OnClickListener listener) { - final var items = new String [] {"true", "false"}; - final var builder = newMaterialDialogBuilder (mContext); - builder.setTitle (mContext.getString(R.string.msg_choose_new_val)); - builder.setItems (items, (dialog, which) -> { - dialog.dismiss (); - final var val = items [which]; - if (listener != null) { - listener.onClick (dialog, which, val); - } - }); - return builder.create (); - } - - /** - * Create a new dialog for editing an attribute with format STRING. - * @param listener The listener that will be invoked when the user clicks the - * positive button. - * @return The newly created dialog instance. - */ - @NonNull - public static AlertDialog stringEditor (OnClickListener listener) { - final var binding = LayoutTextAttrEditorBinding.inflate (LayoutInflater.from (mContext)); - final var builder = newMaterialDialogBuilder (mContext); - builder.setTitle (mContext.getString(R.string.msg_new_str_value)); - builder.setView (binding.getRoot ()); - builder.setPositiveButton (android.R.string.ok, (dialog, which) -> { - final var newValue = binding.valueField.getEditText ().getText ().toString ().trim (); - if (newValue.length () <= 0) { - StudioApp.getInstance ().toast (mContext.getString (R.string.msg_invalid_attr_value), Toaster.Type.ERROR); - return; - } - if (listener != null) { - listener.onClick (dialog, which, newValue); - } - }); - builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ()); - return builder.create (); - } - - /** - * Create a new dialog for editing an attribute which accepts dimension values. - * @param value The current value of the attribute. Used for determining which item must be selected by default. - * @param listener A listener that will be invoked when the user presses the positive button of the dialog. - * This listener is invoked only when the user passes a valid dimension value. - * @return The newly created dialog instance. Note that this dialog is not shown by default. - */ - @NonNull - public static AlertDialog dimensionEditor (CharSequence value, OnClickListener listener) { - final var binding = LayoutDimensionAttrEditorBinding.inflate(LayoutInflater.from (mContext)); - binding.choices.setOnCheckedChangeListener ((group, checkedId) -> binding.otherValue.setVisibility (binding.other.isChecked () ? View.VISIBLE : View.GONE)); - - if (DIMENSION_MATCH.contentEquals (value)) { - binding.choices.check (R.id.match); - } else if (DIMENSION_WRAP.contentEquals (value)) { - binding.choices.check (R.id.wrap); - } else { - binding.choices.check (R.id.other); - } - - final var builder = newMaterialDialogBuilder (mContext); - builder.setView (binding.getRoot ()); - builder.setPositiveButton (android.R.string.ok, ((dialog, which) -> { - var val = ""; - if (binding.choices.getCheckedRadioButtonId () == R.id.match) { - val = DIMENSION_MATCH; - } else if (binding.choices.getCheckedRadioButtonId () == R.id.wrap) { - val = DIMENSION_WRAP; - } else { - if (binding.otherValue.getEditText () != null) { - val = binding.otherValue.getEditText ().getText ().toString ().trim (); - } else { - val = ""; - } - } - - if (val.length () <= 0) { - StudioApp.getInstance ().toast (mContext.getString(R.string.msg_invalid_attr_value), Toaster.Type.ERROR); - return; - } - - dialog.dismiss (); - - if (listener != null) { - listener.onClick (dialog, which, val); - } - })); - builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ()); - return builder.create (); - } - - /** - * @see #enumEditor(CharSequence[], int, OnClickListener) - */ - public static AlertDialog enumEditor (CharSequence[] values, CharSequence value, OnClickListener listener) { - return enumEditor (values, findIndexOf (values, value), listener); - } - - private static int findIndexOf (@NonNull CharSequence[] values, CharSequence value) { - for (int i=0;i { - dialog.dismiss (); - final var val = values [which].toString (); - if (listener != null) { - listener.onClick (dialog, which, val); - } - }); - return builder.show (); - } - - /** - * Creates a new attribute editor dialog to edit flag attributes. - * @param values The possible values. - * @param value The current value of the attribute. - * @param listener The listener that will be invoked when the user presses the positive button. - * @return The newly created dialog instance. - */ - @NonNull - public static AlertDialog flagEditor (@NonNull CharSequence[] values, @NonNull CharSequence value, OnClickListener listener) { - final var checked = new boolean [values.length]; - final var vals = value.toString ().split (Pattern.quote ("|")); - for (var i=0;i (); - for (var i=0;i { - if (isChecked) { - checked.add (values[which]); - } else { - checked.remove (values[which]); - } - }); - builder.setPositiveButton (android.R.string.ok, (dialog, which) -> { - if (checked.size () <= 0) { - StudioApp.getInstance ().toast (mContext.getString(R.string.msg_select_one_flag), Toaster.Type.ERROR); - return; - } - - final var val = TextUtils.join ("|", checked); - - if (listener != null) { - listener.onClick (dialog, which, val); - } - }); - builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ()); - return builder.create (); - } - - @NonNull - public static AlertDialog colorPicker (final ColorPickerView.OnPickListener listener) { - final var view = new ColorPickerView (mContext); - final var builder = newMaterialDialogBuilder (mContext); - builder.setView (view); - - final var dialog = builder.create (); - dialog.getWindow ().setSoftInputMode (WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); - - view.setOnPickListener ((color, hexCode) -> { - dialog.dismiss (); - if (listener != null) { - listener.onPick (color, hexCode); - } - }); - - return dialog; - } - - public interface OnClickListener { - void onClick (DialogInterface dialog, int which, String 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 . + */ + +package com.itsaky.androidide.utils; + +import static com.itsaky.androidide.utils.DialogUtils.newMaterialDialogBuilder; + +import android.content.Context; +import android.content.DialogInterface; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; + +import com.itsaky.androidide.R; +import com.itsaky.androidide.app.StudioApp; +import com.itsaky.androidide.colorpicker.ColorPickerView; +import com.itsaky.androidide.databinding.LayoutDimensionAttrEditorBinding; +import com.itsaky.androidide.databinding.LayoutTextAttrEditorBinding; +import com.itsaky.toaster.Toaster; + +import java.util.ArrayList; +import java.util.regex.Pattern; + +/** + * Helper class for creating dialogs for editing view attributes. + *
+ * TODO Should we merge this into {@link DialogUtils}? + * TODO An attribute can accept multiple types of values (dimension, enum, reference, etc) + * Can we somehow combine the editors according to attribute format? + * @author Akash Yadav + */ +public class AttributeDialogs { + + private static final String DIMENSION_MATCH = "match_parent"; + private static final String DIMENSION_WRAP = "wrap_content"; + + private static final Logger LOG = Logger.instance ("AttributeDialogs"); + + /** + * Context that will be used for creating dialogs. + * This must be initialized by calling {@link #init(Context)} + * and the reference must be released by calling {@link #release()} + */ + private static Context mContext; + + /** + * Initialize this class with the provided context. + * @param ctx The context that will be used for creating dialogs. + * @see #release() + */ + public static void init (Context ctx) { + mContext = ctx; + } + + /** + * Release the context instance stored in this class. + * + * @see #init(Context) + */ + public static void release () { + mContext = null; + } + + @NonNull + public static AlertDialog booleanEditor (OnClickListener listener) { + final var items = new String [] {"true", "false"}; + final var builder = newMaterialDialogBuilder (mContext); + builder.setTitle (mContext.getString(R.string.msg_choose_new_val)); + builder.setItems (items, (dialog, which) -> { + dialog.dismiss (); + final var val = items [which]; + if (listener != null) { + listener.onClick (dialog, which, val); + } + }); + return builder.create (); + } + + /** + * Create a new dialog for editing an attribute with format STRING. + * @param listener The listener that will be invoked when the user clicks the + * positive button. + * @return The newly created dialog instance. + */ + @NonNull + public static AlertDialog stringEditor (OnClickListener listener) { + final var binding = LayoutTextAttrEditorBinding.inflate (LayoutInflater.from (mContext)); + final var builder = newMaterialDialogBuilder (mContext); + builder.setTitle (mContext.getString(R.string.msg_new_str_value)); + builder.setView (binding.getRoot ()); + builder.setPositiveButton (android.R.string.ok, (dialog, which) -> { + final var newValue = binding.valueField.getEditText ().getText ().toString ().trim (); + if (newValue.length () <= 0) { + StudioApp.getInstance ().toast (mContext.getString (R.string.msg_invalid_attr_value), Toaster.Type.ERROR); + return; + } + if (listener != null) { + listener.onClick (dialog, which, newValue); + } + }); + builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ()); + return builder.create (); + } + + /** + * Create a new dialog for editing an attribute which accepts dimension values. + * @param value The current value of the attribute. Used for determining which item must be selected by default. + * @param listener A listener that will be invoked when the user presses the positive button of the dialog. + * This listener is invoked only when the user passes a valid dimension value. + * @return The newly created dialog instance. Note that this dialog is not shown by default. + */ + @NonNull + public static AlertDialog dimensionEditor (CharSequence value, OnClickListener listener) { + final var binding = LayoutDimensionAttrEditorBinding.inflate(LayoutInflater.from (mContext)); + binding.choices.setOnCheckedChangeListener ((group, checkedId) -> binding.otherValue.setVisibility (binding.other.isChecked () ? View.VISIBLE : View.GONE)); + + if (DIMENSION_MATCH.contentEquals (value)) { + binding.choices.check (R.id.match); + } else if (DIMENSION_WRAP.contentEquals (value)) { + binding.choices.check (R.id.wrap); + } else { + binding.choices.check (R.id.other); + } + + final var builder = newMaterialDialogBuilder (mContext); + builder.setView (binding.getRoot ()); + builder.setPositiveButton (android.R.string.ok, ((dialog, which) -> { + var val = ""; + if (binding.choices.getCheckedRadioButtonId () == R.id.match) { + val = DIMENSION_MATCH; + } else if (binding.choices.getCheckedRadioButtonId () == R.id.wrap) { + val = DIMENSION_WRAP; + } else { + if (binding.otherValue.getEditText () != null) { + val = binding.otherValue.getEditText ().getText ().toString ().trim (); + } else { + val = ""; + } + } + + if (val.length () <= 0) { + StudioApp.getInstance ().toast (mContext.getString(R.string.msg_invalid_attr_value), Toaster.Type.ERROR); + return; + } + + dialog.dismiss (); + + if (listener != null) { + listener.onClick (dialog, which, val); + } + })); + builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ()); + return builder.create (); + } + + /** + * @see #enumEditor(CharSequence[], int, OnClickListener) + */ + public static AlertDialog enumEditor (CharSequence[] values, CharSequence value, OnClickListener listener) { + return enumEditor (values, findIndexOf (values, value), listener); + } + + private static int findIndexOf (@NonNull CharSequence[] values, CharSequence value) { + for (int i=0;i { + dialog.dismiss (); + final var val = values [which].toString (); + if (listener != null) { + listener.onClick (dialog, which, val); + } + }); + return builder.show (); + } + + /** + * Creates a new attribute editor dialog to edit flag attributes. + * @param values The possible values. + * @param value The current value of the attribute. + * @param listener The listener that will be invoked when the user presses the positive button. + * @return The newly created dialog instance. + */ + @NonNull + public static AlertDialog flagEditor (@NonNull CharSequence[] values, @NonNull CharSequence value, OnClickListener listener) { + final var checked = new boolean [values.length]; + final var vals = value.toString ().split (Pattern.quote ("|")); + for (var i=0;i (); + for (var i=0;i { + if (isChecked) { + checked.add (values[which]); + } else { + checked.remove (values[which]); + } + }); + builder.setPositiveButton (android.R.string.ok, (dialog, which) -> { + if (checked.size () <= 0) { + StudioApp.getInstance ().toast (mContext.getString(R.string.msg_select_one_flag), Toaster.Type.ERROR); + return; + } + + final var val = TextUtils.join ("|", checked); + + if (listener != null) { + listener.onClick (dialog, which, val); + } + }); + builder.setNegativeButton (android.R.string.cancel, (dialog, which) -> dialog.dismiss ()); + return builder.create (); + } + + @NonNull + public static AlertDialog colorPicker (final ColorPickerView.OnPickListener listener) { + final var view = new ColorPickerView (mContext); + final var builder = newMaterialDialogBuilder (mContext); + builder.setView (view); + + final var dialog = builder.create (); + dialog.getWindow ().setSoftInputMode (WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); + + view.setOnPickListener ((color, hexCode) -> { + dialog.dismiss (); + if (listener != null) { + listener.onPick (color, hexCode); + } + }); + + return dialog; + } + + public interface OnClickListener { + void onClick (DialogInterface dialog, int which, String newValue); + } } \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/utils/DialogUtils.java b/app/src/main/java/com/itsaky/androidide/utils/DialogUtils.java index a54c2ec85..2e0c68205 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/DialogUtils.java +++ b/app/src/main/java/com/itsaky/androidide/utils/DialogUtils.java @@ -1,93 +1,93 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.utils; - -import android.content.Context; -import android.content.DialogInterface; - -import androidx.annotation.NonNull; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import com.itsaky.androidide.R; - -import org.jetbrains.annotations.Contract; - -/** - * Utility class for creating dialogs. - * - * @author Akash Yadav - */ -public class DialogUtils { - - /** - * Creates a new MaterialAlertDialogBuilder with the app's default style. - * @param context The context for the dialog builder. - * @return The new MaterialAlertDialogBuilder instance. - */ - @NonNull - @Contract("_ -> new") - public static MaterialAlertDialogBuilder newMaterialDialogBuilder (Context context) { - return new MaterialAlertDialogBuilder (context, R.style.AppTheme_MaterialAlertDialog); - } - - /** - * Create a new alert dialog with two buttons: Yes and No. - * This method simply calls {@link #newYesNoDialog(Context, String, String, DialogInterface.OnClickListener, DialogInterface.OnClickListener)} - * with default values for title and message. - * - * @param context The context for the dialog. - * @param positiveClickListener A listener that will be invoked on the Yes button click. - * @param negativeClickListener A listener that will be invoked on the No button click. - * @return The newly created dialog. - */ - @NonNull - public static MaterialAlertDialogBuilder newYesNoDialog (Context context, - DialogInterface.OnClickListener positiveClickListener, - DialogInterface.OnClickListener negativeClickListener) { - return newYesNoDialog (context, - context.getString(R.string.msg_yesno_def_title), - context.getString(R.string.msg_yesno_def_message), - positiveClickListener, - negativeClickListener); - } - - /** - * Create a new alert dialog with two buttons: Yes and No. - * - * @param context The context for the dialog. - * @param title The title of the dialog. - * @param message The message of the dialog. - * @param positiveClickListener A listener that will be invoked on the Yes button click. - * @param negativeClickListener A listener that will be invoked on the No button click. - * @return The newly created dialog instance. - */ - @NonNull - public static MaterialAlertDialogBuilder newYesNoDialog (Context context, - String title, - String message, - DialogInterface.OnClickListener positiveClickListener, - DialogInterface.OnClickListener negativeClickListener) { - final var builder = DialogUtils.newMaterialDialogBuilder (context); - builder.setTitle (title); - builder.setMessage (message); - builder.setPositiveButton (android.R.string.yes, positiveClickListener); - builder.setNegativeButton (android.R.string.no, negativeClickListener); - - return builder; - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.utils; + +import android.content.Context; +import android.content.DialogInterface; + +import androidx.annotation.NonNull; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.itsaky.androidide.R; + +import org.jetbrains.annotations.Contract; + +/** + * Utility class for creating dialogs. + * + * @author Akash Yadav + */ +public class DialogUtils { + + /** + * Creates a new MaterialAlertDialogBuilder with the app's default style. + * @param context The context for the dialog builder. + * @return The new MaterialAlertDialogBuilder instance. + */ + @NonNull + @Contract("_ -> new") + public static MaterialAlertDialogBuilder newMaterialDialogBuilder (Context context) { + return new MaterialAlertDialogBuilder (context, R.style.AppTheme_MaterialAlertDialog); + } + + /** + * Create a new alert dialog with two buttons: Yes and No. + * This method simply calls {@link #newYesNoDialog(Context, String, String, DialogInterface.OnClickListener, DialogInterface.OnClickListener)} + * with default values for title and message. + * + * @param context The context for the dialog. + * @param positiveClickListener A listener that will be invoked on the Yes button click. + * @param negativeClickListener A listener that will be invoked on the No button click. + * @return The newly created dialog. + */ + @NonNull + public static MaterialAlertDialogBuilder newYesNoDialog (Context context, + DialogInterface.OnClickListener positiveClickListener, + DialogInterface.OnClickListener negativeClickListener) { + return newYesNoDialog (context, + context.getString(R.string.msg_yesno_def_title), + context.getString(R.string.msg_yesno_def_message), + positiveClickListener, + negativeClickListener); + } + + /** + * Create a new alert dialog with two buttons: Yes and No. + * + * @param context The context for the dialog. + * @param title The title of the dialog. + * @param message The message of the dialog. + * @param positiveClickListener A listener that will be invoked on the Yes button click. + * @param negativeClickListener A listener that will be invoked on the No button click. + * @return The newly created dialog instance. + */ + @NonNull + public static MaterialAlertDialogBuilder newYesNoDialog (Context context, + String title, + String message, + DialogInterface.OnClickListener positiveClickListener, + DialogInterface.OnClickListener negativeClickListener) { + final var builder = DialogUtils.newMaterialDialogBuilder (context); + builder.setTitle (title); + builder.setMessage (message); + builder.setPositiveButton (android.R.string.yes, positiveClickListener); + builder.setNegativeButton (android.R.string.no, negativeClickListener); + + return builder; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/utils/EditorBottomSheetBehavior.java b/app/src/main/java/com/itsaky/androidide/utils/EditorBottomSheetBehavior.java index 714de1f61..adbb11f44 100644 --- a/app/src/main/java/com/itsaky/androidide/utils/EditorBottomSheetBehavior.java +++ b/app/src/main/java/com/itsaky/androidide/utils/EditorBottomSheetBehavior.java @@ -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 . - */ - -package com.itsaky.androidide.utils; - -import android.content.Context; -import android.graphics.Rect; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.coordinatorlayout.widget.CoordinatorLayout; - -import com.google.android.material.bottomsheet.BottomSheetBehavior; -import com.itsaky.androidide.databinding.LayoutEditorBottomSheetBinding; - -public class EditorBottomSheetBehavior extends BottomSheetBehavior { - - private LayoutEditorBottomSheetBinding binding; - private static final Logger LOG = Logger.instance ("EditorBottomSheetBehavior"); - - public EditorBottomSheetBehavior (Context context, AttributeSet attributeSet) { - super(context, attributeSet); - } - - public void setBinding (LayoutEditorBottomSheetBinding binding) { - this.binding = binding; - } - - @Override - public boolean onInterceptTouchEvent (@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { - if (binding != null) { - final var rect = getPagerRect (); - if (rect.contains (event.getX (), event.getY ())) { - return false; - } - } - return super.onInterceptTouchEvent (parent, child, event); - } - - @NonNull - private RectF getPagerRect () { - final var rect = new RectF (); - rect.left = binding.pager.getX (); - rect.top = binding.pager.getY (); - rect.right = rect.left + binding.pager.getWidth (); - rect.bottom = rect.top + binding.pager.getHeight (); - return rect; - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.utils; + +import android.content.Context; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.coordinatorlayout.widget.CoordinatorLayout; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.itsaky.androidide.databinding.LayoutEditorBottomSheetBinding; + +public class EditorBottomSheetBehavior extends BottomSheetBehavior { + + private LayoutEditorBottomSheetBinding binding; + private static final Logger LOG = Logger.instance ("EditorBottomSheetBehavior"); + + public EditorBottomSheetBehavior (Context context, AttributeSet attributeSet) { + super(context, attributeSet); + } + + public void setBinding (LayoutEditorBottomSheetBinding binding) { + this.binding = binding; + } + + @Override + public boolean onInterceptTouchEvent (@NonNull CoordinatorLayout parent, @NonNull V child, @NonNull MotionEvent event) { + if (binding != null) { + final var rect = getPagerRect (); + if (rect.contains (event.getX (), event.getY ())) { + return false; + } + } + return super.onInterceptTouchEvent (parent, child, event); + } + + @NonNull + private RectF getPagerRect () { + final var rect = new RectF (); + rect.left = binding.pager.getX (); + rect.top = binding.pager.getY (); + rect.right = rect.left + binding.pager.getWidth (); + rect.bottom = rect.top + binding.pager.getHeight (); + return rect; + } +} diff --git a/app/src/main/java/com/itsaky/androidide/utils/RuntimeTypeAdapterFactory.java b/app/src/main/java/com/itsaky/androidide/utils/RuntimeTypeAdapterFactory.java deleted file mode 100755 index e7c86e9ff..000000000 --- a/app/src/main/java/com/itsaky/androidide/utils/RuntimeTypeAdapterFactory.java +++ /dev/null @@ -1,276 +0,0 @@ -/* - * Copyright (C) 2011 Google Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.itsaky.androidide.utils; - -import java.io.IOException; -import java.util.LinkedHashMap; -import java.util.Map; - -import com.google.gson.Gson; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; -import com.google.gson.JsonPrimitive; -import com.google.gson.TypeAdapter; -import com.google.gson.TypeAdapterFactory; -import com.google.gson.internal.Streams; -import com.google.gson.reflect.TypeToken; -import com.google.gson.stream.JsonReader; -import com.google.gson.stream.JsonWriter; - -/** - * Adapts values whose runtime type may differ from their declaration type. This - * is necessary when a field's type is not the same type that GSON should create - * when deserializing that field. For example, consider these types: - *
   {@code
- *   abstract class Shape {
- *     int x;
- *     int y;
- *   }
- *   class Circle extends Shape {
- *     int radius;
- *   }
- *   class Rectangle extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Diamond extends Shape {
- *     int width;
- *     int height;
- *   }
- *   class Drawing {
- *     Shape bottomShape;
- *     Shape topShape;
- *   }
- * }
- *

Without additional type information, the serialized JSON is ambiguous. Is - * the bottom shape in this drawing a rectangle or a diamond?

   {@code
- *   {
- *     "bottomShape": {
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * This class addresses this problem by adding type information to the - * serialized JSON and honoring that type information when the JSON is - * deserialized:
   {@code
- *   {
- *     "bottomShape": {
- *       "type": "Diamond",
- *       "width": 10,
- *       "height": 5,
- *       "x": 0,
- *       "y": 0
- *     },
- *     "topShape": {
- *       "type": "Circle",
- *       "radius": 2,
- *       "x": 4,
- *       "y": 1
- *     }
- *   }}
- * Both the type field name ({@code "type"}) and the type labels ({@code - * "Rectangle"}) are configurable. - * - *

Registering Types

- * Create a {@code RuntimeTypeAdapterFactory} by passing the base type and type field - * name to the {@link #of} factory method. If you don't supply an explicit type - * field name, {@code "type"} will be used.
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory
- *       = RuntimeTypeAdapterFactory.of(Shape.class, "type");
- * }
- * Next register all of your subtypes. Every subtype must be explicitly - * registered. This protects your application from injection attacks. If you - * don't supply an explicit type label, the type's simple name will be used. - *
   {@code
- *   shapeAdapterFactory.registerSubtype(Rectangle.class, "Rectangle");
- *   shapeAdapterFactory.registerSubtype(Circle.class, "Circle");
- *   shapeAdapterFactory.registerSubtype(Diamond.class, "Diamond");
- * }
- * Finally, register the type adapter factory in your application's GSON builder: - *
   {@code
- *   Gson gson = new GsonBuilder()
- *       .registerTypeAdapterFactory(shapeAdapterFactory)
- *       .create();
- * }
- * Like {@code GsonBuilder}, this API supports chaining:
   {@code
- *   RuntimeTypeAdapterFactory shapeAdapterFactory = RuntimeTypeAdapterFactory.of(Shape.class)
- *       .registerSubtype(Rectangle.class)
- *       .registerSubtype(Circle.class)
- *       .registerSubtype(Diamond.class);
- * }
- * - *

Serialization and deserialization

- * In order to serialize and deserialize a polymorphic object, - * you must specify the base type explicitly. - *
   {@code
- *   Diamond diamond = new Diamond();
- *   String json = gson.toJson(diamond, Shape.class);
- * }
- * And then: - *
   {@code
- *   Shape shape = gson.fromJson(json, Shape.class);
- * }
- */ -public final class RuntimeTypeAdapterFactory implements TypeAdapterFactory { - private final Class baseType; - private final String typeFieldName; - private final Map> labelToSubtype = new LinkedHashMap>(); - private final Map, String> subtypeToLabel = new LinkedHashMap, String>(); - private final boolean maintainType; - - private RuntimeTypeAdapterFactory(Class baseType, String typeFieldName, boolean maintainType) { - if (typeFieldName == null || baseType == null) { - throw new NullPointerException(); - } - this.baseType = baseType; - this.typeFieldName = typeFieldName; - this.maintainType = maintainType; - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - * {@code maintainType} flag decide if the type will be stored in pojo or not. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName, boolean maintainType) { - return new RuntimeTypeAdapterFactory(baseType, typeFieldName, maintainType); - } - - /** - * Creates a new runtime type adapter using for {@code baseType} using {@code - * typeFieldName} as the type field name. Type field names are case sensitive. - */ - public static RuntimeTypeAdapterFactory of(Class baseType, String typeFieldName) { - return new RuntimeTypeAdapterFactory(baseType, typeFieldName, false); - } - - /** - * Creates a new runtime type adapter for {@code baseType} using {@code "type"} as - * the type field name. - */ - public static RuntimeTypeAdapterFactory of(Class baseType) { - return new RuntimeTypeAdapterFactory(baseType, "type", false); - } - - /** - * Registers {@code type} identified by {@code label}. Labels are case - * sensitive. - * - * @throws IllegalArgumentException if either {@code type} or {@code label} - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type, String label) { - if (type == null || label == null) { - throw new NullPointerException(); - } - if (subtypeToLabel.containsKey(type) || labelToSubtype.containsKey(label)) { - throw new IllegalArgumentException("types and labels must be unique"); - } - labelToSubtype.put(label, type); - subtypeToLabel.put(type, label); - return this; - } - - /** - * Registers {@code type} identified by its {@link Class#getSimpleName simple - * name}. Labels are case sensitive. - * - * @throws IllegalArgumentException if either {@code type} or its simple name - * have already been registered on this type adapter. - */ - public RuntimeTypeAdapterFactory registerSubtype(Class type) { - return registerSubtype(type, type.getSimpleName()); - } - - public TypeAdapter create(Gson gson, TypeToken type) { - if (type.getRawType() != baseType) { - return null; - } - - final Map> labelToDelegate - = new LinkedHashMap>(); - final Map, TypeAdapter> subtypeToDelegate - = new LinkedHashMap, TypeAdapter>(); - for (Map.Entry> entry : labelToSubtype.entrySet()) { - TypeAdapter delegate = gson.getDelegateAdapter(this, TypeToken.get(entry.getValue())); - labelToDelegate.put(entry.getKey(), delegate); - subtypeToDelegate.put(entry.getValue(), delegate); - } - - return new TypeAdapter() { - @Override public R read(JsonReader in) throws IOException { - JsonElement jsonElement = Streams.parse(in); - JsonElement labelJsonElement; - if (maintainType) { - labelJsonElement = jsonElement.getAsJsonObject().get(typeFieldName); - } else { - labelJsonElement = jsonElement.getAsJsonObject().remove(typeFieldName); - } - - if (labelJsonElement == null) { - throw new JsonParseException("cannot deserialize " + baseType - + " because it does not define a field named " + typeFieldName); - } - String label = labelJsonElement.getAsString(); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) labelToDelegate.get(label); - if (delegate == null) { - throw new JsonParseException("cannot deserialize " + baseType + " subtype named " - + label + "; did you forget to register a subtype?"); - } - return delegate.fromJsonTree(jsonElement); - } - - @Override public void write(JsonWriter out, R value) throws IOException { - Class srcType = value.getClass(); - String label = subtypeToLabel.get(srcType); - @SuppressWarnings("unchecked") // registration requires that subtype extends T - TypeAdapter delegate = (TypeAdapter) subtypeToDelegate.get(srcType); - if (delegate == null) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + "; did you forget to register a subtype?"); - } - JsonObject jsonObject = delegate.toJsonTree(value).getAsJsonObject(); - - if (maintainType) { - Streams.write(jsonObject, out); - return; - } - - JsonObject clone = new JsonObject(); - - if (jsonObject.has(typeFieldName)) { - throw new JsonParseException("cannot serialize " + srcType.getName() - + " because it already defines a field named " + typeFieldName); - } - clone.add(typeFieldName, new JsonPrimitive(label)); - - for (Map.Entry e : jsonObject.entrySet()) { - clone.add(e.getKey(), e.getValue()); - } - Streams.write(clone, out); - } - }.nullSafe(); - } -} diff --git a/app/src/main/java/com/itsaky/androidide/utils/Symbols.java b/app/src/main/java/com/itsaky/androidide/utils/Symbols.java index 494dd21fe..60dcbb2a6 100755 --- a/app/src/main/java/com/itsaky/androidide/utils/Symbols.java +++ b/app/src/main/java/com/itsaky/androidide/utils/Symbols.java @@ -22,7 +22,6 @@ package com.itsaky.androidide.utils; import com.itsaky.androidide.app.StudioApp; -import com.itsaky.androidide.fragments.EditorFragment; import com.itsaky.androidide.views.SymbolInputView.Symbol; import java.io.File; @@ -30,11 +29,11 @@ public class Symbols { public static Symbol[] forFile(File file) { if (file.isFile()) { - if (file.getName().endsWith(EditorFragment.EXT_JAVA) - || file.getName().endsWith(EditorFragment.EXT_GRADLE)) + if (file.getName().endsWith(".java") + || file.getName().endsWith(".gradle")) return javaSymbols(); - if (file.getName().endsWith(EditorFragment.EXT_XML)) + if (file.getName().endsWith(".xml")) return xmlSymbols(); } diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.java b/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.java index 855207b33..ef415c630 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.java +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/EditorViewModel.java @@ -1,131 +1,156 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.viewmodel; - -import androidx.annotation.Nullable; -import androidx.core.util.Pair; -import androidx.lifecycle.LifecycleOwner; -import androidx.lifecycle.MutableLiveData; -import androidx.lifecycle.Observer; -import androidx.lifecycle.ViewModel; - -import com.itsaky.androidide.models.OpenedFile; -import com.itsaky.androidide.project.AndroidProject; -import com.itsaky.androidide.project.IDEProject; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; - -/** - * ViewModel for data used in {@link com.itsaky.androidide.EditorActivity} - */ -public class EditorViewModel extends ViewModel { - - private final MutableLiveData > mFiles = new MutableLiveData<> (new ArrayList<> ()); - private final MutableLiveData mProject = new MutableLiveData<> (null); - private final MutableLiveData mIDEProject = new MutableLiveData<> (null); - - /** - * Holds information about the currently selected editor fragment. - * First value in the pair is the index of the editor opened. - * Second value is the file that is opened. - */ - private final MutableLiveData > mCurrentFile = new MutableLiveData<> (null); - - /** - * Get the current {@link AndroidProject}. - * @return The android project. - */ - @Nullable - public AndroidProject getAndroidProject () { - return mProject.getValue (); - } - - /** - * Set the android project currently opened in the editor activity. - * @param project The project that is opened. - */ - public void setAndroidProject (final AndroidProject project) { - this.mProject.setValue (project); - } - - /** - * Get the {@link IDEProject} opened in the editor activity. - * @return The ide project. - */ - @Nullable - public IDEProject getIDEProject () { - return this.mIDEProject.getValue (); - } - - /** - * Set the IDEProject that is currently opened in the editor activity. - * @param project The project that is opened. - */ - public void setIDEProject (final IDEProject project) { - this.mIDEProject.setValue (project); - } - - /** - * Add the given file to the list of opened files. - * @param file The file that has been opened. - */ - public void addFile (final OpenedFile file) { - final var files = mFiles.getValue (); - Objects.requireNonNull (files).add (file); - mFiles.setValue (files); - } - - /** - * Remove the file at the given index from the list of opened files. - * @param index The index of the closed file. - */ - public void removeFile (int index) { - final var files = mFiles.getValue (); - Objects.requireNonNull (files).remove (index); - mFiles.setValue (files); - } - - /** - * Get the opened file at the given index. - * @param index The index of the file. - * @return The file at the given index. - */ - public OpenedFile getOpenedFile (final int index) { - return Objects.requireNonNull (mFiles.getValue ()).get (index); - } - - /** - * Get the number of files opened. - * @return The number of files opened. - */ - public int getOpenedFileCount () { - return Objects.requireNonNull (mFiles.getValue ()).size (); - } - - /** - * Add an observer to the list of opened files. - * @param lifecycleOwner The lifecycle owner. - * @param observer The observer. - */ - public void observeFiles (LifecycleOwner lifecycleOwner, Observer> observer) { - this.mFiles.observe (lifecycleOwner, observer); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.viewmodel; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.Observer; +import androidx.lifecycle.ViewModel; + +import com.itsaky.androidide.project.AndroidProject; +import com.itsaky.androidide.project.IDEProject; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * ViewModel for data used in {@link com.itsaky.androidide.EditorActivity} + */ +public class EditorViewModel extends ViewModel { + + private final MutableLiveData > mFiles = new MutableLiveData<> (new ArrayList<> ()); + private final MutableLiveData mProject = new MutableLiveData<> (null); + private final MutableLiveData mIDEProject = new MutableLiveData<> (null); + + /** + * Holds information about the currently selected editor fragment. + * First value in the pair is the index of the editor opened. + * Second value is the file that is opened. + */ + private final MutableLiveData > mCurrentFile = new MutableLiveData<> (null); + + /** + * Get the current {@link AndroidProject}. + * @return The android project. + */ + @Nullable + public AndroidProject getAndroidProject () { + return mProject.getValue (); + } + + /** + * Set the android project currently opened in the editor activity. + * @param project The project that is opened. + */ + public void setAndroidProject (final AndroidProject project) { + this.mProject.setValue (project); + } + + /** + * Get the {@link IDEProject} opened in the editor activity. + * @return The ide project. + */ + @Nullable + public IDEProject getIDEProject () { + return this.mIDEProject.getValue (); + } + + /** + * Set the IDEProject that is currently opened in the editor activity. + * @param project The project that is opened. + */ + public void setIDEProject (final IDEProject project) { + this.mIDEProject.setValue (project); + } + + /** + * Add the given file to the list of opened files. + * @param file The file that has been opened. + */ + public void addFile (final File file) { + final var files = mFiles.getValue (); + Objects.requireNonNull (files).add (file); + mFiles.setValue (files); + } + + /** + * Remove the file at the given index from the list of opened files. + * @param index The index of the closed file. + */ + public void removeFile (int index) { + final var files = mFiles.getValue (); + Objects.requireNonNull (files).remove (index); + mFiles.setValue (files); + } + + public void removeAllFiles () { + mFiles.setValue (new ArrayList<> ()); + setCurrentFile (-1, null); + } + + /** + * Get the opened file at the given index. + * @param index The index of the file. + * @return The file at the given index. + */ + public File getOpenedFile (final int index) { + return Objects.requireNonNull (mFiles.getValue ()).get (index); + } + + /** + * Get the number of files opened. + * @return The number of files opened. + */ + public int getOpenedFileCount () { + return Objects.requireNonNull (mFiles.getValue ()).size (); + } + + /** + * Add an observer to the list of opened files. + * @param lifecycleOwner The lifecycle owner. + * @param observer The observer. + */ + public void observeFiles (LifecycleOwner lifecycleOwner, Observer> observer) { + this.mFiles.observe (lifecycleOwner, observer); + } + + public int getCurrentFileIndex () { + if (mCurrentFile.getValue () == null) { + return -1; + } + + return mCurrentFile.getValue ().first; + } + + public File getCurrentFile () { + if (mCurrentFile.getValue () == null) { + return null; + } + + return mCurrentFile.getValue ().second; + } + + public void setCurrentFile (final int index, @Nullable final File file) { + mCurrentFile.setValue (Pair.create (index, file)); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java b/app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java new file mode 100644 index 000000000..6989df54e --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/views/CodeEditorView.java @@ -0,0 +1,378 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.views; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.util.Log; +import android.view.LayoutInflater; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; + +import com.blankj.utilcode.util.FileIOUtils; +import com.blankj.utilcode.util.SizeUtils; +import com.itsaky.androidide.adapters.CompletionListAdapter; +import com.itsaky.androidide.app.StudioApp; +import com.itsaky.androidide.databinding.LayoutCodeEditorBinding; +import com.itsaky.androidide.language.groovy.GroovyLanguage; +import com.itsaky.androidide.language.java.JavaLanguage; +import com.itsaky.androidide.language.xml.XMLLanguage; +import com.itsaky.androidide.lexers.xml.XMLLexer; +import com.itsaky.androidide.managers.PreferenceManager; +import com.itsaky.androidide.models.ConstantsBridge; +import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE; +import com.itsaky.androidide.utils.LSPUtils; +import com.itsaky.androidide.utils.Logger; +import com.itsaky.androidide.utils.TypefaceUtils; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.Token; +import org.eclipse.lsp4j.Diagnostic; +import org.eclipse.lsp4j.Range; + +import java.io.File; +import java.io.StringReader; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import io.github.rosemoe.editor.interfaces.EditorEventListener; +import io.github.rosemoe.editor.langs.EmptyLanguage; +import io.github.rosemoe.editor.widget.CodeEditor; + +/** + * A view that handles opened code editors. + * + * @author Akash Yadav + */ +@SuppressLint("ViewConstructor") // This view is always dynamically created. +public class CodeEditorView extends FrameLayout { + + private final File file; + private final LayoutCodeEditorBinding binding; + + private boolean isModified; + private boolean isFirstCreate; + + private static final Logger LOG = Logger.instance ("CodeEditorView"); + + private final EditorEventListener mEventListener = new EditorEventListener () { + @Override + public boolean onRequestFormat (CodeEditor editor, boolean async) { + return false; + } + + @Override + public boolean onFormatFail (CodeEditor editor, Throwable cause) { + return false; + } + + @Override + public void onFormatSucceed (CodeEditor editor) { + } + + @Override + public void onNewTextSet (CodeEditor editor) { + } + + @Override + public void beforeReplace (CodeEditor editor, CharSequence content) { + } + + @Override + public void onSetSelection (int startLine, int startCol, int endLine, int endCol) { + } + + @Override + public void afterDelete (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence deletedContent) { + isModified = true; + } + + @Override + public void afterInsert (CodeEditor editor, CharSequence content, int startLine, int startColumn, int endLine, int endColumn, CharSequence insertedContent) { + isModified = true; + if (file.getName ().endsWith (".xml")) { + boolean isOpen = false; + try { + isOpen = editor.getText ().charAt (editor.getCursor ().getLeft () - 2) == '<'; + } catch (Throwable th) { + LOG.error (th); + } + + if (isOpen && insertedContent.toString ().equals ("/")) { + closeCurrentTag (editor.getText ().toString (), endLine, endColumn); + } + } + } + }; + + public CodeEditorView (@NonNull Context context, @NonNull File file, final @NonNull Range selection) { + super (context); + this.file = file; + this.isModified = false; + this.isFirstCreate = true; + + this.binding = LayoutCodeEditorBinding.inflate (LayoutInflater.from (context)); + this.binding.editor.setOverScrollEnabled (false); + this.binding.editor.setTypefaceText (TypefaceUtils.jetbrainsMono ()); + this.binding.editor.setTextActionMode (CodeEditor.TextActionMode.ACTION_MODE); + this.binding.editor.setHighlightCurrentBlock (true); + this.binding.editor.setEventListener (mEventListener); + this.binding.editor.setAutoCompletionOnComposing (true); + this.binding.editor.setAutoCompletionItemAdapter (new CompletionListAdapter ()); + this.binding.editor.setLineColorsEnabled (true); + this.binding.editor.setDividerWidth (SizeUtils.dp2px (1)); + this.binding.editor.setColorScheme (new SchemeAndroidIDE ()); + + this.binding.diagnosticTextContainer.setVisibility (GONE); + + removeAllViews (); + addView (this.binding.getRoot ()); + + CompletableFuture.runAsync (() -> { + final var contents = FileIOUtils.readFile2String (file); + binding.editor.post (() -> { + binding.editor.setText (contents); + postRead (); + if (LSPUtils.isEqual (selection.getStart (), selection.getEnd ())) { + getEditor ().setSelection (selection.getStart ().getLine (), selection.getStart ().getCharacter ()); + } else { + getEditor ().setSelectionRegion ( + selection.getStart ().getLine (), + selection.getStart ().getCharacter (), + selection.getEnd ().getLine (), + selection.getEnd ().getCharacter () + ); + } + }); + }); + } + + public boolean save () { + final var text = getText (); + final var saved = FileIOUtils.writeFileFromString (getFile (), text); + notifySaved (); + isModified = false; + return saved; + } + + public Editor getEditor () { + return binding.editor; + } + + public LayoutCodeEditorBinding getBinding () { + return binding; + } + + public File getFile () { + return file; + } + + public String getText () { + return binding.editor.getText ().toString (); + } + + public boolean isModified () { + return isModified; + } + + public void onPause () { + CompletableFuture.runAsync (this::save); + } + + public void onResume () { + configureEditorIfNeeded (); + } + + public void undo () { + if (binding.editor.canUndo ()) { + binding.editor.undo (); + } + } + + public void redo () { + if (binding.editor.canRedo ()) { + binding.editor.redo (); + } + } + + public void findDefinition () { + binding.editor.findDefinition (); + } + + public void findReferences () { + binding.editor.findReferences (); + } + + public void commentLine () { + binding.editor.commentLine (); + } + + public void uncommentLine () { + binding.editor.uncommentLine (); + } + + public void beginSearch () { + binding.editor.beginSearchMode (); + } + + public void setDiagnostics (List diagnostics) { + if (diagnostics == null) { + LOG.info ("Clearing diagnostics of code editor", "file:", file); + binding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (new HashMap<> ()); + return; + } + + Map> map = new HashMap<> (); + for (int i = 0; i < diagnostics.size (); i++) { + final Diagnostic d = diagnostics.get (i); + if (d == null) { + continue; + } + final Range range = d.getRange (); + final int line = range.getStart ().getLine (); + final int column = range.getStart ().getCharacter (); + Map mappedByColumn = map.get (line); + if (mappedByColumn == null) { + mappedByColumn = new HashMap<> (); + } + mappedByColumn.put (column, d); + map.put (line, mappedByColumn); + } + + binding.editor.getEditorLanguage ().getAnalyzer ().updateDiagnostics (map); + } + + protected void postRead () { + if (file.isFile () && file.getName ().endsWith (".java")) { + binding.editor.setEditorLanguage (new JavaLanguage (getFile ())); + } else if (file.isFile () && file.getName ().endsWith (".xml")) { + binding.editor.setEditorLanguage (new XMLLanguage (getFile ())); + } else if (file.isFile () && file.getName ().endsWith (".gradle")) { + binding.editor.setEditorLanguage (new GroovyLanguage (getFile ())); + } else { + binding.editor.setEditorLanguage (new EmptyLanguage ()); + } + + // File must be set only after setting the language + // This will make sure that textDocument/didOpen is sent + binding.editor.setFile (getFile ()); + } + + private void configureEditorIfNeeded () { + boolean sizeChanged = isFirstCreate || ConstantsBridge.EDITORPREF_SIZE_CHANGED; + boolean flagsChanged = isFirstCreate || ConstantsBridge.EDITORPREF_FLAGS_CHANGED; + boolean drawHexChanged = isFirstCreate || ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED; + final PreferenceManager prefs = StudioApp.getInstance ().getPrefManager (); + + if (sizeChanged) { + float textSize = prefs.getFloat (PreferenceManager.KEY_EDITOR_FONT_SIZE); + if (textSize < 6 || textSize > 32) { + textSize = 14; + } + + binding.editor.setTextSize (textSize); + ConstantsBridge.EDITORPREF_SIZE_CHANGED = false; + } + + if (flagsChanged) { + int flags = 0; + if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_LEADING, true)) { + flags |= CodeEditor.FLAG_DRAW_WHITESPACE_LEADING; + } + + if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_TRAILING, false)) { + flags |= CodeEditor.FLAG_DRAW_WHITESPACE_TRAILING; + } + + if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_INNER, true)) { + flags |= CodeEditor.FLAG_DRAW_WHITESPACE_INNER; + } + + if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_WS_EMPTY_LINE, true)) { + flags |= CodeEditor.FLAG_DRAW_WHITESPACE_FOR_EMPTY_LINE; + } + + if (prefs.getBoolean (PreferenceManager.KEY_EDITORFLAG_LINE_BREAK, true)) { + flags |= CodeEditor.FLAG_DRAW_LINE_SEPARATOR; + } + + binding.editor.setNonPrintablePaintingFlags (flags); + ConstantsBridge.EDITORPREF_FLAGS_CHANGED = false; + } + + if (drawHexChanged) { + binding.editor.setLineColorsEnabled (prefs.getBoolean (PreferenceManager.KEY_EDITOR_DRAW_HEX, true)); + ConstantsBridge.EDITORPREF_DRAW_HEX_CHANGED = false; + } + + isFirstCreate = false; + } + + private void closeCurrentTag (String text, int line, int col) { + try { + XMLLexer lexer = new XMLLexer (CharStreams.fromReader (new StringReader (text))); + Token token; + boolean wasSlash = false, wasOpen = false; + ArrayList currentNames = new ArrayList<> (); + while (((token = lexer.nextToken ()) != null && token.getType () != token.EOF)) { + final int type = token.getType (); + if (type == XMLLexer.OPEN) { + wasOpen = true; + } else if (type == XMLLexer.Name) { + if (wasOpen && wasSlash && currentNames.size () > 0) { + currentNames.remove (0); + } else if (wasOpen) { + currentNames.add (0, token.getText ()); + wasOpen = false; + } + } else if (type == XMLLexer.OPEN_SLASH) { + int l = token.getLine () - 1; + int c = token.getCharPositionInLine (); + if (l == line && c == col) { + break; + } else if (currentNames.size () > 0) { + currentNames.remove (0); + } + } else if (type == XMLLexer.SLASH_CLOSE + || type == XMLLexer.SPECIAL_CLOSE) { + if (currentNames.size () > 0 && token.getText ().trim ().endsWith ("/>")) { + currentNames.remove (0); + } + } else if (type == XMLLexer.SLASH) { + wasSlash = true; + } else { + wasOpen = wasSlash = false; + } + } + + if (currentNames.size () > 0) { + binding.editor.getText ().insert (line, col + 2, currentNames.get (0)); + } + } catch (Throwable th) { + LOG.error ("Unable to close current tag", th); + } + } + + private void notifySaved () { + binding.editor.didSave (); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/views/DesignerLayoutContainer.java b/app/src/main/java/com/itsaky/androidide/views/DesignerLayoutContainer.java index 2e5641796..f0dd1b8a8 100644 --- a/app/src/main/java/com/itsaky/androidide/views/DesignerLayoutContainer.java +++ b/app/src/main/java/com/itsaky/androidide/views/DesignerLayoutContainer.java @@ -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 . - */ - -package com.itsaky.androidide.views; - -import android.content.Context; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.Nullable; - -import com.itsaky.androidide.R; - -/** - * A container used in UI Designer. This view holds the views inflated from an XML layout. - * When there are no children in this layout, it shows a message. - * - * @author Akash Yadav - */ -public class DesignerLayoutContainer extends LinearLayout { - - private final TextView mMessage; - - public DesignerLayoutContainer (Context context) { - this (context, null); - } - - public DesignerLayoutContainer (Context context, @Nullable AttributeSet attrs) { - this (context, attrs, 0); - } - - public DesignerLayoutContainer (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this (context, attrs, defStyleAttr, 0); - } - - public DesignerLayoutContainer (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super (context, attrs, defStyleAttr, defStyleRes); - - this.mMessage = new TextView (context); - this.mMessage.setText (context.getString(R.string.msg_empty_ui_layout)); - this.mMessage.setLayoutParams (new LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - this.mMessage.setGravity (Gravity.CENTER); - this.mMessage.setTextSize (TypedValue.COMPLEX_UNIT_SP, 16); - } - - @Override - public void onViewAdded (View child) { - if (child != this.mMessage) { - removeView (this.mMessage); - } else { - super.onViewAdded ( child); - } - } - - @Override - public void onViewRemoved (View child) { - if (child != this.mMessage && getChildCount () <= 0) { - addView (mMessage); - } else { - super.onViewRemoved (child); - } - } - - // This is needed by XMLLayoutInflater - @Override - protected LayoutParams generateDefaultLayoutParams () { - return super.generateDefaultLayoutParams (); - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.views; + +import android.content.Context; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import com.itsaky.androidide.R; + +/** + * A container used in UI Designer. This view holds the views inflated from an XML layout. + * When there are no children in this layout, it shows a message. + * + * @author Akash Yadav + */ +public class DesignerLayoutContainer extends LinearLayout { + + private final TextView mMessage; + + public DesignerLayoutContainer (Context context) { + this (context, null); + } + + public DesignerLayoutContainer (Context context, @Nullable AttributeSet attrs) { + this (context, attrs, 0); + } + + public DesignerLayoutContainer (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this (context, attrs, defStyleAttr, 0); + } + + public DesignerLayoutContainer (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super (context, attrs, defStyleAttr, defStyleRes); + + this.mMessage = new TextView (context); + this.mMessage.setText (context.getString(R.string.msg_empty_ui_layout)); + this.mMessage.setLayoutParams (new LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + this.mMessage.setGravity (Gravity.CENTER); + this.mMessage.setTextSize (TypedValue.COMPLEX_UNIT_SP, 16); + } + + @Override + public void onViewAdded (View child) { + if (child != this.mMessage) { + removeView (this.mMessage); + } else { + super.onViewAdded ( child); + } + } + + @Override + public void onViewRemoved (View child) { + if (child != this.mMessage && getChildCount () <= 0) { + addView (mMessage); + } else { + super.onViewRemoved (child); + } + } + + // This is needed by XMLLayoutInflater + @Override + protected LayoutParams generateDefaultLayoutParams () { + return super.generateDefaultLayoutParams (); + } +} diff --git a/app/src/main/java/com/itsaky/androidide/views/TooltipLayout.java b/app/src/main/java/com/itsaky/androidide/views/TooltipLayout.java deleted file mode 100755 index b59db41dd..000000000 --- a/app/src/main/java/com/itsaky/androidide/views/TooltipLayout.java +++ /dev/null @@ -1,77 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.views; - -import android.content.Context; -import android.graphics.Canvas; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.widget.LinearLayout; -import android.graphics.Path; -import android.graphics.Paint; -import android.graphics.Color; - -public class TooltipLayout extends LinearLayout { - - private final float dp8; - private final Path path; - private final Paint paint; - - public TooltipLayout (Context ctx) { - this(ctx, null); - } - - public TooltipLayout (Context ctx, AttributeSet attrs) { - this(ctx, attrs, 0); - } - - public TooltipLayout (Context ctx, AttributeSet attrs, int style) { - super(ctx, attrs, style); - - dp8 = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8, getResources().getDisplayMetrics()); - path = new Path(); - - paint = new Paint(); - paint.setAntiAlias(true); - paint.setStyle(Paint.Style.FILL); - paint.setColor(Color.parseColor("#424242")); - } - - @Override - protected void onDraw(Canvas canvas) { - final int width = getWidth(); - final int height = getHeight(); - final float midX = width / 2; - - path.moveTo(0, 0); - path.lineTo(width, 0); - path.lineTo(width, height - dp8); - path.lineTo(midX + dp8, height - dp8); - path.lineTo(midX, height ); - path.lineTo(midX - dp8, height - dp8); - path.lineTo(0, height - dp8); - path.close(); - - canvas.drawPath(path, paint); - super.onDraw(canvas); - } -} diff --git a/app/src/main/java/com/itsaky/androidide/views/VerticalViewPager.java b/app/src/main/java/com/itsaky/androidide/views/VerticalViewPager.java deleted file mode 100755 index 1a1cb4595..000000000 --- a/app/src/main/java/com/itsaky/androidide/views/VerticalViewPager.java +++ /dev/null @@ -1,113 +0,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 . - * -**************************************************************************************/ - - -package com.itsaky.androidide.views; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import androidx.viewpager.widget.ViewPager; - -public class VerticalViewPager extends ViewPager -{ - public static final String TAG = "VerticalViewPager"; - private boolean scrollEnabled = true; - - public VerticalViewPager(Context context) - { - super(context); - init(); - } - - public VerticalViewPager(Context context, AttributeSet attrs) - { - super(context, attrs); - init(); - } - - public void setScrollEnabled(boolean scrollEnabled) - { - this.scrollEnabled = scrollEnabled; - } - - public boolean isScrollEnabled() - { - return scrollEnabled; - } - - private void init() - { - setPageTransformer(true, new VerticalPageTransformer()); - setOverScrollMode(OVER_SCROLL_NEVER); - } - - private class VerticalPageTransformer implements ViewPager.PageTransformer - { - @Override - public void transformPage(View view, float position) - { - if (position < -1) - { - view.setAlpha(0); - } else if (position <= 1) - { - view.setAlpha(1); - view.setTranslationX(view.getWidth() * -position); - float yPosition = position * view.getHeight(); - view.setTranslationY(yPosition); - } else - { - view.setAlpha(0); - } - } - } - - /** - * Swaps the X and Y coordinates of your touch event. - */ - private MotionEvent swapXY(MotionEvent ev) - { - float width = getWidth(); - float height = getHeight(); - float newX = (ev.getY() / height) * width; - float newY = (ev.getX() / width) * height; - ev.setLocation(newX, newY); - return ev; - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) - { - boolean intercepted = super.onInterceptTouchEvent(swapXY(ev)); - swapXY(ev); // return touch coordinates to original reference frame for any child views - return intercepted; - } - - @SuppressLint("ClickableViewAccessibility") - @Override - public boolean onTouchEvent(MotionEvent ev) - { - return isScrollEnabled() ? super.onTouchEvent(swapXY(ev)) : true; - } -} diff --git a/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java b/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java index 4372a3416..b3b8f699c 100755 --- a/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java +++ b/app/src/main/java/io/github/rosemoe/editor/widget/CodeEditor.java @@ -4618,16 +4618,15 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca params.setPosition (getCursorAsLSPPosition ()); final CompletableFuture, List>> future = mLanguageServer.getTextDocumentService ().definition (params); future.whenComplete ((e, t) -> { - final Either, List> either = e; - final Throwable th = t; - - if (either == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) { + + if (e == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) { + LOG.error ("An error occurred while finding definition", t); showDefinitionNotFound (pd); return; } - if (either.isLeft ()) { - final List locations = either.getLeft (); + if (e.isLeft ()) { + final List locations = e.getLeft (); if (locations == null || locations.size () <= 0) { showDefinitionNotFound (pd); return; @@ -4643,8 +4642,8 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca }); dismissOnUiThread (pd); - } else if (either.isRight ()) { - final List locations = either.getRight (); + } else if (e.isRight ()) { + final List locations = e.getRight (); if (mLanguageClient == null || locations == null || locations.size () <= 0) { showDefinitionNotFound (pd); return; @@ -4663,6 +4662,7 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca } }); } catch (Throwable th) { + LOG.error ("An error occurred while finding definition", th); showDefinitionNotFound (pd); } } @@ -4690,26 +4690,26 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca final CompletableFuture> future = mLanguageServer.getTextDocumentService ().references (params); future.whenComplete ((l, t) -> { - final List locations = l; - final Throwable th = t; - - if (locations == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) { + + if (l == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) { + LOG.error ("An error occurred while finding references", t); showReferencesNotFound (pd); return; } - if (locations.isEmpty ()) { + if (l.isEmpty ()) { showReferencesNotFound (pd); return; } else { ThreadUtils.runOnUiThread (() -> { - mLanguageClient.showLocations (locations); + mLanguageClient.showLocations (l); }); } dismissOnUiThread (pd); }); } catch (Throwable th) { + LOG.error ("An error occurred while finding references", th); showReferencesNotFound (pd); } } @@ -4741,24 +4741,20 @@ public class CodeEditor extends View implements ContentListener, TextAnalyzer.Ca CompletableFuture future = mLanguageServer.getTextDocumentService ().signatureHelp (params); future.whenComplete ((h, t) -> { - final SignatureHelp help = h; - final Throwable th = t; - - if (help == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) { + if (h == null || mLanguageClient == null || future.isCancelled () || future.isCompletedExceptionally ()) { + LOG.error ("An error occurred while finding signature help", t); return; } ThreadUtils.runOnUiThread (() -> { - mLanguageClient.showSignatureHelp (help, getFile ()); + mLanguageClient.showSignatureHelp (h, getFile ()); }); }); } } private void dismissOnUiThread (final Dialog dialog) { - ThreadUtils.runOnUiThread (() -> { - dialog.dismiss (); - }); + ThreadUtils.runOnUiThread (dialog::dismiss); } /** diff --git a/app/src/main/res/drawable/bg_ui_view_pressed.xml b/app/src/main/res/drawable/bg_ui_view_pressed.xml index 949104d81..9cab80594 100644 --- a/app/src/main/res/drawable/bg_ui_view_pressed.xml +++ b/app/src/main/res/drawable/bg_ui_view_pressed.xml @@ -1,28 +1,28 @@ - - - - - - - - - + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_widget_linear.xml b/app/src/main/res/drawable/ic_widget_linear.xml index 1b30d43a2..acda24ede 100644 --- a/app/src/main/res/drawable/ic_widget_linear.xml +++ b/app/src/main/res/drawable/ic_widget_linear.xml @@ -1,42 +1,42 @@ - - - - - - - + + + + + + + diff --git a/app/src/main/res/layout/activity_editor.xml b/app/src/main/res/layout/activity_editor.xml index 7823af778..4606f617d 100755 --- a/app/src/main/res/layout/activity_editor.xml +++ b/app/src/main/res/layout/activity_editor.xml @@ -1,116 +1,119 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_non_editable_editor.xml b/app/src/main/res/layout/fragment_non_editable_editor.xml index 73224fd4d..47eb37a0f 100644 --- a/app/src/main/res/layout/fragment_non_editable_editor.xml +++ b/app/src/main/res/layout/fragment_non_editable_editor.xml @@ -1,22 +1,22 @@ - - - + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_attr_editor_sheet.xml b/app/src/main/res/layout/layout_attr_editor_sheet.xml index b726336ba..925af217f 100644 --- a/app/src/main/res/layout/layout_attr_editor_sheet.xml +++ b/app/src/main/res/layout/layout_attr_editor_sheet.xml @@ -1,81 +1,81 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_attr_editor_sheet_item.xml b/app/src/main/res/layout/layout_attr_editor_sheet_item.xml index c43d6b707..e97e19d4c 100644 --- a/app/src/main/res/layout/layout_attr_editor_sheet_item.xml +++ b/app/src/main/res/layout/layout_attr_editor_sheet_item.xml @@ -1,46 +1,46 @@ - - - - - - - - + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_editor.xml b/app/src/main/res/layout/layout_code_editor.xml old mode 100755 new mode 100644 similarity index 100% rename from app/src/main/res/layout/fragment_editor.xml rename to app/src/main/res/layout/layout_code_editor.xml diff --git a/app/src/main/res/layout/layout_dimension_attr_editor.xml b/app/src/main/res/layout/layout_dimension_attr_editor.xml index c829beded..2853985e7 100644 --- a/app/src/main/res/layout/layout_dimension_attr_editor.xml +++ b/app/src/main/res/layout/layout_dimension_attr_editor.xml @@ -1,60 +1,60 @@ - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_editor_bottom_sheet.xml b/app/src/main/res/layout/layout_editor_bottom_sheet.xml index 848c290a7..1632b4adc 100644 --- a/app/src/main/res/layout/layout_editor_bottom_sheet.xml +++ b/app/src/main/res/layout/layout_editor_bottom_sheet.xml @@ -1,75 +1,75 @@ - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/layout_text_attr_editor.xml b/app/src/main/res/layout/layout_text_attr_editor.xml index 693919ecb..3d1935fc5 100644 --- a/app/src/main/res/layout/layout_text_attr_editor.xml +++ b/app/src/main/res/layout/layout_text_attr_editor.xml @@ -1,36 +1,36 @@ - - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/common/src/main/java/com/itsaky/androidide/colorpicker/ColorPickerView.java b/common/src/main/java/com/itsaky/androidide/colorpicker/ColorPickerView.java index ffb2816b7..b269c50e0 100644 --- a/common/src/main/java/com/itsaky/androidide/colorpicker/ColorPickerView.java +++ b/common/src/main/java/com/itsaky/androidide/colorpicker/ColorPickerView.java @@ -1,227 +1,227 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.colorpicker; - -import static android.graphics.Color.alpha; -import static android.graphics.Color.argb; -import static android.graphics.Color.blue; -import static android.graphics.Color.green; -import static android.graphics.Color.parseColor; -import static android.graphics.Color.red; - -import android.content.Context; -import android.text.Editable; -import android.text.TextWatcher; -import android.util.AttributeSet; -import android.view.LayoutInflater; -import android.view.ViewGroup; -import android.widget.LinearLayout; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.content.ContextCompat; - -import com.google.android.material.slider.Slider; -import com.itsaky.androidide.common.R; -import com.itsaky.androidide.common.databinding.LayoutColorPickerBinding; -import com.itsaky.androidide.utils.Logger; - -/** - * A color picker view. - * - * @author Akash Yadav - */ -public class ColorPickerView extends LinearLayout { - - private int alpha; - private int red; - private int green; - private int blue; - - private OnPickListener mPickListener; - private final LayoutColorPickerBinding binding; - private final int DEFAULT_COLOR; - - private static final Logger LOG = Logger.instance ("ColorPickerView"); - - private final Slider.OnChangeListener mValueChangeListener = (slider, value, fromUser) -> { - - // If it's not from user, then the values have already been updated - if (fromUser) { - final var id = slider.getId (); - final var intVal = (int) value; - if (id == R.id.alpha_slider) { - alpha = intVal; - } else if (id == R.id.red_slider) { - red = intVal; - } else if (id == R.id.green_slider){ - green = intVal; - } else if (id == R.id.blue_slider) { - blue = intVal; - } - - updatePreview (); - } - }; - - public ColorPickerView (Context context) { - this (context, null); - } - - public ColorPickerView (Context context, @Nullable AttributeSet attrs) { - this (context, attrs, 0); - } - - public ColorPickerView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - this (context, attrs, defStyleAttr, 0); - } - - public ColorPickerView (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { - super (context, attrs, defStyleAttr, defStyleRes); - - this.DEFAULT_COLOR = ContextCompat.getColor (context, R.color.color_picker_default_color); - - this.binding = LayoutColorPickerBinding.inflate (LayoutInflater.from (context)); - - removeAllViews (); - addView (binding.getRoot (), new LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); - - init (); - } - - /** - * Set a listener that will be notified when the user picks a color. - * @param listener The listener to set. - */ - public void setOnPickListener (OnPickListener listener) { - this.mPickListener = listener; - } - - private void init () { - - setColor (DEFAULT_COLOR); - - this.binding.alphaSlider.addOnChangeListener (mValueChangeListener); - this.binding.redSlider.addOnChangeListener (mValueChangeListener); - this.binding.greenSlider.addOnChangeListener (mValueChangeListener); - this.binding.blueSlider.addOnChangeListener (mValueChangeListener); - - this.binding.hexField.getEditText ().addTextChangedListener (new ColorTextWatcher ()); - - this.binding.pick.setOnClickListener (v -> { - if (mPickListener != null) { - mPickListener.onPick (getColor (), getHexColor ()); - } - }); - } - - /** - * Get the hex color code of the current color value. - * @return The hex color code of the current color value. - */ - @NonNull - public String getHexColor () { - return "#" + Integer.toHexString (getColor ()); - } - - /** - * Set the color value of this picker to the given hex color code. - * @param hexCode The hex color code. - * @see #setColor(int) - */ - public void setColor (final String hexCode) { - setColor (parseColor (hexCode)); - } - - /** - * Set the color value for this color picker. - * this will in turn update value of the sliders - * and the color preview. - * @param color The new color value. - */ - public void setColor (int color) { - this.alpha = alpha (color); - this.red = red (color); - this.green = green(color); - this.blue = blue (color); - - this.binding.alphaSlider.setValue (alpha); - this.binding.redSlider.setValue (red); - this.binding.greenSlider.setValue (green); - this.binding.blueSlider.setValue (blue); - - updatePreview (); - } - - /** - * Get the int value of the color from the current - * values of alpha, red, green and blue color values. - * @return The int color value. - */ - public int getColor () { - return argb (alpha, red, green, blue); - } - - /** - * Call this method to update the preview of the current color. - */ - private void updatePreview () { - this.binding.colorPreview.setBackgroundColor (getColor ()); - } - - /** - * A listener that can be used to get notified when the user picks a color. - * - * @author Akash Yadav - */ - public interface OnPickListener { - - /** - * Called when the user picks a color using the 'Pick' button. - * @param color The integer value of the color. - * @param hexCode The hex color code of the color. - */ - void onPick (int color, String hexCode); - } - - /** - * A TextWatcher that is used to listen for text updates in the - * hex color input field. - */ - private class ColorTextWatcher implements TextWatcher { - - @Override - public void beforeTextChanged (CharSequence s, int start, int count, int after) { - } - - @Override - public void afterTextChanged (Editable s) { - } - - @Override - public void onTextChanged (CharSequence s, int start, int before, int count) { - try { - final var color = parseColor ("#" + s); - setColor (color); - } catch (Exception e) { - // May happen due to invalid color values - // Simply ignore - } - } - } -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.colorpicker; + +import static android.graphics.Color.alpha; +import static android.graphics.Color.argb; +import static android.graphics.Color.blue; +import static android.graphics.Color.green; +import static android.graphics.Color.parseColor; +import static android.graphics.Color.red; + +import android.content.Context; +import android.text.Editable; +import android.text.TextWatcher; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.google.android.material.slider.Slider; +import com.itsaky.androidide.common.R; +import com.itsaky.androidide.common.databinding.LayoutColorPickerBinding; +import com.itsaky.androidide.utils.Logger; + +/** + * A color picker view. + * + * @author Akash Yadav + */ +public class ColorPickerView extends LinearLayout { + + private int alpha; + private int red; + private int green; + private int blue; + + private OnPickListener mPickListener; + private final LayoutColorPickerBinding binding; + private final int DEFAULT_COLOR; + + private static final Logger LOG = Logger.instance ("ColorPickerView"); + + private final Slider.OnChangeListener mValueChangeListener = (slider, value, fromUser) -> { + + // If it's not from user, then the values have already been updated + if (fromUser) { + final var id = slider.getId (); + final var intVal = (int) value; + if (id == R.id.alpha_slider) { + alpha = intVal; + } else if (id == R.id.red_slider) { + red = intVal; + } else if (id == R.id.green_slider){ + green = intVal; + } else if (id == R.id.blue_slider) { + blue = intVal; + } + + updatePreview (); + } + }; + + public ColorPickerView (Context context) { + this (context, null); + } + + public ColorPickerView (Context context, @Nullable AttributeSet attrs) { + this (context, attrs, 0); + } + + public ColorPickerView (Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + this (context, attrs, defStyleAttr, 0); + } + + public ColorPickerView (Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super (context, attrs, defStyleAttr, defStyleRes); + + this.DEFAULT_COLOR = ContextCompat.getColor (context, R.color.color_picker_default_color); + + this.binding = LayoutColorPickerBinding.inflate (LayoutInflater.from (context)); + + removeAllViews (); + addView (binding.getRoot (), new LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + + init (); + } + + /** + * Set a listener that will be notified when the user picks a color. + * @param listener The listener to set. + */ + public void setOnPickListener (OnPickListener listener) { + this.mPickListener = listener; + } + + private void init () { + + setColor (DEFAULT_COLOR); + + this.binding.alphaSlider.addOnChangeListener (mValueChangeListener); + this.binding.redSlider.addOnChangeListener (mValueChangeListener); + this.binding.greenSlider.addOnChangeListener (mValueChangeListener); + this.binding.blueSlider.addOnChangeListener (mValueChangeListener); + + this.binding.hexField.getEditText ().addTextChangedListener (new ColorTextWatcher ()); + + this.binding.pick.setOnClickListener (v -> { + if (mPickListener != null) { + mPickListener.onPick (getColor (), getHexColor ()); + } + }); + } + + /** + * Get the hex color code of the current color value. + * @return The hex color code of the current color value. + */ + @NonNull + public String getHexColor () { + return "#" + Integer.toHexString (getColor ()); + } + + /** + * Set the color value of this picker to the given hex color code. + * @param hexCode The hex color code. + * @see #setColor(int) + */ + public void setColor (final String hexCode) { + setColor (parseColor (hexCode)); + } + + /** + * Set the color value for this color picker. + * this will in turn update value of the sliders + * and the color preview. + * @param color The new color value. + */ + public void setColor (int color) { + this.alpha = alpha (color); + this.red = red (color); + this.green = green(color); + this.blue = blue (color); + + this.binding.alphaSlider.setValue (alpha); + this.binding.redSlider.setValue (red); + this.binding.greenSlider.setValue (green); + this.binding.blueSlider.setValue (blue); + + updatePreview (); + } + + /** + * Get the int value of the color from the current + * values of alpha, red, green and blue color values. + * @return The int color value. + */ + public int getColor () { + return argb (alpha, red, green, blue); + } + + /** + * Call this method to update the preview of the current color. + */ + private void updatePreview () { + this.binding.colorPreview.setBackgroundColor (getColor ()); + } + + /** + * A listener that can be used to get notified when the user picks a color. + * + * @author Akash Yadav + */ + public interface OnPickListener { + + /** + * Called when the user picks a color using the 'Pick' button. + * @param color The integer value of the color. + * @param hexCode The hex color code of the color. + */ + void onPick (int color, String hexCode); + } + + /** + * A TextWatcher that is used to listen for text updates in the + * hex color input field. + */ + private class ColorTextWatcher implements TextWatcher { + + @Override + public void beforeTextChanged (CharSequence s, int start, int count, int after) { + } + + @Override + public void afterTextChanged (Editable s) { + } + + @Override + public void onTextChanged (CharSequence s, int start, int before, int count) { + try { + final var color = parseColor ("#" + s); + setColor (color); + } catch (Exception e) { + // May happen due to invalid color values + // Simply ignore + } + } + } +} diff --git a/common/src/main/res/drawable-anydpi/ic_hex_icon.xml b/common/src/main/res/drawable-anydpi/ic_hex_icon.xml index a8e3d3203..0fcd1071d 100644 --- a/common/src/main/res/drawable-anydpi/ic_hex_icon.xml +++ b/common/src/main/res/drawable-anydpi/ic_hex_icon.xml @@ -1,18 +1,18 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/common/src/main/res/layout/layout_color_picker.xml b/common/src/main/res/layout/layout_color_picker.xml index e2d1dc1fc..487f3abcd 100644 --- a/common/src/main/res/layout/layout_color_picker.xml +++ b/common/src/main/res/layout/layout_color_picker.xml @@ -1,135 +1,135 @@ - - - - - - - - - - - - - - -