Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
android:theme="@style/Theme.AndroidIDE" />
<activity
android:name=".activities.editor.EditorActivityKt"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|fontScale|density|uiMode"
android:launchMode="singleTask"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
android:windowSoftInputMode="adjustResize" />
<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ open class EditorHandlerActivity :
loadPluginTabs()
}

/**
* Persists which tabs are open (preferences only). Does **not** write project file buffers to disk;
* saving is explicit or prompted (e.g. close project).
*/
override fun onPause() {
super.onPause()
// Record timestamps for all currently open files before saving the cache
Expand All @@ -246,7 +250,6 @@ open class EditorHandlerActivity :
if (!isOpenedFilesSaved.get()) {
saveOpenedFiles()
saveOpenedPluginTabs()
saveAllAsync(notify = false)
}
}

Expand All @@ -271,26 +274,31 @@ open class EditorHandlerActivity :
invalidateOptionsMenu()
}

/**
* Reloads disk content into an open editor only when the file changed on disk since the last
* [onPause] snapshot **and** the in-memory buffer is still clean ([CodeEditorView.isModified] is
* false). Never replaces buffers with unsaved edits or touches undo history for dirty files.
*/
private fun checkForExternalFileChanges() {
// Get the list of files currently managed by the ViewModel
val openFiles = editorViewModel.getOpenedFiles()
if (openFiles.isEmpty() || fileTimestamps.isEmpty()) return

lifecycleScope.launch(Dispatchers.IO) {
// Check each open file
openFiles.forEach { file ->
val lastKnownTimestamp = fileTimestamps[file.absolutePath] ?: return@forEach
val currentTimestamp = file.lastModified()

// If the file on disk is newer.
if (currentTimestamp > lastKnownTimestamp) {
val newContent = runCatching { file.readText() }.getOrNull() ?: return@forEach
withContext(Dispatchers.Main) {
// If the editor for the new file exists AND has no unsaved changes...
val editorView = getEditorForFile(file) ?: return@withContext
if (editorView.isModified) return@withContext
val ideEditor = editorView.editor ?: return@withContext
if (ideEditor.canUndo() || ideEditor.canRedo()) {
return@withContext
}

editorView.editor?.setText(newContent)
ideEditor.setText(newContent)
editorView.markAsSaved()
Comment thread
coderabbitai[bot] marked this conversation as resolved.
updateTabs()
}
Expand Down Expand Up @@ -341,12 +349,19 @@ open class EditorHandlerActivity :
prefs.getString(PREF_KEY_OPEN_FILES_CACHE, null)
} ?: return@launch

if (editorViewModel.getOpenedFileCount() > 0) {
// Returning to an in-memory session (e.g. after onPause/onStop). Replaying the
// snapshot would be redundant and could interfere with dirty buffers and undo.
withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null) }
return@launch
}

val cache = withContext(Dispatchers.Default) {
Gson().fromJson(jsonCache, OpenedFilesCache::class.java)
}
onReadOpenedFilesCache(cache)

// Clear the preference so it's only loaded once on startup
// Clear the preference so it's only loaded once per cold restore
withContext(Dispatchers.IO) { prefs.putString(PREF_KEY_OPEN_FILES_CACHE, null) }
} catch (err: Throwable) {
log.error("Failed to reopen recently opened files", err)
Expand Down Expand Up @@ -747,6 +762,11 @@ open class EditorHandlerActivity :
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)

val safeContent = contentOrNull ?: return
for (i in 0 until safeContent.editorContainer.childCount) {
(safeContent.editorContainer.getChildAt(i) as? CodeEditorView)?.reapplyEditorDisplayPreferences()
}

