Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ package com.itsaky.androidide.fragments.output

import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.CheckedTextView
import android.widget.LinearLayout
import androidx.appcompat.widget.ListPopupWindow
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import com.itsaky.androidide.R
Expand All @@ -27,13 +31,16 @@ import com.itsaky.androidide.editor.ui.EditorSearchLayout
import com.itsaky.androidide.editor.ui.IDEEditor
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.models.LogFilter
import com.itsaky.androidide.preferences.internal.EditorPreferences
import com.itsaky.androidide.utils.BasicBuildInfo
import com.itsaky.androidide.utils.dpToPx
import com.itsaky.androidide.utils.flashInfo
import com.itsaky.androidide.viewmodel.BuildOutputViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
Expand All @@ -43,7 +50,8 @@ import kotlinx.coroutines.withTimeoutOrNull

class BuildOutputFragment :
NonEditableEditorFragment(),
SearchableOutputFragment {
SearchableOutputFragment,
ViewOptionsOutputFragment {
private val buildOutputViewModel: BuildOutputViewModel by activityViewModels()

companion object {
Expand Down Expand Up @@ -75,6 +83,7 @@ class BuildOutputFragment :
super.onViewCreated(view, savedInstanceState)
editor?.tag = TooltipTag.PROJECT_BUILD_OUTPUT
emptyStateViewModel.setEmptyMessage(getString(R.string.msg_emptyview_buildoutput))
setLineNumbersEnabled(buildOutputViewModel.showLineNumbers.value)
setupSearchLayout()

viewLifecycleOwner.lifecycleScope.launch {
Expand All @@ -85,21 +94,31 @@ class BuildOutputFragment :
buildOutputViewModel.setCachedSnapshot(content)
}
launch {
buildOutputViewModel.filterText.drop(1).collectLatest { query ->
renderFiltered(query)
combine(
buildOutputViewModel.filterText,
buildOutputViewModel.showTimestamps,
buildOutputViewModel.showDeltas,
) { query, ts, deltas ->
Triple(query, ts, deltas)
}.drop(1).collectLatest { (query, ts, deltas) ->
renderFiltered(query, ts, deltas)
}
}
}
}

/** Re-renders the editor window from the session file, filtered by [query]. */
private suspend fun renderFiltered(query: String) {
/** Re-renders the editor window from the session file, filtered by [query] and visibility options. */
private suspend fun renderFiltered(
query: String = buildOutputViewModel.filterText.value,
showTimestamps: Boolean = buildOutputViewModel.showTimestamps.value,
showDeltas: Boolean = buildOutputViewModel.showDeltas.value,
) {
editorContentMutex.withLock {
editorContentGeneration++
val window = withContext(Dispatchers.IO) { buildOutputViewModel.getWindowForEditor() }
val filtered =
withContext(Dispatchers.Default) {
BuildOutputViewModel.filterLines(window, query)
BuildOutputViewModel.filterLines(window, query, showTimestamps, showDeltas)
}
withContext(Dispatchers.Main) {
editor?.setText(filtered)
Expand Down Expand Up @@ -127,6 +146,17 @@ class BuildOutputFragment :
searchLayout?.beginSearchMode()
}

/**
* Enables or disables editor line numbers and updates the gutter divider width accordingly.
*
* @param enabled `true` to display line numbers and gutter divider, `false` to hide them.
*/
fun setLineNumbersEnabled(enabled: Boolean) {
val ed = editor ?: return
ed.setLineNumberEnabled(enabled)
ed.setDividerWidth((if (enabled) requireContext().dpToPx(2f) else 0).toFloat())
}

override fun toggleFilterBar() {
val existing = filterBar
existing?.toggle() ?: createFilterBar()
Expand All @@ -152,6 +182,76 @@ class BuildOutputFragment :
this.searchLayout = searchLayout
}

private data class ViewOptionItem(
val title: String,
var isChecked: Boolean,
val onToggle: (Boolean) -> Unit,
)

override fun showViewOptions(anchorView: View) {
val context = anchorView.context
val options =
listOf(
ViewOptionItem(
title = context.getString(R.string.log_filter_line_numbers),
isChecked = buildOutputViewModel.showLineNumbers.value,
onToggle = { enabled ->
EditorPreferences.outputLineNumbers = enabled
buildOutputViewModel.showLineNumbers.value = enabled
setLineNumbersEnabled(enabled)
},
),
ViewOptionItem(
title = context.getString(R.string.log_filter_timestamps),
isChecked = buildOutputViewModel.showTimestamps.value,
onToggle = { enabled ->
EditorPreferences.outputTimestamps = enabled
buildOutputViewModel.showTimestamps.value = enabled
},
),
ViewOptionItem(
title = context.getString(R.string.log_filter_deltas),
isChecked = buildOutputViewModel.showDeltas.value,
onToggle = { enabled ->
EditorPreferences.outputDeltas = enabled
buildOutputViewModel.showDeltas.value = enabled
},
),
)

val adapter =
object : ArrayAdapter<String>(
context,
android.R.layout.simple_list_item_multiple_choice,
options.map { it.title },
) {
override fun getView(
position: Int,
convertView: View?,
parent: ViewGroup,
): View {
val view = super.getView(position, convertView, parent)
if (view is CheckedTextView) {
view.isChecked = options[position].isChecked
}
return view
}
}

val popup = ListPopupWindow(context)
popup.anchorView = anchorView
popup.setAdapter(adapter)
popup.width = context.dpToPx(200f)
popup.isModal = true
popup.setOnItemClickListener { _, _, position, _ ->
val item = options[position]
item.isChecked = !item.isChecked
item.onToggle(item.isChecked)
adapter.notifyDataSetChanged()
}
popup.show()
}

private fun createFilterBar(): LogFilterBarController? {
val stub = _binding?.filterBarStub ?: return null
val barBinding = LayoutLogFilterBarBinding.bind(stub.inflate())
Expand All @@ -176,7 +276,13 @@ class BuildOutputFragment :
private suspend fun restoreWindowFromViewModel() {
val window = withContext(Dispatchers.IO) { buildOutputViewModel.getWindowForEditor() }
val query = buildOutputViewModel.filterText.value
val content = BuildOutputViewModel.filterLines(window, query)
val content =
BuildOutputViewModel.filterLines(
window,
query,
buildOutputViewModel.showTimestamps.value,
buildOutputViewModel.showDeltas.value,
)
val isSourceEmpty = window.isBlank()
val isFilteredEmpty = content.isBlank()

Expand Down Expand Up @@ -311,7 +417,12 @@ class BuildOutputFragment :

// The session file always gets the full text; the editor only shows matching lines
val visibleText =
BuildOutputViewModel.filterLines(text, buildOutputViewModel.filterText.value)
BuildOutputViewModel.filterLines(
text,
buildOutputViewModel.filterText.value,
buildOutputViewModel.showTimestamps.value,
buildOutputViewModel.showDeltas.value,
)

withContext(Dispatchers.Main) {
updateEmptyState(isSourceEmpty = false, isFilterActive = isFilterActive)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ class LogFilterBarController(

init {
binding.levelChipsScroll.isVisible = showLevelChips

chipsByLevel.values.forEach { chip ->
chip.isVisible = showLevelChips
}

binding.filterInput.setText(initialText)
chipsByLevel.forEach { (level, chip) ->
chip.isChecked = level in initialLevels
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* 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.output

import android.view.View

/**
* Interface for output fragments that support toggling display view options.
*/
interface ViewOptionsOutputFragment {
/**
* Shows the view options popup menu anchored to [anchorView].
*/
fun showViewOptions(anchorView: View)
}
Loading
Loading