diff --git a/CHANGELOG.md b/CHANGELOG.md index a1c11e71..c40eff41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,3 +90,7 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). - The legal policies of the application are displayed to the user in the About tab ([#58](https://github.com/scribe-org/Scribe-Android/issues/58)). - A privacy policy was provided to make clear that policies around user data and their security ([#59](https://github.com/scribe-org/Scribe-Android/issues/59)). - Third party licensed code used in the development of the project were detailed ([#60](https://github.com/scribe-org/Scribe-Android/issues/60)). + +### ♻️ Code Refactoring + +- Code quality improvements were continuously done to assure that the application is easy to maintain and meets Kotlin standards ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)). diff --git a/CHANGELOG_CONJUGATE.md b/CHANGELOG_CONJUGATE.md index d21283d9..91fd8f97 100644 --- a/CHANGELOG_CONJUGATE.md +++ b/CHANGELOG_CONJUGATE.md @@ -66,3 +66,7 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). - The legal policies of the application are displayed to the user in the About tab ([#58](https://github.com/scribe-org/Scribe-Android/issues/58), [#561](https://github.com/scribe-org/Scribe-Android/issues/561)). - A privacy policy was provided to make clear that policies around user data and their security ([#59](https://github.com/scribe-org/Scribe-Android/issues/59)). - Third party licensed code used in the development of the project were detailed ([#60](https://github.com/scribe-org/Scribe-Android/issues/60)). + +### ♻️ Code Refactoring + +- Code quality improvements were continuously done to assure that the application is easy to maintain and meets Kotlin standards ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)). diff --git a/app/src/keyboards/java/be/scri/helpers/FloatingKeyboardHandler.kt b/app/src/keyboards/java/be/scri/helpers/FloatingKeyboardHandler.kt index 39ab5832..0c237ef4 100644 --- a/app/src/keyboards/java/be/scri/helpers/FloatingKeyboardHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/FloatingKeyboardHandler.kt @@ -265,7 +265,7 @@ class FloatingKeyboardHandler( root.requestLayout() } } - ime.applyNavBarColorPublic() + ime.applyNavBarColor() } private fun updateFloatingViewsPosition( diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardThemeManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardThemeManager.kt new file mode 100644 index 00000000..788ef0f9 --- /dev/null +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardThemeManager.kt @@ -0,0 +1,237 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers.ui + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.LayerDrawable +import android.graphics.drawable.RippleDrawable +import android.inputmethodservice.InputMethodService +import android.os.Build +import android.view.View +import android.view.Window +import android.widget.Button +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.toColorInt +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import be.scri.R +import be.scri.helpers.PreferencesHelper.getIsDarkModeOrNot + +/** + * Manages UI theme color resolutions, gradient/ripple drawables creation, + * navigation bar colors, and system bar insets for the Scribe keyboard. + */ +class KeyboardThemeManager { + /** + * Calculates whether a given ARGB color is considered light based on relative luminance. + */ + fun isLightColor(color: Int): Boolean { + val red = (color shr 16) and 0xFF + val green = (color shr 8) and 0xFF + val blue = color and 0xFF + val darkness = 1 - (0.299 * red + 0.587 * green + 0.114 * blue) / 255 + return darkness < 0.5 + } + + /** + * Applies navigation bar color, window decor insets, light/dark appearance flags, and system bar behaviors. + */ + fun applyNavBarColor( + service: InputMethodService, + window: Window?, + isFloatingMode: Boolean, + uiManager: KeyboardUIManager?, + ) { + val targetWindow = window ?: return + targetWindow.decorView.post { + val context = service.applicationContext + val isDarkMode = getIsDarkModeOrNot(context) + val colorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color + val color = ContextCompat.getColor(service, colorRes) + + WindowCompat.setDecorFitsSystemWindows(targetWindow, false) + if (Build.VERSION.SDK_INT < 35) { + @Suppress("DEPRECATION") + targetWindow.navigationBarColor = Color.TRANSPARENT + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + targetWindow.isNavigationBarContrastEnforced = false + } + + if (isFloatingMode) { + targetWindow.decorView.setBackgroundColor(Color.TRANSPARENT) + } else { + targetWindow.decorView.setBackgroundColor(color) + } + val insetsController = WindowCompat.getInsetsController(targetWindow, targetWindow.decorView) + insetsController.isAppearanceLightNavigationBars = isLightColor(color) + + if (isFloatingMode) { + insetsController.hide(WindowInsetsCompat.Type.navigationBars()) + insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + @Suppress("DEPRECATION") + targetWindow.decorView.systemUiVisibility = ( + View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY + ) + } else { + insetsController.show(WindowInsetsCompat.Type.navigationBars()) + @Suppress("DEPRECATION") + targetWindow.decorView.systemUiVisibility = 0 + } + + if (uiManager != null) { + if (isFloatingMode) { + uiManager.binding.root.setBackgroundColor(Color.TRANSPARENT) + val kbBgColor = ContextCompat.getColor(service, if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color) + uiManager.binding.floatingDragBar.setBackgroundColor(kbBgColor) + val pillColor = if (isDarkMode) 0x4DFFFFFF.toInt() else 0x40000000.toInt() + uiManager.binding.floatingDragHandle.setColorFilter(pillColor) + } else { + uiManager.binding.root.setBackgroundColor(color) + } + + ViewCompat.setOnApplyWindowInsetsListener(uiManager.binding.root) { view, insets -> + val insetTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() + val navBarHeight = insets.getInsets(insetTypes).bottom + val paddingBottom = if (isFloatingMode) 0 else navBarHeight + view.setPadding(0, 0, 0, paddingBottom) + insets + } + + uiManager.binding.root.post { + ViewCompat.requestApplyInsets(uiManager.binding.root) + } + } + } + } + + /** + * Applies text colors, icon tints, ripple backgrounds, and shadow colors to empty state banner views. + */ + fun applyBannerTheme( + context: Context, + banner: TextView, + bannerContainer: View, + isDarkMode: Boolean = getIsDarkModeOrNot(context), + density: Float = context.resources.displayMetrics.density, + ) { + val bannerColor = if (isDarkMode) R.color.dark_tutorial_button_color else R.color.light_tutorial_button_color + val bannerTextColor = if (isDarkMode) R.color.dark_button_outline_color else R.color.light_text_color + banner.setTextColor(ContextCompat.getColor(context, bannerTextColor)) + + banner.post { + val iconColor = ContextCompat.getColor(context, bannerTextColor) + banner.compoundDrawables.forEach { drawable -> + drawable?.setTint(iconColor) + } + } + + val border = GradientDrawable() + border.cornerRadius = 12f * density + border.setColor(ContextCompat.getColor(context, bannerColor)) + + if (isDarkMode) { + border.setStroke( + (1.5f * density).toInt(), + ContextCompat.getColor(context, bannerTextColor), + ) + } + + val rippleColor = + ColorUtils.setAlphaComponent( + ContextCompat.getColor(context, bannerTextColor), + 51, + ) + val rippleDrawable = RippleDrawable(ColorStateList.valueOf(rippleColor), border, null) + + bannerContainer.background = rippleDrawable + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + bannerContainer.outlineAmbientShadowColor = Color.TRANSPARENT + bannerContainer.outlineSpotShadowColor = Color.TRANSPARENT + } + } + + /** + * Applies a specific style to a suggestion button, including text, color, and a custom background. + */ + fun applyInformativeSuggestionStyle( + context: Context, + button: Button, + colorRes: Int, + text: String, + backgroundRes: Int, + ) { + button.text = text + button.setTextColor(ContextCompat.getColor(context, R.color.white)) + button.isClickable = false + button.setOnClickListener(null) + + val background = ContextCompat.getDrawable(context, backgroundRes)?.mutate() + + if (background is RippleDrawable) { + val contentDrawable = background.getDrawable(0) + + if (contentDrawable is LayerDrawable) { + val shapeDrawable = + contentDrawable.findDrawableByLayerId( + R.id.button_background_shape, + ) as? GradientDrawable + + shapeDrawable?.setColor( + ContextCompat.getColor( + context, + colorRes, + ), + ) + } + } + button.background = background + } + + /** + * Applies rounded background, tint, and text color to a single suggestion button based on color resource and dark mode. + */ + fun applySingleSuggestionStyle( + context: Context, + button: Button, + colorRes: Int, + buttonText: String, + textSizeSp: Float? = null, + ) { + button.visibility = View.VISIBLE + button.text = buttonText + if (textSizeSp != null) { + button.textSize = textSizeSp + } + button.isClickable = false + button.setOnClickListener(null) + + if (colorRes != R.color.transparent) { + button.background = ContextCompat.getDrawable(context, R.drawable.button_background_rounded) + button.backgroundTintList = ContextCompat.getColorStateList(context, colorRes) + button.setTextColor(ContextCompat.getColor(context, R.color.white)) + } else { + button.background = null + val isUserDarkMode = getIsDarkModeOrNot(context) + button.backgroundTintList = ContextCompat.getColorStateList(context, R.color.transparent) + button.setTextColor(ContextCompat.getColor(context, if (isUserDarkMode) R.color.white else android.R.color.black)) + } + } + + /** + * Resolves text color for standard word autocompletion suggestion buttons based on dark/light mode preference. + */ + fun getSuggestionTextColor(context: Context): Int { + val isDarkMode = getIsDarkModeOrNot(context) + return if (isDarkMode) Color.WHITE else "#1E1E1E".toColorInt() + } +} diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 9dd61579..a7bbee29 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -3,17 +3,10 @@ package be.scri.services import DataContract -import android.R.color.white import android.content.Context import android.content.Intent -import android.content.res.ColorStateList -import android.graphics.Color import android.graphics.Rect -import android.graphics.drawable.GradientDrawable -import android.graphics.drawable.LayerDrawable -import android.graphics.drawable.RippleDrawable import android.inputmethodservice.InputMethodService -import android.os.Build import android.text.InputType import android.text.InputType.TYPE_CLASS_DATETIME import android.text.InputType.TYPE_CLASS_NUMBER @@ -29,14 +22,7 @@ import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.InputConnection import android.widget.Button import android.widget.TextView -import androidx.core.content.ContextCompat import androidx.core.content.edit -import androidx.core.graphics.ColorUtils -import androidx.core.graphics.toColorInt -import androidx.core.view.ViewCompat -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.WindowInsetsControllerCompat import be.scri.R import be.scri.activities.MainActivity import be.scri.databinding.InputMethodViewBinding @@ -55,7 +41,6 @@ import be.scri.helpers.LanguageMappingConstants.getLanguageAlias import be.scri.helpers.NativeSuggestionEngine import be.scri.helpers.PreferencesHelper import be.scri.helpers.PreferencesHelper.getHoldKeyStyle -import be.scri.helpers.PreferencesHelper.getIsDarkModeOrNot import be.scri.helpers.PreferencesHelper.getIsEmojiSuggestionsEnabled import be.scri.helpers.PreferencesHelper.getIsSoundEnabled import be.scri.helpers.PreferencesHelper.getIsVibrateEnabled @@ -68,6 +53,7 @@ import be.scri.helpers.clipboard.ClipboardHandler import be.scri.helpers.data.AutocompletionDataManager import be.scri.helpers.english.ENInterfaceVariables.ALREADY_PLURAL_MSG import be.scri.helpers.recordRecentEmoji +import be.scri.helpers.ui.KeyboardThemeManager import be.scri.helpers.ui.KeyboardUIManager import be.scri.models.ScribeLanguage import be.scri.models.ScribeState @@ -177,8 +163,6 @@ abstract class GeneralKeyboardIME( internal fun recreateKeyboardPublic() = recreateKeyboard() - internal fun applyNavBarColorPublic() = applyNavBarColor() - var emojiKeywords: HashMap>? get() = dataHandler.emojiKeywords set(value) { @@ -238,6 +222,7 @@ abstract class GeneralKeyboardIME( private var isNumericKeyboardActive: Boolean = false internal val stateManager = KeyboardStateManager() + internal val themeManager = KeyboardThemeManager() internal var currentState: ScribeState get() = stateManager.currentState @@ -498,42 +483,11 @@ abstract class GeneralKeyboardIME( if (hasData) View.GONE else View.VISIBLE binding.commandOptionsBar.visibility = if (hasData && !isNumericKeyboardActive) View.VISIBLE else View.GONE - val isDarkMode = getIsDarkModeOrNot(applicationContext) - val bannerColor = - if (isDarkMode) R.color.dark_tutorial_button_color else R.color.light_tutorial_button_color - val bannerTextColor = - if (isDarkMode) R.color.dark_button_outline_color else R.color.light_text_color - banner.setTextColor(ContextCompat.getColor(applicationContext, bannerTextColor)) - banner.post { - val iconColor = ContextCompat.getColor(applicationContext, bannerTextColor) - banner.compoundDrawables.forEach { drawable -> - drawable?.setTint(iconColor) - } - } - val border = GradientDrawable() - border.cornerRadius = 12f * resources.displayMetrics.density - border.setColor(ContextCompat.getColor(applicationContext, bannerColor)) - - if (isDarkMode) { - border.setStroke( - (1.5f * resources.displayMetrics.density).toInt(), - ContextCompat.getColor(applicationContext, bannerTextColor), - ) - } - - val rippleColor = - ColorUtils.setAlphaComponent( - ContextCompat.getColor(applicationContext, bannerTextColor), - 51, - ) - val rippleDrawable = - RippleDrawable(ColorStateList.valueOf(rippleColor), border, null) - - bannerContainer.background = rippleDrawable - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - bannerContainer.outlineAmbientShadowColor = Color.TRANSPARENT - bannerContainer.outlineSpotShadowColor = Color.TRANSPARENT - } + themeManager.applyBannerTheme( + context = applicationContext, + banner = banner, + bannerContainer = bannerContainer, + ) bannerContainer.setOnClickListener { val intent = @@ -718,75 +672,13 @@ abstract class GeneralKeyboardIME( dataHandler.loadLanguageData(language) } - private fun isLightColor(color: Int): Boolean { - val darkness = 1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255 - return darkness < 0.5 - } - - private fun applyNavBarColor() { - val window = window?.window ?: return - window.decorView.post { - val isDarkMode = getIsDarkModeOrNot(applicationContext) - val colorRes = if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color - val color = ContextCompat.getColor(this, colorRes) - - WindowCompat.setDecorFitsSystemWindows(window, false) - if (Build.VERSION.SDK_INT < 35) { - window.navigationBarColor = Color.TRANSPARENT - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - window.isNavigationBarContrastEnforced = false - } - - if (isFloatingMode) { - window.decorView.setBackgroundColor(Color.TRANSPARENT) - } else { - window.decorView.setBackgroundColor(color) - } - val insetsController = WindowCompat.getInsetsController(window, window.decorView) - insetsController.isAppearanceLightNavigationBars = isLightColor(color) - - if (isFloatingMode) { - insetsController.hide(WindowInsetsCompat.Type.navigationBars()) - insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - @Suppress("DEPRECATION") - window.decorView.systemUiVisibility = ( - View.SYSTEM_UI_FLAG_HIDE_NAVIGATION - or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY - ) - } else { - insetsController.show(WindowInsetsCompat.Type.navigationBars()) - @Suppress("DEPRECATION") - window.decorView.systemUiVisibility = 0 - } - - if (this::uiManager.isInitialized) { - if (isFloatingMode) { - uiManager.binding.root.setBackgroundColor(Color.TRANSPARENT) - // Keep drag bar and pill color in sync with dark/light mode changes - val kbBgColor = ContextCompat.getColor(this, if (isDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color) - uiManager.binding.floatingDragBar.setBackgroundColor(kbBgColor) - // Pill: dark mode → 30% white, light mode → 25% black - val pillColor = if (isDarkMode) 0x4DFFFFFF.toInt() else 0x40000000.toInt() - uiManager.binding.floatingDragHandle.setColorFilter(pillColor) - } else { - uiManager.binding.root.setBackgroundColor(color) - } - - ViewCompat.setOnApplyWindowInsetsListener(uiManager.binding.root) { view, insets -> - val insetTypes = WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout() - val navBarHeight = insets.getInsets(insetTypes).bottom - val paddingBottom = if (isFloatingMode) 0 else navBarHeight - view.setPadding(0, 0, 0, paddingBottom) - insets - } - - uiManager.binding.root.post { - ViewCompat.requestApplyInsets(uiManager.binding.root) - } - } - } + internal fun applyNavBarColor() { + themeManager.applyNavBarColor( + service = this, + window = window?.window, + isFloatingMode = isFloatingMode, + uiManager = if (this::uiManager.isInitialized) uiManager else null, + ) } /** @@ -1593,16 +1485,13 @@ abstract class GeneralKeyboardIME( if (isPlural) { uiManager.genderSuggestionLeft?.visibility = View.INVISIBLE uiManager.genderSuggestionRight?.visibility = View.INVISIBLE - uiManager.binding.translateBtn.apply { - visibility = View.VISIBLE - text = "PL" - textSize = NOUN_TYPE_SIZE - background = ContextCompat.getDrawable(context, R.drawable.button_background_rounded) - backgroundTintList = ContextCompat.getColorStateList(context, R.color.annotateOrange) - setTextColor(ContextCompat.getColor(context, white)) - isClickable = false - setOnClickListener(null) - } + themeManager.applySingleSuggestionStyle( + context = applicationContext, + button = uiManager.binding.translateBtn, + colorRes = R.color.annotateOrange, + buttonText = "PL", + textSizeSp = NOUN_TYPE_SIZE, + ) return true } return false @@ -1708,25 +1597,14 @@ abstract class GeneralKeyboardIME( uiManager.genderSuggestionLeft?.visibility = View.INVISIBLE uiManager.genderSuggestionRight?.visibility = View.INVISIBLE - uiManager.binding.translateBtn.textSize = NOUN_TYPE_SIZE - - uiManager.binding.translateBtn.apply { - visibility = View.VISIBLE - text = buttonText - isClickable = false - setOnClickListener(null) - - if (colorRes != R.color.transparent) { - background = ContextCompat.getDrawable(context, R.drawable.button_background_rounded) - backgroundTintList = ContextCompat.getColorStateList(context, colorRes) - setTextColor(ContextCompat.getColor(context, white)) - } else { - background = null - val isUserDarkMode = getIsDarkModeOrNot(applicationContext) - backgroundTintList = ContextCompat.getColorStateList(context, R.color.transparent) - setTextColor(ContextCompat.getColor(context, if (isUserDarkMode) white else android.R.color.black)) - } - } + + themeManager.applySingleSuggestionStyle( + context = applicationContext, + button = uiManager.binding.translateBtn, + colorRes = colorRes, + buttonText = buttonText, + textSizeSp = NOUN_TYPE_SIZE, + ) } /** @@ -1743,31 +1621,13 @@ abstract class GeneralKeyboardIME( text: String, backgroundRes: Int, ) { - button.text = text - button.setTextColor(ContextCompat.getColor(applicationContext, be.scri.R.color.white)) - button.isClickable = false - button.setOnClickListener(null) - - val background = ContextCompat.getDrawable(applicationContext, backgroundRes)?.mutate() - - if (background is RippleDrawable) { - val contentDrawable = background.getDrawable(0) - - if (contentDrawable is LayerDrawable) { - val shapeDrawable = - contentDrawable.findDrawableByLayerId( - be.scri.R.id.button_background_shape, - ) as? GradientDrawable - - shapeDrawable?.setColor( - ContextCompat.getColor( - applicationContext, - colorRes, - ), - ) - } - } - button.background = background + themeManager.applyInformativeSuggestionStyle( + context = applicationContext, + button = button, + colorRes = colorRes, + text = text, + backgroundRes = backgroundRes, + ) } /** @@ -1918,8 +1778,6 @@ abstract class GeneralKeyboardIME( button: Button, text: String, ) { - val isUserDarkMode = getIsDarkModeOrNot(applicationContext) - val textColor = if (isUserDarkMode) Color.WHITE else "#1E1E1E".toColorInt() button.text = text button.isAllCaps = false button.visibility = View.VISIBLE @@ -1927,7 +1785,7 @@ abstract class GeneralKeyboardIME( button.setOnClickListener(null) button.background = null button.foreground = null - button.setTextColor(textColor) + button.setTextColor(themeManager.getSuggestionTextColor(applicationContext)) button.setOnClickListener { currentInputConnection?.commitText("$text ", 1) moveToIdleState() diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/ui/KeyboardThemeManagerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/ui/KeyboardThemeManagerTest.kt new file mode 100644 index 00000000..5292ec88 --- /dev/null +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/ui/KeyboardThemeManagerTest.kt @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers.ui + +import android.graphics.Color +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class KeyboardThemeManagerTest { + private lateinit var themeManager: KeyboardThemeManager + + @Before + fun setUp() { + themeManager = KeyboardThemeManager() + } + + @Test + fun isLightColor_whiteColor_returnsTrue() { + assertTrue(themeManager.isLightColor(Color.WHITE)) + } + + @Test + fun isLightColor_blackColor_returnsFalse() { + assertFalse(themeManager.isLightColor(Color.BLACK)) + } + + @Test + fun isLightColor_lightGrayColor_returnsTrue() { + assertTrue(themeManager.isLightColor(Color.LTGRAY)) + } + + @Test + fun isLightColor_darkGrayColor_returnsFalse() { + assertFalse(themeManager.isLightColor(Color.DKGRAY)) + } +}