diff --git a/vector/src/main/java/de/spiritcroc/menu/ArrayOptionsMenuHelper.kt b/vector/src/main/java/de/spiritcroc/menu/ArrayOptionsMenuHelper.kt new file mode 100644 index 0000000000..6be18d9712 --- /dev/null +++ b/vector/src/main/java/de/spiritcroc/menu/ArrayOptionsMenuHelper.kt @@ -0,0 +1,62 @@ +package de.spiritcroc.menu + +import android.content.res.Resources +import android.view.Menu +import android.view.MenuItem +import android.view.View + +object ArrayOptionsMenuHelper { + private data class OptionsMenuMetadata( + val menuId: Int, + val entriesId: Int, + val valuesId: Int, + val values: HashMap = HashMap(), + var action: (String) -> Boolean = { false } + ) + + private val menuLookup = HashMap() + + fun createSubmenu(resources: Resources, + menuItem: MenuItem, + groupId: Int, + entriesId: Int, + valuesId: Int, + initialValue: String, + sortFunction: (List>) -> List> = { it }, + action: (String) -> Boolean) { + val menuId = menuItem.itemId + var metadata = menuLookup[menuId] + if (metadata == null || metadata.entriesId != entriesId || metadata.valuesId != valuesId) { + metadata = OptionsMenuMetadata(menuId, entriesId, valuesId) + menuLookup[menuId] = metadata + } + metadata.action = action + + menuItem.subMenu.apply { + clear() + val themeEntries = resources.getStringArray(entriesId) + val themeValues = resources.getStringArray(valuesId) + sortFunction(themeEntries.zip(themeValues)).forEach { theme -> + val itemId = View.generateViewId() + val item = add(groupId, itemId, Menu.NONE, theme.first).apply { + isCheckable = true + } + metadata.values[itemId] = theme.second + if (initialValue == theme.second) { + item.isChecked = true + } + } + } + } + + fun handleSubmenu(item: MenuItem, vararg menuIds: Int): Boolean { + menuIds.forEach { menuId -> + val metadata = menuLookup[menuId] ?: return@forEach + val value = metadata.values[item.itemId] ?: return@forEach + if (metadata.action(value)) { + return true + } + } + return false + } +} diff --git a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt index fce5485bd1..08a5c13f98 100644 --- a/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt +++ b/vector/src/main/java/im/vector/app/features/home/HomeActivity.kt @@ -35,12 +35,14 @@ import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint +import de.spiritcroc.menu.ArrayOptionsMenuHelper import im.vector.app.AppStateHandler import im.vector.app.R import im.vector.app.core.di.ActiveSessionHolder import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.extensions.replaceFragment +import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.validateBackPressed import im.vector.app.core.platform.VectorBaseActivity import im.vector.app.core.pushers.PushersManager @@ -536,6 +538,38 @@ class HomeActivity : override fun onPrepareOptionsMenu(menu: Menu): Boolean { menu.findItem(R.id.menu_home_init_sync_legacy)?.isVisible = vectorPreferences.developerMode() menu.findItem(R.id.menu_home_init_sync_optimized)?.isVisible = vectorPreferences.developerMode() + menu.findItem(R.id.dev_theming)?.isVisible = vectorPreferences.developerMode() + + // Base theme setting + ArrayOptionsMenuHelper.createSubmenu( + resources, + menu.findItem(R.id.dev_base_theme), + R.id.dev_base_theme_group, + R.array.theme_entries, + R.array.theme_values, + ThemeUtils.getCurrentActiveTheme(this) + ) { value -> + ThemeUtils.setCurrentActiveTheme(this, value) + restart() + true + } + + // Accent color theme setting + val isLightTheme = ThemeUtils.isLightTheme(this) + ArrayOptionsMenuHelper.createSubmenu( + resources, + menu.findItem(R.id.dev_theme_accent), + R.id.dev_theme_accent_group, + if (isLightTheme) R.array.sc_accent_color_light_entries else R.array.sc_accent_color_dark_entries, + if (isLightTheme) R.array.sc_accent_color_light_values else R.array.sc_accent_color_dark_values, + ThemeUtils.getCurrentActiveThemeAccent(this), + sortFunction = { list -> list.sortedBy { it.first } } + ) { value -> + ThemeUtils.setCurrentActiveThemeAccent(this, value) + restart() + true + } + return super.onPrepareOptionsMenu(menu) } @@ -575,7 +609,10 @@ class HomeActivity : } } - return super.onOptionsItemSelected(item) + return ArrayOptionsMenuHelper.handleSubmenu(item, + R.id.dev_base_theme, + R.id.dev_theme_accent + ) || super.onOptionsItemSelected(item) } override fun onBackPressed() { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 79c78c8e48..3b13a510c1 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -73,6 +73,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.vanniktech.emoji.EmojiPopup import de.spiritcroc.matrixsdk.util.DbgUtil import de.spiritcroc.matrixsdk.util.Dimber +import de.spiritcroc.menu.ArrayOptionsMenuHelper import de.spiritcroc.recyclerview.StickyHeaderItemDecoration import de.spiritcroc.recyclerview.widget.BetterLinearLayoutManager import de.spiritcroc.recyclerview.widget.LinearLayoutManager @@ -85,6 +86,7 @@ import im.vector.app.core.epoxy.LayoutManagerStateRestorer import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.hideKeyboard import im.vector.app.core.extensions.registerStartForActivityResult +import im.vector.app.core.extensions.restart import im.vector.app.core.extensions.setTextOrHide import im.vector.app.core.extensions.showKeyboard import im.vector.app.core.extensions.trackItemsVisibilityChange @@ -1165,18 +1167,68 @@ class TimelineFragment @Inject constructor( updateMenuThreadNotificationBadge(menu, state) } - // Selected bubble style - val selectedBubbleStyle = when (bubbleThemeUtils.getBubbleStyle()) { - BubbleThemeUtils.BUBBLE_STYLE_NONE -> R.id.dev_bubble_style_none - BubbleThemeUtils.BUBBLE_STYLE_START -> R.id.dev_bubble_style_start - BubbleThemeUtils.BUBBLE_STYLE_BOTH -> R.id.dev_bubble_style_both - BubbleThemeUtils.BUBBLE_STYLE_ELEMENT -> R.id.dev_bubble_style_element - else -> R.id.dev_bubble_style_both - } - menu.findItem(selectedBubbleStyle).isChecked = true - // Hidden events menu.findItem(R.id.dev_hidden_events).isChecked = vectorPreferences.shouldShowHiddenEvents() + + // Bubble style + ArrayOptionsMenuHelper.createSubmenu( + resources, + menu.findItem(R.id.dev_bubble_style), + R.id.dev_bubble_style_group, + R.array.bubble_style_entries, + R.array.bubble_style_values, + bubbleThemeUtils.getBubbleStyle() + ) { value -> + if (value != bubbleThemeUtils.getBubbleStyle()) { + bubbleThemeUtils.setBubbleStyle(value) + requireActivity().restart() + } + true + } + + // Base theme setting + ArrayOptionsMenuHelper.createSubmenu( + resources, + menu.findItem(R.id.dev_base_theme), + R.id.dev_base_theme_group, + R.array.theme_entries, + R.array.theme_values, + ThemeUtils.getCurrentActiveTheme(requireContext()) + ) { value -> + ThemeUtils.setCurrentActiveTheme(requireContext(), value) + requireActivity().restart() + true + } + + // Accent color theme setting + val isLightTheme = ThemeUtils.isLightTheme(requireContext()) + ArrayOptionsMenuHelper.createSubmenu( + resources, + menu.findItem(R.id.dev_theme_accent), + R.id.dev_theme_accent_group, + if (isLightTheme) R.array.sc_accent_color_light_entries else R.array.sc_accent_color_dark_entries, + if (isLightTheme) R.array.sc_accent_color_light_values else R.array.sc_accent_color_dark_values, + ThemeUtils.getCurrentActiveThemeAccent(requireContext()), + sortFunction = { list -> list.sortedBy { it.first } } + ) { value -> + ThemeUtils.setCurrentActiveThemeAccent(requireContext(), value) + requireActivity().restart() + true + } + + // Bubble corner radius + ArrayOptionsMenuHelper.createSubmenu( + resources, + menu.findItem(R.id.dev_bubble_rounded_corners), + R.id.dev_bubble_rounded_corners_group, + R.array.bubble_appearance_roundness_entries, + R.array.bubble_appearance_roundness_values, + bubbleThemeUtils.getBubbleRoundnessSetting() + ) { value -> + bubbleThemeUtils.setBubbleRoundnessSetting(value) + reloadTimeline() + true + } } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -1209,22 +1261,6 @@ class TimelineFragment @Inject constructor( navigator.openRoomProfile(requireActivity(), timelineArgs.roomId) true } - R.id.dev_bubble_style_none -> { - handleSetBubbleStyle(BubbleThemeUtils.BUBBLE_STYLE_NONE) - true - } - R.id.dev_bubble_style_start -> { - handleSetBubbleStyle(BubbleThemeUtils.BUBBLE_STYLE_START) - true - } - R.id.dev_bubble_style_both -> { - handleSetBubbleStyle(BubbleThemeUtils.BUBBLE_STYLE_BOTH) - true - } - R.id.dev_bubble_style_element -> { - handleSetBubbleStyle(BubbleThemeUtils.BUBBLE_STYLE_ELEMENT) - true - } R.id.dev_hidden_events -> { val shouldShow = !item.isChecked vectorPreferences.setShouldShowHiddenEvents(shouldShow) @@ -1263,7 +1299,12 @@ class TimelineFragment @Inject constructor( } true } - else -> super.onOptionsItemSelected(item) + else -> ArrayOptionsMenuHelper.handleSubmenu(item, + R.id.dev_base_theme, + R.id.dev_bubble_style, + R.id.dev_theme_accent, + R.id.dev_bubble_rounded_corners + ) || super.onOptionsItemSelected(item) } } @@ -2851,13 +2892,6 @@ class TimelineFragment @Inject constructor( } } - private fun handleSetBubbleStyle(bubbleStyle: String) { - if (bubbleStyle != bubbleThemeUtils.getBubbleStyle()) { - bubbleThemeUtils.setBubbleStyle(bubbleStyle) - requireActivity().recreate() - } - } - // AttachmentsHelper.Callback override fun onContentAttachmentsReady(attachments: List) { val grouped = attachments.toGroupedContentAttachmentData() diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index bc11cca2d2..b0b8b1f034 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -785,6 +785,7 @@ class TimelineViewModel @AssistedInject constructor( R.id.show_participants -> true // SC R.id.dev_bubble_style, // SC R.id.dev_hidden_events, // SC + R.id.dev_theming, // SC R.id.dev_tools -> vectorPreferences.developerMode() else -> false } diff --git a/vector/src/main/java/im/vector/app/features/themes/BubbleThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/BubbleThemeUtils.kt index b10edb75d3..911e4a974d 100644 --- a/vector/src/main/java/im/vector/app/features/themes/BubbleThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/BubbleThemeUtils.kt @@ -72,9 +72,18 @@ class BubbleThemeUtils @Inject constructor(private val context: Context) { PreferenceManager.getDefaultSharedPreferences(context).edit().putString(BUBBLE_STYLE_KEY, value).apply() } + fun getBubbleRoundnessSetting(): String { + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + return prefs.getString(BUBBLE_ROUNDNESS_KEY, BUBBLE_ROUNDNESS_DEFAULT) ?: BUBBLE_ROUNDNESS_DEFAULT + } + + fun setBubbleRoundnessSetting(value: String) { + PreferenceManager.getDefaultSharedPreferences(context).edit().putString(BUBBLE_ROUNDNESS_KEY, value).apply() + } + fun getBubbleAppearance(): ScBubbleAppearance { val prefs = PreferenceManager.getDefaultSharedPreferences(context) - val baseAppearance = when (prefs.getString(BUBBLE_ROUNDNESS_KEY, BUBBLE_ROUNDNESS_DEFAULT)) { + val baseAppearance = when (getBubbleRoundnessSetting()) { BUBBLE_ROUNDNESS_R1 -> r1ScBubbleAppearance BUBBLE_ROUNDNESS_R2 -> r2ScBubbleAppearance else -> defaultScBubbleAppearance diff --git a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt index 570425e66d..adec31c410 100644 --- a/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt +++ b/vector/src/main/java/im/vector/app/features/themes/ThemeUtils.kt @@ -273,6 +273,52 @@ object ThemeUtils { mColorByAttr.clear() } + // For developer quick options + fun setCurrentActiveTheme(context: Context, theme: String) { + var darkTheme = getApplicationDarkTheme(context) + var lightTheme = getApplicationLightTheme(context) + val darkAccent = getApplicationDarkThemeAccent(context) + val lightAccent = getApplicationLightThemeAccent(context) + if (useDarkTheme(context)) { + darkTheme = theme + } else { + lightTheme = theme + } + setApplicationTheme(context, lightTheme, darkTheme, lightAccent, darkAccent) + } + + // For developer quick options + fun getCurrentActiveTheme(context: Context): String { + return if (useDarkTheme(context)) { + getApplicationDarkTheme(context) + } else { + getApplicationLightTheme(context) + } + } + + // For developer quick options + fun setCurrentActiveThemeAccent(context: Context, accent: String) { + val darkTheme = getApplicationDarkTheme(context) + val lightTheme = getApplicationLightTheme(context) + var darkAccent = getApplicationDarkThemeAccent(context) + var lightAccent = getApplicationLightThemeAccent(context) + if (useDarkTheme(context)) { + darkAccent = accent + } else { + lightAccent = accent + } + setApplicationTheme(context, lightTheme, darkTheme, lightAccent, darkAccent) + } + + // For developer quick options + fun getCurrentActiveThemeAccent(context: Context): String { + return if (useDarkTheme(context)) { + getApplicationDarkThemeAccent(context) + } else { + getApplicationLightThemeAccent(context) + } + } + fun setApplicationLightTheme(context: Context, theme: String) { setApplicationTheme(context, theme, getApplicationDarkTheme(context), getApplicationLightThemeAccent(context), getApplicationDarkThemeAccent(context)) diff --git a/vector/src/main/res/menu/home.xml b/vector/src/main/res/menu/home.xml index e65b8b83ee..02a7c0cea0 100644 --- a/vector/src/main/res/menu/home.xml +++ b/vector/src/main/res/menu/home.xml @@ -38,4 +38,36 @@ app:iconTint="?vctr_content_secondary" app:showAsAction="always" /> + + + + + + + + + + + + + + + + + + + diff --git a/vector/src/main/res/menu/menu_timeline.xml b/vector/src/main/res/menu/menu_timeline.xml index 72df69f063..00190a2153 100644 --- a/vector/src/main/res/menu/menu_timeline.xml +++ b/vector/src/main/res/menu/menu_timeline.xml @@ -90,22 +90,52 @@ android:visible="false" app:showAsAction="never" tools:visible="true"> + + android:id="@+id/dev_bubble_style_group" + android:checkableBehavior="single" /> + + + + + + + android:id="@+id/dev_base_theme" + android:title="@string/settings_theme"> + + + + + + android:id="@+id/dev_theme_accent" + android:title="@string/setting_sc_accent_color"> + + + + + - + android:id="@+id/dev_bubble_rounded_corners" + android:title="@string/bubble_rounded_corners"> + + + + + diff --git a/vector/src/main/res/values/strings_sc.xml b/vector/src/main/res/values/strings_sc.xml index 387db8fc42..a8bb154a9e 100644 --- a/vector/src/main/res/values/strings_sc.xml +++ b/vector/src/main/res/values/strings_sc.xml @@ -93,6 +93,7 @@ Advanced theme settings User colors Theme colors + Accent color Accent color for light themes Accent color for dark themes Note that these color settings only apply to SC themes, and not Element themes.