fix: add preferences for the termux terminal

This commit is contained in:
Akash Yadav
2023-12-26 10:53:57 +05:30
parent 21d1722cdb
commit b929339dc7
22 changed files with 598 additions and 198 deletions
@@ -26,11 +26,7 @@ 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.AboutPreferences;
import com.itsaky.androidide.preferences.ConfigurationPreferences;
import com.itsaky.androidide.preferences.DeveloperOptionsPreferences;
import com.itsaky.androidide.preferences.IDEPreferences;
import com.itsaky.androidide.preferences.PrivacyPreferences;
import com.itsaky.androidide.preferences.RootPrefExtsKt;
import java.util.ArrayList;
@@ -24,6 +24,7 @@ import android.view.ViewGroup
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceGroup
import com.google.android.material.transition.MaterialSharedAxis
import com.itsaky.androidide.R
import com.itsaky.androidide.preferences.IPreference
import com.itsaky.androidide.preferences.IPreferenceGroup
import com.itsaky.androidide.preferences.IPreferenceScreen
@@ -78,7 +79,6 @@ class IDEPreferencesFragment : BasePreferenceFragment() {
}
companion object {
const val EXTRA_CHILDREN = "idepref_children"
const val EXTRA_CHILDREN = "ide.preferences.fragment.children"
}
}
@@ -101,7 +101,7 @@ private class GradleDistrubution(
override val icon: Int? = drawable.ic_gradle,
) : EditTextPreference() {
override fun onPreferenceChanged(preferece: Preference, newValue: Any?): Boolean {
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
gradleInstallationDir = newValue as String? ?: ""
return true
}
@@ -17,28 +17,34 @@
package com.itsaky.androidide.preferences
import android.content.Context
import androidx.preference.Preference
import com.itsaky.androidide.utils.uncheckedCast
import kotlinx.parcelize.IgnoredOnParcel
import kotlin.reflect.KMutableProperty0
internal abstract class PropertyBasedMultiChoicePreference : MultiChoicePreference() {
@IgnoredOnParcel
private val choices by lazy { getProperties() }
abstract fun getProperties(): Map<String, KMutableProperty0<Boolean>>
override fun getCheckedItems(choices: Array<String>): BooleanArray {
return BooleanArray(choices.size) { this.choices[choices[it]]?.get() == true }
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val properties = getProperties()
val entries = Array(properties.size) { PreferenceChoices.Entry.EMPTY }
var index = 0
properties.forEach { (key, property) ->
entries[index] = PreferenceChoices.Entry(key, property.get(), property)
++index
}
return entries
}
override fun getChoices(context: Context): Array<String> {
return choices.keys.toTypedArray()
}
override fun onChoicesConfirmed(selectedPositions: IntArray, selections: Map<String, Boolean>) {
selections.forEach { (key, value) ->
choices[key]?.set(value)
override fun onChoicesConfirmed(
preference: Preference,
entries: Array<PreferenceChoices.Entry>
) {
entries.forEach { entry ->
uncheckedCast<KMutableProperty0<Boolean>>(entry.data).set(entry.isChecked)
}
}
}
@@ -23,6 +23,7 @@ import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.itsaky.androidide.R
import com.itsaky.androidide.databinding.LayoutTextSizeSliderBinding
import com.itsaky.androidide.editor.schemes.IDEColorScheme
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.preferences.internal.AUTO_SAVE
import com.itsaky.androidide.preferences.internal.COLOR_SCHEME
@@ -176,26 +177,25 @@ private class TabSize(
override val dialogCancellable = true
@IgnoredOnParcel
private val choices = arrayOf("2", "4", "6", "8")
private val tabSizes = intArrayOf(2, 4, 6, 8)
override fun getChoices(context: Context): Array<String> {
return choices
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val currentTabSize = tabSize
return Array(tabSizes.size) { index ->
PreferenceChoices.Entry(
label = tabSizes[index].toString(),
_isChecked = currentTabSize == tabSizes[index],
data = tabSizes[index]
)
}
}
override fun getInitiallySelectionItemPosition(context: Context): Int {
var current = tabSize / 2 - 1
if (current < 0 || current >= choices.size) {
current = 1
}
return current
}
override fun onChoiceConfirmed(position: Int) {
var size = (position + 1) * 2
if (size < 2 || size > 8) {
size = 4
}
tabSize = size
override fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {
tabSize = entry.data as Int
}
}
@@ -213,16 +213,23 @@ private class ColorSchemePreference(
@IgnoredOnParcel
private val schemes = IDEColorSchemeProvider.list()
override fun getChoices(context: Context): Array<String> {
return schemes.map { it.name }.toTypedArray()
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val currentColorScheme = colorScheme
return Array(schemes.size) { index ->
PreferenceChoices.Entry(
schemes[index].name,
currentColorScheme == schemes[index].key,
schemes[index]
)
}
}
override fun getInitiallySelectionItemPosition(context: Context): Int {
return schemes.indexOfFirst { it.key == colorScheme }
}
override fun onChoiceConfirmed(position: Int) {
colorScheme = schemes[position].key
override fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {
colorScheme = (entry.data as IDEColorScheme).key
}
}
@@ -19,6 +19,7 @@ package com.itsaky.androidide.preferences
import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import com.itsaky.androidide.R
import com.itsaky.androidide.preferences.internal.CONFIRM_PROJECT_OPEN
@@ -38,6 +39,7 @@ import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.resources.localization.LocaleProvider
import com.itsaky.androidide.ui.themes.IDETheme
import com.itsaky.androidide.ui.themes.IThemeManager
import com.itsaky.androidide.ui.themes.ThemeManager
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@@ -106,28 +108,28 @@ class UiMode(
@IgnoredOnParcel
override val dialogCancellable = true
override fun getChoices(context: Context): Array<String> {
return arrayOf(
context.getString(R.string.uiMode_light),
context.getString(R.string.uiMode_dark),
context.getString(R.string.uiMode_system)
)
}
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val context = preference.context
val currentUiMode = uiMode
override fun getInitiallySelectionItemPosition(context: Context): Int {
return when (uiMode) {
AppCompatDelegate.MODE_NIGHT_NO -> 0
AppCompatDelegate.MODE_NIGHT_YES -> 1
else -> 2
return Array(3) { index ->
val (label, mode) = when (index) {
0 -> context.getString(R.string.uiMode_light) to AppCompatDelegate.MODE_NIGHT_NO
1 -> context.getString(R.string.uiMode_dark) to AppCompatDelegate.MODE_NIGHT_YES
2 -> context.getString(R.string.uiMode_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
else -> throw IllegalStateException("Invalid index")
}
PreferenceChoices.Entry(label, currentUiMode == mode, mode)
}
}
override fun onChoiceConfirmed(position: Int) {
uiMode = when (position) {
0 -> AppCompatDelegate.MODE_NIGHT_NO
1 -> AppCompatDelegate.MODE_NIGHT_YES
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
}
override fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {
uiMode = entry.data as Int
}
}
@@ -140,18 +142,27 @@ class ThemeSelector(
) : SingleChoicePreference() {
@IgnoredOnParcel
private val themes = IDETheme.values()
private val themes = IDETheme.entries
override fun getChoices(context: Context): Array<String> {
return themes.map { context.getString(it.title) }.toTypedArray()
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val context = preference.context
val currentTheme = IThemeManager.getInstance().getCurrentTheme()
return Array(themes.size) { index ->
val ideTheme = themes[index]
PreferenceChoices.Entry(
label = ContextCompat.getString(context, ideTheme.title),
_isChecked = currentTheme.name == ideTheme.name,
data = ideTheme
)
}
}
override fun getInitiallySelectionItemPosition(context: Context): Int {
return themes.indexOf(IThemeManager.getInstance().getCurrentTheme())
}
override fun onChoiceConfirmed(position: Int) {
selectedTheme = themes[position].name
override fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {
selectedTheme = (entry.data as IDETheme).name
}
}
@@ -163,39 +174,37 @@ class LocaleSelector(
override val icon: Int? = R.drawable.ic_translate
) : SingleChoicePreference() {
@IgnoredOnParcel
private val locales by lazy {
mutableListOf<String>().apply {
add("")
addAll(LocaleProvider.SUPPORTED_LOCALES.keys)
}
}
override fun getChoices(context: Context): Array<String> {
return locales.mapIndexed { index, locale ->
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val context = preference.context
val currentLocale = selectedLocale
val supportedLocales = LocaleProvider.SUPPORTED_LOCALES.keys.toList()
return Array(supportedLocales.size + 1) { index ->
if (index == 0) {
context.getString(R.string.locale_system_default)
PreferenceChoices.Entry(
label = ContextCompat.getString(context, R.string.locale_system_default),
_isChecked = selectedLocale == null,
data = 0
)
} else {
LocaleProvider.getLocale(locale)!!.let { it.getDisplayName(it) }
val localeKey = supportedLocales[index - 1]
val locale = LocaleProvider.getLocale(localeKey)!!
PreferenceChoices.Entry(
label = locale.getDisplayName(locale),
_isChecked = currentLocale == localeKey,
data = localeKey
)
}
}.toTypedArray()
}
override fun getInitiallySelectionItemPosition(context: Context): Int {
val applicationLocales = AppCompatDelegate.getApplicationLocales()
val locale = if (applicationLocales.size() > 0) {
applicationLocales.get(0)
} else {
LocaleProvider.getLocale(selectedLocale)
}
val key = LocaleProvider.getKey(locale) ?: return 0
return locales.indexOf(key).coerceAtLeast(0)
}
override fun onChoiceConfirmed(position: Int) {
// Reset to null if 'System Default' is selected
selectedLocale = if (position == 0) null else locales[position]
override fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {
selectedLocale = entry.data.let { localeKey ->
if (localeKey is Int) null else localeKey as String
}
}
}
@@ -213,7 +222,7 @@ class OpenLastProject(
return pref
}
override fun onPreferenceChanged(preferece: Preference, newValue: Any?): Boolean {
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
autoOpenProjects = newValue as Boolean? ?: autoOpenProjects
return true
}
@@ -233,7 +242,7 @@ class ConfirmProjectOpen(
return pref
}
override fun onPreferenceChanged(preferece: Preference, newValue: Any?): Boolean {
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
confirmProjectOpen = newValue as Boolean? ?: confirmProjectOpen
return true
}
@@ -253,7 +262,7 @@ class UseSytemShell(
return pref
}
override fun onPreferenceChanged(preferece: Preference, newValue: Any?): Boolean {
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
useSystemShell = newValue as Boolean? ?: useSystemShell
return true
}
@@ -17,7 +17,6 @@
package com.itsaky.androidide.preferences
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.resources.R.string
import kotlinx.parcelize.Parcelize
@@ -39,6 +38,7 @@ class ConfigurationPreferences(
addPreference(GeneralPreferences())
addPreference(EditorPreferences())
addPreference(BuildAndRunPreferences())
addPreference(TermuxPreferences())
}
}
@@ -69,7 +69,7 @@ class DeveloperOptionsPreferences(
@Parcelize
class AboutPreferences(
override val key: String = "idepref_category_about",
override val title: Int = R.string.about,
override val title: Int = string.about,
override val children: List<IPreference> = mutableListOf()
) : IPreferenceGroup() {
@@ -0,0 +1,257 @@
/*
* 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.preferences
import android.content.Context
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.preference.Preference
import com.itsaky.androidide.R
import com.itsaky.androidide.app.IDEApplication
import com.termux.shared.logger.Logger
import com.termux.shared.termux.settings.preferences.TermuxAppSharedPreferences
import kotlinx.parcelize.Parcelize
import kotlin.reflect.KMutableProperty0
private const val KEY_TERMUX_PREFERENCES = "ide.preferences.terminal"
private const val KEY_TERMUX_DEBUGGING_PREFERENCES = "${KEY_TERMUX_PREFERENCES}.debugging"
private const val KEY_TERMUX_DEBUGGING_LOG_LEVEL_PREFERENCE = "${KEY_TERMUX_DEBUGGING_PREFERENCES}.logLevel"
private const val KEY_TERMUX_DEBUGGING_TERMINAL_VIEW_KEY_LOGGING_PREFERENCE = "${KEY_TERMUX_DEBUGGING_PREFERENCES}.terminalViewKeyLogging"
private const val KEY_TERMUX_DEBUGGING_CRASH_REPORT_NOTIFICATIONS_PREFERENCE = "${KEY_TERMUX_DEBUGGING_PREFERENCES}.crashReportNotifications"
private const val KEY_TERMUX_KBD_PREFERENCES = "${KEY_TERMUX_PREFERENCES}.keyboard"
private const val KEY_TERMUX_KBD_SOFT_KDB_ENABLED_PREFERENCE = "${KEY_TERMUX_KBD_PREFERENCES}.softKeyboardEnabled"
private const val KEY_TERMUX_KBD_SOFT_KDB_ONLY_IF_NO_HARD_KBD_PREFERENCE = "${KEY_TERMUX_KBD_PREFERENCES}.softKeyboardEnabled"
private const val KEY_TERMUX_VIEW_PREFERENCES = "${KEY_TERMUX_PREFERENCES}.view"
private const val KEY_TERMUX_VIEW_MARGIN_ADJUSTMENT_ENABLED_PREFERENCE = "${KEY_TERMUX_VIEW_PREFERENCES}.marginAdjustment"
abstract class TermuxSwitchPreference(
@StringRes private val summaryOn: Int,
@StringRes private val summaryOff: Int,
property: KMutableProperty0<Boolean>
) : SwitchPreference(property) {
override fun onCreatePreference(context: Context): Preference {
return super.onCreatePreference(context).also { preference ->
updateSummary(preference)
}
}
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
return super.onPreferenceChanged(preference, newValue).also {
updateSummary(preference)
}
}
private fun updateSummary(preference: Preference) {
preference.summary = (if (getValue?.invoke() == true) {
summaryOn
} else {
summaryOff
}).let { summary ->
ContextCompat.getString(preference.context, summary)
}
}
}
@Parcelize
class TermuxPreferences(
override val key: String = KEY_TERMUX_PREFERENCES,
override val title: Int = R.string.termux_preferences_title,
override val summary: Int? = R.string.termux_preferences_summary,
override val children: List<IPreference> = mutableListOf()
) : IPreferenceScreen() {
init {
addPreference(TermuxDebuggingPreferences())
addPreference(TermuxKeyboardPreferences())
addPreference(TermuxViewPreferences())
}
}
@Parcelize
class TermuxDebuggingPreferences(
override val key: String = KEY_TERMUX_DEBUGGING_PREFERENCES,
override val title: Int = R.string.termux_debugging_preferences_title,
override val children: List<IPreference> = mutableListOf()
) : IPreferenceGroup() {
init {
addPreference(TermuxDebuggingLogLevelPreference())
addPreference(TermuxDebuggingTerminalViewKeyLoggingPreference())
addPreference(TermuxDebuggingCrashReportNotificationsPreference())
}
}
@Parcelize
class TermuxDebuggingLogLevelPreference(
override val key: String = KEY_TERMUX_DEBUGGING_LOG_LEVEL_PREFERENCE,
override val title: Int = R.string.log_level_title,
override val icon: Int? = R.drawable.ic_bug
) : SingleChoicePreference() {
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val logLevels = Logger.getLogLevelsArray()
val logLevelLabels = Logger.getLogLevelLabelsArray(preference.context, logLevels, true)
val currentLogLevel = TermuxAppSharedPreferences.build(preference.context, false)?.logLevel
?: Logger.DEFAULT_LOG_LEVEL
return Array(logLevels.size) {
PreferenceChoices.Entry(logLevelLabels[it], currentLogLevel == logLevels[it],
logLevels[it])
}
}
override fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {
val newLevel = entry.data as Int
TermuxAppSharedPreferences.build(preference.context, false)
?.setLogLevel(preference.context, newLevel)
preference.summary = Logger.getLogLevelLabel(preference.context, newLevel, true)
}
override fun onCreatePreference(context: Context): Preference {
return super.onCreatePreference(context).also { preference ->
val currentLogLevel = TermuxAppSharedPreferences.build(preference.context, false)?.logLevel
?: Logger.DEFAULT_LOG_LEVEL
preference.summary = Logger.getLogLevelLabel(context, currentLogLevel, true)
}
}
}
private var isTerminalViewKeyLoggingEnabled: Boolean
get() = TermuxAppSharedPreferences.build(IDEApplication.instance,
true).isTerminalViewKeyLoggingEnabled
set(value) {
TermuxAppSharedPreferences.build(IDEApplication.instance,
true).isTerminalViewKeyLoggingEnabled = value
}
@Parcelize
class TermuxDebuggingTerminalViewKeyLoggingPreference(
override val key: String = KEY_TERMUX_DEBUGGING_TERMINAL_VIEW_KEY_LOGGING_PREFERENCE,
override val title: Int = R.string.termux_terminal_view_key_logging_enabled_title,
override val icon: Int? = R.drawable.ic_keyboard,
) : TermuxSwitchPreference(
summaryOn = R.string.termux_terminal_view_key_logging_enabled_on,
summaryOff = R.string.termux_terminal_view_key_logging_enabled_off,
property = ::isTerminalViewKeyLoggingEnabled
)
private var isTerminalCrashReportNotificationsEnabled: Boolean
get() = TermuxAppSharedPreferences.build(IDEApplication.instance,
true).areCrashReportNotificationsEnabled(false)
set(value) {
TermuxAppSharedPreferences.build(IDEApplication.instance,
true).setCrashReportNotificationsEnabled(value)
}
@Parcelize
class TermuxDebuggingCrashReportNotificationsPreference(
override val key: String = KEY_TERMUX_DEBUGGING_CRASH_REPORT_NOTIFICATIONS_PREFERENCE,
override val title: Int = R.string.termux_crash_report_notifications_enabled_title,
override val icon: Int? = R.drawable.ic_bell,
) : TermuxSwitchPreference(
summaryOn = R.string.termux_crash_report_notifications_enabled_on,
summaryOff = R.string.termux_crash_report_notifications_enabled_off,
property = ::isTerminalCrashReportNotificationsEnabled
)
@Parcelize
class TermuxKeyboardPreferences(
override val key: String = KEY_TERMUX_KBD_PREFERENCES,
override val title: Int = R.string.termux_keyboard_header,
override val children: List<IPreference> = mutableListOf()
) : IPreferenceGroup() {
init {
addPreference(TermuxKbdSoftKbdEnabledPreference())
addPreference(TermuxKbdSoftKbdOnlyIfNoHardKbdEnabledPreference())
}
}
private var isSoftKbdEnabled: Boolean
get() = TermuxAppSharedPreferences.build(IDEApplication.instance,
true).isSoftKeyboardEnabled
set(value) {
TermuxAppSharedPreferences.build(IDEApplication.instance,
true).isSoftKeyboardEnabled = value
}
@Parcelize
class TermuxKbdSoftKbdEnabledPreference(
override val key: String = KEY_TERMUX_KBD_SOFT_KDB_ENABLED_PREFERENCE,
override val title: Int = R.string.termux_soft_keyboard_enabled_title,
override val icon: Int? = R.drawable.ic_keyboard_soft,
) : TermuxSwitchPreference(
summaryOn = R.string.termux_soft_keyboard_enabled_on,
summaryOff = R.string.termux_soft_keyboard_enabled_off,
property = ::isSoftKbdEnabled
)
private var isSoftKbdOnlyIfNoHardKbdEnabled: Boolean
get() = TermuxAppSharedPreferences.build(IDEApplication.instance,
true).isSoftKeyboardEnabledOnlyIfNoHardware
set(value) {
TermuxAppSharedPreferences.build(IDEApplication.instance,
true).isSoftKeyboardEnabledOnlyIfNoHardware = value
}
@Parcelize
class TermuxKbdSoftKbdOnlyIfNoHardKbdEnabledPreference(
override val key: String = KEY_TERMUX_KBD_SOFT_KDB_ONLY_IF_NO_HARD_KBD_PREFERENCE,
override val title: Int = R.string.termux_soft_keyboard_enabled_only_if_no_hardware_title,
override val icon: Int? = R.drawable.ic_keyboard,
) : TermuxSwitchPreference(
summaryOn = R.string.termux_soft_keyboard_enabled_only_if_no_hardware_on,
summaryOff = R.string.termux_soft_keyboard_enabled_only_if_no_hardware_off,
property = ::isSoftKbdOnlyIfNoHardKbdEnabled
)
@Parcelize
class TermuxViewPreferences(
override val key: String = KEY_TERMUX_VIEW_PREFERENCES,
override val title: Int = R.string.termux_terminal_view_view_header,
override val children: List<IPreference> = mutableListOf()
) : IPreferenceGroup() {
init {
addPreference(TermuxViewMarginAdjustmentEnabledPreference())
}
}
private var isViewMarginAdjustmentEnabled: Boolean
get() = TermuxAppSharedPreferences.build(IDEApplication.instance,
true).isTerminalMarginAdjustmentEnabled
set(value) {
TermuxAppSharedPreferences.build(IDEApplication.instance,
true).setTerminalMarginAdjustment(value)
}
@Parcelize
class TermuxViewMarginAdjustmentEnabledPreference(
override val key: String = KEY_TERMUX_VIEW_MARGIN_ADJUSTMENT_ENABLED_PREFERENCE,
override val title: Int = R.string.termux_terminal_view_terminal_margin_adjustment_title,
override val icon: Int? = R.drawable.ic_space,
) : TermuxSwitchPreference(
summaryOn = R.string.termux_terminal_view_terminal_margin_adjustment_on,
summaryOff = R.string.termux_terminal_view_terminal_margin_adjustment_off,
property = ::isViewMarginAdjustmentEnabled
)
@@ -18,6 +18,7 @@
package com.itsaky.androidide.preferences
import android.content.Context
import androidx.preference.Preference
import com.itsaky.androidide.resources.R.string
import com.itsaky.androidide.preferences.internal.CLOSING_BRACKET_NEW_LINE
import com.itsaky.androidide.preferences.internal.EMPTY_ELEMENTS_BEHAVIOR
@@ -230,15 +231,24 @@ private class EmptyElementsBehavior(
@IgnoredOnParcel
override val dialogCancellable = true
override fun getInitiallySelectionItemPosition(context: Context): Int {
return EmptyElements.valueOf(emptyElementsBehavior).ordinal
override fun getEntries(preference: Preference): Array<PreferenceChoices.Entry> {
val entries = EmptyElements.entries
val currentBehavior = EmptyElements.valueOf(emptyElementsBehavior)
return Array(entries.size) { index ->
PreferenceChoices.Entry(
label = entries[index].toString(),
_isChecked = currentBehavior == entries[index],
data = entries[index]
)
}
}
override fun getChoices(context: Context): Array<String> {
return EmptyElements.values().map { it.toString() }.toTypedArray()
}
override fun onChoiceConfirmed(position: Int) {
emptyElementsBehavior = EmptyElements.values()[position].toString()
override fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {
emptyElementsBehavior = (entry.data as EmptyElements).toString()
}
}
@@ -53,7 +53,7 @@ abstract class BasePreference : IPreference() {
return pref
}
protected open fun onPreferenceChanged(preferece: Preference, newValue: Any?): Boolean {
protected open fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
return false
}
@@ -17,49 +17,45 @@
package com.itsaky.androidide.preferences
import androidx.annotation.CallSuper
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
/**
* Base class for dialog preferences which allows users to choose from multiple items.
* Base class for dialog preferences which allows users to choose from multiple items. Subclasses
* must call [onSelectionChanged] to notify about selection changes.
*
* @author Akash Yadav
*/
abstract class ChoiceBasedDialogPreference : DialogPreference(), PreferenceChoices {
private var checkedPositions = intArrayOf()
private var choices = emptyArray<PreferenceChoices.Entry>()
final override fun onConfigureDialog(preference: Preference, dialog: MaterialAlertDialogBuilder) {
val choices = getChoices(preference.context)
val checkedItems = getCheckedItems(choices)
choices = getEntries(preference)
checkedPositions = checkedItems?.map { if(it) 1 else 0 }?.toIntArray() ?: intArrayOf()
onConfigureDialogChoices(preference, dialog, choices, checkedItems)
val selections = BooleanArray(choices.size) { choices[it].isChecked }
onConfigureDialogChoices(preference, dialog, choices, selections)
dialog.setPositiveButton(android.R.string.ok) { dialogInterface, _ ->
dialogInterface.dismiss()
val selections = HashMap<String, Boolean>(choices.size)
for (i in 0..checkedPositions.lastIndex) {
selections[choices[i]] = checkedPositions[i] == 1
}
onChoicesConfirmed(checkedPositions, selections)
onChoicesConfirmed(preference, choices)
}
dialog.setNegativeButton(android.R.string.cancel) { dialogInterface, _ ->
dialogInterface.dismiss()
onChoicesCancelled()
onChoicesCancelled(preference)
}
}
override fun onSelectionChanged(position: Int, isSelected: Boolean) {
if (position < 0 || position >= checkedPositions.size) {
return
}
checkedPositions[position] = if (isSelected) 1 else 0
@CallSuper
override fun onSelectionChanged(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int,
isSelected: Boolean
) {
entry._isChecked = isSelected
}
/**
@@ -68,11 +64,12 @@ abstract class ChoiceBasedDialogPreference : DialogPreference(), PreferenceChoic
protected abstract fun onConfigureDialogChoices(
preference: Preference,
dialog: MaterialAlertDialogBuilder,
choices: Array<String>,
checkedItems: BooleanArray?
entries: Array<PreferenceChoices.Entry>,
selections: BooleanArray
)
override fun onChoicesConfirmed(selectedPositions: IntArray, selections: Map<String, Boolean>) {}
override fun onChoicesConfirmed(preference: Preference, entries: Array<PreferenceChoices.Entry>) {
}
override fun onChoicesCancelled() {}
override fun onChoicesCancelled(preference: Preference) {}
}
@@ -34,15 +34,17 @@ abstract class MultiChoicePreference : ChoiceBasedDialogPreference(), Preference
override fun onConfigureDialogChoices(
preference: Preference,
dialog: MaterialAlertDialogBuilder,
choices: Array<String>,
checkedItems: BooleanArray?
entries: Array<PreferenceChoices.Entry>,
selections: BooleanArray
) {
val labels = Array(entries.size) { entries[it].label }
dialog.setMultiChoiceItems(
choices,
checkedItems!!
labels,
selections
) { _, which, checked ->
onSelectionChanged(which, checked)
onSelectionChanged(preference, entries[which], which, checked)
}
}
}
@@ -59,7 +59,7 @@ abstract class NumberInputEditTextPreference(
}
}
override fun onPreferenceChanged(preferece: Preference, newValue: Any?): Boolean {
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
setValue?.let { it((newValue as String?)?.toInt() ?: 0) }
return true
}
@@ -17,8 +17,8 @@
package com.itsaky.androidide.preferences
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.annotation.RestrictTo
import androidx.preference.Preference
/**
* A preference with choices.
@@ -27,15 +27,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
*/
interface PreferenceChoices {
/** Get the choices to show in the preference. */
fun getChoices(context: Context): Array<String>
/**
* Get all checked and unchecked items.
*
* @see MaterialAlertDialogBuilder.setMultiChoiceItems
* Get the entries for this preference.
*/
fun getCheckedItems(choices: Array<String>): BooleanArray?
fun getEntries(preference: Preference): Array<Entry>
/**
* Called when an item is selected from the single choice list.
@@ -43,17 +38,46 @@ interface PreferenceChoices {
* @param position The position of the selected item.
* @param isSelected Whether the item is selected.
*/
fun onSelectionChanged(position: Int, isSelected: Boolean)
fun onSelectionChanged(preference: Preference, entry: Entry, position: Int, isSelected: Boolean)
/**
* Called when the user confirms the selections.
*
* @param selectedPositions The positions of the selected items.
* @param entries The entries.
*/
fun onChoicesConfirmed(selectedPositions: IntArray, selections: Map<String, Boolean>)
fun onChoicesConfirmed(preference: Preference, entries: Array<Entry>)
/**
* Called when the user cancels the selections.
*/
fun onChoicesCancelled()
fun onChoicesCancelled(preference: Preference)
/**
* Entry in [PreferenceChoices].
*
* @property label The label for the entry.
* @property isChecked Whether the item is checked or not.
* @property data The data object for the value.
*/
data class Entry(
val label: CharSequence,
@Suppress("PropertyName") internal var _isChecked: Boolean,
val data: Any,
) {
val isChecked: Boolean
get() = _isChecked
companion object {
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
val EMPTY = Entry("", false, 0)
}
}
}
/**
* Map this [PreferenceChoices.Entry] array to entry labels.
*/
internal val Array<PreferenceChoices.Entry>.labels: Array<CharSequence>
get() = Array(size) { this[it].label }
@@ -17,10 +17,8 @@
package com.itsaky.androidide.preferences
import android.content.Context
import androidx.preference.Preference
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.lang.IndexOutOfBoundsException
/**
* A preference which allows selecting a single value from a list of values.
@@ -39,49 +37,44 @@ abstract class SingleChoicePreference : ChoiceBasedDialogPreference(), Preferenc
*/
protected open var currentSelection: Int = -1
/**
* Get the index of the selected item.
* @see MaterialAlertDialogBuilder.setSingleChoiceItems
*/
abstract fun getInitiallySelectionItemPosition(context: Context): Int
final override fun getCheckedItems(choices: Array<String>): BooleanArray? {
return null
}
override fun onConfigureDialogChoices(
preference: Preference,
dialog: MaterialAlertDialogBuilder,
choices: Array<String>,
checkedItems: BooleanArray?
entries: Array<PreferenceChoices.Entry>,
selections: BooleanArray
) {
this.currentSelection = getInitiallySelectionItemPosition(preference.context)
if (currentSelection < 0 || currentSelection >= choices.size) {
throw IndexOutOfBoundsException("Initial selection: $currentSelection, size=${choices.size}")
val checkedCount = entries.count { it.isChecked }
if (checkedCount != 1) {
throw IllegalArgumentException(
"Entries must have exactly one item checked, however, $checkedCount were checked")
}
currentSelection = entries.indexOfFirst { it.isChecked }
dialog.setSingleChoiceItems(
choices,
entries.labels,
currentSelection
)
{ _, position ->
if (currentSelection != -1) {
onSelectionChanged(currentSelection, false)
onSelectionChanged(preference, entries[currentSelection], currentSelection, false)
}
currentSelection = position
onSelectionChanged(position, true)
onSelectionChanged(preference, entries[currentSelection], position, true)
}
}
final override fun onChoicesConfirmed(
selectedPositions: IntArray,
selections: Map<String, Boolean>
override fun onChoicesConfirmed(
preference: Preference,
entries: Array<PreferenceChoices.Entry>
) {
onChoiceConfirmed(currentSelection)
onChoiceConfirmed(preference, entries[currentSelection], currentSelection)
}
protected open fun onChoiceConfirmed(position: Int) {}
protected open fun onChoiceConfirmed(
preference: Preference,
entry: PreferenceChoices.Entry,
position: Int
) {}
}
@@ -19,6 +19,7 @@ package com.itsaky.androidide.preferences
import android.content.Context
import androidx.preference.Preference
import kotlin.reflect.KMutableProperty0
/**
* A switch preference.
@@ -29,13 +30,16 @@ abstract class SwitchPreference
@JvmOverloads
constructor(val setValue: ((Boolean) -> Unit)? = null, val getValue: (() -> Boolean)? = null) :
BasePreference() {
constructor(property: KMutableProperty0<Boolean>) : this(property::set, property::get)
override fun onCreatePreference(context: Context): Preference {
val pref = androidx.preference.SwitchPreference(context)
pref.isChecked = prefValue()
return pref
}
override fun onPreferenceChanged(preferece: Preference, newValue: Any?): Boolean {
override fun onPreferenceChanged(preference: Preference, newValue: Any?): Boolean {
setValue?.let { it(newValue as Boolean? ?: prefValue()) }
return true
}
@@ -0,0 +1,26 @@
<?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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="#000000"
android:pathData="M10 21H14C14 22.1 13.1 23 12 23S10 22.1 10 21M21 19V20H3V19L5 17V11C5 7.9 7 5.2 10 4.3V4C10 2.9 10.9 2 12 2S14 2.9 14 4V4.3C17 5.2 19 7.9 19 11V17L21 19M17 11C17 8.2 14.8 6 12 6S7 8.2 7 11V18H17V11Z" />
</vector>
@@ -0,0 +1,20 @@
<?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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#000000" android:pathData="M20,8H17.19C16.74,7.2 16.12,6.5 15.37,6L17,4.41L15.59,3L13.42,5.17C12.96,5.06 12.5,5 12,5C11.5,5 11.05,5.06 10.59,5.17L8.41,3L7,4.41L8.62,6C7.87,6.5 7.26,7.21 6.81,8H4V10H6.09C6.03,10.33 6,10.66 6,11V12H4V14H6V15C6,15.34 6.03,15.67 6.09,16H4V18H6.81C8.47,20.87 12.14,21.84 15,20.18C15.91,19.66 16.67,18.9 17.19,18H20V16H17.91C17.97,15.67 18,15.34 18,15V14H20V12H18V11C18,10.66 17.97,10.33 17.91,10H20V8M16,15A4,4 0 0,1 12,19A4,4 0 0,1 8,15V11A4,4 0 0,1 12,7A4,4 0 0,1 16,11V15M14,10V12H10V10H14M10,14H14V16H10V14Z"/>
</vector>
@@ -0,0 +1,26 @@
<?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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="#000000"
android:pathData="M4,5A2,2 0 0,0 2,7V17A2,2 0 0,0 4,19H20A2,2 0 0,0 22,17V7A2,2 0 0,0 20,5H4M4,7H20V17H4V7M5,8V10H7V8H5M8,8V10H10V8H8M11,8V10H13V8H11M14,8V10H16V8H14M17,8V10H19V8H17M5,11V13H7V11H5M8,11V13H10V11H8M11,11V13H13V11H11M14,11V13H16V11H14M17,11V13H19V11H17M8,14V16H16V14H8Z" />
</vector>
@@ -0,0 +1,26 @@
<?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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24"
android:width="24dp">
<path
android:fillColor="#000000"
android:pathData="M6,16H18V18H6V16M6,13V15H2V13H6M7,15V13H10V15H7M11,15V13H13V15H11M14,15V13H17V15H14M18,15V13H22V15H18M2,10H5V12H2V10M19,12V10H22V12H19M18,12H16V10H18V12M8,12H6V10H8V12M12,12H9V10H12V12M15,12H13V10H15V12M2,9V7H4V9H2M5,9V7H7V9H5M8,9V7H10V9H8M11,9V7H13V9H11M14,9V7H16V9H14M17,9V7H22V9H17Z" />
</vector>
@@ -115,12 +115,9 @@
<string name="error_termux_service_start_failed_general">Failed to start TermuxService. Check logcat for exception message.</string>
<string name="error_termux_service_start_failed_bg">Failed to start TermuxService while app is in background due to android bg restrictions.</string>
<!-- Termux Settings -->
<string name="title_activity_termux_settings">&TERMUX_APP_NAME; Settings</string>
<!-- Termux App Preferences -->
<string name="termux_preferences_title">&TERMUX_APP_NAME;</string>
<string name="termux_preferences_summary">Preferences for &TERMUX_APP_NAME; app</string>
<string name="termux_preferences_title">Termux</string>
<string name="termux_preferences_summary">Preferences for the Termux terminal.</string>
<!-- Debugging Preferences -->
<string name="termux_debugging_preferences_title">Debugging</string>
@@ -403,22 +403,22 @@ public class Logger {
public static CharSequence[] getLogLevelsArray() {
return new CharSequence[]{
String.valueOf(LOG_LEVEL_OFF),
String.valueOf(LOG_LEVEL_NORMAL),
String.valueOf(LOG_LEVEL_DEBUG),
String.valueOf(LOG_LEVEL_VERBOSE)
public static int[] getLogLevelsArray() {
return new int[]{
LOG_LEVEL_OFF,
LOG_LEVEL_NORMAL,
LOG_LEVEL_DEBUG,
LOG_LEVEL_VERBOSE
};
}
public static CharSequence[] getLogLevelLabelsArray(Context context, CharSequence[] logLevels, boolean addDefaultTag) {
public static CharSequence[] getLogLevelLabelsArray(Context context, int[] logLevels, boolean addDefaultTag) {
if (logLevels == null) return null;
CharSequence[] logLevelLabels = new CharSequence[logLevels.length];
for(int i=0; i<logLevels.length; i++) {
logLevelLabels[i] = getLogLevelLabel(context, Integer.parseInt(logLevels[i].toString()), addDefaultTag);
logLevelLabels[i] = getLogLevelLabel(context, logLevels[i], addDefaultTag);
}
return logLevelLabels;