//-----------------------------------------------------------------------------
// MenuComponent.cs
//
// Microsoft XNA Community Game Platform
// Copyright (C) Microsoft Corporation. All rights reserved.
//-----------------------------------------------------------------------------
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
namespace XnaGraphicsDemo
{
///
/// Base class for all the different screens used in the demo. This provides
/// a simple touch menu which can display a list of options, and detect when
/// a menu item is clicked.
///
class MenuComponent : DrawableGameComponent
{
// Properties.
///
/// Gets the game instance as a DemoGame.
///
new public DemoGame Game { get { return (DemoGame)base.Game; } }
/// The game instance as a DemoGame.
///
/// Gets the SpriteBatch used for drawing.
///
public SpriteBatch SpriteBatch { get { return Game.SpriteBatch; } }
/// The SpriteBatch used for drawing.
///
/// Gets the default font for menu text.
///
public SpriteFont Font { get { return Game.Font; } }
/// The default font for menu text.
///
/// Gets the large font for menu titles.
///
public SpriteFont BigFont { get { return Game.BigFont; } }
/// The large font for menu titles.
///
/// Gets the list of menu entries.
///
protected List Entries { get; private set; }
/// The list of menu entries.
///
/// Gets the last touch point position.
///
protected Vector2 LastTouchPoint { get; private set; }
/// The last touch point position.
// Fields.
///
/// Indicates whether a touch is currently active.
///
bool touchDown = true;
///
/// The index of the currently selected menu entry for navigation.
/// Also used in attract mode to keep everything in sync.
///
protected int selectedEntry = 0;
// Static field to track the last selected menu item across all menus
///
/// Tracks the last selected menu item across all menus.
///
static int lastSelectedMenuItem = 0;
///
/// Timer for attract (demo) mode inactivity.
///
static TimeSpan attractTimer;
///
/// Stores the last mouse input state.
///
static MouseState lastInputState = new MouseState(-1, -1, -1, 0, 0, 0, 0, 0);
///
/// Stores the last keyboard input state.
///
static KeyboardState lastKeyboardState;
///
/// Constructor.
///
public MenuComponent(DemoGame game)
: base(game)
{
Entries = new List();
}
///
/// Initializes the menu, computing the screen position of each entry.
///
public override void Initialize()
{
Vector2 pos = new Vector2(MenuEntry.Border, 800 - MenuEntry.Border - Entries.Count * MenuEntry.Height);
foreach (MenuEntry entry in Entries)
{
entry.Position = pos;
pos.Y += MenuEntry.Height;
}
base.Initialize();
}
///
/// Resets the menu, whenever we transition to or from a different screen.
///
virtual public void Reset()
{
if (selectedEntry >= 0)
Entries[selectedEntry].IsFocused = false;
touchDown = true;
// Restore the last selected menu item if valid, otherwise select the first item
selectedEntry = (lastSelectedMenuItem >= 0 && lastSelectedMenuItem < Entries.Count)
? lastSelectedMenuItem
: 0;
// Set initial keyboard focus
UpdateMenuFocus();
}
///
/// Updates the menu state, processing user input.
///
public override void Update(GameTime gameTime)
{
// We read input using the mouse API, which will report the first touch point
// when run on the phone, but also works on Windows using a regular mouse.
MouseState input = Game.IsActive ? Mouse.GetState() : new MouseState();
KeyboardState keyboardInput = Game.IsActive ? Keyboard.GetState() : new KeyboardState();
// Handle keyboard input
HandleKeyboardInput(keyboardInput);
// Scale input if we are running in an unusual screen resolution.
int touchX = input.X * 480 / Game.Graphics.PreferredBackBufferWidth;
int touchY = input.Y * 800 / Game.Graphics.PreferredBackBufferHeight;
// Process the input.
if (input.LeftButton == ButtonState.Pressed)
{
HandleTouchDown(touchX, touchY);
}
else
{
HandleTouchUp();
}
HandleAttractMode(gameTime, input, keyboardInput);
}
///
/// Handles input while a touch is occurring.
///
///
/// Handles input while a touch is occurring, updating selection and focus.
///
void HandleTouchDown(int touchX, int touchY)
{
// Hit test the touch position against the list of menu items.
int currentEntry = -1;
for (int i = 0; i < Entries.Count; i++)
{
if ((touchY >= Entries[i].Position.Y) && (touchY < Entries[i].Position.Y + MenuEntry.Height))
{
currentEntry = i;
break;
}
}
if (touchDown)
{
// Are we already processing a touch?
if (selectedEntry >= 0)
{
if (currentEntry == selectedEntry || Entries[selectedEntry].IsDraggable)
{
// Pass drag input to the currently selected item.
Entries[selectedEntry].IsFocused = true;
Entries[selectedEntry].OnDragged(touchX - LastTouchPoint.X);
}
else
{
// If the drag moves off the selected item, unfocus it.
Entries[selectedEntry].IsFocused = false;
}
}
else
{
// If the touch was not on any menu item, process a backgroun drag.
OnDrag(new Vector2(touchX, touchY) - LastTouchPoint);
}
}
else
{
// We are not currently processing a touch.
touchDown = true;
selectedEntry = currentEntry;
// Clear keyboard focus when using touch
foreach (MenuEntry entry in Entries)
entry.IsFocused = false;
if (selectedEntry >= 0)
{
// Focus the menu item that has just been touched.
Entries[selectedEntry].IsFocused = true;
}
}
// Store the most recent touch location.
LastTouchPoint = new Vector2(touchX, touchY);
}
///
/// Handles input when the touch is released.
///
///
/// Handles input when the touch is released, triggering click actions if needed.
///
void HandleTouchUp()
{
if (touchDown && selectedEntry >= 0 && Entries[selectedEntry].IsFocused)
{
// Save the current selection as the last selected menu item
lastSelectedMenuItem = selectedEntry;
// If we were touching a menu item, and just released it, process the click action.
Entries[selectedEntry].IsFocused = false;
Entries[selectedEntry].OnClicked();
}
touchDown = false;
}
///
/// Checks if there's any actual keyboard activity between two keyboard states.
///
///
/// Checks if there is any keyboard activity between two keyboard states.
///
/// The current keyboard state.
/// The previous keyboard state.
/// True if there is keyboard activity; otherwise, false.
bool HasKeyboardActivity(KeyboardState current, KeyboardState previous)
{
// Check if any key was pressed or released
Keys[] currentKeys = current.GetPressedKeys();
Keys[] previousKeys = previous.GetPressedKeys();
// If the number of pressed keys changed, there's activity
if (currentKeys.Length != previousKeys.Length)
return true;
// Check if any different keys are pressed
foreach (Keys key in currentKeys)
{
if (!previous.IsKeyDown(key))
return true;
}
foreach (Keys key in previousKeys)
{
if (!current.IsKeyDown(key))
return true;
}
return false;
}
///
/// If no input is provided, we go into an automatic attract mode, which cycles
/// through the various options. This was great for leaving the demo unattended
/// at the kiosk during the MIX10 conference!
///
/// The current game time.
/// The current mouse state.
/// The current keyboard state.
void HandleAttractMode(GameTime gameTime, MouseState input, KeyboardState keyboardInput)
{
// Check if there's any actual keyboard activity
bool keyboardActivity = HasKeyboardActivity(keyboardInput, lastKeyboardState);
if (input != lastInputState || keyboardActivity || touchDown)
{
// If input has changed, reset the timer.
attractTimer = TimeSpan.FromSeconds(-15);
lastInputState = input;
lastKeyboardState = keyboardInput;
}
else
{
// If no input occurs, increment the timer.
attractTimer += gameTime.ElapsedGameTime;
if (attractTimer > AttractDelay)
{
// Timeout! Run the attract action.
attractTimer = TimeSpan.Zero;
OnAttract();
}
}
}
///
/// Allows subclasses to customize their attract behavior. The default is
/// to simulate a click on the last menu entry, which is usually "back".
///
protected virtual void OnAttract()
{
Entries[Entries.Count - 1].OnClicked();
}
///
/// Allows subclasses to customize how long they wait before cycling through the attract sequence.
///
protected virtual TimeSpan AttractDelay { get { return TimeSpan.FromSeconds(10); } }
///
/// Draws the list of menu entries.
///
public override void Draw(GameTime gameTime)
{
SpriteBatch.Begin(0, null, null, null, null, null, Game.ScaleMatrix);
foreach (MenuEntry entry in Entries)
{
entry.Draw(SpriteBatch, Font, Game.BlankTexture);
}
SpriteBatch.End();
}
///
/// Draws the menu title at the top of the screen, optionally with a background color.
///
/// The title text to display.
/// The background color to use, or null for none.
/// The color to use for the title text.
protected void DrawTitle(string title, Color? backgroundColor, Color titleColor)
{
if (backgroundColor.HasValue)
GraphicsDevice.Clear(backgroundColor.Value);
SpriteBatch.Begin(0, null, null, null, null, null, Game.ScaleMatrix);
SpriteBatch.DrawString(BigFont, title, new Vector2(480, 24), titleColor, MathHelper.PiOver2, Vector2.Zero, 1, 0, 0);
SpriteBatch.End();
}
///
/// Handles a drag on the background of the screen. Subclasses can override to implement custom drag behavior.
///
/// The amount the pointer has moved since the last drag event.
protected virtual void OnDrag(Vector2 delta)
{
}
///
/// Handles keyboard input for menu navigation and selection.
///
/// The current keyboard state.
void HandleKeyboardInput(KeyboardState keyboardInput)
{
// Check for new key presses
bool upPressed = keyboardInput.IsKeyDown(Keys.Up) && !lastKeyboardState.IsKeyDown(Keys.Up);
bool downPressed = keyboardInput.IsKeyDown(Keys.Down) && !lastKeyboardState.IsKeyDown(Keys.Down);
bool enterPressed = (keyboardInput.IsKeyDown(Keys.Enter) && !lastKeyboardState.IsKeyDown(Keys.Enter)) ||
(keyboardInput.IsKeyDown(Keys.Space) && !lastKeyboardState.IsKeyDown(Keys.Space));
bool escapePressed = keyboardInput.IsKeyDown(Keys.Escape) && !lastKeyboardState.IsKeyDown(Keys.Escape);
// Handle navigation
if (upPressed && Entries.Count > 0)
{
// Clear touch selection and focus when using keyboard
if (selectedEntry >= 0)
Entries[selectedEntry].IsFocused = false;
selectedEntry--;
if (selectedEntry < 0)
selectedEntry = Entries.Count - 1;
UpdateMenuFocus();
}
else if (downPressed && Entries.Count > 0)
{
// Clear touch selection and focus when using keyboard
if (selectedEntry >= 0)
Entries[selectedEntry].IsFocused = false;
selectedEntry++;
if (selectedEntry >= Entries.Count)
selectedEntry = 0;
UpdateMenuFocus();
}
else if (enterPressed && Entries.Count > 0)
{
// Save the current selection as the last selected menu item
lastSelectedMenuItem = selectedEntry;
// Execute the currently selected menu item
Entries[selectedEntry].OnClicked();
}
else if (escapePressed)
{
// Go back - simulate clicking the last menu entry (usually "back" or "quit")
if (Entries.Count > 0)
Entries[Entries.Count - 1].OnClicked();
}
lastKeyboardState = keyboardInput;
}
///
/// Updates the focus state for keyboard navigation, ensuring only the selected entry is focused.
///
protected void UpdateMenuFocus()
{
// Clear all focus first
foreach (MenuEntry entry in Entries)
entry.IsFocused = false;
// Set focus on the keyboard-selected item
if (selectedEntry >= 0 && selectedEntry < Entries.Count)
Entries[selectedEntry].IsFocused = true;
}
///
/// Gets the currently selected menu item index for keyboard navigation.
///
/// The index of the selected menu item.
public int GetSelectedIndex()
{
return selectedEntry;
}
///
/// Sets the selected menu item index for keyboard navigation, updating focus accordingly.
///
/// The index of the menu item to select.
public void SetSelectedIndex(int index)
{
if (index >= 0 && index < Entries.Count)
{
selectedEntry = index;
lastSelectedMenuItem = index;
UpdateMenuFocus();
}
}
}
}