fix: implement edge-to-edge layout for editor activity

This commit is contained in:
Akash Yadav
2024-05-13 00:56:40 +05:30
parent 53a2d636b2
commit c316ac9852
23 changed files with 611 additions and 276 deletions
+8
View File
@@ -4,6 +4,14 @@
<selectionStates>
<SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" />
<DropdownSelection timestamp="2024-05-12T19:07:48.842669075Z">
<Target type="DEFAULT_BOOT">
<handle>
<DeviceId pluginId="PhysicalDevice" identifier="serial=fd22c9e0" />
</handle>
</Target>
</DropdownSelection>
<DialogSelection />
</SelectionState>
</selectionStates>
</component>
@@ -34,7 +34,7 @@ import com.itsaky.androidide.BuildConfig
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.SimpleIconTitleDescriptionAdapter
import com.itsaky.androidide.app.BaseApplication
import com.itsaky.androidide.app.LimitlessIDEActivity
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.app.configuration.IDEBuildConfigProvider
import com.itsaky.androidide.buildinfo.BuildInfo
import com.itsaky.androidide.databinding.ActivityAboutBinding
@@ -44,7 +44,7 @@ import com.itsaky.androidide.utils.BuildInfoUtils
import com.itsaky.androidide.utils.flashSuccess
import com.itsaky.androidide.utils.resolveAttr
class AboutActivity : LimitlessIDEActivity() {
class AboutActivity : EdgeToEdgeIDEActivity() {
private var _binding: ActivityAboutBinding? = null
private val binding: ActivityAboutBinding
@@ -76,7 +76,7 @@ class AboutActivity : LimitlessIDEActivity() {
setSupportActionBar(toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
supportActionBar!!.setTitle(R.string.about)
toolbar.setNavigationOnClickListener { finish() }
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
aboutHeader.apply {
ideVersion.text = createVersionText()
@@ -24,7 +24,7 @@ import androidx.activity.viewModels
import androidx.core.view.isVisible
import com.itsaky.androidide.R
import com.itsaky.androidide.adapters.ContributorsGridAdapter
import com.itsaky.androidide.app.LimitlessIDEActivity
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityContributorsBinding
import com.itsaky.androidide.utils.getConnectionInfo
import com.itsaky.androidide.viewmodel.ContributorsViewModel
@@ -32,7 +32,7 @@ import com.itsaky.androidide.viewmodel.ContributorsViewModel
/**
* @author Akash Yadav
*/
class ContributorsActivity : LimitlessIDEActivity() {
class ContributorsActivity : EdgeToEdgeIDEActivity() {
private var _binding: ActivityContributorsBinding? = null
private val binding: ActivityContributorsBinding
@@ -54,7 +54,7 @@ class ContributorsActivity : LimitlessIDEActivity() {
setSupportActionBar(toolbar)
supportActionBar!!.setTitle(R.string.title_contributors)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { finish() }
toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
githubContributors.apply {
sectionTitle.setText(R.string.title_github_contributors)
@@ -19,6 +19,7 @@ package com.itsaky.androidide.activities;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import com.itsaky.androidide.app.IDEActivity;
import com.itsaky.androidide.databinding.ActivityCrashHandlerBinding;
import com.itsaky.androidide.fragments.CrashReportFragment;
@@ -50,16 +51,12 @@ public class CrashHandlerActivity extends IDEActivity {
}
@Override
@NonNull
protected View bindLayout() {
binding = ActivityCrashHandlerBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
@Override
public void onBackPressed() {
finishAffinity();
}
@Override
protected void onDestroy() {
super.onDestroy();
@@ -29,7 +29,7 @@ import androidx.transition.TransitionManager
import androidx.transition.doOnEnd
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.activities.editor.EditorActivityKt
import com.itsaky.androidide.app.LimitlessIDEActivity
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityMainBinding
import com.itsaky.androidide.preferences.internal.GeneralPreferences
import com.itsaky.androidide.projects.ProjectManagerImpl
@@ -43,7 +43,7 @@ import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_TEMPLATE_D
import com.itsaky.androidide.viewmodel.MainViewModel.Companion.SCREEN_TEMPLATE_LIST
import java.io.File
class MainActivity : LimitlessIDEActivity() {
class MainActivity : EdgeToEdgeIDEActivity() {
private val viewModel by viewModels<MainViewModel>()
private var _binding: ActivityMainBinding? = null
@@ -1,118 +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.activities;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import com.itsaky.androidide.R;
import com.itsaky.androidide.app.LimitlessIDEActivity;
import com.itsaky.androidide.databinding.ActivityPreferencesBinding;
import com.itsaky.androidide.fragments.IDEPreferencesFragment;
import com.itsaky.androidide.preferences.IDEPreferences;
import com.itsaky.androidide.preferences.RootPrefExtsKt;
import java.util.ArrayList;
public class PreferencesActivity extends LimitlessIDEActivity {
private ActivityPreferencesBinding binding;
private IDEPreferencesFragment rootFragment;
public PreferencesActivity() {
super(true);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setSupportActionBar(binding.toolbar);
getSupportActionBar().setTitle(R.string.ide_preferences);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
binding.toolbar.setNavigationOnClickListener(v -> onBackPressed());
if (savedInstanceState != null) {
return;
}
final var prefs = IDEPreferences.INSTANCE;
prefs.getChildren().clear();
RootPrefExtsKt.addRootPreferences(prefs);
final var args = new Bundle();
args.putParcelableArrayList(
IDEPreferencesFragment.EXTRA_CHILDREN,
new ArrayList<>(prefs.getChildren())
);
final var root = getRootFragment();
root.setArguments(args);
loadFragment(root);
}
@Override
public void onInsetsUpdated(@NonNull Rect insets) {
super.onInsetsUpdated(insets);
View toolbar = binding.toolbar;
toolbar.setPadding(
toolbar.getPaddingLeft() + insets.left,
toolbar.getPaddingTop(),
toolbar.getPaddingRight() + insets.right,
toolbar.getPaddingBottom()
);
View fragmentContainer = binding.fragmentContainerParent;
fragmentContainer.setPadding(
fragmentContainer.getPaddingLeft() + insets.left,
fragmentContainer.getPaddingTop(),
fragmentContainer.getPaddingRight() + insets.right,
fragmentContainer.getPaddingBottom()
);
}
@Override
@NonNull
protected View bindLayout() {
binding = ActivityPreferencesBinding.inflate(getLayoutInflater());
return binding.getRoot();
}
private IDEPreferencesFragment getRootFragment() {
return rootFragment == null ? rootFragment = new IDEPreferencesFragment() : rootFragment;
}
private void loadFragment(Fragment fragment) {
super.loadFragment(fragment, binding.fragmentContainer.getId());
}
@Override
public void onBackPressed() {
super.onBackPressed();
}
@Override
protected void onDestroy() {
super.onDestroy();
binding = null;
}
}
@@ -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://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.activities
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import com.itsaky.androidide.R
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityPreferencesBinding
import com.itsaky.androidide.fragments.IDEPreferencesFragment
import com.itsaky.androidide.preferences.addRootPreferences
import com.itsaky.androidide.preferences.IDEPreferences as prefs
class PreferencesActivity : EdgeToEdgeIDEActivity() {
private var _binding: ActivityPreferencesBinding? = null
private val binding: ActivityPreferencesBinding
get() = checkNotNull(_binding) { "Activity has been destroyed" }
private val rootFragment by lazy {
IDEPreferencesFragment()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setSupportActionBar(binding.toolbar)
supportActionBar!!.setTitle(R.string.ide_preferences)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() }
if (savedInstanceState != null) {
return
}
(prefs.children as MutableList?)?.clear()
prefs.addRootPreferences()
val args = Bundle()
args.putParcelableArrayList(
IDEPreferencesFragment.EXTRA_CHILDREN,
ArrayList(prefs.children)
)
rootFragment.arguments = args
loadFragment(rootFragment)
}
override fun onInsetsUpdated(insets: Rect) {
super.onInsetsUpdated(insets)
val toolbar: View = binding.toolbar
toolbar.setPadding(
toolbar.paddingLeft + insets.left,
toolbar.paddingTop,
toolbar.paddingRight + insets.right,
toolbar.paddingBottom
)
val fragmentContainer: View = binding.fragmentContainerParent
fragmentContainer.setPadding(
fragmentContainer.paddingLeft + insets.left,
fragmentContainer.paddingTop,
fragmentContainer.paddingRight + insets.right,
fragmentContainer.paddingBottom
)
}
override fun bindLayout(): View {
_binding = ActivityPreferencesBinding.inflate(
layoutInflater)
return binding.root
}
private fun loadFragment(fragment: Fragment) {
super.loadFragment(fragment, binding.fragmentContainer.id)
}
override fun onDestroy() {
super.onDestroy()
_binding = null
}
}
@@ -20,6 +20,7 @@ package com.itsaky.androidide.activities.editor
import android.content.Intent
import android.content.pm.PackageInstaller.SessionCallback
import android.graphics.Color
import android.graphics.Rect
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.text.SpannableStringBuilder
@@ -28,6 +29,7 @@ import android.text.TextUtils
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.activity.OnBackPressedCallback
import androidx.activity.result.ActivityResult
@@ -38,6 +40,8 @@ import androidx.annotation.GravityInt
import androidx.annotation.StringRes
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.GravityCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePaddingRelative
import com.blankj.utilcode.util.FileUtils
import com.blankj.utilcode.util.KeyboardUtils
import com.blankj.utilcode.util.ThreadUtils
@@ -50,11 +54,12 @@ import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_FILE_TABS
import com.itsaky.androidide.adapters.DiagnosticsAdapter
import com.itsaky.androidide.adapters.SearchListAdapter
import com.itsaky.androidide.app.IDEActivity
import com.itsaky.androidide.app.EdgeToEdgeIDEActivity
import com.itsaky.androidide.databinding.ActivityEditorBinding
import com.itsaky.androidide.databinding.LayoutDiagnosticInfoBinding
import com.itsaky.androidide.events.InstallationResultEvent
import com.itsaky.androidide.fragments.SearchResultFragment
import com.itsaky.androidide.fragments.sidebar.EditorSidebarFragment
import com.itsaky.androidide.fragments.sidebar.FileTreeFragment
import com.itsaky.androidide.handlers.EditorActivityLifecyclerObserver
import com.itsaky.androidide.handlers.LspHandler.registerLanguageServers
@@ -96,7 +101,7 @@ import java.io.File
* @author Akash Yadav
*/
@Suppress("MemberVisibilityCanBePrivate")
abstract class BaseEditorActivity : IDEActivity(), TabLayout.OnTabSelectedListener,
abstract class BaseEditorActivity : EdgeToEdgeIDEActivity(), TabLayout.OnTabSelectedListener,
DiagnosticClickListener {
protected val mLifecycleObserver = EditorActivityLifecyclerObserver()
@@ -194,6 +199,19 @@ abstract class BaseEditorActivity : IDEActivity(), TabLayout.OnTabSelectedListen
return this.binding.root
}
override fun onInsetsUpdated(insets: Rect) {
super.onInsetsUpdated(insets)
this._binding?.apply {
drawerSidebar.getFragment<EditorSidebarFragment>()
.onApplyWindowInsets(insets)
editorToolbar.updatePaddingRelative(
start = editorToolbar.paddingStart + insets.left,
end = editorToolbar.paddingEnd + insets.right
)
}
}
@Subscribe(threadMode = MAIN)
open fun onInstallationResult(event: InstallationResultEvent) {
val intent = event.intent
@@ -544,7 +562,7 @@ abstract class BaseEditorActivity : IDEActivity(), TabLayout.OnTabSelectedListen
binding.apply {
viewContainer.viewTreeObserver.addOnGlobalLayoutListener(observer)
bottomSheet.setOffsetAnchor(editorToolbar)
bottomSheet.setOffsetAnchor(editorAppBarLayout)
}
}
@@ -20,4 +20,6 @@ package com.itsaky.androidide.activities.editor
/**
* @author Akash Yadav
*/
class EditorActivityKt : EditorHandlerActivity()
class EditorActivityKt : EditorHandlerActivity()
// TODO: Should we adjust to the display cutout insets?
@@ -0,0 +1,149 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have 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.annotation.SuppressLint
import android.content.res.Configuration
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.view.WindowInsets
import android.view.WindowManager
import androidx.core.view.OnApplyWindowInsetsListener
import androidx.core.view.ViewCompat
import androidx.core.view.doOnAttach
import androidx.core.view.setPadding
import com.itsaky.androidide.utils.EdgeToEdgeUtils
import com.itsaky.androidide.utils.getInsets
import org.slf4j.LoggerFactory
/**
* Same as IDEActivity but DecorFitsSystemWindows is set to false
* Useful for creating immersive edge-to-edge experiences.
*
* @author Smooth E
* @author Akash Yadav
*/
abstract class EdgeToEdgeIDEActivity : IDEActivity() {
/**
* Whether edge-to-edge should be applied to the activity.
*/
protected open var edgeToEdgeEnabled = true
/**
* Whether the decor view padding should be updated to match the system bars in landscape orientation.
*/
protected open var eteUpdateDecorViewPaddingInLandscape = true
/**
* Style for the status bar.
*/
protected open val statusBarStyle = EdgeToEdgeUtils.DEFAULT_STATUS_BAR_STYLE!!
/**
* Style for the navigation bar.
*/
protected open val navigationBarStyle = EdgeToEdgeUtils.DEFAULT_NAVIGATION_BAR_STYLE!!
/**
* Original padding of the window's decor view.
*/
protected open var decorViewPadding: Rect? = null
override var enableSystemBarTheming: Boolean
get() = false
set(@Suppress("UNUSED_PARAMETER") value) {
throw UnsupportedOperationException("Use edgeToEdgeEnabled and systemBarStyles instead")
}
private val log = LoggerFactory.getLogger(EdgeToEdgeIDEActivity::class.java)
@SuppressLint("WrongConstant")
private val onApplyWindowInsetsListener = OnApplyWindowInsetsListener { v, insets ->
if (!eteUpdateDecorViewPaddingInLandscape ||
v.resources.configuration.orientation != Configuration.ORIENTATION_LANDSCAPE
) {
decorViewPadding?.also { p ->
// when switching from landscape to portrait, restore the original padding
v.setPadding(p.left, p.top, p.right, p.bottom)
}
return@OnApplyWindowInsetsListener insets
}
if (decorViewPadding == null) {
Rect().apply {
left = v.paddingLeft
top = v.paddingTop
right = v.paddingRight
bottom = v.paddingBottom
}.also { paddings ->
decorViewPadding = paddings
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
v.setPadding(
insets.getInsets(WindowInsets.Type.systemBars()).left,
0,
insets.getInsets(WindowInsets.Type.systemBars()).right,
insets.getInsets(WindowInsets.Type.systemBars()).bottom)
} else {
@Suppress("DEPRECATION")
v.setPadding(
insets.stableInsetLeft,
0,
insets.stableInsetRight,
insets.stableInsetBottom)
}
insets
}
override fun onCreate(savedInstanceState: Bundle?) {
applyEdgeToEdge()
super.onCreate(savedInstanceState)
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
applyEdgeToEdge()
super.onCreate(savedInstanceState, persistentState)
}
@SuppressLint("RestrictedApi")
private fun applyEdgeToEdge() {
this.window!!.apply {
addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
@Suppress("DEPRECATION")
clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
}
EdgeToEdgeUtils.applyEdgeToEdge(this, this.statusBarStyle, this.navigationBarStyle)
ViewCompat.setOnApplyWindowInsetsListener(
this.window.decorView,
this.onApplyWindowInsetsListener
)
this.window.decorView.doOnAttach { decorView -> onInsetsUpdated(getInsets(decorView)) }
}
/** Called whenever insets are updated */
protected open fun onInsetsUpdated(insets: Rect) {}
}
@@ -1,98 +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.content.res.Configuration
import android.graphics.Color
import android.graphics.Rect
import android.os.Build
import android.os.Bundle
import android.os.PersistableBundle
import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
import androidx.core.view.WindowCompat
import androidx.core.view.doOnAttach
import com.itsaky.androidide.utils.NavigationBar
import com.itsaky.androidide.utils.getInsets
import com.itsaky.androidide.utils.resolveAttr
/**
* Same as IDEActivity but DecorFitsSystemWindows is set to false
* Useful for creating immersive edge-to-edge experiences
*
* @author Smooth E
*/
abstract class LimitlessIDEActivity(
private val highlightNavigationBar: Boolean = false
) : IDEActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
makeLimitless()
installOnDecorViewAttachedListener()
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
makeLimitless()
installOnDecorViewAttachedListener()
}
private fun makeLimitless() {
WindowCompat.setDecorFitsSystemWindows(window, false)
paintNavigationBar()
// This removes a black strip on the side where the camera cutout is in landscape mode
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
window.attributes.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
}
}
private fun installOnDecorViewAttachedListener() {
window.decorView.doOnAttach { decorView -> onInsetsUpdated(getInsets(decorView)) }
}
/** Called whenever insets are updated */
open fun onInsetsUpdated(insets: Rect) { }
private fun paintNavigationBar() {
window.statusBarColor = Color.TRANSPARENT
window.navigationBarColor = Color.TRANSPARENT
if (!highlightNavigationBar) {
return
}
val refrainHighlighting =
NavigationBar.getInteractionMode(this) == NavigationBar.MODE_GESTURES ||
resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
if (refrainHighlighting) {
return
}
val navigationBarColor = resolveAttr(com.google.android.material.R.attr.colorSurface)
window.navigationBarColor = navigationBarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
val dividerColor = resolveAttr(com.google.android.material.R.attr.colorOutlineVariant)
window.navigationBarDividerColor = dividerColor
}
}
}
@@ -89,10 +89,6 @@ class RunTasksDialogFragment : BottomSheetDialogFragment() {
val dialog = 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, _, margins ->
insets.getInsets(statusBars() or navigationBars()).apply {
@@ -17,8 +17,16 @@
package com.itsaky.androidide.fragments.sidebar
import android.graphics.Rect
import android.os.Bundle
import android.view.View
import android.view.ViewGroup.MarginLayoutParams
import androidx.core.view.marginBottom
import androidx.core.view.marginTop
import androidx.core.view.updateLayoutParams
import androidx.core.view.updateMarginsRelative
import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative
import com.itsaky.androidide.databinding.FragmentEditorSidebarBinding
import com.itsaky.androidide.fragments.FragmentWithBinding
import com.itsaky.androidide.utils.EditorSidebarActions
@@ -32,14 +40,33 @@ class EditorSidebarFragment : FragmentWithBinding<FragmentEditorSidebarBinding>(
FragmentEditorSidebarBinding::inflate
) {
internal fun onApplyWindowInsets(insets: Rect) {
_binding?.apply {
title.updateLayoutParams<MarginLayoutParams> {
updateMarginsRelative(
top = title.marginTop + insets.top,
)
}
fragmentContainer.updateLayoutParams<MarginLayoutParams> {
updateMarginsRelative(
bottom = fragmentContainer.marginBottom + insets.bottom,
)
}
navigation.updatePadding(
top = navigation.paddingTop + insets.top,
bottom = navigation.paddingBottom + insets.bottom,
left = navigation.paddingLeft + insets.left,
)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
EditorSidebarActions.setup(this)
}
/**
* Performs the action only if the binding is not null.
* Get the (nullable) binding object for this fragment.
*/
internal fun getBinding() = _binding
}
@@ -19,16 +19,20 @@ package com.itsaky.androidide.ui
import android.app.Activity
import android.content.Context
import android.graphics.Rect
import android.text.TextUtils
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.RelativeLayout
import androidx.annotation.GravityInt
import androidx.appcompat.widget.TooltipCompat
import androidx.core.view.ViewCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.core.view.updatePaddingRelative
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.transition.TransitionManager
@@ -54,6 +58,7 @@ import com.itsaky.androidide.tasks.TaskExecutor.executeAsyncProvideError
import com.itsaky.androidide.utils.IntentUtils.shareFile
import com.itsaky.androidide.utils.Symbols.forFile
import com.itsaky.androidide.utils.flashError
import com.itsaky.androidide.utils.getInsets
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException
@@ -63,6 +68,7 @@ import java.nio.file.Path
import java.nio.file.StandardOpenOption.CREATE_NEW
import java.nio.file.StandardOpenOption.WRITE
import java.util.concurrent.Callable
import kotlin.math.roundToInt
/**
* Bottom sheet shown in editor activity.
@@ -84,12 +90,14 @@ constructor(
@JvmField
var binding: LayoutEditorBottomSheetBinding
val pagerAdapter: EditorBottomSheetTabAdapter
private var windowInsets: Rect? = null
companion object {
private val log = LoggerFactory.getLogger(EditorBottomSheet::class.java)
private const val COLLAPSE_HEADER_AT_OFFSET = 0.5f
const val CHILD_HEADER = 0
const val CHILD_SYMBOL_INPUT = 1
@@ -160,6 +168,11 @@ constructor(
sheet.state = BottomSheetBehavior.STATE_EXPANDED
}
}
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
this.windowInsets = getInsets(view)
insets
}
}
init {
@@ -184,23 +197,52 @@ constructor(
override fun onGlobalLayout() {
val sheet = BottomSheetBehavior.from(this@EditorBottomSheet)
val offset = view.height + SizeUtils.dp2px(1f)
val collapsedHeight = collapsedHeight
sheet.isFitToContents = false
sheet.halfExpandedRatio = 0.3f
sheet.skipCollapsed = true
sheet.isGestureInsetBottomIgnored = false
sheet.peekHeight = binding.headerContainer.height
sheet.peekHeight = collapsedHeight.roundToInt()
sheet.expandedOffset = offset
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
binding.root.updatePadding(bottom = offset + SizeUtils.dp2px(16f))
val insetBottom = windowInsets?.bottom ?: 0
binding.root.updatePadding(bottom = offset + insetBottom)
binding.headerContainer.apply {
updatePaddingRelative(bottom = paddingBottom + insetBottom)
updateLayoutParams<ViewGroup.LayoutParams> {
height = (collapsedHeight + insetBottom).roundToInt()
}
}
}
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
}
fun onSlide(offset: Float) {
if (offset >= 0.5f) {
updateCollapsedHeight(((0.5f - offset) + 0.5f) * 2f)
} else updateCollapsedHeight(1f)
fun onSlide(sheetOffset: Float) {
val heightScale = if (sheetOffset >= COLLAPSE_HEADER_AT_OFFSET) {
((COLLAPSE_HEADER_AT_OFFSET - sheetOffset) + COLLAPSE_HEADER_AT_OFFSET) * 2f
} else {
1f
}
val paddingScale = if (sheetOffset <= COLLAPSE_HEADER_AT_OFFSET) {
((1f - sheetOffset) * 2f) - 1f
} else {
0f
}
val insetBottom = windowInsets?.bottom ?: 0
val padding = insetBottom * paddingScale
binding.headerContainer.apply {
updateLayoutParams<ViewGroup.LayoutParams> {
height = ((collapsedHeight + padding) * heightScale).roundToInt()
}
updatePaddingRelative(
bottom = padding.roundToInt()
)
}
}
fun showChild(index: Int) {
@@ -276,12 +318,6 @@ 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")
}
+4 -5
View File
@@ -6,8 +6,8 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true"
tools:ignore="DuplicateIncludedIds"
tools:openDrawer="start">
android:background="?attr/colorSurface"
tools:ignore="DuplicateIncludedIds">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/realContainer"
@@ -19,6 +19,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/colorSurface"
android:fitsSystemWindows="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$Behavior">
<com.itsaky.androidide.ui.ExtendedMenuToolbar
@@ -47,7 +48,6 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="16dp"
android:background="?attr/colorSurface"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior">
<!--Views in this flipper are dynamically added and must be instances of CodeEditorView-->
@@ -55,8 +55,7 @@
android:id="@+id/editor_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="@dimen/editor_sheet_peek_height"
android:background="?attr/colorSurface" />
android:layout_marginBottom="@dimen/editor_sheet_peek_height" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@@ -32,6 +32,8 @@ abstract class BaseIDEActivity : AppCompatActivity() {
open val subscribeToEvents: Boolean = false
open var enableSystemBarTheming: Boolean = true
open val navigationBarColor: Int
get() = resolveAttr(R.attr.colorSurface)
@@ -44,9 +46,11 @@ abstract class BaseIDEActivity : AppCompatActivity() {
val activityScope = CoroutineScope(Dispatchers.Default)
override fun onCreate(savedInstanceState: Bundle?) {
window?.apply {
navigationBarColor = this@BaseIDEActivity.navigationBarColor
statusBarColor = this@BaseIDEActivity.statusBarColor
if (enableSystemBarTheming) {
window?.apply {
navigationBarColor = this@BaseIDEActivity.navigationBarColor
statusBarColor = this@BaseIDEActivity.statusBarColor
}
}
IThemeManager.getInstance().applyTheme(this)
super.onCreate(savedInstanceState)
@@ -0,0 +1,219 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
/*
* Copyright (C) 2022 The Android Open Source Project
*
* 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 static android.graphics.Color.TRANSPARENT;
import static com.google.android.material.color.MaterialColors.isColorLight;
import android.content.Context;
import android.graphics.Color;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.view.Window;
import androidx.activity.EdgeToEdge;
import androidx.activity.SystemBarStyle;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.activity.ComponentActivity;
import androidx.core.graphics.ColorUtils;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.google.android.material.color.MaterialColors;
/**
* A util class that helps apply edge-to-edge mode to activity/dialog windows.
* <p>
* <b>AndroidIDE Changed</b>: Allow to choose if the status bar and navigation bar's color should
* be changed or not.
*
* @see <a
* href="https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/internal/EdgeToEdgeUtils.java">EdgeToEdgeUtils.java</a>
*/
public class EdgeToEdgeUtils {
private static final int EDGE_TO_EDGE_BAR_ALPHA = 128;
public static final int DEFAULT_LIGHT_SCRIM = EdgeToEdge.getDefaultLightScrim();
public static final int DEFAULT_DARK_SCRIM = EdgeToEdge.getDefaultDarkScrim();
public static final SystemBarStyle DEFAULT_STATUS_BAR_STYLE = SystemBarStyle.auto(TRANSPARENT,
TRANSPARENT);
public static final SystemBarStyle DEFAULT_NAVIGATION_BAR_STYLE = SystemBarStyle.auto(
DEFAULT_LIGHT_SCRIM, DEFAULT_DARK_SCRIM);
private EdgeToEdgeUtils() {
}
/**
* Enable edge-to-edge mode for the provided activity, using the built-in
* {@link EdgeToEdge#enable(androidx.activity.ComponentActivity, SystemBarStyle,
* SystemBarStyle)}.
*
* @param activity The activity to enable edge-to-edge mode for.
* @param statusBarStyle The status bar style to use.
* @param navigationBarStyle The navigation bar style to use.
*/
public static void applyEdgeToEdge(
ComponentActivity activity,
SystemBarStyle statusBarStyle,
SystemBarStyle navigationBarStyle
) {
EdgeToEdge.enable(activity, statusBarStyle, navigationBarStyle);
}
/**
* Applies or removes edge-to-edge mode to the provided {@link Window}. When edge-to-edge mode is
* applied, the activities, or the non-floating dialogs, that host the provided window will be
* drawn over the system bar area by default and the system bar colors will be adjusted according
* to the background color you provide.
*/
public static void applyEdgeToEdgeManually(
@NonNull Window window,
boolean edgeToEdgeEnabled,
@Nullable @ColorInt Integer statusBarColor,
@Nullable @ColorInt Integer navBarColor) {
applyEdgeToEdgeManually(window, edgeToEdgeEnabled, null, null, statusBarColor,
navBarColor);
}
/**
* Applies or removes edge-to-edge mode to the provided {@link Window}. When edge-to-edge mode is
* applied, the activities, or the non-floating dialogs, that host the provided window will be
* drawn over the system bar area by default and the system bar colors will be adjusted according
* to the background color you provide.
*
* @param statusBarOverlapBackgroundColor The reference background color to decide the
* text/icon colors on status bars. {@code null} to use
* the default color from
* {@code ?android:attr/colorBackground}.
* @param navigationBarOverlapBackgroundColor The reference background color to decide the icon
* colors on navigation bars.{@code null} to use the
* default color from
* {@code ?android:attr/colorBackground}.
*/
public static void applyEdgeToEdgeManually(
@NonNull Window window,
boolean edgeToEdgeEnabled,
@Nullable @ColorInt Integer statusBarOverlapBackgroundColor,
@Nullable @ColorInt Integer navigationBarOverlapBackgroundColor,
@Nullable @ColorInt Integer statusBarColor,
@Nullable @ColorInt Integer navBarColor
) {
// If the overlapping background color is unknown or TRANSPARENT, use the default one.
boolean useDefaultBackgroundColorForStatusBar =
statusBarOverlapBackgroundColor == null || statusBarOverlapBackgroundColor == 0;
boolean useDefaultBackgroundColorForNavigationBar =
navigationBarOverlapBackgroundColor == null || navigationBarOverlapBackgroundColor == 0;
if (useDefaultBackgroundColorForStatusBar || useDefaultBackgroundColorForNavigationBar) {
int defaultBackgroundColor =
MaterialColors.getColor(window.getContext(), android.R.attr.colorBackground, Color.BLACK);
if (useDefaultBackgroundColorForStatusBar) {
statusBarOverlapBackgroundColor = defaultBackgroundColor;
}
if (useDefaultBackgroundColorForNavigationBar) {
navigationBarOverlapBackgroundColor = defaultBackgroundColor;
}
}
WindowCompat.setDecorFitsSystemWindows(window, !edgeToEdgeEnabled);
int color = statusBarColor != null ? statusBarColor
: getStatusBarColor(window.getContext(), edgeToEdgeEnabled);
window.setStatusBarColor(color);
setLightStatusBar(
window,
isUsingLightSystemBar(color, isColorLight(statusBarOverlapBackgroundColor)));
color = navBarColor != null ? navBarColor
: getNavigationBarColor(window.getContext(), edgeToEdgeEnabled);
window.setNavigationBarColor(color);
setLightNavigationBar(
window,
isUsingLightSystemBar(
color, isColorLight(navigationBarOverlapBackgroundColor)));
}
/**
* Changes the foreground color of the status bars to light or dark so that the items on the bar
* can be read clearly.
*
* @param window Window that hosts the status bars
* @param isLight {@code true} to make the foreground color light
*/
public static void setLightStatusBar(@NonNull Window window, boolean isLight) {
WindowInsetsControllerCompat insetsController =
WindowCompat.getInsetsController(window, window.getDecorView());
insetsController.setAppearanceLightStatusBars(isLight);
}
/**
* Changes the foreground color of the navigation bars to light or dark so that the items on the
* bar can be read clearly.
*
* @param window Window that hosts the status bars
* @param isLight {@code true} to make the foreground color light.
*/
public static void setLightNavigationBar(@NonNull Window window, boolean isLight) {
WindowInsetsControllerCompat insetsController =
WindowCompat.getInsetsController(window, window.getDecorView());
insetsController.setAppearanceLightNavigationBars(isLight);
}
private static int getStatusBarColor(Context context, boolean isEdgeToEdgeEnabled) {
if (isEdgeToEdgeEnabled) {
return TRANSPARENT;
}
return MaterialColors.getColor(context, android.R.attr.statusBarColor, Color.BLACK);
}
private static int getNavigationBarColor(Context context, boolean isEdgeToEdgeEnabled) {
// Light navigation bars are only supported on O_MR1+. So we need to use a translucent black
// navigation bar instead to ensure the text/icon contrast of it.
if (isEdgeToEdgeEnabled && VERSION.SDK_INT < VERSION_CODES.O_MR1) {
int opaqueNavBarColor =
MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK);
return ColorUtils.setAlphaComponent(opaqueNavBarColor, EDGE_TO_EDGE_BAR_ALPHA);
}
if (isEdgeToEdgeEnabled) {
return TRANSPARENT;
}
return MaterialColors.getColor(context, android.R.attr.navigationBarColor, Color.BLACK);
}
private static boolean isUsingLightSystemBar(int systemBarColor, boolean isLightBackground) {
return isColorLight(systemBarColor) || (systemBarColor == TRANSPARENT && isLightBackground);
}
}
@@ -42,6 +42,7 @@ fun getInsets(view: View): Rect {
val typeMask = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
rootWindowInsets.getInsetsIgnoringVisibility(typeMask)
} else {
@Suppress("DEPRECATION")
view.rootWindowInsets.stableInsets
}
@@ -52,6 +53,7 @@ fun getInsets(view: View): Rect {
receivedInsets.bottom
)
} else {
@Suppress("DEPRECATION")
insets = Rect(
rootWindowInsets.stableInsetLeft,
rootWindowInsets.stableInsetTop,
+5 -5
View File
@@ -8,7 +8,7 @@ kotlin-coroutines = "1.8.0"
tree-sitter = "4.3.1"
editor = "0.23.4-ce8de8e-SNAPSHOT"
glide = "4.16.0"
androidx-vectordrawable = "1.1.0"
androidx-vectordrawable = "1.2.0"
androidx-navigation = "2.7.7"
ksp = "1.9.22-1.0.17"
antlr4 = "4.13.1"
@@ -84,12 +84,12 @@ androidx-preference = { module = "androidx.preference:preference", version = "1.
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version = "1.3.2" }
androidx-vectors = { module = "androidx.vectordrawable:vectordrawable", version.ref = "androidx-vectordrawable" }
androidx-animated_vectors = { module = "androidx.vectordrawable:vectordrawable-animated", version.ref = "androidx-vectordrawable" }
androidx-core = { module = "androidx.core:core", version = "1.12.0" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.12.0" }
androidx-fragment_ktx = { module = "androidx.fragment:fragment-ktx", version = "1.6.2" }
androidx-core = { module = "androidx.core:core", version = "1.13.1" }
androidx-core-ktx = { module = "androidx.core:core-ktx", version = "1.13.1" }
androidx-fragment_ktx = { module = "androidx.fragment:fragment-ktx", version = "1.7.0" }
androidx-libDesugaring = { module = "com.android.tools:desugar_jdk_libs", version = "2.0.4" }
androidx-splashscreen = { module = "androidx.core:core-splashscreen", version = "1.0.1" }
androidx-transition = { module = "androidx.transition:transition-ktx", version = "1.4.1" }
androidx-transition = { module = "androidx.transition:transition-ktx", version = "1.5.0" }
androidx-nav-fragment = { module = "androidx.navigation:navigation-fragment-ktx", version.ref = "androidx-navigation" }
androidx-nav-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
androidx-tracing = { module = "androidx.tracing:tracing", version = "1.2.0" }
@@ -25,7 +25,8 @@ import android.os.Parcelable
*
* @author Akash Yadav
*/
object IDEPreferences : BaseIDEPreferences(){
data object IDEPreferences : BaseIDEPreferences() {
override val children: List<IPreference> = mutableListOf()
override fun describeContents(): Int = 0
override fun writeToParcel(dest: Parcel, flags: Int) {}
@@ -20,9 +20,6 @@
<style name="Theme.AndroidIDE.Material3" parent="Theme.Material3.Dark.NoActionBar"/>
<style name="Theme.AndroidIDE" parent="Theme.AndroidIDE.Styled">
<item name="android:windowLightStatusBar">false</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
<!-- Left drawer. -->
<item name="buttonBarButtonStyle">@style/TermuxActivity.Drawer.ButtonBarStyle.Dark</item>
<item name="termuxActivityDrawerBackground">@color/black</item>
-5
View File
@@ -28,8 +28,6 @@
</style>
<style name="Theme.AndroidIDE.Styled" parent="Theme.AndroidIDE.Base">
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="switchPreferenceStyle">@style/AppTheme.SwitchPreference</item>
<item name="shapeAppearanceLargeComponent">@style/AppTheme.ShapeOverlay.Large</item>
<item name="textAppearanceBodySmall">@style/TextAppearance.AndroidIDE.BodySmall</item>
@@ -37,9 +35,6 @@
</style>
<style name="Theme.AndroidIDE" parent="Theme.AndroidIDE.Styled">
<item name="android:windowLightStatusBar">true</item>
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
<!-- Left drawer. -->
<item name="buttonBarButtonStyle">@style/TermuxActivity.Drawer.ButtonBarStyle.Light</item>
<item name="termuxActivityDrawerBackground">@android:color/white</item>
@@ -119,7 +119,7 @@ class ProjectManagerImpl : IProjectManager, EventReceiver {
withStopWatch("Setup project") {
val indexerScope = CoroutineScope(Dispatchers.Default)
val modulesFlow = flow {
rootProject.subProjects.filterIsInstance(ModuleProject::class.java).forEach {
rootProject.subProjects.filterIsInstance<ModuleProject>().forEach {
emit(it)
}
}