Skip to content

Commit a73139a

Browse files
committed
Merge remote-tracking branch 'origin/stage' into fix/ADFA-4387
2 parents 7bca4d7 + b8c64c3 commit a73139a

38 files changed

Lines changed: 1037 additions & 127 deletions

File tree

‎app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt‎

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ import com.google.android.material.tabs.TabLayout.Tab
8282
import com.itsaky.androidide.FeedbackButtonManager
8383
import com.itsaky.androidide.R
8484
import com.itsaky.androidide.R.string
85+
import com.itsaky.androidide.activities.MainActivity
8586
import com.itsaky.androidide.actions.build.DebugAction
8687
import com.itsaky.androidide.adapters.DiagnosticsAdapter
8788
import com.itsaky.androidide.adapters.SearchListAdapter
@@ -111,6 +112,7 @@ import com.itsaky.androidide.models.Range
111112
import com.itsaky.androidide.models.SearchResult
112113
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
113114
import com.itsaky.androidide.preferences.internal.BuildPreferences
115+
import com.itsaky.androidide.preferences.internal.GeneralPreferences
114116
import com.itsaky.androidide.projects.IProjectManager
115117
import com.itsaky.androidide.projects.ProjectManagerImpl
116118
import com.itsaky.androidide.services.debug.DebuggerService
@@ -631,11 +633,35 @@ abstract class BaseEditorActivity :
631633
builder.show()
632634
}
633635

636+
/**
637+
* Restores the project path on recreation (saved state, launch intent, or last opened
638+
* project) and routes back to MainActivity if none is available, rather than crashing while
639+
* building the editor UI.
640+
*/
634641
override fun onCreate(savedInstanceState: Bundle?) {
635-
savedInstanceState?.getString(KEY_PROJECT_PATH)
636-
?.let(ProjectManagerImpl.getInstance()::projectPath::set)
642+
// The OS can recreate EditorActivity after process death without routing through
643+
// MainActivity, leaving the ProjectManagerImpl singleton's lateinit projectPath unset.
644+
// Restore it from the saved state, the launch intent, or the last opened project.
645+
val restoredProjectPath =
646+
savedInstanceState?.getString(KEY_PROJECT_PATH)?.takeIf { it.isNotBlank() }
647+
?: intent?.getStringExtra("PROJECT_PATH")?.takeIf { it.isNotBlank() }
648+
?: GeneralPreferences.lastOpenedProject
649+
.takeIf { it.isNotBlank() && it != GeneralPreferences.NO_OPENED_PROJECT }
650+
if (restoredProjectPath != null) {
651+
ProjectManagerImpl.getInstance().projectPath = restoredProjectPath
652+
}
637653
super.onCreate(savedInstanceState)
638654

655+
// If we still have no project path after every fallback, we cannot safely build the
656+
// editor UI (setupToolbar -> getProjectName dereferences the project path). Route the
657+
// user back to MainActivity instead of crashing.
658+
if (ProjectManagerImpl.getInstance().projectDirPath.isBlank()) {
659+
log.warn("No project path available in EditorActivity.onCreate(); returning to MainActivity")
660+
startActivity(Intent(this, MainActivity::class.java))
661+
finish()
662+
return
663+
}
664+
639665
editorViewModel.isBuildInProgress = false
640666
editorViewModel.isInitializing = false
641667

@@ -670,6 +696,7 @@ abstract class BaseEditorActivity :
670696

671697
setupStateObservers()
672698
setupFullscreenObserver()
699+
setupBottomSheetObserver()
673700
setupViews()
674701

675702
fullscreenManager = FullscreenManager(
@@ -1327,11 +1354,29 @@ abstract class BaseEditorActivity :
13271354
repeatOnLifecycle(Lifecycle.State.STARTED) {
13281355
editorViewModel.uiState.collectLatest { uiState ->
13291356
fullscreenManager?.render(uiState.isFullscreen, animate = true)
1357+
updateSwipeRevealDragState()
13301358
}
13311359
}
13321360
}
13331361
}
13341362

