diff --git a/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java b/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java index 1d5371818..8cceb0def 100755 --- a/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java +++ b/app/src/main/java/com/itsaky/androidide/activities/PreferencesActivity.java @@ -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; diff --git a/app/src/main/java/com/itsaky/androidide/fragments/IDEPreferencesFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/IDEPreferencesFragment.kt index 66df2c4b1..7a75cf49e 100755 --- a/app/src/main/java/com/itsaky/androidide/fragments/IDEPreferencesFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/IDEPreferencesFragment.kt @@ -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" } } diff --git a/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt b/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt index 3ffaac728..d68d4baa0 100644 --- a/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt +++ b/app/src/main/java/com/itsaky/androidide/preferences/buildAndRunPrefExts.kt @@ -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 } diff --git a/app/src/main/java/com/itsaky/androidide/preferences/commonPrefExts.kt b/app/src/main/java/com/itsaky/androidide/preferences/commonPrefExts.kt index 84e7425bd..367202347 100644 --- a/app/src/main/java/com/itsaky/androidide/preferences/commonPrefExts.kt +++ b/app/src/main/java/com/itsaky/androidide/preferences/commonPrefExts.kt @@ -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> - override fun getCheckedItems(choices: Array): BooleanArray { - return BooleanArray(choices.size) { this.choices[choices[it]]?.get() == true } + override fun getEntries(preference: Preference): Array { + 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 { - return choices.keys.toTypedArray() - } - - override fun onChoicesConfirmed(selectedPositions: IntArray, selections: Map) { - selections.forEach { (key, value) -> - choices[key]?.set(value) + override fun onChoicesConfirmed( + preference: Preference, + entries: Array + ) { + entries.forEach { entry -> + uncheckedCast>(entry.data).set(entry.isChecked) } } } \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/preferences/editorPrefExts.kt b/app/src/main/java/com/itsaky/androidide/preferences/editorPrefExts.kt index 9c40b6bdd..be45b4e52 100644 --- a/app/src/main/java/com/itsaky/androidide/preferences/editorPrefExts.kt +++ b/app/src/main/java/com/itsaky/androidide/preferences/editorPrefExts.kt @@ -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 { - return choices + override fun getEntries(preference: Preference): Array { + 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 { - return schemes.map { it.name }.toTypedArray() + override fun getEntries(preference: Preference): Array { + 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 } } diff --git a/app/src/main/java/com/itsaky/androidide/preferences/generalPrefExts.kt b/app/src/main/java/com/itsaky/androidide/preferences/generalPrefExts.kt index 87625e916..d87e9da0a 100644 --- a/app/src/main/java/com/itsaky/androidide/preferences/generalPrefExts.kt +++ b/app/src/main/java/com/itsaky/androidide/preferences/generalPrefExts.kt @@ -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 { - 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 { + 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 { - return themes.map { context.getString(it.title) }.toTypedArray() + override fun getEntries(preference: Preference): Array { + 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().apply { - add("") - addAll(LocaleProvider.SUPPORTED_LOCALES.keys) - } - } - - override fun getChoices(context: Context): Array { - return locales.mapIndexed { index, locale -> + override fun getEntries(preference: Preference): Array { + 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 } diff --git a/app/src/main/java/com/itsaky/androidide/preferences/rootPrefExts.kt b/app/src/main/java/com/itsaky/androidide/preferences/rootPrefExts.kt index 703039e44..70529c56e 100644 --- a/app/src/main/java/com/itsaky/androidide/preferences/rootPrefExts.kt +++ b/app/src/main/java/com/itsaky/androidide/preferences/rootPrefExts.kt @@ -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 = mutableListOf() ) : IPreferenceGroup() { diff --git a/app/src/main/java/com/itsaky/androidide/preferences/termuxPrefsExt.kt b/app/src/main/java/com/itsaky/androidide/preferences/termuxPrefsExt.kt new file mode 100644 index 000000000..08d4c92d2 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/preferences/termuxPrefsExt.kt @@ -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 . + */ + +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 +) : 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 = 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 = 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 { + 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 = 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 = 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 +) \ No newline at end of file diff --git a/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefExts.kt b/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefExts.kt index 3f33a53ae..56851c17f 100644 --- a/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefExts.kt +++ b/app/src/main/java/com/itsaky/androidide/preferences/xmlPrefExts.kt @@ -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 { + 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 { - 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() } } diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/BasePreference.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/BasePreference.kt index eece3b3cd..c5a6c38a0 100644 --- a/preferences/src/main/java/com/itsaky/androidide/preferences/BasePreference.kt +++ b/preferences/src/main/java/com/itsaky/androidide/preferences/BasePreference.kt @@ -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 } diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/ChoiceBasedDialogPreference.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/ChoiceBasedDialogPreference.kt index 2a47fab12..e44dacb81 100644 --- a/preferences/src/main/java/com/itsaky/androidide/preferences/ChoiceBasedDialogPreference.kt +++ b/preferences/src/main/java/com/itsaky/androidide/preferences/ChoiceBasedDialogPreference.kt @@ -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() 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(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, - checkedItems: BooleanArray? + entries: Array, + selections: BooleanArray ) - override fun onChoicesConfirmed(selectedPositions: IntArray, selections: Map) {} + override fun onChoicesConfirmed(preference: Preference, entries: Array) { + } - override fun onChoicesCancelled() {} + override fun onChoicesCancelled(preference: Preference) {} } \ No newline at end of file diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/MultiChoicePreference.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/MultiChoicePreference.kt index 56d94cc6f..ba3e66178 100644 --- a/preferences/src/main/java/com/itsaky/androidide/preferences/MultiChoicePreference.kt +++ b/preferences/src/main/java/com/itsaky/androidide/preferences/MultiChoicePreference.kt @@ -34,15 +34,17 @@ abstract class MultiChoicePreference : ChoiceBasedDialogPreference(), Preference override fun onConfigureDialogChoices( preference: Preference, dialog: MaterialAlertDialogBuilder, - choices: Array, - checkedItems: BooleanArray? + entries: Array, + 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) } } } diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/NumberInputEditTextPreference.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/NumberInputEditTextPreference.kt index 0df67de92..a8b04280d 100644 --- a/preferences/src/main/java/com/itsaky/androidide/preferences/NumberInputEditTextPreference.kt +++ b/preferences/src/main/java/com/itsaky/androidide/preferences/NumberInputEditTextPreference.kt @@ -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 } diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/PreferenceChoices.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/PreferenceChoices.kt index b9afbbbd7..220ba8ad7 100644 --- a/preferences/src/main/java/com/itsaky/androidide/preferences/PreferenceChoices.kt +++ b/preferences/src/main/java/com/itsaky/androidide/preferences/PreferenceChoices.kt @@ -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 - /** - * Get all checked and unchecked items. - * - * @see MaterialAlertDialogBuilder.setMultiChoiceItems + * Get the entries for this preference. */ - fun getCheckedItems(choices: Array): BooleanArray? + fun getEntries(preference: Preference): Array /** * 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) + fun onChoicesConfirmed(preference: Preference, entries: Array) /** * 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.labels: Array + get() = Array(size) { this[it].label } \ No newline at end of file diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/SingleChoicePreference.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/SingleChoicePreference.kt index 369211573..548d52a25 100644 --- a/preferences/src/main/java/com/itsaky/androidide/preferences/SingleChoicePreference.kt +++ b/preferences/src/main/java/com/itsaky/androidide/preferences/SingleChoicePreference.kt @@ -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): BooleanArray? { - return null - } - override fun onConfigureDialogChoices( preference: Preference, dialog: MaterialAlertDialogBuilder, - choices: Array, - checkedItems: BooleanArray? + entries: Array, + 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 + override fun onChoicesConfirmed( + preference: Preference, + entries: Array ) { - onChoiceConfirmed(currentSelection) + onChoiceConfirmed(preference, entries[currentSelection], currentSelection) } - protected open fun onChoiceConfirmed(position: Int) {} + protected open fun onChoiceConfirmed( + preference: Preference, + entry: PreferenceChoices.Entry, + position: Int + ) {} } diff --git a/preferences/src/main/java/com/itsaky/androidide/preferences/SwitchPreference.kt b/preferences/src/main/java/com/itsaky/androidide/preferences/SwitchPreference.kt index 9a6c8e22b..6b5d977bf 100644 --- a/preferences/src/main/java/com/itsaky/androidide/preferences/SwitchPreference.kt +++ b/preferences/src/main/java/com/itsaky/androidide/preferences/SwitchPreference.kt @@ -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) : 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 } diff --git a/resources/src/main/res/drawable/ic_bell.xml b/resources/src/main/res/drawable/ic_bell.xml new file mode 100644 index 000000000..05dd41453 --- /dev/null +++ b/resources/src/main/res/drawable/ic_bell.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/resources/src/main/res/drawable/ic_bug.xml b/resources/src/main/res/drawable/ic_bug.xml new file mode 100644 index 000000000..bf37b2a65 --- /dev/null +++ b/resources/src/main/res/drawable/ic_bug.xml @@ -0,0 +1,20 @@ + + + + + \ No newline at end of file diff --git a/resources/src/main/res/drawable/ic_keyboard.xml b/resources/src/main/res/drawable/ic_keyboard.xml new file mode 100644 index 000000000..04203f4a1 --- /dev/null +++ b/resources/src/main/res/drawable/ic_keyboard.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/resources/src/main/res/drawable/ic_keyboard_soft.xml b/resources/src/main/res/drawable/ic_keyboard_soft.xml new file mode 100644 index 000000000..b7317fbd4 --- /dev/null +++ b/resources/src/main/res/drawable/ic_keyboard_soft.xml @@ -0,0 +1,26 @@ + + + + + \ No newline at end of file diff --git a/resources/src/main/res/values/termux_app_strings.xml b/resources/src/main/res/values/termux_app_strings.xml index 416936708..4e1d13497 100644 --- a/resources/src/main/res/values/termux_app_strings.xml +++ b/resources/src/main/res/values/termux_app_strings.xml @@ -115,12 +115,9 @@ Failed to start TermuxService. Check logcat for exception message. Failed to start TermuxService while app is in background due to android bg restrictions. - - &TERMUX_APP_NAME; Settings - - &TERMUX_APP_NAME; - Preferences for &TERMUX_APP_NAME; app + Termux + Preferences for the Termux terminal. Debugging diff --git a/termux/termux-shared/src/main/java/com/termux/shared/logger/Logger.java b/termux/termux-shared/src/main/java/com/termux/shared/logger/Logger.java index e5075b1f9..fa5afbf47 100644 --- a/termux/termux-shared/src/main/java/com/termux/shared/logger/Logger.java +++ b/termux/termux-shared/src/main/java/com/termux/shared/logger/Logger.java @@ -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