Migrate navigation graph xml to kotlin DSL

see also: https://developer.android.com/guide/navigation/design/kotlin-dsl
This commit is contained in:
Rocka 2025-07-19 22:09:55 +08:00
parent 1858aff3e6
commit a7af9912a5
No known key found for this signature in database
GPG Key ID: 28031158FFDD6853
22 changed files with 495 additions and 585 deletions

View File

@ -1,11 +1,12 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.core
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
data class InputMethodSubMode(val name: String, val label: String, val icon: String) {
constructor() : this("", "", "")
@ -74,6 +75,7 @@ data class InputMethodEntry(
}
@Parcelize
@Serializable
data class RawConfig(
val name: String,
val comment: String,
@ -158,6 +160,7 @@ data class AddonInfo(
val dependencies: Array<String> = arrayOf(),
val optionalDependencies: Array<String> = arrayOf(),
) {
@Suppress("UNUSED") // used in JNI
constructor(
uniqueName: String,
name: String,

View File

@ -1,23 +1,26 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.navigation.fragment.findNavController
import org.fcitx.fcitx5.android.BuildConfig
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.ui.common.PaddingPreferenceFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import org.fcitx.fcitx5.android.utils.Const
import org.fcitx.fcitx5.android.utils.addCategory
import org.fcitx.fcitx5.android.utils.addPreference
import org.fcitx.fcitx5.android.utils.formatDateTime
import org.fcitx.fcitx5.android.utils.navigateWithAnim
class AboutFragment : PaddingPreferenceFragment() {
@SuppressLint("UseKtx")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = preferenceManager.createPreferenceScreen(requireContext()).apply {
addPreference(R.string.privacy_policy) {
@ -27,7 +30,7 @@ class AboutFragment : PaddingPreferenceFragment() {
R.string.open_source_licenses,
R.string.licenses_of_third_party_libraries
) {
findNavController().navigate(R.id.action_aboutFragment_to_licensesFragment)
navigateWithAnim(SettingsRoute.License)
}
addPreference(R.string.source_code, R.string.github_repo) {
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Const.githubRepo)))

View File

@ -1,10 +1,11 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main
import android.Manifest
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@ -17,20 +18,23 @@ import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.updateLayoutParams
import androidx.navigation.NavController
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.fragment.NavHostFragment
import org.fcitx.fcitx5.android.BuildConfig
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.databinding.ActivityMainBinding
import org.fcitx.fcitx5.android.ui.main.settings.PinyinDictionaryFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import org.fcitx.fcitx5.android.ui.setup.SetupActivity
import org.fcitx.fcitx5.android.utils.Const
import org.fcitx.fcitx5.android.utils.item
import org.fcitx.fcitx5.android.utils.navigateWithAnim
import org.fcitx.fcitx5.android.utils.parcelable
import org.fcitx.fcitx5.android.utils.startActivity
import splitties.dimensions.dp
import splitties.resources.styledColor
@ -65,6 +69,7 @@ class MainActivity : AppCompatActivity() {
// because navController would change toolbar title, we need to control it by ourselves
setupToolbarMenu(binding.toolbar.menu)
navController = binding.navHostFragment.getFragment<NavHostFragment>().navController
navController.graph = SettingsRoute.createGraph(navController)
binding.toolbar.setNavigationOnClickListener {
// prevent navigate up when child fragment has enabled `OnBackPressedCallback`
if (onBackPressedDispatcher.hasEnabledCallbacks()) {
@ -82,38 +87,43 @@ class MainActivity : AppCompatActivity() {
}
navController.addOnDestinationChangedListener { _, dest, _ ->
dest.label?.let { viewModel.setToolbarTitle(it.toString()) }
when (dest.id) {
R.id.themeFragment -> viewModel.disableToolbarShadow()
else -> viewModel.enableToolbarShadow()
if (dest.hasRoute<SettingsRoute.Theme>()) {
viewModel.disableToolbarShadow()
} else {
viewModel.enableToolbarShadow()
}
}
if (intent?.action == Intent.ACTION_MAIN && SetupActivity.shouldShowUp()) {
startActivity<SetupActivity>()
} else {
processIntent(intent)
}
addOnNewIntentListener {
processIntent(it)
}
processIntent(intent)
checkNotificationPermission()
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
processIntent(intent)
}
private fun processIntent(intent: Intent?) {
if (intent?.action == Intent.ACTION_VIEW) {
intent.data?.let {
val action = intent?.action ?: return
when (action) {
Intent.ACTION_MAIN -> if (SetupActivity.shouldShowUp()) {
startActivity<SetupActivity>()
}
Intent.ACTION_VIEW -> intent.data?.let {
AlertDialog.Builder(this)
.setTitle(R.string.pinyin_dict)
.setMessage(R.string.whether_import_dict)
.setNegativeButton(android.R.string.cancel) { _, _ -> }
.setPositiveButton(android.R.string.ok) { _, _ ->
navController.popBackStack(R.id.mainFragment, false)
navController.navigate(
R.id.action_mainFragment_to_pinyinDictionaryFragment,
bundleOf(PinyinDictionaryFragment.INTENT_DATA_URI to it)
)
navController.popBackStack(SettingsRoute.Index, false)
navController.navigateWithAnim(SettingsRoute.PinyinDict(it))
}
.show()
}
Intent.ACTION_RUN -> {
val route = intent.parcelable<SettingsRoute>(EXTRA_SETTINGS_ROUTE) ?: return
navController.popBackStack(SettingsRoute.Index, false)
navController.navigateWithAnim(route)
}
}
}
@ -127,13 +137,14 @@ class MainActivity : AppCompatActivity() {
}
val aboutMenuItems = listOf(
menu.item(R.string.faq) {
@SuppressLint("UseKtx")
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(Const.faqUrl)))
},
menu.item(R.string.developer) {
navController.navigate(R.id.action_mainFragment_to_developerFragment)
navController.navigateWithAnim(SettingsRoute.Developer)
},
menu.item(R.string.about) {
navController.navigate(R.id.action_mainFragment_to_aboutFragment)
navController.navigateWithAnim(SettingsRoute.About)
}
)
viewModel.aboutButton.observe(this@MainActivity) { enabled ->
@ -199,4 +210,8 @@ class MainActivity : AppCompatActivity() {
super.onStop()
}
companion object {
const val EXTRA_SETTINGS_ROUTE = "${BuildConfig.APPLICATION_ID}.EXTRA_SETTINGS_ROUTE"
}
}

View File

@ -6,15 +6,15 @@ package org.fcitx.fcitx5.android.ui.main
import android.os.Bundle
import androidx.annotation.DrawableRes
import androidx.annotation.IdRes
import androidx.annotation.StringRes
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceCategory
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.ui.common.PaddingPreferenceFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import org.fcitx.fcitx5.android.utils.addCategory
import org.fcitx.fcitx5.android.utils.addPreference
import org.fcitx.fcitx5.android.utils.navigateWithAnim
class MainFragment : PaddingPreferenceFragment() {
@ -33,10 +33,10 @@ class MainFragment : PaddingPreferenceFragment() {
private fun PreferenceCategory.addDestinationPreference(
@StringRes title: Int,
@DrawableRes icon: Int,
@IdRes destination: Int
route: SettingsRoute
) {
addPreference(title, icon = icon) {
findNavController().navigate(destination)
navigateWithAnim(route)
}
}
@ -46,54 +46,54 @@ class MainFragment : PaddingPreferenceFragment() {
addDestinationPreference(
R.string.global_options,
R.drawable.ic_baseline_tune_24,
R.id.action_mainFragment_to_globalConfigFragment
SettingsRoute.GlobalConfig
)
addDestinationPreference(
R.string.input_methods,
R.drawable.ic_baseline_language_24,
R.id.action_mainFragment_to_imListFragment
SettingsRoute.InputMethodList
)
addDestinationPreference(
R.string.addons,
R.drawable.ic_baseline_extension_24,
R.id.action_mainFragment_to_addonListFragment
SettingsRoute.AddonList
)
}
addCategory("Android") {
addDestinationPreference(
R.string.theme,
R.drawable.ic_baseline_palette_24,
R.id.action_mainFragment_to_themeFragment
SettingsRoute.Theme
)
addDestinationPreference(
R.string.virtual_keyboard,
R.drawable.ic_baseline_keyboard_24,
R.id.action_mainFragment_to_keyboardSettingsFragment
SettingsRoute.VirtualKeyboard
)
addDestinationPreference(
R.string.candidates_window,
R.drawable.ic_baseline_list_alt_24,
R.id.action_mainFragment_to_candidatesSettingsFragment
SettingsRoute.CandidatesWindow
)
addDestinationPreference(
R.string.clipboard,
R.drawable.ic_clipboard,
R.id.action_mainFragment_to_clipboardSettingsFragment
SettingsRoute.Clipboard
)
addDestinationPreference(
R.string.emoji_and_symbols,
R.drawable.ic_baseline_emoji_symbols_24,
R.id.action_mainFragment_to_symbolSettingsFragment
SettingsRoute.Symbol
)
addDestinationPreference(
R.string.plugins,
R.drawable.ic_baseline_android_24,
R.id.action_mainFragment_to_pluginFragment
SettingsRoute.Plugin
)
addDestinationPreference(
R.string.advanced,
R.drawable.ic_baseline_more_horiz_24,
R.id.action_mainFragment_to_advancedSettingsFragment
SettingsRoute.Advanced
)
}
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
@ -39,10 +39,6 @@ abstract class FcitxPreferenceFragment : PaddingPreferenceFragment() {
private val fcitx: FcitxConnection
get() = viewModel.fcitx
fun requireStringArg(key: String) =
requireArguments().getString(key)
?: throw IllegalStateException("No $key found in bundle")
private fun save() {
if (!configLoaded) return
// launch "saveConfig" job under supervisorJob scope

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
@ -19,15 +19,18 @@ import org.fcitx.fcitx5.android.ui.common.DynamicListUi
import org.fcitx.fcitx5.android.ui.main.MainViewModel
import org.fcitx.fcitx5.android.utils.config.ConfigDescriptor
import org.fcitx.fcitx5.android.utils.config.ConfigType
import org.fcitx.fcitx5.android.utils.parcelable
import org.fcitx.fcitx5.android.utils.lazyRoute
class ListFragment : Fragment() {
private val descriptor: ConfigDescriptor<*, out List<*>> by lazy {
requireArguments().parcelable(ARG_DESC)!!
val args by lazyRoute<SettingsRoute.ListConfig>()
private val descriptor: ConfigDescriptor<*, *> by lazy {
args.desc
}
private val cfg: RawConfig by lazy {
requireArguments().parcelable(ARG_CFG)!!
args.cfg
}
private val viewModel: MainViewModel by activityViewModels()
@ -158,8 +161,6 @@ class ListFragment : Fragment() {
}
companion object {
const val ARG_DESC = "desc"
const val ARG_CFG = "cfg"
val supportedSubtypes = listOf(
ConfigType.TyEnum,
ConfigType.TyString,

View File

@ -1,9 +1,10 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.net.Uri
@ -33,13 +34,15 @@ import org.fcitx.fcitx5.android.ui.common.OnItemChangedListener
import org.fcitx.fcitx5.android.ui.main.MainViewModel
import org.fcitx.fcitx5.android.utils.NaiveDustman
import org.fcitx.fcitx5.android.utils.importErrorDialog
import org.fcitx.fcitx5.android.utils.lazyRoute
import org.fcitx.fcitx5.android.utils.notificationManager
import org.fcitx.fcitx5.android.utils.parcelable
import org.fcitx.fcitx5.android.utils.queryFileName
import java.util.concurrent.atomic.AtomicBoolean
class PinyinDictionaryFragment : Fragment(), OnItemChangedListener<PinyinDictionary> {
private val args by lazyRoute<SettingsRoute.PinyinDict>()
private val viewModel: MainViewModel by activityViewModels()
private lateinit var launcher: ActivityResultLauncher<String>
@ -105,8 +108,8 @@ class PinyinDictionaryFragment : Fragment(), OnItemChangedListener<PinyinDiction
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
arguments?.parcelable<Uri>(INTENT_DATA_URI)
?.let { importFromUri(it) }
@SuppressLint("UseKtx")
args.uri?.let { importFromUri(Uri.parse(it)) }
super.onViewCreated(view, savedInstanceState)
}
@ -247,6 +250,5 @@ class PinyinDictionaryFragment : Fragment(), OnItemChangedListener<PinyinDiction
private var RELOAD_ID = 0
private var IMPORT_ID = 0
const val CHANNEL_ID = "pinyin_dict"
const val INTENT_DATA_URI = "uri"
}
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2024 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
@ -8,9 +8,7 @@ import android.app.AlertDialog
import android.content.Context
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.navigation.fragment.findNavController
import androidx.preference.DialogPreference
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@ -26,9 +24,6 @@ import org.fcitx.fcitx5.android.core.Key
import org.fcitx.fcitx5.android.core.RawConfig
import org.fcitx.fcitx5.android.data.prefs.AppPrefs
import org.fcitx.fcitx5.android.ui.main.modified.MySwitchPreference
import org.fcitx.fcitx5.android.ui.main.settings.addon.AddonConfigFragment
import org.fcitx.fcitx5.android.ui.main.settings.global.GlobalConfigFragment
import org.fcitx.fcitx5.android.ui.main.settings.im.InputMethodConfigFragment
import org.fcitx.fcitx5.android.utils.LongClickPreference
import org.fcitx.fcitx5.android.utils.buildDocumentsProviderIntent
import org.fcitx.fcitx5.android.utils.buildPrimaryStorageIntent
@ -43,8 +38,10 @@ import org.fcitx.fcitx5.android.utils.config.ConfigDescriptor.ConfigKey
import org.fcitx.fcitx5.android.utils.config.ConfigDescriptor.ConfigList
import org.fcitx.fcitx5.android.utils.config.ConfigDescriptor.ConfigString
import org.fcitx.fcitx5.android.utils.config.ConfigType
import org.fcitx.fcitx5.android.utils.navigateWithAnim
import org.fcitx.fcitx5.android.utils.parcelableArray
import org.fcitx.fcitx5.android.utils.toast
import timber.log.Timber
object PreferenceScreenFactory {
@ -61,24 +58,19 @@ object PreferenceScreenFactory {
val cfg = raw["cfg"]
val desc = raw["desc"]
val store = FcitxRawConfigStore(cfg)
ConfigDescriptor
.parseTopLevel(desc)
.getOrElse { throw it }
.let {
screen.title = it.name
it.values.forEach { d ->
general(context, fragmentManager, cfg, screen, d, store, save)
}
}
// TODO: needs some error handling
val topLevelDesc = ConfigDescriptor.parseTopLevel(desc).getOrElse { throw it }
screen.title = topLevelDesc.name
topLevelDesc.values.forEach {
general(context, fragmentManager, cfg.findByName(it.name), screen, it, store, save)
}
return screen
}
private fun general(
context: Context,
fragmentManager: FragmentManager,
cfg: RawConfig,
cfg: RawConfig?,
screen: PreferenceScreen,
descriptor: ConfigDescriptor<*, *>,
store: PreferenceDataStore,
@ -86,8 +78,9 @@ object PreferenceScreenFactory {
) {
// Hide key related configs
if (hideKeyConfig && ConfigType.pretty(descriptor.type).contains("Key"))
if (hideKeyConfig && ConfigType.pretty(descriptor.type).contains("Key")) {
return
}
if (descriptor is ConfigCustom) {
custom(context, fragmentManager, cfg, screen, descriptor, save)
@ -99,70 +92,43 @@ object PreferenceScreenFactory {
"${context.getString(R.string.unimplemented_type)} '${ConfigType.pretty(descriptor.type)}'"
}
fun <T : Any> navigate(route: T): Boolean {
return try {
fragmentManager.primaryNavigationFragment!!.navigateWithAnim(route)
true
} catch (e: Exception) {
Timber.w("Unable to navigate(route=$route): $e")
false
}
}
fun pinyinDictionary() = Preference(context).apply {
setOnPreferenceClickListener {
val currentFragment = fragmentManager.findFragmentById(R.id.nav_host_fragment)!!
val action = when (currentFragment) {
is AddonConfigFragment -> R.id.action_addonConfigFragment_to_pinyinDictionaryFragment
is InputMethodConfigFragment -> R.id.action_imConfigFragment_to_pinyinDictionaryFragment
else -> throw IllegalStateException("Can not navigate to pinyin dictionary from current fragment")
}
currentFragment.findNavController().navigate(action)
true
navigate(SettingsRoute.PinyinDict(""))
}
}
fun punctuationEditor(title: String, lang: String?) = Preference(context).apply {
setOnPreferenceClickListener {
val currentFragment = fragmentManager.findFragmentById(R.id.nav_host_fragment)!!
val action = when (currentFragment) {
is AddonConfigFragment -> R.id.action_addonConfigFragment_to_punctuationEditorFragment
is InputMethodConfigFragment -> R.id.action_imConfigFragment_to_punctuationEditorFragment
else -> throw IllegalStateException("Can not navigate to punctuation editor from current fragment")
}
currentFragment.findNavController().navigate(
action,
bundleOf(
PunctuationEditorFragment.TITLE to title,
PunctuationEditorFragment.LANG to lang
)
)
true
navigate(SettingsRoute.Punctuation(title, lang))
}
}
fun quickPhraseEditor() = Preference(context).apply {
setOnPreferenceClickListener {
val currentFragment = fragmentManager.findFragmentById(R.id.nav_host_fragment)!!
val action = when (currentFragment) {
is AddonConfigFragment -> R.id.action_addonConfigFragment_to_quickPhraseListFragment
is InputMethodConfigFragment -> R.id.action_imConfigFragment_to_quickPhraseListFragment
else -> throw IllegalStateException("Can not navigate to quick phrase editor from current fragment")
}
currentFragment.findNavController().navigate(action)
true
navigate(SettingsRoute.QuickPhraseList)
}
}
fun tableInputMethod() = Preference(context).apply {
setOnPreferenceClickListener {
val currentFragment = fragmentManager.findFragmentById(R.id.nav_host_fragment)!!
currentFragment.findNavController()
.navigate(R.id.action_addonConfigFragment_to_tableInputMethodFragment)
true
navigate(SettingsRoute.TableInputMethods)
}
}
fun pinyinCustomPhrase() = Preference(context).apply {
setOnPreferenceClickListener {
val currentFragment = fragmentManager.findFragmentById(R.id.nav_host_fragment)!!
val action = when (currentFragment) {
is AddonConfigFragment -> R.id.action_addonConfigFragment_to_pinyinCustomPhraseFragment
is InputMethodConfigFragment -> R.id.action_imConfigFragment_to_pinyinCustomPhraseFragment
else -> throw IllegalStateException("Can not navigate to custom phrase editor from current fragment")
}
currentFragment.findNavController().navigate(action)
true
navigate(SettingsRoute.PinyinCustomPhrase)
}
}
@ -197,25 +163,12 @@ object PreferenceScreenFactory {
fun listPreference(subtype: ConfigType<*>): Preference = object : Preference(context) {
override fun onClick() {
val currentFragment = fragmentManager.findFragmentById(R.id.nav_host_fragment)!!
val action = when (currentFragment) {
is GlobalConfigFragment -> R.id.action_globalConfigFragment_to_listFragment
is InputMethodConfigFragment -> R.id.action_imConfigFragment_to_listFragment
is AddonConfigFragment -> R.id.action_addonConfigFragment_to_listFragment
else -> throw IllegalStateException("Can not navigate to listFragment from current fragment")
}
currentFragment.findNavController().navigate(
action,
bundleOf(
ListFragment.ARG_CFG to cfg[descriptor.name],
ListFragment.ARG_DESC to descriptor,
)
)
navigate(SettingsRoute.ListConfig(cfg ?: RawConfig(), descriptor.raw))
fragmentManager.setFragmentResultListener(
descriptor.name,
currentFragment
fragmentManager.primaryNavigationFragment!!
) { _, v ->
cfg[descriptor.name].subItems = v.parcelableArray(descriptor.name)
cfg?.subItems = v.parcelableArray(descriptor.name)
if (callChangeListener(null)) {
notifyChanged()
}
@ -224,30 +177,19 @@ object PreferenceScreenFactory {
}.apply {
if (subtype == ConfigType.TyKey) {
summaryProvider = SummaryProvider<Preference> {
val str = cfg[descriptor.name].subItems?.joinToString("\n") {
val keys = cfg?.subItems?.joinToString("\n") {
Key.parse(it.value).localizedString
} ?: ""
str.ifEmpty { context.getString(R.string.none) }
}
if (keys.isNullOrEmpty()) context.getString(R.string.none) else keys
}
}
}
fun addonConfigPreference(addon: String) = Preference(context).apply {
setOnPreferenceClickListener {
val currentFragment = fragmentManager.findFragmentById(R.id.nav_host_fragment)!!
val action = when (currentFragment) {
is AddonConfigFragment -> R.id.action_addonConfigFragment_self
is InputMethodConfigFragment -> R.id.action_imConfigFragment_to_addonConfigFragment
else -> throw IllegalStateException("Can not navigate to addonConfigFragment from current fragment")
}
currentFragment.findNavController().navigate(
action,
bundleOf(
AddonConfigFragment.ARG_UNIQUE_NAME to addon,
AddonConfigFragment.ARG_NAME to (descriptor.description ?: descriptor.name)
)
navigate(
SettingsRoute.AddonConfig(descriptor.description ?: descriptor.name, addon)
)
true
}
}
@ -337,12 +279,12 @@ object PreferenceScreenFactory {
private fun custom(
context: Context,
fragmentManager: FragmentManager,
cfg: RawConfig,
cfg: RawConfig?,
screen: PreferenceScreen,
descriptor: ConfigCustom,
save: () -> Unit
) {
val subStore = FcitxRawConfigStore(cfg[descriptor.name])
val subStore = FcitxRawConfigStore(cfg ?: RawConfig())
val subPref = PreferenceCategory(context).apply {
key = descriptor.name
title = descriptor.description ?: descriptor.name
@ -350,16 +292,8 @@ object PreferenceScreenFactory {
isIconSpaceReserved = false
}
screen.addPreference(subPref)
descriptor.customTypeDef!!.values.forEach {
general(
context,
fragmentManager,
cfg[descriptor.name],
screen,
it,
subStore,
save
)
descriptor.customTypeDef?.values?.forEach {
general(context, fragmentManager, cfg?.findByName(it.name), screen, it, subStore, save)
}
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
@ -15,6 +15,7 @@ import org.fcitx.fcitx5.android.data.punctuation.PunctuationMapEntry
import org.fcitx.fcitx5.android.ui.common.BaseDynamicListUi
import org.fcitx.fcitx5.android.ui.common.OnItemChangedListener
import org.fcitx.fcitx5.android.utils.NaiveDustman
import org.fcitx.fcitx5.android.utils.lazyRoute
import org.fcitx.fcitx5.android.utils.materialTextInput
import org.fcitx.fcitx5.android.utils.onPositiveButtonClick
import org.fcitx.fcitx5.android.utils.str
@ -26,6 +27,8 @@ import splitties.views.setPaddingDp
class PunctuationEditorFragment : ProgressFragment(), OnItemChangedListener<PunctuationMapEntry> {
private val args by lazyRoute<SettingsRoute.Punctuation>()
private lateinit var lang: String
private lateinit var keyDesc: String
private lateinit var mappingDesc: String
@ -60,7 +63,7 @@ class PunctuationEditorFragment : ProgressFragment(), OnItemChangedListener<Punc
}
override suspend fun initialize(): View {
lang = requireArguments().getString(LANG, DEFAULT_LANG)
lang = args.lang ?: DEFAULT_LANG
val raw = fcitx.runOnReady { getPunctuationConfig(lang) }
findDesc(raw)
val initialEntries = PunctuationManager.parseRawConfig(raw)
@ -163,7 +166,7 @@ class PunctuationEditorFragment : ProgressFragment(), OnItemChangedListener<Punc
override fun onStart() {
super.onStart()
viewModel.setToolbarTitle(requireArguments().getString(TITLE)!!)
viewModel.setToolbarTitle(args.title)
if (isInitialized) {
viewModel.enableToolbarEditButton(ui.entries.isNotEmpty()) {
ui.enterMultiSelect(requireActivity().onBackPressedDispatcher)
@ -188,8 +191,6 @@ class PunctuationEditorFragment : ProgressFragment(), OnItemChangedListener<Punc
}
companion object {
const val TITLE = "title"
const val LANG = "lang"
const val DEFAULT_LANG = "zh_CN"
}
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
@ -14,32 +14,41 @@ import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.data.quickphrase.BuiltinQuickPhrase
import org.fcitx.fcitx5.android.data.quickphrase.CustomQuickPhrase
import org.fcitx.fcitx5.android.data.quickphrase.QuickPhrase
import org.fcitx.fcitx5.android.data.quickphrase.QuickPhraseData
import org.fcitx.fcitx5.android.data.quickphrase.QuickPhraseEntry
import org.fcitx.fcitx5.android.ui.common.BaseDynamicListUi
import org.fcitx.fcitx5.android.ui.common.OnItemChangedListener
import org.fcitx.fcitx5.android.utils.NaiveDustman
import org.fcitx.fcitx5.android.utils.lazyRoute
import org.fcitx.fcitx5.android.utils.materialTextInput
import org.fcitx.fcitx5.android.utils.onPositiveButtonClick
import org.fcitx.fcitx5.android.utils.serializable
import org.fcitx.fcitx5.android.utils.str
import splitties.views.dsl.core.add
import splitties.views.dsl.core.lParams
import splitties.views.dsl.core.matchParent
import splitties.views.dsl.core.verticalLayout
import splitties.views.setPaddingDp
import java.io.File
class QuickPhraseEditFragment : ProgressFragment(), OnItemChangedListener<QuickPhraseEntry> {
private val args by lazyRoute<SettingsRoute.QuickPhraseEdit>()
private val quickPhrase: QuickPhrase by lazy {
if (args.isBuiltIn) {
BuiltinQuickPhrase(File(args.path), File(args.override ?: ""))
} else {
CustomQuickPhrase(File(args.path))
}
}
private lateinit var ui: BaseDynamicListUi<QuickPhraseEntry>
private lateinit var quickPhrase: QuickPhrase
private val dustman = NaiveDustman<QuickPhraseEntry>()
override suspend fun initialize(): View {
quickPhrase = requireArguments().serializable(ARG)!!
val initialEntries = withContext(Dispatchers.IO) {
quickPhrase.loadData()
}
@ -185,7 +194,6 @@ class QuickPhraseEditFragment : ProgressFragment(), OnItemChangedListener<QuickP
}
companion object {
const val ARG = "quickphrase"
const val RESULT = "dirty"
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
@ -17,11 +17,9 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.core.app.NotificationCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch
@ -39,6 +37,7 @@ import org.fcitx.fcitx5.android.utils.NaiveDustman
import org.fcitx.fcitx5.android.utils.importErrorDialog
import org.fcitx.fcitx5.android.utils.item
import org.fcitx.fcitx5.android.utils.materialTextInput
import org.fcitx.fcitx5.android.utils.navigateWithAnim
import org.fcitx.fcitx5.android.utils.notificationManager
import org.fcitx.fcitx5.android.utils.onPositiveButtonClick
import org.fcitx.fcitx5.android.utils.queryFileName
@ -82,10 +81,7 @@ class QuickPhraseListFragment : Fragment(), OnItemChangedListener<QuickPhrase> {
initSettingsButton = { entry ->
visibility = if (!entry.isEnabled) View.GONE else View.VISIBLE
fun edit() {
findNavController().navigate(
R.id.action_quickPhraseListFragment_to_quickPhraseEditFragment,
bundleOf(QuickPhraseEditFragment.ARG to entry)
)
navigateWithAnim(SettingsRoute.QuickPhraseEdit.from(entry))
parentFragmentManager.setFragmentResultListener(
QuickPhraseEditFragment.RESULT,
this@QuickPhraseListFragment

View File

@ -0,0 +1,249 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings
import android.net.Uri
import android.os.Parcelable
import androidx.navigation.NavController
import androidx.navigation.NavType
import androidx.navigation.createGraph
import androidx.navigation.fragment.fragment
import androidx.savedstate.SavedState
import arrow.core.getOrElse
import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.RawConfig
import org.fcitx.fcitx5.android.data.quickphrase.BuiltinQuickPhrase
import org.fcitx.fcitx5.android.data.quickphrase.CustomQuickPhrase
import org.fcitx.fcitx5.android.data.quickphrase.QuickPhrase
import org.fcitx.fcitx5.android.ui.main.AboutFragment
import org.fcitx.fcitx5.android.ui.main.DeveloperFragment
import org.fcitx.fcitx5.android.ui.main.LicensesFragment
import org.fcitx.fcitx5.android.ui.main.MainFragment
import org.fcitx.fcitx5.android.ui.main.PluginFragment
import org.fcitx.fcitx5.android.ui.main.settings.addon.AddonConfigFragment
import org.fcitx.fcitx5.android.ui.main.settings.addon.AddonListFragment
import org.fcitx.fcitx5.android.ui.main.settings.behavior.AdvancedSettingsFragment
import org.fcitx.fcitx5.android.ui.main.settings.behavior.CandidatesSettingsFragment
import org.fcitx.fcitx5.android.ui.main.settings.behavior.ClipboardSettingsFragment
import org.fcitx.fcitx5.android.ui.main.settings.behavior.KeyboardSettingsFragment
import org.fcitx.fcitx5.android.ui.main.settings.behavior.SymbolSettingsFragment
import org.fcitx.fcitx5.android.ui.main.settings.global.GlobalConfigFragment
import org.fcitx.fcitx5.android.ui.main.settings.im.InputMethodConfigFragment
import org.fcitx.fcitx5.android.ui.main.settings.im.InputMethodListFragment
import org.fcitx.fcitx5.android.ui.main.settings.theme.ThemeFragment
import org.fcitx.fcitx5.android.utils.config.ConfigDescriptor
import org.fcitx.fcitx5.android.utils.parcelable
import kotlin.reflect.typeOf
@Parcelize
sealed class SettingsRoute : Parcelable {
/* ========== Index ========== */
@Serializable
data object Index : SettingsRoute()
/* ========== Fcitx ========== */
@Serializable
data object GlobalConfig : SettingsRoute()
@Serializable
data object InputMethodList : SettingsRoute()
@Serializable
data class InputMethodConfig(val name: String, val uniqueName: String) : SettingsRoute()
@Serializable
data object AddonList : SettingsRoute()
@Serializable
data class AddonConfig(val name: String, val uniqueName: String) : SettingsRoute()
/* ========== Android ========== */
@Serializable
data object Theme : SettingsRoute()
@Serializable
data object VirtualKeyboard : SettingsRoute()
@Serializable
data object CandidatesWindow : SettingsRoute()
@Serializable
data object Clipboard : SettingsRoute()
@Serializable
data object Symbol : SettingsRoute()
@Serializable
data object Plugin : SettingsRoute()
@Serializable
data object Advanced : SettingsRoute()
@Serializable
data object Developer : SettingsRoute()
@Serializable
data object License : SettingsRoute()
@Serializable
data object About : SettingsRoute()
/* ========== External ========== */
@Serializable
data class ListConfig(val params: Params) : SettingsRoute() {
@Parcelize
@Serializable
data class Params(val cfg: RawConfig, val desc: RawConfig) : Parcelable {
companion object {
// https://developer.android.com/guide/navigation/design/kotlin-dsl#custom-types
val NavType = object : NavType<Params>(isNullableAllowed = false) {
override fun put(bundle: SavedState, key: String, value: Params) {
bundle.putParcelable(key, value)
}
override fun get(bundle: SavedState, key: String): Params? {
return bundle.parcelable<Params>(key)
}
override fun serializeAsValue(value: Params): String {
// Serialized values must always be Uri encoded
return Uri.encode(Json.encodeToString(value))
}
override fun parseValue(value: String): Params {
// Navigation decodes the string before passing it to parseValue()
return Json.decodeFromString(value)
}
}
}
}
constructor(cfg: RawConfig, desc: RawConfig) : this(Params(cfg, desc))
val desc: ConfigDescriptor<*, *>
get() = ConfigDescriptor.parse(params.desc).getOrElse { throw it }
val cfg: RawConfig
get() = params.cfg
}
@Serializable
data class PinyinDict(val uri: String? = null) : SettingsRoute() {
constructor(uri: Uri) : this(uri.toString())
}
@Serializable
data class Punctuation(val title: String, val lang: String? = null) : SettingsRoute()
@Serializable
data object QuickPhraseList : SettingsRoute()
@Serializable
data class QuickPhraseEdit(
val path: String,
val isBuiltIn: Boolean = false,
val override: String? = null
) : SettingsRoute() {
companion object {
fun from(q: QuickPhrase) = when (q) {
is CustomQuickPhrase -> QuickPhraseEdit(q.file.absolutePath)
is BuiltinQuickPhrase -> QuickPhraseEdit(
q.file.absolutePath, true, q.override?.file?.absolutePath
)
else -> throw IllegalArgumentException()
}
}
}
@Serializable
data object TableInputMethods : SettingsRoute()
@Serializable
data object PinyinCustomPhrase : SettingsRoute()
companion object {
fun createGraph(controller: NavController) = controller.createGraph(Index) {
val ctx = controller.context
/* ========== Index ========== */
fragment<MainFragment, Index> {
label = ctx.getString(R.string.app_name)
}
/* ========== Fcitx ========== */
fragment<GlobalConfigFragment, GlobalConfig>()
fragment<InputMethodListFragment, InputMethodList> {
label = ctx.getString(R.string.input_methods)
}
fragment<InputMethodConfigFragment, InputMethodConfig>()
fragment<AddonListFragment, AddonList> {
label = ctx.getString(R.string.addons)
}
fragment<AddonConfigFragment, AddonConfig>()
/* ========== Android ========== */
fragment<ThemeFragment, Theme> {
label = ctx.getString(R.string.theme)
}
fragment<KeyboardSettingsFragment, VirtualKeyboard> {
label = ctx.getString(R.string.virtual_keyboard)
}
fragment<CandidatesSettingsFragment, CandidatesWindow> {
label = ctx.getString(R.string.candidates_window)
}
fragment<ClipboardSettingsFragment, Clipboard> {
label = ctx.getString(R.string.clipboard)
}
fragment<SymbolSettingsFragment, Symbol> {
label = ctx.getString(R.string.emoji_and_symbols)
}
fragment<PluginFragment, Plugin> {
label = ctx.getString(R.string.plugins)
}
fragment<AdvancedSettingsFragment, Advanced> {
label = ctx.getString(R.string.advanced)
}
fragment<DeveloperFragment, Developer> {
label = ctx.getString(R.string.developer)
}
fragment<LicensesFragment, License> {
label = ctx.getString(R.string.license)
}
fragment<AboutFragment, About> {
label = ctx.getString(R.string.about)
}
/* ========== External ========== */
fragment<ListFragment, ListConfig>(
typeMap = mapOf(typeOf<ListConfig.Params>() to ListConfig.Params.NavType)
)
fragment<PinyinDictionaryFragment, PinyinDict> {
label = ctx.getString(R.string.pinyin_dict)
}
fragment<PunctuationEditorFragment, Punctuation>()
fragment<QuickPhraseListFragment, QuickPhraseList> {
label = ctx.getString(R.string.quickphrase_editor)
}
fragment<QuickPhraseEditFragment, QuickPhraseEdit>()
fragment<TableInputMethodFragment, TableInputMethods> {
label = ctx.getString(R.string.table_im)
}
fragment<PinyinCustomPhraseFragment, PinyinCustomPhrase>()
}
}
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings.addon
@ -8,12 +8,16 @@ import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.FcitxAPI
import org.fcitx.fcitx5.android.core.RawConfig
import org.fcitx.fcitx5.android.ui.main.settings.FcitxPreferenceFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import org.fcitx.fcitx5.android.utils.lazyRoute
class AddonConfigFragment : FcitxPreferenceFragment() {
override fun getPageTitle(): String = requireStringArg(ARG_NAME)
private val args by lazyRoute<SettingsRoute.AddonConfig>()
override fun getPageTitle(): String = args.name
override suspend fun obtainConfig(fcitx: FcitxAPI): RawConfig {
val addon = requireStringArg(ARG_UNIQUE_NAME)
val addon = args.uniqueName
val raw = fcitx.getAddonConfig(addon)
if (addon == "table") {
// append android specific "Manage Table Input Methods" to config of table addon
@ -30,12 +34,6 @@ class AddonConfigFragment : FcitxPreferenceFragment() {
}
override suspend fun saveConfig(fcitx: FcitxAPI, newConfig: RawConfig) {
fcitx.setAddonConfig(requireStringArg(ARG_UNIQUE_NAME), newConfig)
fcitx.setAddonConfig(args.uniqueName, newConfig)
}
companion object {
const val ARG_UNIQUE_NAME = "addon"
const val ARG_NAME = "addon_"
}
}
}

View File

@ -1,14 +1,12 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings.addon
import android.view.View
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.navigation.findNavController
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.AddonInfo
import org.fcitx.fcitx5.android.core.FcitxAPI
@ -17,6 +15,8 @@ import org.fcitx.fcitx5.android.ui.common.BaseDynamicListUi
import org.fcitx.fcitx5.android.ui.common.CheckBoxListUi
import org.fcitx.fcitx5.android.ui.common.OnItemChangedListener
import org.fcitx.fcitx5.android.ui.main.settings.ProgressFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import org.fcitx.fcitx5.android.utils.navigateWithAnim
class AddonListFragment : ProgressFragment(), OnItemChangedListener<AddonInfo> {
@ -120,12 +120,8 @@ class AddonListFragment : ProgressFragment(), OnItemChangedListener<AddonInfo> {
entry.uniqueName != "clipboard"
) View.VISIBLE else View.INVISIBLE
setOnClickListener {
it.findNavController().navigate(
R.id.action_addonListFragment_to_addonConfigFragment,
bundleOf(
AddonConfigFragment.ARG_UNIQUE_NAME to entry.uniqueName,
AddonConfigFragment.ARG_NAME to entry.displayName
)
navigateWithAnim(
SettingsRoute.AddonConfig(entry.displayName, entry.uniqueName)
)
}
},

View File

@ -1,26 +1,25 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings.im
import org.fcitx.fcitx5.android.core.FcitxAPI
import org.fcitx.fcitx5.android.core.RawConfig
import org.fcitx.fcitx5.android.ui.main.settings.FcitxPreferenceFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import org.fcitx.fcitx5.android.utils.lazyRoute
class InputMethodConfigFragment : FcitxPreferenceFragment() {
override fun getPageTitle(): String = requireStringArg(ARG_NAME)
val args by lazyRoute<SettingsRoute.InputMethodConfig>()
override fun getPageTitle(): String = args.name
override suspend fun obtainConfig(fcitx: FcitxAPI): RawConfig {
return fcitx.getImConfig(requireStringArg(ARG_UNIQUE_NAME))
return fcitx.getImConfig(args.uniqueName)
}
override suspend fun saveConfig(fcitx: FcitxAPI, newConfig: RawConfig) {
fcitx.setImConfig(requireStringArg(ARG_UNIQUE_NAME), newConfig)
}
companion object {
const val ARG_UNIQUE_NAME = "im"
const val ARG_NAME = "im_"
fcitx.setImConfig(args.uniqueName, newConfig)
}
}

View File

@ -1,21 +1,20 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.ui.main.settings.im
import android.os.Build
import android.view.View
import androidx.core.os.bundleOf
import androidx.navigation.findNavController
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.core.InputMethodEntry
import org.fcitx.fcitx5.android.daemon.launchOnReady
import org.fcitx.fcitx5.android.core.SubtypeManager
import org.fcitx.fcitx5.android.daemon.launchOnReady
import org.fcitx.fcitx5.android.ui.common.BaseDynamicListUi
import org.fcitx.fcitx5.android.ui.common.DynamicListUi
import org.fcitx.fcitx5.android.ui.common.OnItemChangedListener
import org.fcitx.fcitx5.android.ui.main.settings.ProgressFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import org.fcitx.fcitx5.android.utils.navigateWithAnim
class InputMethodListFragment : ProgressFragment(), OnItemChangedListener<InputMethodEntry> {
@ -43,12 +42,8 @@ class InputMethodListFragment : ProgressFragment(), OnItemChangedListener<InputM
enableOrder = true,
initSettingsButton = { entry ->
setOnClickListener {
it.findNavController().navigate(
R.id.action_imListFragment_to_imConfigFragment,
bundleOf(
InputMethodConfigFragment.ARG_UNIQUE_NAME to entry.uniqueName,
InputMethodConfigFragment.ARG_NAME to entry.displayName
)
navigateWithAnim(
SettingsRoute.InputMethodConfig(entry.displayName, entry.uniqueName),
)
}
},

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.utils
@ -10,15 +10,11 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.annotation.IdRes
import androidx.core.app.NotificationCompat
import androidx.core.os.bundleOf
import androidx.navigation.NavDeepLinkBuilder
import org.fcitx.fcitx5.android.R
import org.fcitx.fcitx5.android.ui.main.ClipboardEditActivity
import org.fcitx.fcitx5.android.ui.main.MainActivity
import org.fcitx.fcitx5.android.ui.main.settings.im.InputMethodConfigFragment
import org.fcitx.fcitx5.android.ui.main.settings.SettingsRoute
import kotlin.system.exitProcess
object AppUtil {
@ -29,39 +25,25 @@ object AppUtil {
}
}
private fun launchMainToDest(context: Context, @IdRes dest: Int, arguments: Bundle? = null) {
NavDeepLinkBuilder(context)
.setComponentName(MainActivity::class.java)
.setGraph(R.navigation.settings_nav)
.addDestination(dest, arguments)
.createTaskStackBuilder()
/**
* [androidx.core.app.TaskStackBuilder.getIntents] would add unwanted flags
* [Intent.FLAG_ACTIVITY_CLEAR_TASK] and [Intent.FLAG_ACTIVITY_TASK_ON_HOME]
* so we must launch the Intent by ourselves
*/
.editIntentAt(0)?.apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
context.startActivity(this)
}
private fun launchMainToDest(context: Context, route: SettingsRoute) {
context.startActivity<MainActivity> {
action = Intent.ACTION_RUN
putExtra(MainActivity.EXTRA_SETTINGS_ROUTE, route)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS)
}
}
fun launchMainToKeyboard(context: Context) =
launchMainToDest(context, R.id.keyboardSettingsFragment)
launchMainToDest(context, SettingsRoute.VirtualKeyboard)
fun launchMainToInputMethodList(context: Context) =
launchMainToDest(context, R.id.imListFragment)
launchMainToDest(context, SettingsRoute.InputMethodList)
fun launchMainToThemeList(context: Context) =
launchMainToDest(context, R.id.themeFragment)
launchMainToDest(context, SettingsRoute.Theme)
fun launchMainToInputMethodConfig(context: Context, uniqueName: String, displayName: String) =
launchMainToDest(
context, R.id.imConfigFragment, bundleOf(
InputMethodConfigFragment.ARG_NAME to displayName,
InputMethodConfigFragment.ARG_UNIQUE_NAME to uniqueName
)
)
launchMainToDest(context, SettingsRoute.InputMethodConfig(displayName, uniqueName))
fun launchClipboardEdit(context: Context, id: Int, lastEntry: Boolean = false) {
context.startActivity<ClipboardEditActivity> {

View File

@ -0,0 +1,18 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.utils
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.toRoute
fun <T : Any> Fragment.navigateWithAnim(route: T) {
findNavController().navigateWithAnim(route)
}
inline fun <reified T : Any> Fragment.lazyRoute() = lazy {
findNavController().getBackStackEntry<T>().toRoute<T>()
}

View File

@ -0,0 +1,21 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.utils
import androidx.navigation.NavController
import androidx.navigation.navOptions
import androidx.navigation.ui.R
fun <T : Any> NavController.navigateWithAnim(route: T) {
navigate(route, navOptions {
anim {
enter = R.animator.nav_default_enter_anim
exit = R.animator.nav_default_exit_anim
popEnter = R.animator.nav_default_pop_enter_anim
popExit = R.animator.nav_default_pop_exit_anim
}
})
}

View File

@ -1,6 +1,6 @@
/*
* SPDX-License-Identifier: LGPL-2.1-or-later
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
* SPDX-FileCopyrightText: Copyright 2021-2025 Fcitx5 for Android Contributors
*/
package org.fcitx.fcitx5.android.utils.config
@ -11,8 +11,10 @@ import arrow.core.raise.either
import kotlinx.parcelize.Parcelize
import kotlinx.parcelize.RawValue
import org.fcitx.fcitx5.android.core.RawConfig
import org.fcitx.fcitx5.android.utils.config.ConfigDescriptor.Companion.parse
sealed class ConfigDescriptor<T, U> : Parcelable {
abstract val raw: RawConfig
abstract val name: String
abstract val type: ConfigType<T>
abstract val description: String?
@ -34,6 +36,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigInt(
override val raw: RawConfig,
override val name: String,
override val description: String? = null,
override val defaultValue: Int? = null,
@ -47,6 +50,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigString(
override val raw: RawConfig,
override val name: String,
override val description: String? = null,
override val defaultValue: String? = null,
@ -58,6 +62,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigBool(
override val raw: RawConfig,
override val name: String,
override val description: String? = null,
override val defaultValue: Boolean? = null,
@ -70,6 +75,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigKey(
override val raw: RawConfig,
override val name: String,
override val description: String? = null,
override val defaultValue: String? = null,
@ -82,6 +88,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigEnum(
override val raw: RawConfig,
override val name: String,
override val description: String? = null,
override val defaultValue: String? = null,
@ -96,6 +103,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigCustom(
override val raw: RawConfig,
override val name: String,
override val type: ConfigType.TyCustom,
override val description: String? = null,
@ -109,6 +117,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigList(
override val raw: RawConfig,
override val name: String,
override val type: ConfigType.TyList,
override val description: String? = null,
@ -124,6 +133,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
*/
@Parcelize
data class ConfigEnumList(
override val raw: RawConfig,
override val name: String,
override val description: String? = null,
override val defaultValue: List<String>? = null,
@ -138,6 +148,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
@Parcelize
data class ConfigExternal(
override val raw: RawConfig,
override val name: String,
override val description: String? = null,
override val tooltip: String? = null,
@ -206,11 +217,13 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
when (it) {
ConfigType.TyBool ->
ConfigBool(
raw,
raw.name,
raw.description,
raw.defaultValue?.toBoolean()
)
is ConfigType.TyCustom -> ConfigCustom(
raw,
raw.name,
it,
raw.description
@ -218,6 +231,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
ConfigType.TyEnum -> {
val entries = raw.enum ?: raise(ParseException.NoEnumFound(raw))
ConfigEnum(
raw,
raw.name,
raw.description,
raw.defaultValue,
@ -227,6 +241,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
)
}
ConfigType.TyInt -> ConfigInt(
raw,
raw.name,
raw.description,
raw.defaultValue?.toInt(),
@ -234,11 +249,17 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
raw.intMax,
raw.intMin
)
ConfigType.TyKey -> ConfigKey(raw.name, raw.description, raw.defaultValue)
ConfigType.TyKey -> ConfigKey(
raw,
raw.name,
raw.description,
raw.defaultValue
)
is ConfigType.TyList ->
if (it.subtype == ConfigType.TyEnum) {
val entries = raw.enum ?: raise(ParseException.NoEnumFound(raw))
ConfigEnumList(
raw,
raw.name,
raw.description,
raw.findByName("DefaultValue")?.subItems?.map { ele -> ele.value },
@ -248,6 +269,7 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
)
} else
ConfigList(
raw,
raw.name,
it,
raw.description,
@ -264,11 +286,13 @@ sealed class ConfigDescriptor<T, U> : Parcelable {
}
)
ConfigType.TyString -> ConfigString(
raw,
raw.name,
raw.description,
raw.defaultValue
)
ConfigType.TyExternal -> ConfigExternal(
raw,
raw.name,
raw.description,
raw.tooltip,

View File

@ -25,7 +25,6 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar"
app:navGraph="@navigation/settings_nav" />
app:layout_constraintTop_toBottomOf="@id/toolbar" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,330 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/settings_nav"
app:startDestination="@id/mainFragment">
<fragment
android:id="@+id/mainFragment"
android:name="org.fcitx.fcitx5.android.ui.main.MainFragment"
android:label="@string/app_name">
<action
android:id="@+id/action_mainFragment_to_globalConfigFragment"
app:destination="@id/globalConfigFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_addonListFragment"
app:destination="@id/addonListFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_imListFragment"
app:destination="@id/imListFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_imConfigFragment"
app:destination="@id/imConfigFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_keyboardSettingsFragment"
app:destination="@id/keyboardSettingsFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_candidatesSettingsFragment"
app:destination="@id/candidatesSettingsFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_clipboardSettingsFragment"
app:destination="@id/clipboardSettingsFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_symbolSettingsFragment"
app:destination="@id/symbolSettingsFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_advancedSettingsFragment"
app:destination="@id/advancedSettingsFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_themeFragment"
app:destination="@id/themeFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_pinyinDictionaryFragment"
app:destination="@id/pinyinDictionaryFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_developerFragment"
app:destination="@id/developerFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_aboutFragment"
app:destination="@id/aboutFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_mainFragment_to_pluginFragment"
app:destination="@id/pluginFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/globalConfigFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.global.GlobalConfigFragment">
<action
android:id="@+id/action_globalConfigFragment_to_listFragment"
app:destination="@id/listFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/imListFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.im.InputMethodListFragment"
android:label="@string/input_methods">
<action
android:id="@+id/action_imListFragment_to_imConfigFragment"
app:destination="@id/imConfigFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/imConfigFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.im.InputMethodConfigFragment">
<action
android:id="@+id/action_imConfigFragment_to_listFragment"
app:destination="@id/listFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_imConfigFragment_to_pinyinDictionaryFragment"
app:destination="@id/pinyinDictionaryFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_imConfigFragment_to_punctuationEditorFragment"
app:destination="@id/punctuationEditorFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_imConfigFragment_to_quickPhraseListFragment"
app:destination="@id/quickPhraseListFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_imConfigFragment_to_addonConfigFragment"
app:destination="@id/addonConfigFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_imConfigFragment_to_pinyinCustomPhraseFragment"
app:destination="@id/pinyinCustomPhraseFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/addonListFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.addon.AddonListFragment"
android:label="@string/addons">
<action
android:id="@+id/action_addonListFragment_to_addonConfigFragment"
app:destination="@id/addonConfigFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/addonConfigFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.addon.AddonConfigFragment">
<action
android:id="@+id/action_addonConfigFragment_to_listFragment"
app:destination="@id/listFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_addonConfigFragment_to_pinyinDictionaryFragment"
app:destination="@id/pinyinDictionaryFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_addonConfigFragment_to_punctuationEditorFragment"
app:destination="@id/punctuationEditorFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_addonConfigFragment_to_quickPhraseListFragment"
app:destination="@id/quickPhraseListFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_addonConfigFragment_self"
app:destination="@id/addonConfigFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_addonConfigFragment_to_tableInputMethodFragment"
app:destination="@id/tableInputMethodFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
<action
android:id="@+id/action_addonConfigFragment_to_pinyinCustomPhraseFragment"
app:destination="@id/pinyinCustomPhraseFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/keyboardSettingsFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.behavior.KeyboardSettingsFragment"
android:label="@string/virtual_keyboard" />
<fragment
android:id="@+id/candidatesSettingsFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.behavior.CandidatesSettingsFragment"
android:label="@string/candidates_window" />
<fragment
android:id="@+id/clipboardSettingsFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.behavior.ClipboardSettingsFragment"
android:label="@string/clipboard" />
<fragment
android:id="@+id/symbolSettingsFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.behavior.SymbolSettingsFragment"
android:label="@string/emoji_and_symbols" />
<fragment
android:id="@+id/advancedSettingsFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.behavior.AdvancedSettingsFragment"
android:label="@string/advanced" />
<fragment
android:id="@+id/themeFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.theme.ThemeFragment"
android:label="@string/theme">
</fragment>
<fragment
android:id="@+id/listFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.ListFragment" />
<fragment
android:id="@+id/pinyinDictionaryFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.PinyinDictionaryFragment"
android:label="@string/pinyin_dict" />
<fragment
android:id="@+id/punctuationEditorFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.PunctuationEditorFragment" />
<fragment
android:id="@+id/developerFragment"
android:name="org.fcitx.fcitx5.android.ui.main.DeveloperFragment"
android:label="@string/developer" />
<fragment
android:id="@+id/aboutFragment"
android:name="org.fcitx.fcitx5.android.ui.main.AboutFragment"
android:label="@string/about">
<action
android:id="@+id/action_aboutFragment_to_licensesFragment"
app:destination="@id/licensesFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/licensesFragment"
android:name="org.fcitx.fcitx5.android.ui.main.LicensesFragment"
android:label="@string/open_source_licenses" />
<fragment
android:id="@+id/quickPhraseListFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.QuickPhraseListFragment"
android:label="@string/quickphrase_editor">
<action
android:id="@+id/action_quickPhraseListFragment_to_quickPhraseEditFragment"
app:destination="@id/quickPhraseEditFragment"
app:enterAnim="@animator/nav_default_enter_anim"
app:exitAnim="@animator/nav_default_exit_anim"
app:popEnterAnim="@animator/nav_default_pop_enter_anim"
app:popExitAnim="@animator/nav_default_pop_exit_anim" />
</fragment>
<fragment
android:id="@+id/quickPhraseEditFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.QuickPhraseEditFragment" />
<fragment
android:id="@+id/tableInputMethodFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.TableInputMethodFragment"
android:label="@string/table_im" />
<fragment
android:id="@+id/pluginFragment"
android:name="org.fcitx.fcitx5.android.ui.main.PluginFragment"
android:label="@string/plugins" />
<fragment
android:id="@+id/pinyinCustomPhraseFragment"
android:name="org.fcitx.fcitx5.android.ui.main.settings.PinyinCustomPhraseFragment" />
</navigation>