1363+
private fun setupBottomSheetObserver() {
1364+
lifecycleScope.launch {
1365+
repeatOnLifecycle(Lifecycle.State.STARTED) {
1366+
bottomSheetViewModel.sheetState.collectLatest { _ ->
1367+
updateSwipeRevealDragState()
1368+
}
1369+
}
1370+
}
1371+
}
1372+
1373+
private fun updateSwipeRevealDragState() {
1374+
val isFullscreen = editorViewModel.isFullscreen
1375+
val isBottomSheetOpen = bottomSheetViewModel.sheetBehaviorState != STATE_COLLAPSED &&
1376+
bottomSheetViewModel.sheetBehaviorState != STATE_HIDDEN
1377+
binding.swipeReveal.setVerticalDragEnabled(!isFullscreen && !isBottomSheetOpen)
1378+
}
1379+
13351380
private fun setupViews() {
13361381
setupNoEditorView()
13371382
setupBottomSheet()
@@ -1583,7 +1628,13 @@ abstract class BaseEditorActivity :
15831628
hasHorizontalVelocity &&
15841629
isHorizontalSwipe
15851630

1586-
// Fullscreen mode can be dismissed with an inward fling from either vertical edge.
1631+
val isBottomSheetOpen = bottomSheetViewModel.sheetBehaviorState != STATE_COLLAPSED &&
1632+
bottomSheetViewModel.sheetBehaviorState != STATE_HIDDEN
1633+
1634+
if (isBottomSheetOpen) {
1635+
return false
1636+
}
1637+
15871638
if (isTopEdgeDismissFling && editorViewModel.isFullscreen) {
15881639
editorViewModel.exitFullscreen()
15891640
return true

‎app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt‎

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import android.content.res.Configuration
2424
import android.os.Bundle
2525
import android.text.TextUtils
2626
import android.util.Log
27+
import android.util.TypedValue
2728
import android.view.KeyEvent
2829
import android.view.View
2930
import android.view.ViewGroup.LayoutParams
@@ -32,6 +33,8 @@ import androidx.collection.MutableIntObjectMap
3233
import androidx.core.content.res.ResourcesCompat
3334
import androidx.core.view.GravityCompat
3435
import androidx.core.view.doOnNextLayout
36+
import androidx.fragment.app.Fragment
37+
import androidx.fragment.app.FragmentManager
3538
import androidx.lifecycle.lifecycleScope
3639
import com.blankj.utilcode.util.ImageUtils
3740
import com.google.android.material.tabs.TabLayout
@@ -63,6 +66,7 @@ import com.itsaky.androidide.eventbus.events.editor.DocumentChangeEvent
6366
import com.itsaky.androidide.eventbus.events.file.FileRenameEvent
6467
import com.itsaky.androidide.activities.PluginManagerActivity
6568
import com.itsaky.androidide.eventbus.events.plugin.PluginCrashedEvent
69+
import com.itsaky.androidide.eventbus.events.preferences.PreferenceChangeEvent
6670
import com.itsaky.androidide.idetooltips.TooltipManager
6771
import com.itsaky.androidide.idetooltips.TooltipTag
6872
import com.itsaky.androidide.interfaces.IEditorHandler
@@ -73,6 +77,7 @@ import com.itsaky.androidide.models.Range
7377
import com.itsaky.androidide.models.SaveResult
7478
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
7579
import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
80+
import com.itsaky.androidide.preferences.internal.EditorPreferences
7681
import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver
7782
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
7883
import com.itsaky.androidide.projects.ProjectManagerImpl
@@ -90,6 +95,7 @@ import com.itsaky.androidide.utils.EditorSidebarActions
9095
import com.itsaky.androidide.utils.IntentUtils.openImage
9196
import com.itsaky.androidide.utils.UniqueNameBuilder
9297
import com.itsaky.androidide.utils.flashSuccess
98+
import com.itsaky.androidide.utils.forEachViewRecursively
9399
import kotlinx.coroutines.Dispatchers
94100
import kotlinx.coroutines.NonCancellable
95101
import kotlinx.coroutines.launch
@@ -98,6 +104,7 @@ import org.adfa.constants.CONTENT_KEY
98104
import org.greenrobot.eventbus.Subscribe
99105
import org.greenrobot.eventbus.ThreadMode
100106
import java.io.File
107+
import java.util.WeakHashMap
101108
import java.util.concurrent.ConcurrentHashMap
102109
import java.util.concurrent.CopyOnWriteArrayList
103110
import java.util.concurrent.atomic.AtomicBoolean
@@ -125,6 +132,22 @@ open class EditorHandlerActivity :
125132

126133
private val pluginTabIndices = mutableMapOf<String, Int>()
127134
private val tabIndexToPluginId = mutableMapOf<Int, String>()
135+
private var lastAppliedPluginFontScale = EditorPreferences.editorFontScale
136+
private val pluginTextBaseSizes = WeakHashMap<TextView, Float>()
137+
138+
private val pluginFontScalingListener = object : FragmentManager.FragmentLifecycleCallbacks() {
139+
override fun onFragmentViewCreated(
140+
mFragmentManager: FragmentManager,
141+
mFragment: Fragment,
142+
view: View,
143+
savedInstanceState: Bundle?
144+
) {
145+
val scale = EditorPreferences.editorFontScale
146+
if (scale != 1f && isPluginFragment(mFragment)) {
147+
applyPluginFontScale(view, scale)
148+
}
149+
}
150+
}
128151
private val shortcutManager by lazy { ShortcutManager(applicationContext) }
129152

130153
private var pluginEditorProvider: EditorProviderImpl? = null
@@ -194,6 +217,8 @@ open class EditorHandlerActivity :
194217
mBuildEventListener.setActivity(this)
195218
super.onCreate(savedInstanceState)
196219

220+
supportFragmentManager.registerFragmentLifecycleCallbacks(pluginFontScalingListener, true)
221+
197222
editorViewModel._displayedFile.observe(
198223
this,
199224
) { fileIndex ->
@@ -392,25 +417,38 @@ open class EditorHandlerActivity :
392417
}
393418

394419
restoreOpenedPluginTabs()
420+
syncPluginUiFontSize()
395421
}
396422

423+
/**
424+
* Restores the plugin tabs cached from the previous session, running the
425+
* SharedPreferences IO and Gson decode off the main thread to avoid a startup UI stall.
426+
*/
397427
private fun restoreOpenedPluginTabs() {
398-
try {
399-
val prefs = (application as BaseApplication).prefManager
400-
val json = prefs.getString(PREF_KEY_OPEN_PLUGIN_TABS, null) ?: return
428+
lifecycleScope.launch {
429+
try {
430+
val prefs = (application as BaseApplication).prefManager
431+
val json = withContext(Dispatchers.IO) {
432+
prefs.getString(PREF_KEY_OPEN_PLUGIN_TABS, null)
433+
} ?: return@launch
401434

402-
val tabIds = Gson().fromJson(json, Array<String>::class.java)?.toList() ?: return
403-
Log.d("EditorHandlerActivity", "Restoring plugin tabs: $tabIds")
435+
// Decoding the cached JSON off the main thread avoids a UI stall on startup.
436+
val tabIds = withContext(Dispatchers.Default) {
437+
Gson().fromJson(json, Array<String>::class.java)?.toList()
438+
} ?: return@launch
439+
Log.d("EditorHandlerActivity", "Restoring plugin tabs: $tabIds")
404440

405-
tabIds.forEach { tabId ->
406-
if (!pluginTabIndices.containsKey(tabId)) {
407-
selectPluginTabById(tabId)
441+
// Tab selection touches UI state, so keep it on the main thread.
442+
tabIds.forEach { tabId ->
443+
if (!pluginTabIndices.containsKey(tabId)) {
444+
selectPluginTabById(tabId)
445+
}
408446
}
409-
}
410447

411-
prefs.putString(PREF_KEY_OPEN_PLUGIN_TABS, null)
412-
} catch (e: Exception) {
413-
Log.e("EditorHandlerActivity", "Failed to restore plugin tabs", e)
448+
withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_PLUGIN_TABS, null) }
449+
} catch (e: Exception) {
450+
Log.e("EditorHandlerActivity", "Failed to restore plugin tabs", e)
451+
}
414452
}
415453
}
416454

@@ -1113,6 +1151,49 @@ open class EditorHandlerActivity :
11131151
showPluginCrashDialog(event)
11141152
}
11151153

