From 71a8efff91a0cb104d1df6d294ac0a3d9ad188f9 Mon Sep 17 00:00:00 2001 From: cmclark00 Date: Mon, 31 Mar 2025 16:02:30 -0400 Subject: [PATCH] Fix customization menu navigation and layout issues: - Fix D-pad navigation between theme and block skin selectors - Prevent word wrapping in customization title - Add ThemeManager for theme color management - Improve menu item highlighting and focus handling --- app/src/main/java/com/mintris/MainActivity.kt | 427 ++++++++++++++---- app/src/main/java/com/mintris/ThemeManager.kt | 19 + .../res/drawable/menu_section_background.xml | 6 + .../main/res/layout-land/activity_main.xml | 122 ++++- app/src/main/res/layout/activity_main.xml | 94 +++- app/src/main/res/values/strings.xml | 1 + 6 files changed, 555 insertions(+), 114 deletions(-) create mode 100644 app/src/main/java/com/mintris/ThemeManager.kt create mode 100644 app/src/main/res/drawable/menu_section_background.xml diff --git a/app/src/main/java/com/mintris/MainActivity.kt b/app/src/main/java/com/mintris/MainActivity.kt index 50821e2..8eb5557 100644 --- a/app/src/main/java/com/mintris/MainActivity.kt +++ b/app/src/main/java/com/mintris/MainActivity.kt @@ -105,6 +105,10 @@ class MainActivity : AppCompatActivity(), // Track currently selected menu item in pause menu for gamepad navigation private var currentMenuSelection = 0 private val pauseMenuItems = mutableListOf() + private val customizationMenuItems = mutableListOf() + + // Add these new properties at the class level + private var currentCustomizationMenuSelection = 0 override fun onCreate(savedInstanceState: Bundle?) { // Register activity result launcher for high score entry @@ -130,8 +134,8 @@ class MainActivity : AppCompatActivity(), highScoreManager = HighScoreManager(this) statsManager = StatsManager(this) progressionManager = PlayerProgressionManager(this) - themeSelector = binding.themeSelector - blockSkinSelector = binding.blockSkinSelector + themeSelector = binding.customizationThemeSelector!! + blockSkinSelector = binding.customizationBlockSkinSelector!! pauseMenuScrollView = binding.pauseMenuScrollView // Initialize gamepad controller @@ -190,44 +194,6 @@ class MainActivity : AppCompatActivity(), } } - // Set up landscape mode theme selector if available - val inPauseThemeSelector = findViewById(R.id.inPauseThemeSelector) - inPauseThemeSelector?.onThemeSelected = { themeId: String -> - // Apply the new theme - applyTheme(themeId) - - // Provide haptic feedback - gameHaptics.vibrateForPieceLock() - - // Refresh the pause menu - showPauseMenu() - } - - // Set up block skin selector - blockSkinSelector.onBlockSkinSelected = { skinId: String -> - // Apply the new block skin - gameView.setBlockSkin(skinId) - - // Save the selection - progressionManager.setSelectedBlockSkin(skinId) - - // Provide haptic feedback - gameHaptics.vibrateForPieceLock() - } - - // Set up landscape mode block skin selector if available - val inPauseBlockSkinSelector = findViewById(R.id.inPauseBlockSkinSelector) - inPauseBlockSkinSelector?.onBlockSkinSelected = { skinId: String -> - // Apply the new block skin - gameView.setBlockSkin(skinId) - - // Save the selection - progressionManager.setSelectedBlockSkin(skinId) - - // Provide haptic feedback - gameHaptics.vibrateForPieceLock() - } - // Set up title screen titleScreen.onStartGame = { titleScreen.visibility = View.GONE @@ -405,6 +371,18 @@ class MainActivity : AppCompatActivity(), startActivity(intent) } + // Set up customization button + binding.customizationButton.setOnClickListener { + gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) + showCustomizationMenu() + } + + // Set up customization back button + binding.customizationBackButton.setOnClickListener { + gameHaptics.performHapticFeedback(it, HapticFeedbackConstants.VIRTUAL_KEY) + hideCustomizationMenu() + } + // Initialize level selector updateLevelSelector() @@ -493,12 +471,6 @@ class MainActivity : AppCompatActivity(), Log.d("MainActivity", "Triggering game over animation") gameView.startGameOverAnimation() - // Hide game UI elements in landscape mode - if (resources.configuration.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE) { - binding.leftControlsPanel?.visibility = View.GONE - binding.rightControlsPanel?.visibility = View.GONE - } - // Wait a moment before showing progression screen to let animation be visible Handler(Looper.getMainLooper()).postDelayed({ // Show progression screen first with XP animation @@ -549,11 +521,119 @@ class MainActivity : AppCompatActivity(), progressionScreen.visibility = View.GONE } + /** + * Show customization menu + */ + private fun showCustomizationMenu() { + binding.customizationContainer.visibility = View.VISIBLE + binding.pauseContainer.visibility = View.GONE + + // Update theme colors + val themeColors = ThemeManager.getThemeColors() + binding.customizationContainer.setBackgroundColor(themeColors.background) + + // Update level badge + binding.customizationLevelBadge.setLevel(progressionManager.getPlayerLevel()) + binding.customizationLevelBadge.setThemeColor(themeColors.accent) + + // Apply theme colors to text + binding.customizationTitle.setTextColor(themeColors.text.toInt()) + + // Update theme and block skin selectors + binding.customizationThemeSelector.updateThemes( + progressionManager.getUnlockedThemes(), + currentTheme + ) + + // Set up block skin selector callback + binding.customizationBlockSkinSelector.onBlockSkinSelected = { selectedSkin -> + // Update the game view with the selected block skin + gameView.setBlockSkin(selectedSkin) + // Save the selection + progressionManager.setSelectedBlockSkin(selectedSkin) + } + + // Update block skin selector with current selection + binding.customizationBlockSkinSelector.updateBlockSkins( + progressionManager.getUnlockedBlocks(), + gameView.getCurrentBlockSkin(), + progressionManager.getPlayerLevel() + ) + + // Reset scroll position + binding.customizationMenuScrollView.scrollTo(0, 0) + + // Initialize customization menu items + customizationMenuItems.clear() + customizationMenuItems.addAll(listOf( + binding.customizationThemeSelector, + binding.customizationBlockSkinSelector, + binding.customizationBackButton + ).filterNotNull().filter { it.visibility == View.VISIBLE }) + + // Set initial selection and highlight + currentCustomizationMenuSelection = 0 + highlightCustomizationMenuItem(currentCustomizationMenuSelection) + + // Set initial focus to theme selector + binding.customizationThemeSelector.requestFocus() + + // Initialize customization menu navigation + initCustomizationMenuNavigation() + } + + private fun initCustomizationMenuNavigation() { + customizationMenuItems.clear() + + // Add items in order + customizationMenuItems.addAll(listOf( + binding.customizationThemeSelector, + binding.customizationBlockSkinSelector, + binding.customizationBackButton + ).filterNotNull().filter { it.visibility == View.VISIBLE }) + + // Set up focus change listener for scrolling + binding.customizationMenuScrollView.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + // When scroll view gets focus, scroll to focused item + val focusedView = currentFocus + if (focusedView != null) { + val scrollView = binding.customizationMenuScrollView + val itemTop = focusedView.top + val scrollViewHeight = scrollView.height + + // Calculate scroll position to center the focused item + val scrollY = itemTop - (scrollViewHeight / 2) + (focusedView.height / 2) + scrollView.smoothScrollTo(0, scrollY) + } + } + } + + // Set up focus handling between items + customizationMenuItems.forEachIndexed { index, item -> + item.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + currentCustomizationMenuSelection = index + highlightCustomizationMenuItem(index) + } + } + } + } + + /** + * Hide customization menu + */ + private fun hideCustomizationMenu() { + binding.customizationContainer.visibility = View.GONE + binding.pauseContainer.visibility = View.VISIBLE + } + /** * Show settings menu */ private fun showPauseMenu() { binding.pauseContainer.visibility = View.VISIBLE + binding.customizationContainer.visibility = View.GONE // Set button visibility based on game state if (gameView.isPaused) { @@ -589,6 +669,7 @@ class MainActivity : AppCompatActivity(), binding.pauseLevelDownButton?.setTextColor(themeColor) // Safe call binding.settingsButton?.setTextColor(themeColor) // Safe call for sound toggle button text binding.musicToggle?.setColorFilter(themeColor) // Safe call + binding.customizationButton?.setTextColor(themeColor) // Safe call // Apply theme colors to text elements (using safe calls) binding.settingsTitle?.setTextColor(themeColor) @@ -596,30 +677,8 @@ class MainActivity : AppCompatActivity(), binding.musicText?.setTextColor(themeColor) binding.pauseLevelText?.setTextColor(themeColor) - // Apply theme to specific labels in portrait/landscape - RELY ON HELPER BELOW - if (isLandscape) { - // findViewById(R.id.selectThemeTextLandscape)?.setTextColor(themeColor) // REMOVED - // findViewById(R.id.selectBlockSkinTextLandscape)?.setTextColor(themeColor) // REMOVED - // Also color general text views in the landscape container, avoiding selectors - applyThemeColorToTextViews(findViewById(R.id.pauseContainer), themeColor) - } else { - // binding.selectThemeText?.setTextColor(themeColor) // REMOVED - // binding.selectBlockSkinText?.setTextColor(themeColor) // REMOVED - // Apply to portrait container as well if needed (assuming root or specific container) - applyThemeColorToTextViews(binding.pauseContainer, themeColor) // Apply to main container - } - - // Update theme selector instances - val currentUnlockedThemes = progressionManager.getUnlockedThemes() - binding.themeSelector.updateThemes(currentUnlockedThemes, currentTheme) - findViewById(R.id.inPauseThemeSelector)?.updateThemes(currentUnlockedThemes, currentTheme) - - // Update block skin selector instances - val currentBlockSkin = gameView.getCurrentBlockSkin() - val currentUnlockedBlocks = progressionManager.getUnlockedBlocks() - val currentLevel = progressionManager.getPlayerLevel() - binding.blockSkinSelector.updateBlockSkins(currentUnlockedBlocks, currentBlockSkin, currentLevel) - findViewById(R.id.inPauseBlockSkinSelector)?.updateBlockSkins(currentUnlockedBlocks, currentBlockSkin, currentLevel) + // Apply to portrait container as well if needed (assuming root or specific container) + applyThemeColorToTextViews(binding.pauseContainer, themeColor) // Apply to main container // Reset scroll position binding.pauseMenuScrollView?.scrollTo(0, 0) @@ -913,7 +972,7 @@ class MainActivity : AppCompatActivity(), * Update the theme selector with unlocked themes */ private fun updateThemeSelector() { - binding.themeSelector.updateThemes( + binding.customizationThemeSelector?.updateThemes( unlockedThemes = progressionManager.getUnlockedThemes(), currentTheme = currentTheme ) @@ -1224,8 +1283,10 @@ class MainActivity : AppCompatActivity(), runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE) { moveMenuSelectionUp() - // Ensure the selected item is visible scrollToSelectedItem() + } else if (binding.customizationContainer.visibility == View.VISIBLE) { + moveCustomizationMenuSelectionUp() + scrollToSelectedCustomizationItem() } } } @@ -1234,8 +1295,10 @@ class MainActivity : AppCompatActivity(), runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE) { moveMenuSelectionDown() - // Ensure the selected item is visible scrollToSelectedItem() + } else if (binding.customizationContainer.visibility == View.VISIBLE) { + moveCustomizationMenuSelectionDown() + scrollToSelectedCustomizationItem() } } } @@ -1248,18 +1311,30 @@ class MainActivity : AppCompatActivity(), is ThemeSelector -> selectedItem.focusPreviousItem() is BlockSkinSelector -> selectedItem.focusPreviousItem() } + } else if (binding.customizationContainer.visibility == View.VISIBLE && customizationMenuItems.isNotEmpty()) { + val selectedItem = customizationMenuItems.getOrNull(currentCustomizationMenuSelection) + when (selectedItem) { + is ThemeSelector -> selectedItem.focusPreviousItem() + is BlockSkinSelector -> selectedItem.focusPreviousItem() + } } } } override fun onMenuRight() { runOnUiThread { - if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) { + if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) { val selectedItem = pauseMenuItems.getOrNull(currentMenuSelection) when (selectedItem) { is ThemeSelector -> selectedItem.focusNextItem() is BlockSkinSelector -> selectedItem.focusNextItem() } + } else if (binding.customizationContainer.visibility == View.VISIBLE && customizationMenuItems.isNotEmpty()) { + val selectedItem = customizationMenuItems.getOrNull(currentCustomizationMenuSelection) + when (selectedItem) { + is ThemeSelector -> selectedItem.focusNextItem() + is BlockSkinSelector -> selectedItem.focusNextItem() + } } } } @@ -1267,7 +1342,9 @@ class MainActivity : AppCompatActivity(), override fun onMenuSelect() { runOnUiThread { if (binding.pauseContainer.visibility == View.VISIBLE && pauseMenuItems.isNotEmpty()) { - activateSelectedMenuItem() // Handles selectors and other items + activateSelectedMenuItem() + } else if (binding.customizationContainer.visibility == View.VISIBLE && customizationMenuItems.isNotEmpty()) { + activateSelectedCustomizationMenuItem() } else if (titleScreen.visibility == View.VISIBLE) { titleScreen.performClick() } else if (progressionScreen.visibility == View.VISIBLE) { @@ -1295,12 +1372,9 @@ class MainActivity : AppCompatActivity(), orderedViews.add(binding.resumeButton) } if (binding.pauseStartButton.visibility == View.VISIBLE) { - // In pause menu, the 'Start New Game' button might be visible if paused from title? - // Let's assume pauseRestartButton is the primary one during active pause. - // Keep the logic simple: add if visible. orderedViews.add(binding.pauseStartButton) } - orderedViews.add(binding.pauseRestartButton) // Always add restart + orderedViews.add(binding.pauseRestartButton) // Group 2: Stats and Scoring orderedViews.add(binding.highScoresButton) @@ -1314,24 +1388,11 @@ class MainActivity : AppCompatActivity(), orderedViews.add(binding.settingsButton) // Sound toggle (Button) orderedViews.add(binding.musicToggle) // Music toggle (ImageButton) - // Group 5: Visual customization (Theme and Block Skins) - // Choose the correct instance based on orientation and visibility - val themeSelectorInstance = if (isLandscape) findViewById(R.id.inPauseThemeSelector) else binding.themeSelector - val blockSkinSelectorInstance = if (isLandscape) findViewById(R.id.inPauseBlockSkinSelector) else binding.blockSkinSelector - - // Only add the selectors if they are actually visible in the current layout - if (themeSelectorInstance?.visibility == View.VISIBLE) { - orderedViews.add(themeSelectorInstance) - } - if (blockSkinSelectorInstance?.visibility == View.VISIBLE) { - orderedViews.add(blockSkinSelectorInstance) - } + // Group 5: Customization Menu + orderedViews.add(binding.customizationButton) // Add the customization button // Add all non-null, visible items from the defined order to the final navigation list pauseMenuItems.addAll(orderedViews.filterNotNull().filter { it.visibility == View.VISIBLE }) - - // Reset selection index (will be set and highlighted in showPauseMenu) - // currentMenuSelection = 0 } /** @@ -1609,4 +1670,182 @@ class MainActivity : AppCompatActivity(), // If no landscape ScrollView is found, fall back to the default one return binding.pauseMenuScrollView } + + // Add these new methods for customization menu navigation + private fun moveCustomizationMenuSelectionUp() { + if (customizationMenuItems.isEmpty()) return + + val previousSelection = currentCustomizationMenuSelection + currentCustomizationMenuSelection = (currentCustomizationMenuSelection - 1) + + if (currentCustomizationMenuSelection < 0) { + currentCustomizationMenuSelection = 0 + return + } + + if (previousSelection != currentCustomizationMenuSelection) { + highlightCustomizationMenuItem(currentCustomizationMenuSelection) + } + } + + private fun moveCustomizationMenuSelectionDown() { + if (customizationMenuItems.isEmpty()) return + + val previousSelection = currentCustomizationMenuSelection + currentCustomizationMenuSelection = (currentCustomizationMenuSelection + 1) + + if (currentCustomizationMenuSelection >= customizationMenuItems.size) { + currentCustomizationMenuSelection = customizationMenuItems.size - 1 + return + } + + if (previousSelection != currentCustomizationMenuSelection) { + highlightCustomizationMenuItem(currentCustomizationMenuSelection) + } + } + + private fun highlightCustomizationMenuItem(index: Int) { + if (customizationMenuItems.isEmpty() || index < 0 || index >= customizationMenuItems.size) { + Log.w(TAG, "highlightCustomizationMenuItem called with invalid index: $index or empty items") + return + } + + val themeColor = getThemeColor(currentTheme) + val highlightBorderColor = themeColor + + customizationMenuItems.forEachIndexed { i, item -> + val isSelected = (i == index) + + item.alpha = if (isSelected) 1.0f else 0.7f + if (item is Button || item is ImageButton) { + item.scaleX = 1.0f + item.scaleY = 1.0f + } + + when (item) { + is Button -> item.background = ColorDrawable(Color.TRANSPARENT) + is ImageButton -> item.background = null + is ThemeSelector -> { + item.background = ColorDrawable(Color.TRANSPARENT) + item.setHasFocus(false) + } + is BlockSkinSelector -> { + item.background = ColorDrawable(Color.TRANSPARENT) + item.setHasFocus(false) + } + } + + if (isSelected) { + if (item is Button || item is ImageButton) { + item.scaleX = 1.1f + item.scaleY = 1.1f + } + + val borderWidth = 4 + val cornerRadius = 12f + + when (item) { + is Button -> { + ContextCompat.getDrawable(this, R.drawable.menu_item_selected)?.mutate()?.let { + if (it is GradientDrawable) { + it.setStroke(borderWidth, highlightBorderColor) + item.background = it + } else { + it.setTint(highlightBorderColor) + item.background = it + } + } ?: run { + item.background = GradientDrawable().apply { + setColor(Color.TRANSPARENT) + setStroke(borderWidth, highlightBorderColor) + setCornerRadius(cornerRadius) + } + } + } + is ImageButton -> { + item.background = GradientDrawable().apply { + setColor(Color.TRANSPARENT) + setStroke(borderWidth, highlightBorderColor) + setCornerRadius(cornerRadius) + } + } + is ThemeSelector -> { + item.background = GradientDrawable().apply { + setColor(Color.TRANSPARENT) + setStroke(borderWidth, highlightBorderColor) + setCornerRadius(cornerRadius) + } + item.setHasFocus(true) + } + is BlockSkinSelector -> { + item.background = GradientDrawable().apply { + setColor(Color.TRANSPARENT) + setStroke(borderWidth, highlightBorderColor) + setCornerRadius(cornerRadius) + } + item.setHasFocus(true) + } + } + } + } + + gameHaptics.vibrateForPieceMove() + scrollToSelectedCustomizationItem() + } + + private fun scrollToSelectedCustomizationItem() { + if (customizationMenuItems.isEmpty() || currentCustomizationMenuSelection < 0 || currentCustomizationMenuSelection >= customizationMenuItems.size) return + + val selectedItem = customizationMenuItems[currentCustomizationMenuSelection] + val scrollView = binding.customizationMenuScrollView + + scrollView.post { + try { + val scrollBounds = Rect() + scrollView.getHitRect(scrollBounds) + + val itemLocation = IntArray(2) + selectedItem.getLocationOnScreen(itemLocation) + + val scrollLocation = IntArray(2) + scrollView.getLocationOnScreen(scrollLocation) + + val itemY = itemLocation[1] - scrollLocation[1] + val itemTop = itemY + val itemBottom = itemY + selectedItem.height + val visibleHeight = scrollView.height + val padding = 50 + val scrollY = scrollView.scrollY + + if (itemTop < padding) { + scrollView.smoothScrollTo(0, scrollY + (itemTop - padding)) + } else if (itemBottom > visibleHeight - padding) { + scrollView.smoothScrollTo(0, scrollY + (itemBottom - visibleHeight + padding)) + } + } catch (e: Exception) { + Log.e(TAG, "Error scrolling to selected customization item", e) + } + } + } + + private fun activateSelectedCustomizationMenuItem() { + if (customizationMenuItems.isEmpty() || currentCustomizationMenuSelection < 0 || currentCustomizationMenuSelection >= customizationMenuItems.size) return + + val selectedItem = customizationMenuItems[currentCustomizationMenuSelection] + gameHaptics.vibrateForPieceLock() + + when (selectedItem) { + is ThemeSelector -> { + selectedItem.confirmSelection() + Handler(Looper.getMainLooper()).postDelayed({ showCustomizationMenu() }, 100) + } + is BlockSkinSelector -> { + selectedItem.confirmSelection() + // The onBlockSkinSelected callback will handle updating the game view and saving the selection + } + else -> { + selectedItem.performClick() + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/mintris/ThemeManager.kt b/app/src/main/java/com/mintris/ThemeManager.kt new file mode 100644 index 0000000..56ce224 --- /dev/null +++ b/app/src/main/java/com/mintris/ThemeManager.kt @@ -0,0 +1,19 @@ +package com.mintris + +import android.graphics.Color + +object ThemeManager { + data class ThemeColors( + val background: Int, + val text: Int, + val accent: Int + ) + + fun getThemeColors(): ThemeColors { + return ThemeColors( + background = Color.BLACK, + text = Color.WHITE, + accent = Color.WHITE + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/menu_section_background.xml b/app/src/main/res/drawable/menu_section_background.xml new file mode 100644 index 0000000..683c792 --- /dev/null +++ b/app/src/main/res/drawable/menu_section_background.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml index 35d814f..04a9726 100644 --- a/app/src/main/res/layout-land/activity_main.xml +++ b/app/src/main/res/layout-land/activity_main.xml @@ -421,22 +421,23 @@ android:src="@drawable/ic_volume_up" android:contentDescription="@string/music" /> - - - - - - +