//----------------------------------------------------------------------------- // 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(); } } } }