1154+
@Subscribe(threadMode = ThreadMode.MAIN)
1155+
fun onPreferenceChanged(event: PreferenceChangeEvent) {
1156+
if (event.key == EditorPreferences.FONT_SIZE) {
1157+
syncPluginUiFontSize()
1158+
}
1159+
}
1160+
1161+
private fun syncPluginUiFontSize() {
1162+
val scale = EditorPreferences.editorFontScale
1163+
if (scale == lastAppliedPluginFontScale) {
1164+
return
1165+
}
1166+
lastAppliedPluginFontScale = scale
1167+
1168+
val pluginFragments = mutableListOf<Fragment>()
1169+
collectPluginFragments(supportFragmentManager, pluginFragments)
1170+
pluginFragments.forEach { fragment ->
1171+
fragment.view?.let { applyPluginFontScale(it, scale) }
1172+
}
1173+
}
1174+
1175+
private fun isPluginFragment(fragment: Fragment): Boolean =
1176+
fragment.javaClass.classLoader !== javaClass.classLoader
1177+
1178+
private fun applyPluginFontScale(root: View, scale: Float) {
1179+
root.forEachViewRecursively { view ->
1180+
if (view is TextView) {
1181+
val baseSize = pluginTextBaseSizes.getOrPut(view) { view.textSize }
1182+
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, baseSize * scale)
1183+
}
1184+
}
1185+
}
1186+
1187+
private fun collectPluginFragments(manager: FragmentManager, into: MutableList<Fragment>) {
1188+
manager.fragments.forEach { fragment ->
1189+
if (isPluginFragment(fragment)) {
1190+
into.add(fragment)
1191+
} else {
1192+
collectPluginFragments(fragment.childFragmentManager, into)
1193+
}
1194+
}
1195+
}
1196+
11161197
private fun showPluginCrashDialog(event: PluginCrashedEvent) {
11171198
val dialogView = layoutInflater.inflate(R.layout.dialog_plugin_crash, null)
11181199
dialogView.findViewById<TextView>(R.id.plugin_crash_message).text =

‎app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt‎

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
7777
},
7878
onSelectionChanged = {
7979
validateCommitButton()
80+
updateCheckAllButton()
8081
},
8182
onResolveConflict = { change ->
8283
viewModel.resolveConflict(change.path)
@@ -118,6 +119,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
118119
emptyView.visibility = View.VISIBLE
119120
emptyView.text = getString(R.string.not_a_git_repo)
120121
recyclerView.visibility = View.GONE
122+
btnCheckAll.visibility = View.GONE
121123
commitSection.visibility = View.GONE
122124
authorWarning.visibility = View.GONE
123125
commitHistoryButton.visibility = View.GONE
@@ -128,24 +130,32 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
128130
emptyView.visibility = View.VISIBLE
129131
emptyView.text = getString(R.string.no_uncommitted_changes)
130132
recyclerView.visibility = View.GONE
133+
btnCheckAll.visibility = View.GONE
131134
commitSection.visibility = View.GONE
132135
authorWarning.visibility = View.GONE
133136
commitHistoryButton.visibility = View.VISIBLE
134137
btnAbortMerge.visibility = View.GONE
135138
}
136139