getCurrentEditor()?.editor?.apply {
doOnNextLayout {
cursor?.let { c -> ensurePositionVisible(c.leftLine, c.leftColumn, true) }
Expand Down Expand Up @@ -1069,17 +1089,20 @@ open class EditorHandlerActivity :
nameBuilder.addPath(it, it.path)
}

for (index in 0 until content.tabs.tabCount) {
val file = files.getOrNull(index) ?: continue
for (tabPos in 0 until content.tabs.tabCount) {
if (isPluginTab(tabPos)) continue
val fileIndex = getFileIndexForTabPosition(tabPos)
if (fileIndex < 0) continue
val file = files.getOrNull(fileIndex) ?: continue
val count = dupliCount[file.name] ?: 0

val isModified = getEditorAtIndex(index)?.isModified ?: false
val isModified = getEditorAtIndex(fileIndex)?.isModified ?: false
var name = if (count > 1) nameBuilder.getShortPath(file) else file.name
if (isModified) {
name = "*$name"
}

names[index] = name to FileExtension.Factory.forFile(file, file.isDirectory).icon
names[tabPos] = name to FileExtension.Factory.forFile(file, file.isDirectory).icon
}

withContext(Dispatchers.Main) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,15 @@ package com.itsaky.androidide.fragments
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.GestureDetector
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import com.itsaky.androidide.databinding.FragmentRecyclerviewBinding
import androidx.viewbinding.ViewBinding
import com.itsaky.androidide.R
import com.itsaky.androidide.idetooltips.TooltipManager

/**
Expand All @@ -34,7 +37,7 @@ import com.itsaky.androidide.idetooltips.TooltipManager
* @author Akash Yadav
*/
abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
EmptyStateFragment<FragmentRecyclerviewBinding>(FragmentRecyclerviewBinding::inflate) {
EmptyStateFragment<FragmentRecyclerviewManualBinding>(FragmentRecyclerviewManualBinding::inflate) {
protected abstract val fragmentTooltipTag: String?

private var unsavedAdapter: A? = null
Expand Down Expand Up @@ -86,7 +89,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
* Sets up the recycler view in the fragment.
*/
protected open fun onSetupRecyclerView() {
binding.root.apply {
binding.list.apply {
layoutManager = onCreateLayoutManager()
adapter = unsavedAdapter ?: onCreateAdapter()
}
Expand All @@ -107,7 +110,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :

onSetupRecyclerView()

binding.root.addOnItemTouchListener(touchListener)
binding.list.addOnItemTouchListener(touchListener)

unsavedAdapter = null

Expand All @@ -123,7 +126,7 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :
* Set the adapter for the [RecyclerView].
*/
fun setAdapter(adapter: A) {
_binding?.root?.let { list -> list.adapter = adapter } ?: run { unsavedAdapter = adapter }
_binding?.list?.let { list -> list.adapter = adapter } ?: run { unsavedAdapter = adapter }
if (isAdded && view != null) {
checkIsEmpty()
}
Expand All @@ -142,6 +145,33 @@ abstract class RecyclerViewFragment<A : RecyclerView.Adapter<*>> :

private fun checkIsEmpty() {
if (!isAdded || isDetached) return
isEmpty = _binding?.root?.adapter?.itemCount == 0
isEmpty = _binding?.list?.adapter?.itemCount == 0
}
}

/**
* Manual [ViewBinding] for [R.layout.fragment_recyclerview] so annotation processors (kapt) do not
* depend on generated `FragmentRecyclerviewBinding` during stub analysis.
*
* Public (not internal/file-private): [RecyclerViewFragment] is public and Kotlin forbids a public
* class from using a non-public type as a [EmptyStateFragment] type argument.
*
* [getRoot] returns [RecyclerView] (covariant override), matching generated view binding so
* subclasses can use `binding.root.adapter` and other [RecyclerView] APIs.
*/
class FragmentRecyclerviewManualBinding(
val list: RecyclerView,
) : ViewBinding {
override fun getRoot(): RecyclerView = list

companion object {
fun inflate(
inflater: LayoutInflater,
parent: ViewGroup?,
attachToParent: Boolean,
): FragmentRecyclerviewManualBinding {
val root = inflater.inflate(R.layout.fragment_recyclerview, parent, false) as RecyclerView
return FragmentRecyclerviewManualBinding(root)
}
}
}
9 changes: 9 additions & 0 deletions app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,15 @@ class CodeEditorView(
onPinLineNumbersPrefChanged()
}

/**
* Re-applies display-related preferences (font size, typeface, flags) after a configuration change
* such as system font scale, so the editor activity can handle `fontScale` without being recreated.
*/
fun reapplyEditorDisplayPreferences() {
if (_binding == null) return
configureEditorIfNeeded()
}

private fun onMagnifierPrefChanged() {
binding.editor.getComponent(Magnifier::class.java).isEnabled =
EditorPreferences.useMagnifier
Expand Down
Loading