UI enhancements

This commit is contained in:
Akash Yadav
2022-10-27 11:38:34 +05:30
parent 223e20ac8d
commit e764c7eac7
28 changed files with 1031 additions and 799 deletions
+1 -2
View File
@@ -109,12 +109,11 @@ dependencies {
implementation common.editor
implementation common.utilcode
implementation common.glide
implementation common.jsoup
implementation common.quickaction
implementation google.gson
implementation google.guava
implementation common.jsoup
// AndroidX
implementation androidx.splashscreen
implementation androidx.annotation
@@ -20,11 +20,11 @@
package com.itsaky.androidide;
import static com.blankj.utilcode.util.IntentUtils.getShareTextIntent;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.NO_OPENED_PROJECT;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.setLastOpenedProject;
import static com.itsaky.androidide.resources.R.color;
import static com.itsaky.androidide.resources.R.drawable;
import static com.itsaky.androidide.resources.R.string;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.NO_OPENED_PROJECT;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.setLastOpenedProject;
import static com.itsaky.toaster.ToastUtilsKt.toast;
import android.annotation.SuppressLint;
@@ -53,6 +53,7 @@ import android.view.ViewTreeObserver;
import android.widget.CheckBox;
import android.widget.LinearLayout;
import androidx.activity.OnBackPressedCallback;
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -65,7 +66,6 @@ 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 com.blankj.utilcode.util.FileUtils;
@@ -92,6 +92,7 @@ import com.itsaky.androidide.fragments.sheets.ProgressSheet;
import com.itsaky.androidide.fragments.sheets.TextSheetFragment;
import com.itsaky.androidide.handlers.EditorActivityLifecyclerObserver;
import com.itsaky.androidide.handlers.EditorEventListener;
import com.itsaky.androidide.handlers.EditorInsetsHandler;
import com.itsaky.androidide.interfaces.DiagnosticClickListener;
import com.itsaky.androidide.interfaces.EditorActivityProvider;
import com.itsaky.androidide.lookup.Lookup;
@@ -196,6 +197,24 @@ public class EditorActivity extends IDEActivity
LOG.info("Disconnected from Gradle build service...");
}
};
private final OnBackPressedCallback onBackPressedCallback =
new OnBackPressedCallback(true) {
@Override
public void handleOnBackPressed() {
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 (mEditorBottomSheet.getState() != BottomSheetBehavior.STATE_COLLAPSED) {
mEditorBottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
confirmProjectClose();
}
}
};
private boolean isFinishing = false;
@SuppressLint("RestrictedApi")
@@ -312,8 +331,8 @@ public class EditorActivity extends IDEActivity
.bottomSheet
.getPagerAdapter()
.findIndexOfFragmentByClass(SearchResultFragment.class);
if (index >= 0 && index < mBinding.bottomSheet.getTabs().getTabCount()) {
final var tab = mBinding.bottomSheet.getTabs().getTabAt(index);
if (index >= 0 && index < mBinding.bottomSheet.binding.tabs.getTabCount()) {
final var tab = mBinding.bottomSheet.binding.tabs.getTabAt(index);
if (tab != null) {
tab.select();
}
@@ -482,6 +501,14 @@ public class EditorActivity extends IDEActivity
return mBinding;
}
public FileTreeFragment getFileTreeFragment() {
if (mFileTreeFragment == null) {
mFileTreeFragment =
(FileTreeFragment) getSupportFragmentManager().findFragmentByTag(FileTreeFragment.TAG);
}
return mFileTreeFragment;
}
@Override
public void onTabSelected(@NonNull TabLayout.Tab tab) {
final var position = tab.getPosition();
@@ -532,10 +559,6 @@ public class EditorActivity extends IDEActivity
return false;
}
public void loadFragment(Fragment fragment) {
super.loadFragment(fragment, mBinding.editorFrameLayout.getId());
}
public ProgressSheet getProgressSheet(int msg) {
if (mSearchingProgress != null && mSearchingProgress.isShowing()) {
mSearchingProgress.dismiss();
@@ -852,25 +875,24 @@ public class EditorActivity extends IDEActivity
@Override
protected void onCreate(Bundle savedInstanceState) {
fitsSystemWindows = true;
super.onCreate(savedInstanceState);
EditorInsetsHandler.setupInsets(this);
ILanguageServerRegistry.getDefault().register(new JavaLanguageServer());
ILanguageServerRegistry.getDefault().register(new XMLLanguageServer());
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_PROJECT_PATH)) {
ProjectManager.INSTANCE.setProjectPath(savedInstanceState.getString(KEY_PROJECT_PATH));
}
getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
getLifecycle().addObserver(mLifecycleObserver);
setSupportActionBar(mBinding.editorToolbar);
mViewModel = new ViewModelProvider(this).get(EditorViewModel.class);
mFileTreeFragment = FileTreeFragment.newInstance();
mDaemonStatusFragment = new TextSheetFragment().setTextSelectable(true);
setupDrawerToggle();
loadFragment(mFileTreeFragment);
mBinding.tabs.addOnTabSelectedListener(this);
setupViews();
@@ -903,8 +925,9 @@ public class EditorActivity extends IDEActivity
protected void onPause() {
super.onPause();
if (mFileTreeFragment != null) {
mFileTreeFragment.saveTreeState();
final var frag = getFileTreeFragment();
if (frag != null) {
frag.saveTreeState();
}
}
@@ -918,8 +941,9 @@ public class EditorActivity extends IDEActivity
invalidateOptionsMenu();
try {
if (mFileTreeFragment != null) {
mFileTreeFragment.listProjectFiles();
final var frag = getFileTreeFragment();
if (frag != null) {
frag.listProjectFiles();
}
} catch (Throwable th) {
LOG.error("Failed to update files list", th);
@@ -933,21 +957,6 @@ public class EditorActivity extends IDEActivity
super.onSaveInstanceState(outState);
}
@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 (mEditorBottomSheet.getState() == BottomSheetBehavior.STATE_EXPANDED) {
mEditorBottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED);
} else {
confirmProjectClose();
}
}
@Override
protected void onDestroy() {
isFinishing = true;
@@ -1189,16 +1198,12 @@ public class EditorActivity extends IDEActivity
mEditorBottomSheet =
(EditorBottomSheetBehavior<? extends View>)
EditorBottomSheetBehavior.from(mBinding.bottomSheet);
mEditorBottomSheet.setBinding(mBinding.bottomSheet.getPager());
mEditorBottomSheet.setBinding(mBinding.bottomSheet.binding.pager);
mEditorBottomSheet.addBottomSheetCallback(
new BottomSheetBehavior.BottomSheetCallback() {
@Override
public void onStateChanged(@NonNull View bottomSheet, int newState) {
mBinding
.bottomSheet
.getHeaderContainer()
.setVisibility(
newState == BottomSheetBehavior.STATE_EXPANDED ? View.INVISIBLE : View.VISIBLE);
mBinding.bottomSheet.onStateChanged(newState);
if (newState == BottomSheetBehavior.STATE_EXPANDED) {
final var editor = getCurrentEditor();
if (editor != null && editor.getEditor() != null) {
@@ -1209,23 +1214,19 @@ public class EditorActivity extends IDEActivity
@Override
public void onSlide(@NonNull View bottomSheet, float slideOffset) {
mBinding.bottomSheet.getHeaderContainer().setAlpha(1f - slideOffset);
mBinding.bottomSheet.onSlide(slideOffset);
final var editorScale = 1 - (slideOffset * (1 - EDITOR_CONTAINER_SCALE_FACTOR));
mBinding.viewContainer.setScaleX(editorScale);
mBinding.viewContainer.setScaleY(editorScale);
}
});
mBinding
.bottomSheet
.getHeaderContainer()
.setOnClickListener(
v -> {
if (mEditorBottomSheet.getState() != BottomSheetBehavior.STATE_EXPANDED) {
mEditorBottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
mBinding.bottomSheet.binding.headerContainer.setOnClickListener(
v -> {
if (mEditorBottomSheet.getState() != BottomSheetBehavior.STATE_EXPANDED) {
mEditorBottomSheet.setState(BottomSheetBehavior.STATE_EXPANDED);
}
});
final var observer =
new ViewTreeObserver.OnGlobalLayoutListener() {
@@ -1,189 +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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide;
import static com.itsaky.androidide.R.id;
import static com.itsaky.androidide.resources.R.string;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.NO_OPENED_PROJECT;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.getAutoOpenProjects;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.getConfirmProjectOpen;
import static com.itsaky.androidide.preferences.internal.GeneralPreferencesKt.getLastOpenedProject;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.splashscreen.SplashScreen;
import androidx.core.text.HtmlCompat;
import com.blankj.utilcode.util.SizeUtils;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.itsaky.androidide.app.IDEActivity;
import com.itsaky.androidide.app.IDEApplication;
import com.itsaky.androidide.databinding.ActivityMainBinding;
import com.itsaky.androidide.fragments.MainFragment;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.utils.DialogUtils;
import com.itsaky.androidide.utils.Environment;
import com.itsaky.toaster.Toaster;
import com.itsaky.toaster.ToastUtilsKt;
import java.io.File;
public class MainActivity extends IDEActivity {
private ActivityMainBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (!IDEApplication.isAbiSupported()) {
showDeviceNotSupported();
return;
}
if (!checkToolsIsInstalled()) {
showDialogInstallJdkSdk();
} else {
openLastProject();
}
getSupportFragmentManager()
.beginTransaction()
.replace(id.container, new MainFragment(), MainFragment.TAG)
.commit();
}
@Override
protected void onStorageGranted() {}
@Override
protected void onStorageDenied() {
ToastUtilsKt.toast(string.msg_storage_denied, Toaster.Type.ERROR);
finishAffinity();
}
@Override
protected void preSetContentLayout() {
SplashScreen.installSplashScreen(this);
}
@Override
protected View bindLayout() {
binding = ActivityMainBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
private void showDialogInstallJdkSdk() {
final var dp24 = SizeUtils.dp2px(24);
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(string.title_warning);
final var view = new TextView(this);
view.setPaddingRelative(dp24, dp24, dp24, dp24);
view.setText(
HtmlCompat.fromHtml(
getString(string.msg_require_install_jdk_and_android_sdk),
HtmlCompat.FROM_HTML_MODE_COMPACT));
view.setMovementMethod(LinkMovementMethod.getInstance());
builder.setView(view);
builder.setCancelable(false);
builder.setPositiveButton(android.R.string.ok, (d, w) -> openTerminal());
builder.setNegativeButton(android.R.string.cancel, (d, w) -> finishAffinity());
builder.setNeutralButton(string.btn_docs, (d, w) -> getApp().openDocs());
builder.show();
}
private void openTerminal() {
startActivity(new Intent(this, TerminalActivity.class));
}
private void showDeviceNotSupported() {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(string.title_device_not_supported);
builder.setMessage(string.msg_device_not_supported);
builder.setCancelable(false);
builder.setPositiveButton(android.R.string.ok, (p1, p2) -> finishAffinity());
builder.create().show();
}
private void openLastProject() {
binding.getRoot().post(this::tryOpenLastProject);
}
private void tryOpenLastProject() {
if (!getAutoOpenProjects()) {
return;
}
final var openedProject = getLastOpenedProject();
if (NO_OPENED_PROJECT.equals(openedProject)) {
return;
}
if (TextUtils.isEmpty(openedProject)) {
getApp();
ToastUtilsKt.toast(string.msg_opened_project_does_not_exist, Toaster.Type.INFO);
return;
}
final var project = new File(openedProject);
if (!project.exists()) {
ToastUtilsKt.toast(string.msg_opened_project_does_not_exist, Toaster.Type.INFO);
return;
}
if (getConfirmProjectOpen()) {
askProjectOpenPermission(project);
return;
}
openProject(project);
}
private void askProjectOpenPermission(File root) {
final MaterialAlertDialogBuilder builder = DialogUtils.newMaterialDialogBuilder(this);
builder.setTitle(string.title_confirm_open_project);
builder.setMessage(getString(string.msg_confirm_open_project, root.getAbsolutePath()));
builder.setCancelable(false);
builder.setPositiveButton(string.yes, (d, w) -> openProject(root));
builder.setNegativeButton(string.no, null);
builder.show();
}
public void openProject(@NonNull File root) {
ProjectManager.INSTANCE.setProjectPath(root.getAbsolutePath());
startActivity(new Intent(this, EditorActivity.class));
}
private boolean checkToolsIsInstalled() {
return Environment.JAVA.exists() && Environment.ANDROID_HOME.exists();
}
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
}
}
+174
View File
@@ -0,0 +1,174 @@
/*
* This file is part of AndroidIDE.
*
*
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide
import android.R
import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import android.widget.TextView
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.text.HtmlCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import com.blankj.utilcode.util.SizeUtils
import com.itsaky.androidide.R.id
import com.itsaky.androidide.app.IDEActivity
import com.itsaky.androidide.app.IDEApplication
import com.itsaky.androidide.databinding.ActivityMainBinding
import com.itsaky.androidide.fragments.MainFragment
import com.itsaky.androidide.preferences.internal.NO_OPENED_PROJECT
import com.itsaky.androidide.preferences.internal.autoOpenProjects
import com.itsaky.androidide.preferences.internal.confirmProjectOpen
import com.itsaky.androidide.preferences.internal.lastOpenedProject
import com.itsaky.androidide.projects.ProjectManager.projectPath
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.Environment
import com.itsaky.toaster.Toaster.Type.ERROR
import com.itsaky.toaster.Toaster.Type.INFO
import com.itsaky.toaster.toast
import java.io.File
class MainActivity : IDEActivity() {
private var binding: ActivityMainBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (!IDEApplication.isAbiSupported()) {
showDeviceNotSupported()
return
}
if (!checkToolsIsInstalled()) {
showDialogInstallJdkSdk()
} else {
openLastProject()
}
supportFragmentManager
.beginTransaction()
.replace(id.container, MainFragment(), MainFragment.TAG)
.commit()
}
override fun onStorageGranted() {}
override fun onStorageDenied() {
toast(string.msg_storage_denied, ERROR)
finishAffinity()
}
override fun preSetContentLayout() {
installSplashScreen()
}
override fun bindLayout(): View {
binding = ActivityMainBinding.inflate(layoutInflater)
return binding!!.root
}
private fun showDialogInstallJdkSdk() {
val dp24 = SizeUtils.dp2px(24f)
val builder = DialogUtils.newMaterialDialogBuilder(this)
builder.setTitle(string.title_warning)
val view = TextView(this)
view.setPaddingRelative(dp24, dp24, dp24, dp24)
view.text =
HtmlCompat.fromHtml(
getString(string.msg_require_install_jdk_and_android_sdk),
HtmlCompat.FROM_HTML_MODE_COMPACT
)
view.movementMethod = LinkMovementMethod.getInstance()
builder.setView(view)
builder.setCancelable(false)
builder.setPositiveButton(R.string.ok) { _, _ -> openTerminal() }
builder.setNegativeButton(R.string.cancel) { _, _ -> finishAffinity() }
builder.setNeutralButton(string.btn_docs) { _, _ -> app.openDocs() }
builder.show()
}
private fun openTerminal() {
startActivity(Intent(this, TerminalActivity::class.java))
}
private fun showDeviceNotSupported() {
val builder = DialogUtils.newMaterialDialogBuilder(this)
builder.setTitle(string.title_device_not_supported)
builder.setMessage(string.msg_device_not_supported)
builder.setCancelable(false)
builder.setPositiveButton(R.string.ok) { _, _ -> finishAffinity() }
builder.create().show()
}
private fun openLastProject() {
binding!!.root.post { tryOpenLastProject() }
}
private fun tryOpenLastProject() {
if (!autoOpenProjects) {
return
}
val openedProject = lastOpenedProject
if (NO_OPENED_PROJECT == openedProject) {
return
}
if (TextUtils.isEmpty(openedProject)) {
app
toast(string.msg_opened_project_does_not_exist, INFO)
return
}
val project = File(openedProject)
if (!project.exists()) {
toast(string.msg_opened_project_does_not_exist, INFO)
return
}
if (confirmProjectOpen) {
askProjectOpenPermission(project)
return
}
openProject(project)
}
private fun askProjectOpenPermission(root: File) {
val builder = DialogUtils.newMaterialDialogBuilder(this)
builder.setTitle(string.title_confirm_open_project)
builder.setMessage(getString(string.msg_confirm_open_project, root.absolutePath))
builder.setCancelable(false)
builder.setPositiveButton(string.yes) { _, _ -> openProject(root) }
builder.setNegativeButton(string.no, null)
builder.show()
}
private fun openProject(root: File) {
projectPath = root.absolutePath
startActivity(Intent(this, EditorActivity::class.java))
}
private fun checkToolsIsInstalled(): Boolean {
return Environment.JAVA.exists() && Environment.ANDROID_HOME.exists()
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
}
@@ -1,106 +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 <https://www.gnu.org/licenses/>.
*
**************************************************************************************/
package com.itsaky.androidide.app;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
import com.itsaky.androidide.utils.ILogger;
public abstract class IDEActivity extends AppCompatActivity {
public static final int REQCODE_STORAGE = 1009;
protected static ILogger LOG = ILogger.newInstance("StudioActivity");
public void loadFragment(Fragment fragment, int id) {
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
transaction.replace(id, fragment);
transaction.commit();
}
public IDEApplication getApp() {
return (IDEApplication) getApplication();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
preSetContentLayout();
setContentView(bindLayout());
if (!isStoragePermissionGranted()) {
requestStorage();
return;
}
if (isStoragePermissionGranted()) {
onStorageAlreadyGranted();
} else {
onStorageDenied();
}
}
public boolean isStoragePermissionGranted() {
return ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED
&& ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
== PackageManager.PERMISSION_GRANTED;
}
protected void requestStorage() {
if (isStoragePermissionGranted()) {
onStorageGranted();
return;
}
ActivityCompat.requestPermissions(
this,
new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE
},
REQCODE_STORAGE);
}
protected void onStorageGranted() {}
@Override
public void onRequestPermissionsResult(
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQCODE_STORAGE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
onStorageGranted();
else onStorageDenied();
}
}
protected void onStorageAlreadyGranted() {}
protected void onStorageDenied() {}
protected void preSetContentLayout() {}
protected abstract View bindLayout();
}
+101
View File
@@ -0,0 +1,101 @@
/**
* ********************************************************************************** This file is
* part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with AndroidIDE. If not,
* see <https:></https:>//www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.app
import android.Manifest.permission
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import com.itsaky.androidide.utils.ILogger
abstract class IDEActivity : AppCompatActivity() {
fun loadFragment(fragment: Fragment?, id: Int) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(id, fragment!!)
transaction.commit()
}
val app: IDEApplication
get() = application as IDEApplication
@JvmField
var fitsSystemWindows = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, !fitsSystemWindows)
preSetContentLayout()
setContentView(bindLayout())
if (!isStoragePermissionGranted) {
requestStorage()
return
}
if (isStoragePermissionGranted) {
onStorageAlreadyGranted()
} else {
onStorageDenied()
}
}
val isStoragePermissionGranted: Boolean
get() =
(ContextCompat.checkSelfPermission(this, permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED)
protected fun requestStorage() {
if (isStoragePermissionGranted) {
onStorageGranted()
return
}
ActivityCompat.requestPermissions(
this,
arrayOf(permission.WRITE_EXTERNAL_STORAGE, permission.READ_EXTERNAL_STORAGE),
REQCODE_STORAGE
)
}
protected open fun onStorageGranted() {}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQCODE_STORAGE) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)
onStorageGranted()
else onStorageDenied()
}
}
protected open fun onStorageAlreadyGranted() {}
protected open fun onStorageDenied() {}
protected open fun preSetContentLayout() {}
protected abstract fun bindLayout(): View
companion object {
const val REQCODE_STORAGE = 1009
protected var LOG = ILogger.newInstance("StudioActivity")
}
}
@@ -23,6 +23,6 @@ import com.unnamed.b.atv.model.TreeNode
internal data class FileContextMenuItemClickEvent(val option: SheetOption) : Event()
internal data class ExpandTreeNodeRequestEvent(val node: TreeNode) : Event()
data class ExpandTreeNodeRequestEvent(val node: TreeNode) : Event()
internal class ListProjectFilesRequestEvent : Event()
class ListProjectFilesRequestEvent : Event()
@@ -1,328 +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 <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.fragments;
import static com.unnamed.b.atv.view.AndroidTreeView.NODES_PATH_SEPARATOR;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.transition.ChangeBounds;
import androidx.transition.TransitionManager;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.itsaky.androidide.resources.R;
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder;
import com.itsaky.androidide.databinding.LayoutEditorFileTreeBinding;
import com.itsaky.androidide.eventbus.events.filetree.FileClickEvent;
import com.itsaky.androidide.eventbus.events.filetree.FileLongClickEvent;
import com.itsaky.androidide.events.ExpandTreeNodeRequestEvent;
import com.itsaky.androidide.events.ListProjectFilesRequestEvent;
import com.itsaky.androidide.projects.ProjectManager;
import com.itsaky.androidide.tasks.TaskExecutor;
import com.itsaky.androidide.tasks.callables.FileTreeCallable;
import com.itsaky.androidide.utils.ILogger;
import com.unnamed.b.atv.model.TreeNode;
import com.unnamed.b.atv.view.AndroidTreeView;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.File;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class FileTreeFragment extends BottomSheetDialogFragment
implements TreeNode.TreeNodeClickListener, TreeNode.TreeNodeLongClickListener {
private static final String KEY_STORED_TREE_STATE = "fileTree_state";
private static final ILogger LOG = ILogger.newInstance("FileTreeFragment");
private LayoutEditorFileTreeBinding binding;
private AndroidTreeView mFileTreeView;
private TreeNode mRoot;
private String mTreeState;
public FileTreeFragment() {}
@NonNull
public static FileTreeFragment newInstance() {
return new FileTreeFragment();
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = LayoutEditorFileTreeBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_TREE_STATE)) {
mTreeState = savedInstanceState.getString(KEY_STORED_TREE_STATE, null);
}
listProjectFiles();
}
@Override
public void onStart() {
super.onStart();
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this);
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
saveTreeState();
outState.putString(KEY_STORED_TREE_STATE, mTreeState);
}
@Override
public void onStop() {
super.onStop();
EventBus.getDefault().unregister(this);
}
@Override
public void onDestroyView() {
super.onDestroyView();
binding = null;
mFileTreeView = null;
}
public void saveTreeState() {
if (mFileTreeView != null) {
mTreeState = mFileTreeView.getSaveState();
} else {
LOG.error("Unable to save tree state. TreeView is null.");
mTreeState = null;
}
}
@Override
public void onClick(TreeNode node, Object p2) {
final File file = (File) p2;
if (!file.exists()) {
return;
}
if (file.isDirectory()) {
if (node.isExpanded()) {
collapseNode(node);
} else {
setLoading(node);
listNode(node, () -> expandNode(node));
}
}
final var event = new FileClickEvent(file);
event.put(Context.class, requireContext());
EventBus.getDefault().post(event);
}
public void collapseNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.collapseNode(node);
updateChevron(node);
}
private void updateChevron(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).updateChevron(node.isExpanded());
}
}
public void expandNode(TreeNode node) {
if (mFileTreeView == null) {
return;
}
TransitionManager.beginDelayedTransition(binding.getRoot(), new ChangeBounds());
mFileTreeView.expandNode(node);
updateChevron(node);
}
private void setLoading(@NonNull TreeNode node) {
if (node.getViewHolder() instanceof FileTreeViewHolder) {
((FileTreeViewHolder) node.getViewHolder()).setLoading(true);
}
}
private void listNode(@NonNull TreeNode node, Runnable whenDone) {
if (whenDone == null) {
whenDone = () -> {};
}
node.getChildren().clear();
node.setExpanded(false);
final var finalWhenDone = whenDone;
TaskExecutor.executeAsync(
() -> {
listFilesForNode(node.getValue().listFiles(), node);
TreeNode temp = node;
while (temp.size() == 1) {
temp = temp.childAt(0);
if (!temp.getValue().isDirectory()) {
break;
}
listFilesForNode(temp.getValue().listFiles(), temp);
temp.setExpanded(true);
}
return null;
},
__ -> finalWhenDone.run());
}
private void listFilesForNode(File[] files, TreeNode parent) {
Arrays.sort(files, new FileTreeCallable.SortFileName());
Arrays.sort(files, new FileTreeCallable.SortFolder());
for (File file : files) {
TreeNode node = new TreeNode(file);
node.setViewHolder(new FileTreeViewHolder(getContext()));
parent.addChild(node);
}
}
@Override
public boolean onLongClick(TreeNode node, Object value) {
final var event = new FileLongClickEvent((File) value);
event.put(Context.class, requireContext());
event.put(TreeNode.class, node);
EventBus.getDefault().post(event);
return true;
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onGetListFilesRequested(ListProjectFilesRequestEvent event) {
if (!isVisible() || getContext() == null) {
return;
}
listProjectFiles();
}
@SuppressWarnings("unused")
@Subscribe(threadMode = ThreadMode.MAIN)
public void onGetExpandTreeNodeRequest(ExpandTreeNodeRequestEvent event) {
if (!isVisible() || getContext() == null) {
return;
} else {
event.getNode();
}
expandNode(event.getNode());
}
public void listProjectFiles() {
if (binding == null) {
// Fragment has been destroyed
return;
}
final var projectDirPath = ProjectManager.INSTANCE.getProjectDirPath();
final var projectDir = new File(projectDirPath);
mRoot = TreeNode.root(projectDir);
mRoot.setViewHolder(new FileTreeViewHolder(getContext()));
binding.filetreeHorizontalScrollView.setVisibility(View.GONE);
binding.fileTreeLoadingProgress.setVisibility(View.VISIBLE);
TaskExecutor.executeAsync(
new FileTreeCallable(getContext(), mRoot, projectDir),
(result) -> {
if (binding == null) {
// Fragment has been destroyed
return;
}
binding.filetreeHorizontalScrollView.setVisibility(View.VISIBLE);
binding.fileTreeLoadingProgress.setVisibility(View.GONE);
AndroidTreeView tree = createTreeView(mRoot);
if (tree != null) {
tree.setUseAutoToggle(false);
tree.setDefaultNodeClickListener(FileTreeFragment.this);
tree.setDefaultNodeLongClickListener(FileTreeFragment.this);
binding.filetreeHorizontalScrollView.removeAllViews();
final var view = tree.getView();
binding.filetreeHorizontalScrollView.addView(view);
view.post(this::tryRestoreState);
}
});
}
public AndroidTreeView createTreeView(TreeNode node) {
Context ctx = null;
if (getActivity() != null) {
ctx = getActivity();
} else if (getContext() != null) {
ctx = getContext();
}
if (ctx == null) {
return null;
}
return mFileTreeView = new AndroidTreeView(ctx, node, R.drawable.bg_ripple);
}
private void tryRestoreState() {
tryRestoreState(mTreeState);
}
private void tryRestoreState(String state) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
mFileTreeView.collapseAll();
final var openNodesArray = state.split(NODES_PATH_SEPARATOR);
final var openNodes = new HashSet<>(Arrays.asList(openNodesArray));
restoreNodeState(mRoot, openNodes);
}
}
private void restoreNodeState(@NonNull TreeNode root, Set<String> openNodes) {
final var children = root.getChildren();
for (int i = 0, childrenSize = children.size(); i < childrenSize; i++) {
final var node = children.get(i);
if (openNodes.contains(node.getPath())) {
listNode(
node,
() -> {
expandNode(node);
restoreNodeState(node, openNodes);
});
}
}
}
}
@@ -0,0 +1,295 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*
*/
package com.itsaky.androidide.fragments
import android.content.Context
import android.os.Bundle
import android.text.TextUtils
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.WindowInsetsCompat.Type.statusBars
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.fragment.app.FragmentContainerView
import androidx.transition.ChangeBounds
import androidx.transition.TransitionManager
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.itsaky.androidide.adapters.viewholders.FileTreeViewHolder
import com.itsaky.androidide.databinding.LayoutEditorFileTreeBinding
import com.itsaky.androidide.eventbus.events.filetree.FileClickEvent
import com.itsaky.androidide.eventbus.events.filetree.FileLongClickEvent
import com.itsaky.androidide.events.ExpandTreeNodeRequestEvent
import com.itsaky.androidide.events.ListProjectFilesRequestEvent
import com.itsaky.androidide.projects.ProjectManager.getProjectDirPath
import com.itsaky.androidide.resources.R.drawable
import com.itsaky.androidide.tasks.TaskExecutor.executeAsync
import com.itsaky.androidide.tasks.callables.FileTreeCallable
import com.itsaky.androidide.tasks.callables.FileTreeCallable.SortFileName
import com.itsaky.androidide.tasks.callables.FileTreeCallable.SortFolder
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.doOnApplyWindowInsets
import com.unnamed.b.atv.model.TreeNode
import com.unnamed.b.atv.model.TreeNode.TreeNodeClickListener
import com.unnamed.b.atv.model.TreeNode.TreeNodeLongClickListener
import com.unnamed.b.atv.view.AndroidTreeView
import java.io.File
import java.util.Arrays
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode.MAIN
class FileTreeFragment :
BottomSheetDialogFragment(), TreeNodeClickListener, TreeNodeLongClickListener {
private var binding: LayoutEditorFileTreeBinding? = null
private var mFileTreeView: AndroidTreeView? = null
private var mRoot: TreeNode? = null
private var mTreeState: String? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = LayoutEditorFileTreeBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState != null && savedInstanceState.containsKey(KEY_STORED_TREE_STATE)) {
mTreeState = savedInstanceState.getString(KEY_STORED_TREE_STATE, null)
}
listProjectFiles()
}
override fun onStart() {
super.onStart()
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this)
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
saveTreeState()
outState.putString(KEY_STORED_TREE_STATE, mTreeState)
}
override fun onStop() {
super.onStop()
EventBus.getDefault().unregister(this)
}
override fun onDestroyView() {
super.onDestroyView()
binding = null
mFileTreeView = null
}
fun saveTreeState() {
mTreeState =
if (mFileTreeView != null) {
mFileTreeView!!.saveState
} else {
LOG.error("Unable to save tree state. TreeView is null.")
null
}
}
override fun onClick(node: TreeNode, p2: Any) {
val file = p2 as File
if (!file.exists()) {
return
}
if (file.isDirectory) {
if (node.isExpanded) {
collapseNode(node)
} else {
setLoading(node)
listNode(node) { expandNode(node) }
}
}
val event = FileClickEvent(file)
event.put(Context::class.java, requireContext())
EventBus.getDefault().post(event)
}
private fun collapseNode(node: TreeNode) {
if (mFileTreeView == null) {
return
}
TransitionManager.beginDelayedTransition(binding!!.root, ChangeBounds())
mFileTreeView!!.collapseNode(node)
updateChevron(node)
}
private fun updateChevron(node: TreeNode) {
if (node.viewHolder is FileTreeViewHolder) {
(node.viewHolder as FileTreeViewHolder).updateChevron(node.isExpanded)
}
}
private fun expandNode(node: TreeNode) {
if (mFileTreeView == null) {
return
}
TransitionManager.beginDelayedTransition(binding!!.root, ChangeBounds())
mFileTreeView!!.expandNode(node)
updateChevron(node)
}
private fun setLoading(node: TreeNode) {
if (node.viewHolder is FileTreeViewHolder) {
(node.viewHolder as FileTreeViewHolder).setLoading(true)
}
}
private fun listNode(node: TreeNode, whenDone: Runnable) {
node.children.clear()
node.isExpanded = false
executeAsync({
listFilesForNode(node.value.listFiles() ?: return@executeAsync null, node)
var temp = node
while (temp.size() == 1) {
temp = temp.childAt(0)
if (!temp.value.isDirectory) {
break
}
listFilesForNode(temp.value.listFiles() ?: continue, temp)
temp.isExpanded = true
}
null
}) {
whenDone.run()
}
}
private fun listFilesForNode(files: Array<File>, parent: TreeNode) {
Arrays.sort(files, SortFileName())
Arrays.sort(files, SortFolder())
for (file in files) {
val node = TreeNode(file)
node.viewHolder = FileTreeViewHolder(context)
parent.addChild(node)
}
}
override fun onLongClick(node: TreeNode, value: Any): Boolean {
val event = FileLongClickEvent((value as File))
event.put(Context::class.java, requireContext())
event.put(TreeNode::class.java, node)
EventBus.getDefault().post(event)
return true
}
@Suppress("unused", "UNUSED_PARAMETER")
@Subscribe(threadMode = MAIN)
fun onGetListFilesRequested(event: ListProjectFilesRequestEvent?) {
if (!isVisible || context == null) {
return
}
listProjectFiles()
}
@Suppress("unused")
@Subscribe(threadMode = MAIN)
fun onGetExpandTreeNodeRequest(event: ExpandTreeNodeRequestEvent) {
if (!isVisible || context == null) {
return
} else {
event.node
}
expandNode(event.node)
}
fun listProjectFiles() {
if (binding == null) {
// Fragment has been destroyed
return
}
val projectDirPath = getProjectDirPath()
val projectDir = File(projectDirPath)
mRoot = TreeNode.root(projectDir)
mRoot?.viewHolder = FileTreeViewHolder(context)
binding!!.filetreeHorizontalScrollView.visibility = View.GONE
binding!!.fileTreeLoadingProgress.visibility = View.VISIBLE
executeAsync(FileTreeCallable(context, mRoot, projectDir)) {
if (binding == null) {
// Fragment has been destroyed
return@executeAsync
}
binding!!.filetreeHorizontalScrollView.visibility = View.VISIBLE
binding!!.fileTreeLoadingProgress.visibility = View.GONE
val tree = createTreeView(mRoot)
if (tree != null) {
tree.setUseAutoToggle(false)
tree.setDefaultNodeClickListener(this@FileTreeFragment)
tree.setDefaultNodeLongClickListener(this@FileTreeFragment)
binding!!.filetreeHorizontalScrollView.removeAllViews()
val view = tree.view
binding!!.filetreeHorizontalScrollView.addView(view)
view.post { tryRestoreState() }
}
}
}
private fun createTreeView(node: TreeNode?): AndroidTreeView? {
return if (context == null) {
null
} else AndroidTreeView(context, node, drawable.bg_ripple).also { mFileTreeView = it }
}
private fun tryRestoreState(state: String? = mTreeState) {
if (!TextUtils.isEmpty(state) && mFileTreeView != null) {
mFileTreeView!!.collapseAll()
val openNodes =
state!!.split(AndroidTreeView.NODES_PATH_SEPARATOR.toRegex()).dropLastWhile { it.isEmpty() }
restoreNodeState(mRoot!!, HashSet(openNodes))
}
}
private fun restoreNodeState(root: TreeNode, openNodes: Set<String>) {
val children = root.children
var i = 0
val childrenSize = children.size
while (i < childrenSize) {
val node = children[i]
if (openNodes.contains(node.path)) {
listNode(node) {
expandNode(node)
restoreNodeState(node, openNodes)
}
}
i++
}
}
companion object {
// Should be same as defined in layout/activity_editor.xml
const val TAG = "editor.fileTree"
private const val KEY_STORED_TREE_STATE = "fileTree_state"
private val LOG = ILogger.newInstance("FileTreeFragment")
@JvmStatic
fun newInstance(): FileTreeFragment {
return FileTreeFragment()
}
}
}
@@ -17,16 +17,25 @@
package com.itsaky.androidide.fragments
import android.app.Dialog
import android.graphics.Color
import android.os.Bundle
import android.os.Handler
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat.Type.navigationBars
import androidx.core.view.WindowInsetsCompat.Type.statusBars
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMargins
import androidx.core.view.updatePadding
import androidx.fragment.app.viewModels
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionManager
import com.blankj.utilcode.util.ThreadUtils
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.R.string
@@ -44,6 +53,8 @@ import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.tooling.api.model.GradleTask
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.SingleTextWatcher
import com.itsaky.androidide.utils.doOnApplyWindowInsets
import com.itsaky.androidide.utils.updateSystemBarColors
import com.itsaky.androidide.viewmodel.RunTasksViewModel
import com.itsaky.toaster.toastInfo
@@ -73,6 +84,32 @@ class RunTasksDialogFragment : BottomSheetDialogFragment() {
private const val SEARCH_DELAY = 500L
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
return object : BottomSheetDialog(requireContext(), theme) {
override fun onAttachedToWindow() {
super.onAttachedToWindow()
window?.apply {
WindowCompat.setDecorFitsSystemWindows(this, false)
updateSystemBarColors(navigationBarColor = Color.TRANSPARENT)
}
findViewById<View>(com.google.android.material.R.id.container)?.apply {
doOnApplyWindowInsets { view, insets, paddings, margins ->
insets.getInsets(statusBars() or navigationBars()).apply {
view.updateLayoutParams<MarginLayoutParams> {
updateMargins(top = margins.top + top)
}
run.categories.apply {
updatePadding(bottom = bottom)
clipToPadding = false
clipChildren = false
}
}
}
}
}
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@@ -0,0 +1,67 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.handlers
import android.graphics.Color
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import com.blankj.utilcode.util.SizeUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.itsaky.androidide.EditorActivity
import com.itsaky.androidide.R
import com.itsaky.androidide.utils.doOnApplyWindowInsets
import com.itsaky.androidide.utils.updateSystemBarColors
/** @author Akash Yadav */
object EditorInsetsHandler {
@JvmStatic
fun setupInsets(activity: EditorActivity) {
activity.window.updateSystemBarColors(
statusBarColor = Color.TRANSPARENT,
navigationBarColor = ContextCompat.getColor(activity, R.color.primaryDarkColor)
)
val binding = activity.binding
binding.root.doOnApplyWindowInsets { view, insets, _, _ ->
val appBarHeight = binding.editorAppBarLayout.height
insets.getInsets(WindowInsetsCompat.Type.statusBars()).apply {
binding.editorAppBarLayout.updatePadding(top = top)
binding.endNav.getChildAt(0).updatePadding(top = top + SizeUtils.dp2px(16f))
val behaviour = BottomSheetBehavior.from(binding.bottomSheet)
behaviour.isFitToContents = false
behaviour.expandedOffset = appBarHeight + SizeUtils.dp2px(1f)
}
insets.getInsets(WindowInsetsCompat.Type.navigationBars()).apply {
binding.root.updatePadding(bottom = bottom)
binding.bottomSheet.binding.spaceBottom.updateLayoutParams<LayoutParams> {
height = bottom + appBarHeight + SizeUtils.dp2px(1f)
}
}
(view as? ViewGroup)?.forEach {
ViewCompat.dispatchApplyWindowInsets(it, insets)
}
}
}
}
@@ -27,11 +27,9 @@ 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<V extends View> extends BottomSheetBehavior<V> {
private static final ILogger LOG = ILogger.newInstance("EditorBottomSheetBehavior");
private View pager;
public EditorBottomSheetBehavior(Context context, AttributeSet attributeSet) {
@@ -39,7 +37,7 @@ public class EditorBottomSheetBehavior<V extends View> extends BottomSheetBehavi
}
public void setBinding(View pager) {
this.pager= pager;
this.pager = pager;
}
@Override
@@ -0,0 +1,31 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.utils
import android.view.View
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
/**
* A [BottomSheetCallback] which allows you to override any single method.
*
* @author Akash Yadav
*/
open class SingleBottomSheetCallback : BottomSheetCallback() {
override fun onStateChanged(bottomSheet: View, newState: Int) {}
override fun onSlide(bottomSheet: View, slideOffset: Float) {}
}
@@ -25,21 +25,19 @@ import android.util.AttributeSet
import android.view.Gravity
import android.view.LayoutInflater
import android.widget.RelativeLayout
import android.widget.ViewFlipper
import androidx.annotation.GravityInt
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.transition.Slide
import androidx.transition.TransitionManager
import androidx.viewpager2.widget.ViewPager2
import com.blankj.utilcode.util.KeyboardUtils
import com.blankj.utilcode.util.ThreadUtils.runOnUiThread
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayout.OnTabSelectedListener
import com.google.android.material.tabs.TabLayout.Tab
import com.google.android.material.tabs.TabLayoutMediator
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.DiagnosticsAdapter
import com.itsaky.androidide.adapters.EditorBottomSheetTabAdapter
import com.itsaky.androidide.adapters.SearchListAdapter
@@ -47,6 +45,7 @@ import com.itsaky.androidide.databinding.LayoutBottomActionBinding
import com.itsaky.androidide.databinding.LayoutEditorBottomSheetBinding
import com.itsaky.androidide.fragments.ShareableOutputFragment
import com.itsaky.androidide.models.LogLine
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.tasks.TaskExecutor.CallbackWithError
import com.itsaky.androidide.tasks.TaskExecutor.executeAsync
import com.itsaky.androidide.tasks.TaskExecutor.executeAsyncProvideError
@@ -78,21 +77,17 @@ constructor(
defStyleRes: Int = 0,
) : RelativeLayout(context, attrs, defStyleAttr, defStyleRes) {
private var binding: LayoutEditorBottomSheetBinding
private var action: LayoutBottomActionBinding
private var symbolInput: SymbolInputView
private val collapsedHeight: Float by lazy {
val localContext = getContext() ?: return@lazy 0f
localContext.resources.getDimension(R.dimen.editor_sheet_collapsed_height)
}
@JvmField var binding: LayoutEditorBottomSheetBinding
val pagerAdapter: EditorBottomSheetTabAdapter
val tabs: TabLayout
get() = binding.tabs
val pager: ViewPager2
get() = binding.pager
val headerContainer: ViewFlipper
get() = binding.headerContainer
private val log = ILogger.newInstance("EditorBottomSheet")
companion object {
@@ -102,6 +97,7 @@ constructor(
}
private fun initialize(context: FragmentActivity) {
val mediator =
TabLayoutMediator(binding.tabs, binding.pager, true, true) { tab, position ->
tab.text = pagerAdapter.getTitle(position)
@@ -138,6 +134,7 @@ constructor(
}
val filename = fragment.getFilename()
@Suppress("DEPRECATION")
val progress = ProgressDialog.show(context, null, context.getString(string.please_wait))
executeAsync(fragment::getContent) {
@@ -178,6 +175,17 @@ constructor(
initialize(context)
}
fun onStateChanged(newState: Int) {
// binding.headerContainer.visibility =
// if (newState == BottomSheetBehavior.STATE_EXPANDED) INVISIBLE else VISIBLE
}
fun onSlide(offset: Float) {
if (offset >= 0.5f) {
updateCollapsedHeight(((0.5f - offset) + 0.5f) * 2f)
} else updateCollapsedHeight(1f)
}
fun showChild(index: Int) {
binding.headerContainer.displayedChild = index
}
@@ -243,6 +251,12 @@ constructor(
}
}
private fun updateCollapsedHeight(offset: Float) {
binding.headerContainer.updateLayoutParams<android.view.ViewGroup.LayoutParams> {
height = (collapsedHeight * offset).toInt()
}
}
private fun shareFile(file: File) {
shareFile(context, file, "text/plain")
}
+12 -7
View File
@@ -17,13 +17,13 @@
android:id="@+id/editor_appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/editor_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor"
android:elevation="10dp" />
<com.google.android.material.progressindicator.LinearProgressIndicator
@@ -42,7 +42,7 @@
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="@dimen/file_tab_height"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor"
app:tabGravity="start"
app:tabIndicatorAnimationMode="elastic"
@@ -113,7 +113,7 @@
android:layout_height="match_parent"
app:behavior_hideable="false"
app:behavior_peekHeight="@dimen/editor_sheet_peek_height"
app:layout_behavior="com.itsaky.androidide.utils.EditorBottomSheetBehavior"/>
app:layout_behavior="com.itsaky.androidide.utils.EditorBottomSheetBehavior" />
<include
android:id="@+id/diagnosticInfo"
@@ -132,14 +132,19 @@
app:menu="@menu/menu_editor_nav" />
<com.google.android.material.navigation.NavigationView
android:id="@+id/endNav"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end">
android:layout_gravity="end"
android:fitsSystemWindows="false">
<FrameLayout
android:id="@+id/editor_frameLayout"
<!-- When changing android:tag here, also change it in FileTreeFragment.kt -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/file_tree"
android:name="com.itsaky.androidide.fragments.FileTreeFragment"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
android:tag="editor.fileTree" />
</com.google.android.material.navigation.NavigationView>
+44 -42
View File
@@ -1,48 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical"
tools:ignore="RtlCompat">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?attr/colorPrimaryDark"
android:gravity="center"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/toolbar_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:clickable="false"
android:focusable="false"
android:gravity="center"
android:text="@string/app_name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/primaryTextColor"
android:textSize="24sp"
android:textStyle="bold" />
</RelativeLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
android:orientation="vertical"
tools:ignore="RtlCompat">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="?attr/colorPrimaryDark"
android:gravity="center"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/toolbar_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:clickable="false"
android:focusable="false"
android:gravity="center"
android:text="@string/app_name"
android:textAppearance="?android:attr/textAppearanceLarge"
android:textColor="@color/primaryTextColor"
android:textSize="24sp"
android:textStyle="bold" />
</RelativeLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:name="com.itsaky.androidide.fragments.MainFragment" />
</LinearLayout>
@@ -16,29 +16,30 @@
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ViewFlipper
android:id="@+id/header_container"
android:layout_width="match_parent"
android:layout_height="56dp">
android:layout_height="@dimen/editor_sheet_collapsed_height"
android:background="@color/primaryDarkColor">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_marginBottom="12dp"
android:background="@android:color/black"
android:orientation="vertical">
android:orientation="vertical"
tools:ignore="UselessParent">
<TextView
android:id="@+id/statusText"
android:layout_width="match_parent"
android:layout_height="20dp"
android:gravity="center"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:singleLine="true"
android:textColor="@color/primaryTextColor"
android:textSize="14sp" />
@@ -47,6 +48,7 @@
android:layout_width="match_parent"
android:layout_height="18dp"
android:gravity="center"
android:maxLines="1"
android:text="@string/msg_swipe_up"
android:textColor="@color/secondaryTextColor"
android:textSize="11sp" />
@@ -58,10 +60,9 @@
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="36dp"
android:layout_height="wrap_content"
android:layout_below="@id/header_container"
android:background="@android:color/black"
app:tabGravity="fill"
android:backgroundTint="@color/primaryDarkColor"
app:tabIndicatorAnimationMode="elastic"
app:tabIndicatorColor="@color/secondaryColor"
app:tabIndicatorGravity="bottom"
@@ -70,10 +71,18 @@
app:tabSelectedTextColor="@color/primaryTextColor"
app:tabTextAppearance="@style/AppTheme.TabLayout.Title" />
<androidx.legacy.widget.Space
android:id="@+id/space_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
tools:layout_height="56dp" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/space_bottom"
android:layout_below="@id/tabs"
android:background="@color/content_background" />
@@ -81,8 +90,8 @@
android:id="@+id/clear_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/space_bottom"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true"
android:layout_margin="16dp"
android:src="@drawable/ic_close"
app:backgroundTint="@color/secondaryColor" />
@@ -93,8 +102,12 @@
android:layout_height="wrap_content"
android:layout_above="@id/clear_fab"
android:layout_alignParentEnd="true"
android:layout_margin="16dp"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:src="@drawable/ic_share"
app:backgroundTint="@color/secondaryColor" />
</RelativeLayout>
@@ -28,7 +28,6 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.4"
app:srcCompat="@drawable/ic_android"
app:tint="@color/green_500" />
+7 -14
View File
@@ -15,25 +15,22 @@
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="16dp">
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/search_input"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.Dense"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:hint="@string/hint_search_tasks"
android:theme="@style/AppTheme.TextInputLayout"
app:boxBackgroundColor="@color/content_background"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
app:boxBackgroundColor="@color/content_background">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
@@ -43,17 +40,13 @@
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/categories"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:descendantFocusability="blocksDescendants"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/search_input"
tools:itemCount="3"
tools:listitem="@layout/layout_run_tasks_category" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.LinearLayoutCompat>
@@ -15,55 +15,51 @@
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:nestedScrollingEnabled="true">
<TextView
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:gravity="center_vertical"
android:text="@string/title_run_tasks"
android:textAppearance="@style/TextAppearance.Material3.ActionBar.Title"
app:layout_constraintBottom_toBottomOf="@id/exec"
app:layout_constraintEnd_toStartOf="@id/exec"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/exec" />
<com.google.android.material.button.MaterialButton
android:id="@+id/exec"
style="@style/Widget.Material3.Button.IconButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:tooltipText="@string/run"
app:icon="@drawable/ic_run"
app:iconTint="@color/green_500"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:iconTint="@color/green_500" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@+id/exec"
android:layout_alignBottom="@+id/exec"
android:layout_marginStart="16dp"
android:layout_marginTop="-8dp"
android:layout_marginBottom="1dp"
android:gravity="center_vertical"
android:text="@string/title_run_tasks"
android:textAppearance="@style/TextAppearance.Material3.ActionBar.Title" />
<View
android:id="@+id/separator"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/primaryLightColor"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/exec" />
android:layout_below="@id/exec"
android:background="@color/primaryLightColor" />
<ViewFlipper
android:id="@+id/flipper"
android:layout_width="0dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/separator">
android:layout_below="@id/separator">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/loading_tasks"
@@ -87,5 +83,4 @@
</ViewFlipper>
</androidx.constraintlayout.widget.ConstraintLayout>
</RelativeLayout>
+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ This file is part of AndroidIDE.
~
~ AndroidIDE is free software: you can redistribute it and/or modify
~ it under the terms of the GNU General Public License as published by
~ the Free Software Foundation, either version 3 of the License, or
~ (at your option) any later version.
~
~ AndroidIDE is distributed in the hope that it will be useful,
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
~ GNU General Public License for more details.
~
~ You should have received a copy of the GNU General Public License
~ along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<dimen name="editor_sheet_collapsed_height">56dp</dimen>
</resources>
@@ -0,0 +1,71 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.utils
import android.view.View
import androidx.core.graphics.Insets
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.marginBottom
import androidx.core.view.marginEnd
import androidx.core.view.marginStart
import androidx.core.view.marginTop
/**
* A helper method wrapping [ViewCompat.setOnApplyWindowInsetsListener], additionally providing the
* initial padding and margins of the view.
*
* This method does not consume any window insets, allowing any and all children to receive the same
* insets.
*
* This is a `set` listener, so only the last [windowInsetsListener] applied by
* [doOnApplyWindowInsets] will be ran.
*/
fun View.doOnApplyWindowInsets(
windowInsetsListener:
(
insetView: View,
windowInsets: WindowInsetsCompat,
initialPadding: Insets,
initialMargins: Insets
) -> Unit
) {
val initialPadding = Insets.of(paddingStart, paddingTop, paddingEnd, paddingBottom)
val initialMargins = Insets.of(marginStart, marginTop, marginEnd, marginBottom)
ViewCompat.setOnApplyWindowInsetsListener(this) { insetView, windowInsets ->
windowInsets.also {
windowInsetsListener(insetView, windowInsets, initialPadding, initialMargins)
}
}
addOnAttachStateChangeListener(
object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.requestApplyInsets()
}
override fun onViewDetachedFromWindow(v: View) = Unit
}
)
// If the view is already attached, immediately request insets be applied.
if (isAttachedToWindow) {
requestApplyInsets()
}
}
@@ -0,0 +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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.utils
import android.view.Window
import android.view.WindowManager
@JvmOverloads
fun Window.updateSystemBarColors(statusBarColor: Int = -1, navigationBarColor: Int = -1) {
@Suppress("DEPRECATION") clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
if (statusBarColor != -1) {
this.statusBarColor = statusBarColor
}
if (navigationBarColor != -1) {
this.navigationBarColor = navigationBarColor
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ ext {
]
google = [
material : 'com.google.android.material:material:1.7.0-rc01',
material : 'com.google.android.material:material:1.7.0',
gson : 'com.google.code.gson:gson:2.9.1',
guava : 'com.google.guava:guava:31.1-android',
guava_jre : 'com.google.guava:guava:31.1-jre',
+1
View File
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="primaryColor">#121212</color>
<color name="primaryColor_translucent">#90121212</color>
<color name="primaryLightColor">#373737</color>
<color name="primaryDarkColor">#010101</color>
<color name="secondaryColor">#ff5252</color>
-1
View File
@@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="file_tab_height">36dp</dimen>
<dimen name="editor_sheet_peek_height">40dp</dimen>
</resources>
+6
View File
@@ -48,6 +48,7 @@
<item name="chipStyle">@style/AppTheme.UIDesigner.WidgetGroupChip</item>
<item name="switchPreferenceStyle">@style/AppTheme.SwitchPreference</item>
<item name="shapeAppearanceLargeComponent">@style/AppTheme.ShapeOverlay.Large</item>
<item name="bottomSheetDialogTheme">@style/AppTheme.BottomSheetDialog</item>
</style>
<style name="Theme.AndroidIDE" parent="Theme.AndroidIDE.Styled" />
@@ -117,6 +118,11 @@
<item name="cornerSize">25dp</item>
</style>
<style name="AppTheme.BottomSheetDialog" parent="Theme.Material3.Dark.BottomSheetDialog">
<item name="android:windowIsFloating">false</item>
<item name="android:windowLightStatusBar">false</item>
</style>
<!-- Alert Dialog -->
<style name="AppTheme.MaterialAlertDialog" parent="@style/ThemeOverlay.Material3.MaterialAlertDialog">
<item name="shapeAppearanceOverlay">@style/MaterialDialog.ShapeAppearance</item>
Binary file not shown.