137140
else -> {
141+
// Only offer "Check All" when there is at least one
142+
// non-conflicted file; conflicted files can't be staged.
143+
val hasSelectable = allChanges.any { it.type != ChangeType.CONFLICTED }
138144
binding.apply {
139145
emptyView.visibility = View.GONE
140146
recyclerView.visibility = View.VISIBLE
147+
btnCheckAll.visibility =
148+
if (hasSelectable) View.VISIBLE else View.GONE
141149
commitSection.visibility = View.VISIBLE
142150
authorWarning.visibility =
143151
if (hasAuthorInfo()) View.GONE else View.VISIBLE
144152
commitHistoryButton.visibility = View.VISIBLE
145153
btnAbortMerge.visibility =
146154
if (status.isMerging) View.VISIBLE else View.GONE
147155
}
148-
fileChangeAdapter.submitList(allChanges)
156+
fileChangeAdapter.submitList(allChanges) {
157+
updateCheckAllButton()
158+
}
149159
}
150160
}
151161
}.collectLatest { }
@@ -186,6 +196,14 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
186196
binding.commitSummary.doAfterTextChanged { validateCommitButton() }
187197
binding.commitDescription.doAfterTextChanged { validateCommitButton() }
188198

199+
binding.btnCheckAll.setOnClickListener {
200+
if (fileChangeAdapter.areAllSelected()) {
201+
fileChangeAdapter.clearSelection()
202+
} else {
203+
fileChangeAdapter.selectAll()
204+
}
205+
}
206+
189207
binding.btnAbortMerge.apply {
190208
setOnClickListener {
191209
val dialog = MaterialAlertDialogBuilder(requireContext())
@@ -228,6 +246,7 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
228246
binding.commitSummary.text?.clear()
229247
binding.commitDescription.text?.clear()
230248
fileChangeAdapter.selectedFiles.clear()
249+
updateCheckAllButton()
231250
}
232251
}
233252
}
@@ -279,12 +298,22 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) {
279298
}
280299

281300
private fun validateCommitButton() {
301+
// May be invoked from async adapter callbacks; bail if the view is gone.
302+
val binding = _binding ?: return
282303
val hasSummary = !binding.commitSummary.text.isNullOrBlank()
283304
val hasSelection = fileChangeAdapter.selectedFiles.isNotEmpty()
284305
val hasAuthor = hasAuthorInfo()
285306
binding.commitButton.isEnabled = hasSummary && hasSelection && hasAuthor
286307
}
287308

309+
private fun updateCheckAllButton() {
310+
// May be invoked from the async submitList commit callback; bail if the view is gone.
311+
val binding = _binding ?: return
312+
binding.btnCheckAll.setText(
313+
if (fileChangeAdapter.areAllSelected()) R.string.uncheck_all else R.string.check_all
314+
)
315+
}
316+
288317
private fun setupPullUI() {
289318
viewLifecycleOwner.lifecycleScope.launch {
290319
viewModel.isGitRepository.collectLatest { isRepo ->

0 commit comments

Comments
 (0)