File NativeMenu.cs
File List > LemonUI > LemonUI > Menus > NativeMenu.cs
Go to the documentation of this file
#if FIVEM
using CitizenFX.Core;
using CitizenFX.Core.Native;
using CitizenFX.Core.UI;
using Font = CitizenFX.Core.UI.Font;
#elif ALTV
using AltV.Net.Client;
using Font = LemonUI.Elements.Font;
using CancelEventArgs = System.ComponentModel.CancelEventArgs;
using CancelEventHandler = System.ComponentModel.CancelEventHandler;
#elif RAGEMP
using RAGE.Game;
using InstructionalButton = LemonUI.Scaleform.InstructionalButton;
#elif RPH
using Rage;
using Rage.Native;
using System.ComponentModel;
using Control = Rage.GameControl;
using Font = LemonUI.Elements.Font;
#elif SHVDN3 || SHVDNC
using GTA;
using GTA.Native;
using GTA.UI;
using CancelEventArgs = System.ComponentModel.CancelEventArgs;
using CancelEventHandler = System.ComponentModel.CancelEventHandler;
using Font = GTA.UI.Font;
#endif
using LemonUI.Elements;
using LemonUI.Scaleform;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using LemonUI.Tools;
namespace LemonUI.Menus
{
public class NativeMenu : IContainer<NativeItem>, IEnumerable<NativeItem>
{
#region Constants
internal const float nameHeight = 38;
internal const float itemHeight = 37.4f;
internal const float heightDiffDescImg = 4;
internal const float heightDiffDescTxt = 3;
internal const float posXDescTxt = 6;
internal const float itemOffsetX = 6;
internal const float itemOffsetY = 3;
#endregion
#region Fields
public static readonly Sound DefaultActivatedSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "SELECT");
public static readonly Sound DefaultCloseSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "BACK");
public static readonly Sound DefaultUpDownSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "NAV_UP_DOWN");
public static readonly Sound DefaultLeftRightSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "NAV_LEFT_RIGHT");
public static readonly Sound DefaultDisabledSound = new Sound("HUD_FRONTEND_DEFAULT_SOUNDSET", "ERROR");
internal static readonly Color colorWhiteSmoke = Color.FromArgb(255, 245, 245, 245);
internal static readonly SizeF searchAreaSize = new SizeF(30, 1080);
internal static readonly List<Control> controlsCamera = new List<Control>
{
#if ALTV
Control.LookUd,
Control.LookLr,
#else
Control.LookUpDown,
Control.LookLeftRight,
#endif
};
internal static readonly List<Control> controlsGamepad = new List<Control>
{
Control.Aim,
Control.Attack
};
private static readonly Control[] controls;
private bool justOpenedControlChecks = false;
private List<NativeItem> visibleItems = new List<NativeItem>();
private string name = string.Empty;
private bool visible = false;
private bool justClosed = false;
private bool safeZoneAware = true;
private int index = -1;
private float width = 433;
private Alignment alignment = Alignment.Left;
private PointF offset = PointF.Empty;
private I2Dimensional bannerImage = null;
private readonly ScaledRectangle nameImage = new ScaledRectangle(PointF.Empty, SizeF.Empty)
{
Color = Color.FromArgb(255, 0, 0, 0)
};
private readonly ScaledText nameText = new ScaledText(PointF.Empty, string.Empty, 0.345f, Font.ChaletLondon)
{
Color = colorWhiteSmoke
};
private readonly ScaledText countText = new ScaledText(PointF.Empty, string.Empty, 0.345f, Font.ChaletLondon)
{
Color = colorWhiteSmoke
};
private readonly ScaledTexture backgroundImage = new ScaledTexture("commonmenu", "gradient_bgd");
private readonly ScaledTexture selectedRect = new ScaledTexture("commonmenu", "gradient_nav");
private readonly ScaledTexture hoveredRect = new ScaledTexture("commonmenu", "gradient_nav")
{
Color = Color.FromArgb(20, 255, 255, 255)
};
private readonly ScaledTexture descriptionRect = new ScaledTexture("commonmenu", "gradient_bgd");
private readonly ScaledText descriptionText = new ScaledText(PointF.Empty, string.Empty, 0.351f);
private int maxItems = 10;
private int firstItem = 0;
private PointF searchAreaRight = PointF.Empty;
private long heldSince = -1;
private HeaderBehavior nameBehavior = HeaderBehavior.AlwaysShow;
private string description = string.Empty;
private string noItemsText = "There are no items available";
#endregion
#region Properties
private bool ShouldDrawNameBackground => nameBehavior == HeaderBehavior.AlwaysShow || (nameBehavior == HeaderBehavior.ShowIfRequired && (ShouldDrawCount || !string.IsNullOrWhiteSpace(name)));
private bool ShouldDrawCount => ItemCount == CountVisibility.Always || (ItemCount == CountVisibility.Auto && Items.Count > MaxItems);
public bool Visible
{
get => visible;
set
{
if (visible == value)
{
return;
}
if (value)
{
CancelEventArgs args = new CancelEventArgs();
Opening?.Invoke(this, args);
if (args.Cancel)
{
return;
}
if (ResetCursorWhenOpened)
{
ResetCursor();
}
justOpenedControlChecks = true;
visible = true;
SoundOpened?.PlayFrontend();
Shown?.Invoke(this, EventArgs.Empty);
TriggerSelectedItem();
}
else
{
CancelEventArgs args = new CancelEventArgs();
Closing?.Invoke(this, args);
if (args.Cancel)
{
return;
}
visible = false;
justClosed = true;
Closed?.Invoke(this, EventArgs.Empty);
SoundClose?.PlayFrontend();
}
}
}
public ScaledText BannerText { get; set; }
[Obsolete("Please use BannerText instead.", true)]
public ScaledText Title
{
get => BannerText;
set => BannerText = value;
}
[Obsolete("Please use BannerText.Font instead.", true)]
public Font TitleFont
{
get => BannerText.Font;
set => BannerText.Font = value;
}
public Font NameFont
{
get => nameText.Font;
set => nameText.Font = value;
}
public Font DescriptionFont
{
get => descriptionText.Font;
set => descriptionText.Font = value;
}
public Font ItemCountFont
{
get => countText.Font;
set => countText.Font = value;
}
public I2Dimensional Banner
{
get => bannerImage;
set
{
bannerImage = value;
Recalculate();
}
}
public PointF Offset
{
get => offset;
set
{
offset = value;
Recalculate();
}
}
public NativeItem SelectedItem
{
get
{
// If there are no items or is over the maximum, return null
int currentIndex = SelectedIndex;
if (Items.Count == 0 || currentIndex >= Items.Count || currentIndex == -1)
{
return null;
}
// Otherwise, return the correct item from the list
return Items[currentIndex];
}
set
{
// If the item is not part of the menu, raise an exception
if (!Items.Contains(value))
{
throw new InvalidOperationException("Item is not part of the Menu.");
}
// Otherwise, set the correct index
SelectedIndex = Items.IndexOf(value);
}
}
public int SelectedIndex
{
get
{
// If there are no items or is over the maximum, return -1
if (Items.Count == 0 || index >= Items.Count)
{
return -1;
}
// Otherwise, return the real index
return index;
}
set
{
// If the list of items is empty, don't allow the user to set the index
if (Items == null || Items.Count == 0)
{
throw new InvalidOperationException("There are no items in this menu.");
}
// If the value is over or equal than the number of items, raise an exception
else if (value >= Items.Count)
{
throw new InvalidOperationException($"The index is over {Items.Count - 1}.");
}
// If the value is under zero, raise an exception
else if (value < 0)
{
throw new InvalidOperationException($"The index is under zero.");
}
// Calculate the bounds of the menu
int lower = firstItem;
int upper = firstItem + maxItems;
// Time to set the first item based on the total number of items
// If the item is between the allowed values, do nothing because we are on the correct first item
if (value >= lower && value < upper - 1)
{
}
// If the upper bound + 1 equals the new index, increase it by one
else if (upper == value)
{
firstItem += 1;
}
// If the first item minus one equals the value, decrease it by one
else if (lower - 1 == value)
{
firstItem -= 1;
}
// Otherwise, set it somewhere
else
{
// If the value is under the max items, set it to zero
if (value < maxItems)
{
firstItem = 0;
}
// Otherwise, set it at the bottom
else
{
firstItem = value - maxItems + 1;
}
}
// Save the index
index = value;
// And update the items
UpdateItemList();
UpdateItems();
// If the menu is visible, play the up and down sound
if (Visible)
{
SoundUpDown?.PlayFrontend();
}
// If an item was selected
if (SelectedItem != null)
{
// And trigger it
TriggerSelectedItem();
}
}
}
public float Width
{
get => width;
set
{
width = value;
Recalculate();
}
}
public Alignment Alignment
{
get => alignment;
set
{
if (alignment == value)
{
return;
}
if (!Enum.IsDefined(typeof(Alignment), value) || value == Alignment.Center)
{
throw new ArgumentException("The Menu can only be aligned to the Left and Right.", nameof(value));
}
alignment = value;
Recalculate();
}
}
public string Name
{
get => name;
set
{
name = value ?? throw new ArgumentNullException(nameof(value));
nameText.Text = value.ToUpperInvariant();
}
}
[Obsolete("Please use Name instead.", true)]
public string Subtitle
{
get => Name;
set => Name = value ?? throw new ArgumentNullException(nameof(value));
}
public string Description
{
get => description;
set => description = value ?? throw new ArgumentNullException(nameof(value));
}
public bool UseMouse { get; set; } = true;
public bool CloseOnInvalidClick { get; set; } = true;
public bool RotateCamera { get; set; } = false;
public List<NativeItem> Items { get; } = new List<NativeItem>();
public string NoItemsText
{
get => noItemsText;
set => noItemsText = value ?? throw new ArgumentNullException(nameof(value));
}
public bool ResetCursorWhenOpened { get; set; } = true;
public int MaxItems
{
get => maxItems;
set
{
// If the number is under one, raise an exception
if (value < 1)
{
throw new InvalidOperationException("The maximum numbers on the screen can't be under 1.");
}
// Otherwise, save it
maxItems = value;
}
}
public bool SafeZoneAware
{
get => safeZoneAware;
set
{
safeZoneAware = value;
Recalculate();
}
}
public CountVisibility ItemCount { get; set; }
public InstructionalButtons Buttons { get; } = new InstructionalButtons(new InstructionalButton("Select", (Control)176 /*PhoneSelect*/), new InstructionalButton("Back", (Control)177 /*PhoneCancel*/))
{
Visible = true
};
public NativeMenu Parent { get; set; } = null;
public bool AcceptsInput { get; set; } = true;
public bool DisableControls { get; set; } = true;
public int HeldTime { get; set; } = 166;
public List<Control> RequiredControls { get; } = new List<Control>();
[Obsolete("Please use NameBehavior instead.", true)]
public SubtitleBehavior SubtitleBehavior
{
get => (SubtitleBehavior)HeaderBehavior;
set => HeaderBehavior = (HeaderBehavior)value;
}
public HeaderBehavior HeaderBehavior
{
get => nameBehavior;
set
{
if (nameBehavior == value)
{
return;
}
nameBehavior = value;
Recalculate();
}
}
public Sound SoundOpened { get; set; } = DefaultActivatedSound;
public Sound SoundActivated { get; set; } = DefaultActivatedSound;
public Sound SoundClose { get; set; } = DefaultCloseSound;
public Sound SoundUpDown { get; set; } = DefaultUpDownSound;
public Sound SoundLeftRight { get; set; } = DefaultLeftRightSound;
public Sound SoundDisabled { get; set; } = DefaultDisabledSound;
#endregion
#region Events
public event CancelEventHandler Opening;
public event EventHandler Shown;
public event CancelEventHandler Closing;
public event EventHandler Closed;
public event SelectedEventHandler SelectedIndexChanged;
public event ItemActivatedEventHandler ItemActivated;
public event MenuModifiedEventHandler MenuModified;
#endregion
#region Constructors
static NativeMenu()
{
// The controls required by the menu with both a gamepad and mouse + keyboard
HashSet<Control> controlsRequired = new HashSet<Control>
{
// Menu Controls
Control.FrontendAccept,
Control.FrontendAxisX,
Control.FrontendAxisY,
Control.FrontendDown,
Control.FrontendUp,
Control.FrontendLeft,
Control.FrontendRight,
Control.FrontendCancel,
Control.FrontendSelect,
Control.CursorScrollDown,
Control.CursorScrollUp,
Control.CursorX,
Control.CursorY,
#if ALTV
Control.MoveUd,
Control.MoveLr,
#else
Control.MoveUpDown,
Control.MoveLeftRight,
#endif
// Camera
Control.LookBehind,
#if ALTV
Control.VehLookBehind,
#else
Control.VehicleLookBehind,
#endif
// Player
Control.Sprint,
Control.Jump,
Control.Enter,
Control.SpecialAbility,
Control.SpecialAbilityPC,
Control.SpecialAbilitySecondary,
#if ALTV
Control.VehSpecialAbilityFranklin,
// Driving
Control.VehExit,
Control.VehAccelerate,
Control.VehBrake,
Control.VehMoveLr,
Control.VehHandbrake,
Control.VehHorn,
// Bikes
Control.VehPushbikePedal,
Control.VehPushbikeSprint,
Control.VehPushbikeFrontBrake,
Control.VehPushbikeRearBrake,
// Flying
Control.VehFlyThrottleUp,
Control.VehFlyThrottleDown,
Control.VehFlyYawLeft,
Control.VehFlyYawRight,
Control.VehFlyRollLR,
Control.VehFlyRollLeftOnly,
Control.VehFlyRollRightOnly,
Control.VehFlyPitchUD,
Control.VehFlyPitchUpOnly,
Control.VehFlyPitchDownOnly,
#else
Control.VehicleSpecialAbilityFranklin,
// Driving
Control.VehicleExit,
Control.VehicleAccelerate,
Control.VehicleBrake,
Control.VehicleMoveLeftRight,
Control.VehicleHandbrake,
Control.VehicleHorn,
// Bikes
Control.VehiclePushbikePedal,
Control.VehiclePushbikeSprint,
Control.VehiclePushbikeFrontBrake,
Control.VehiclePushbikeRearBrake,
// Flying
Control.VehicleFlyThrottleUp,
Control.VehicleFlyThrottleDown,
Control.VehicleFlyYawLeft,
Control.VehicleFlyYawRight,
Control.VehicleFlyRollLeftRight,
Control.VehicleFlyRollLeftOnly,
Control.VehicleFlyRollRightOnly,
Control.VehicleFlyPitchUpDown,
Control.VehicleFlyPitchUpOnly,
Control.VehicleFlyPitchDownOnly,
#endif
#if RPH
Control.ScriptedFlyUpDown,
Control.ScriptedFlyLeftRight,
#elif ALTV
Control.ScriptedFlyUd,
Control.ScriptedFlyLr,
#else
Control.FlyUpDown,
Control.FlyLeftRight,
#endif
// Rockstar Editor
Control.SaveReplayClip,
Control.ReplayStartStopRecording,
Control.ReplayStartStopRecordingSecondary,
Control.ReplayRecord,
Control.ReplaySave,
};
controls = ((Control[])Enum.GetValues(typeof(Control))).Except(controlsRequired).ToArray();
}
public NativeMenu(string title) : this(title, string.Empty, string.Empty)
{
}
public NativeMenu(string bqnnerText, string name) : this(bqnnerText, name, string.Empty)
{
}
public NativeMenu(string bqnnerText, string name, string description) : this(bqnnerText, name, description, new ScaledTexture(PointF.Empty, new SizeF(0, 108), "commonmenu", "interaction_bgd"))
{
}
public NativeMenu(string bqnnerText, string name, string description, I2Dimensional banner)
{
this.name = name;
Description = description;
bannerImage = banner;
BannerText = new ScaledText(PointF.Empty, bqnnerText, 1.02f, Font.HouseScript)
{
Alignment = Alignment.Center
};
nameText.Text = name.ToUpperInvariant();
Recalculate();
}
#endregion
#region Tools
private void UpdateItemList()
{
// Create a new list for the items
List<NativeItem> list = new List<NativeItem>();
// Iterate over the number of items while staying under the maximum
for (int i = 0; i < MaxItems; i++)
{
// Calculate the start of our items
int start = firstItem + i;
// If the number of items is over the ones in the list, something went wrong
// TODO: Decide what to do in this case (exception? silently ignore?)
if (start >= Items.Count)
{
break;
}
// Otherwise, return it as part of the iterator or add it to the list
list.Add(Items[start]);
}
// Finally, replace the list of items
visibleItems = list;
}
private void TriggerSelectedItem()
{
// Get the currently selected item
NativeItem item = SelectedItem;
// If is null or the menu is closed, return
if (item == null || !Visible)
{
return;
}
// Update the panel
RecalculatePanel();
// And trigger the selected event for this menu
SelectedEventArgs args = new SelectedEventArgs(index, index - firstItem);
SelectedItem.OnSelected(this, args);
SelectedIndexChanged?.Invoke(this, args);
}
private void RecalculatePanel()
{
if (SelectedItem?.Panel == null)
{
return;
}
const int separation = 10;
PointF position = new PointF(descriptionRect.Position.X, descriptionRect.Position.Y + descriptionRect.Size.Height + separation);
SelectedItem.Panel.Recalculate(position, Width);
}
public void ResetCursor()
{
const float extraX = 35;
const float extraY = 325;
PointF pos = PointF.Empty;
if (SafeZoneAware)
{
SafeZone.SetAlignment(Alignment, GFXAlignment.Top);
float x = 0;
switch (Alignment)
{
case Alignment.Left:
x = Offset.X + Width + extraX;
break;
case Alignment.Right:
x = Offset.X - Width - extraX;
break;
}
pos = SafeZone.GetSafePosition(x, Offset.Y + extraY).ToRelative();
SafeZone.ResetAlignment();
}
else
{
float x = 0;
switch (Alignment)
{
case Alignment.Left:
x = Offset.X + Width + extraX;
break;
case Alignment.Right:
x = 1f.ToXScaled() - Offset.X - Width - extraX;
break;
}
pos = new PointF(x, Offset.Y + extraY).ToRelative();
}
// And set the position of the cursor
#if FIVEM
API.SetCursorLocation(pos.X, pos.Y);
#elif RAGEMP
Invoker.Invoke(Natives.SetCursorLocation, pos.X, pos.Y);
#elif RPH
NativeFunction.CallByHash<int>(0xFC695459D4D0E219, pos.X, pos.Y);
#elif ALTV
Alt.Natives.SetCursorPosition(pos.X, pos.Y);
#elif SHVDN3 || SHVDNC
Function.Call(Hash.SET_CURSOR_POSITION, pos.X, pos.Y);
#endif
}
private void UpdateItems()
{
// Store the current values of X and Y
PointF pos;
if (SafeZoneAware)
{
SafeZone.SetAlignment(Alignment, GFXAlignment.Top);
float x = 0;
switch (Alignment)
{
case Alignment.Left:
x = Offset.X;
break;
case Alignment.Right:
x = Offset.X - Width;
break;
}
pos = SafeZone.GetSafePosition(x, Offset.Y);
SafeZone.ResetAlignment();
}
else
{
float x = 0;
switch (Alignment)
{
case Alignment.Left:
x = Offset.X;
break;
case Alignment.Right:
x = 1f.ToXScaled() - Width - Offset.X;
break;
}
pos = new PointF(x, Offset.Y);
}
// Add the heights of the banner and title (if there are any)
if (bannerImage != null)
{
pos.Y += bannerImage.Size.Height;
}
if (ShouldDrawNameBackground || ShouldDrawCount)
{
countText.Text = $"{SelectedIndex + 1} / {Items.Count}";
countText.Position = new PointF(pos.X + width - countText.Width - 6, pos.Y + 4.2f);
pos.Y += nameImage.Size.Height;
}
// Set the position and size of the background image
backgroundImage.literalPosition = new PointF(pos.X, pos.Y);
backgroundImage.literalSize = new SizeF(width, itemHeight * visibleItems.Count);
backgroundImage.Recalculate();
// Set the position of the rectangle that marks the current item
selectedRect.Position = new PointF(pos.X, pos.Y + ((index - firstItem) * itemHeight));
// And then do the description background and text
float description = pos.Y + ((Items.Count > maxItems ? maxItems : Items.Count) * itemHeight) + heightDiffDescImg;
descriptionRect.Position = new PointF(pos.X, description);
descriptionText.Position = new PointF(pos.X + posXDescTxt, description + heightDiffDescTxt);
UpdateDescription();
// Save the size of the items
SizeF size = new SizeF(width, itemHeight);
// And start recalculating them
int i = 0;
foreach (NativeItem item in visibleItems)
{
// Tell the item to recalculate the position
item.Recalculate(new PointF(pos.X, pos.Y), size, item == SelectedItem);
// And increase the index of the item and Y position
i++;
pos.Y += itemHeight;
}
// Finally, recalculate the panel of the selected item
RecalculatePanel();
}
private void UpdateDescription()
{
descriptionText.Text = Items.Count == 0 || SelectedIndex == -1 ? NoItemsText : SelectedItem.Description;
int lineCount = descriptionText.LineCount;
descriptionRect.Size = new SizeF(width, (lineCount * (descriptionText.LineHeight + 5)) + (lineCount - 1) + 10);
}
private void ProcessControls()
{
// If the user wants to disable the controls, do so but only the ones required
if (DisableControls)
{
bool isUsingController = Controls.IsUsingController;
foreach (Control control in controls)
{
// If the player is using a controller and is required on gamepads
if (isUsingController && controlsGamepad.Contains(control))
{
continue;
}
// If the player is usinng a controller or mouse usage is disabled and is a camera control
if ((isUsingController || !UseMouse) && controlsCamera.Contains(control))
{
continue;
}
// If the control is required by the mod developer
if (RequiredControls.Contains(control))
{
continue;
}
Controls.DisableThisFrame(control);
}
}
// If the menu is just opened, don't start processing controls until the player has stopped pressing the accept or cancel buttons
if (justOpenedControlChecks)
{
if (Controls.IsPressed((Control)177 /*PhoneCancel*/) || Controls.IsPressed(Control.FrontendPause) ||
Controls.IsPressed(Control.FrontendAccept) || Controls.IsPressed((Control)176 /*PhoneSelect*/) ||
Controls.IsPressed(Control.CursorAccept))
{
return;
}
justOpenedControlChecks = false;
}
// If the controls are disabled, the menu has just been opened or the text input field is active, return
#if FIVEM
bool isKeyboardActive = API.UpdateOnscreenKeyboard() == 0;
#elif ALTV
bool isKeyboardActive = Alt.Natives.UpdateOnscreenKeyboard() == 0;
#elif RAGEMP
bool isKeyboardActive = Invoker.Invoke<int>(Natives.UpdateOnscreenKeyboard) == 0;
#elif RPH
bool isKeyboardActive = NativeFunction.CallByHash<int>(0x0CF2B696BBF945AE) == 0;
#elif SHVDN3 || SHVDNC
bool isKeyboardActive = Function.Call<int>(Hash.UPDATE_ONSCREEN_KEYBOARD) == 0;
#endif
if (!AcceptsInput || isKeyboardActive)
{
return;
}
// Check if the controls necessary were pressed
bool backPressed = Controls.IsJustPressed((Control)177 /*PhoneCancel*/) || Controls.IsJustPressed(Control.FrontendPause);
bool upPressed = Controls.IsJustPressed((Control)172 /*PhoneUp*/) || Controls.IsJustPressed(Control.CursorScrollUp);
bool downPressed = Controls.IsJustPressed((Control)173 /*PhoneDown*/) || Controls.IsJustPressed(Control.CursorScrollDown);
bool selectPressed = Controls.IsJustPressed(Control.FrontendAccept) || Controls.IsJustPressed((Control)176 /*PhoneSelect*/);
bool clickSelected = Controls.IsJustPressed(Control.CursorAccept);
bool leftPressed = Controls.IsJustPressed((Control)174 /*PhoneLeft*/);
bool rightPressed = Controls.IsJustPressed((Control)175 /*PhoneRight*/);
bool leftHeld = Controls.IsPressed((Control)174 /*PhoneLeft*/);
bool rightHeld = Controls.IsPressed((Control)175 /*PhoneRight*/);
bool upHeld = Controls.IsPressed((Control)172 /*PhoneUp*/) || Controls.IsPressed(Control.CursorScrollUp);
bool downHeld = Controls.IsPressed((Control)173 /*PhoneDown*/) || Controls.IsPressed(Control.CursorScrollDown);
// If the player pressed the back button, go back or close the menu
if (backPressed)
{
Back();
return;
}
#if ALTV
long time = Alt.Natives.GetGameTimer();
#elif RAGEMP
long time = Misc.GetGameTimer();
#elif FIVEM || RPH || SHVDN3 || SHVDNC
long time = Game.GameTime;
#endif
if (HeldTime > 0 && (upHeld || downHeld || leftHeld || rightHeld))
{
if (heldSince == -1)
{
heldSince = time;
}
}
else
{
heldSince = -1;
}
// If the player pressed up, go to the previous item
if ((upPressed && !downPressed) || (upHeld && !downHeld && heldSince > 0 && heldSince + HeldTime < time))
{
heldSince = time;
Previous();
return;
}
// If he pressed down, go to the next item
if ((downPressed && !upPressed) || (downHeld && !upHeld && heldSince > 0 && heldSince + HeldTime < time))
{
heldSince = time;
Next();
return;
}
// Get the currently selected item for later use (for the sake of performance)
NativeItem selectedItem = SelectedItem;
// If the mouse controls are enabled and the user is not using a controller
if (UseMouse && !Controls.IsUsingController)
{
// Enable the mouse cursor
GameScreen.ShowCursorThisFrame();
// If the camera should be rotated when the cursor is on the left and right sections of the screen, do so
if (RotateCamera)
{
if (GameScreen.IsCursorInArea(PointF.Empty, searchAreaSize))
{
#if FIVEM || SHVDN3 || SHVDNC
GameplayCamera.RelativeHeading += 5;
#elif ALTV
float current = Alt.Natives.GetGameplayCamRelativeHeading();
Alt.Natives.SetGameplayCamRelativeHeading(current + 5);
#elif RAGEMP
float current = Invoker.Invoke<float>(0x743607648ADD4587);
Invoker.Invoke(0xB4EC2312F4E5B1F1, current + 5);
#elif RPH
Camera.RenderingCamera.Heading += 5;
#endif
}
else if (GameScreen.IsCursorInArea(searchAreaRight, searchAreaSize))
{
#if FIVEM || SHVDN3 || SHVDNC
GameplayCamera.RelativeHeading -= 5;
#elif ALTV
float current = Alt.Natives.GetGameplayCamRelativeHeading();
Alt.Natives.SetGameplayCamRelativeHeading(current - 5);
#elif RAGEMP
float current = Invoker.Invoke<float>(0x743607648ADD4587);
Invoker.Invoke(0xB4EC2312F4E5B1F1, current - 5);
#elif RPH
Camera.RenderingCamera.Heading -= 5;
#endif
}
}
// If the player pressed the click button
if (clickSelected)
{
// Iterate over the items on the screen
foreach (NativeItem item in visibleItems)
{
// If the item is selected and slidable
if (item == selectedItem && item is NativeSlidableItem slidable)
{
// If the right arrow was pressed, go to the right
if (GameScreen.IsCursorInArea(slidable.RightArrow.Position, slidable.RightArrow.Size))
{
if (item.Enabled)
{
slidable.GoRight();
SoundLeftRight?.PlayFrontend();
}
else
{
SoundDisabled?.PlayFrontend();
}
return;
}
// If the user pressed the left arrow, go to the right
else if (GameScreen.IsCursorInArea(slidable.LeftArrow.Position, slidable.LeftArrow.Size))
{
if (item.Enabled)
{
slidable.GoLeft();
SoundLeftRight?.PlayFrontend();
}
else
{
SoundDisabled?.PlayFrontend();
}
return;
}
}
// If the cursor is inside of the selection rectangle
if (item.IsHovered)
{
if (item is NativeSeparatorItem)
{
return;
}
// If the item is selected, activate it
if (item == selectedItem)
{
if (item.Enabled)
{
ItemActivated?.Invoke(this, new ItemActivatedArgs(selectedItem));
item.OnActivated(this);
SoundActivated?.PlayFrontend();
if (item is NativeCheckboxItem checkboxItem)
{
checkboxItem.UpdateTexture(true);
}
}
else
{
SoundDisabled?.PlayFrontend();
}
}
// If is is not, set it as the selected item
else
{
SelectedItem = item;
}
// We found the item that was clicked, stop the function
return;
}
}
// If we got here, the user clicked outside of the selected item area
// So close the menu if required (same behavior of the interaction menu)
if (CloseOnInvalidClick)
{
if (selectedItem.Panel != null && selectedItem.Panel.Clickable && selectedItem.IsHovered)
{
return;
}
Visible = false;
}
return;
}
}
// If the player pressed the left or right button, trigger the event and sound
if (SelectedItem is NativeSlidableItem slidableItem && !upHeld && !downHeld)
{
if ((leftPressed && !rightPressed) || (leftHeld && heldSince > 0 && heldSince + HeldTime < time))
{
heldSince = time;
if (SelectedItem.Enabled)
{
slidableItem.GoLeft();
SoundLeftRight?.PlayFrontend();
}
else
{
SoundDisabled?.PlayFrontend();
}
return;
}
if ((rightPressed && !leftPressed) || (rightHeld && heldSince > 0 && heldSince + HeldTime < time))
{
heldSince = time;
if (SelectedItem.Enabled)
{
slidableItem.GoRight();
SoundLeftRight?.PlayFrontend();
}
else
{
SoundDisabled?.PlayFrontend();
}
return;
}
}
// If the player selected an item, activate it
if (selectPressed)
{
if (SelectedItem != null && SelectedItem.Enabled)
{
ItemActivated?.Invoke(this, new ItemActivatedArgs(selectedItem));
SelectedItem.OnActivated(this);
SoundActivated?.PlayFrontend();
if (SelectedItem is NativeCheckboxItem check)
{
check.UpdateTexture(true);
}
}
else
{
SoundDisabled?.PlayFrontend();
}
}
}
private void Draw()
{
NativeItem selected = SelectedItem;
if (bannerImage != null)
{
bannerImage.Draw();
BannerText?.Draw();
}
if (ShouldDrawNameBackground)
{
nameImage.Draw();
nameText?.Draw();
if (ShouldDrawCount)
{
countText.Draw();
}
}
if (!string.IsNullOrWhiteSpace(descriptionText.Text))
{
descriptionRect.Draw();
descriptionText.Draw();
}
if (Items.Count == 0)
{
return;
}
backgroundImage?.Draw();
for (int i = 0; i < visibleItems.Count; i++)
{
NativeItem item = visibleItems[i];
if (item == selected)
{
continue;
}
if (item.IsHovered && UseMouse && !(item is NativeSeparatorItem))
{
hoveredRect.Position = item.lastPosition;
hoveredRect.Size = item.lastSize;
hoveredRect.Draw();
}
item.Draw();
}
// Continue with the white selection rectangle
if (selected != null && !selected.UseCustomBackground)
{
selectedRect.Draw();
}
// And finish with the selected item on top (if any)
if (selected != null)
{
selected.Draw();
if (selected.Panel != null && selected.Panel.Visible)
{
selected.Panel.Process();
}
}
}
#endregion
#region Functions
public IEnumerator<NativeItem> GetEnumerator() => Items.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
public void Add(NativeItem item) => Add(Items.Count, item);
public void Add(NativeMenu menu) => AddSubMenu(menu);
public virtual void Add(int position, NativeItem item)
{
if (Items.Contains(item))
{
throw new InvalidOperationException("The item is already part of the menu.");
}
if (position < 0 || position > Items.Count)
{
throw new ArgumentOutOfRangeException(nameof(position), "The index under Zero or is over the Item Count.");
}
if (item == null)
{
throw new ArgumentNullException(nameof(item));
}
Items.Insert(position, item);
if (Items.Count != 0 && SelectedIndex == -1)
{
SelectedIndex = 0;
}
else
{
UpdateItemList();
}
MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Added));
UpdateItems();
}
public NativeSubmenuItem AddSubMenu(NativeMenu menu)
{
// If the menu is null, raise an exception
if (menu == null)
{
throw new ArgumentNullException(nameof(menu));
}
// Create a new menu item, add it and return it
NativeSubmenuItem item = new NativeSubmenuItem(menu, this);
Add(item);
return item;
}
public NativeSubmenuItem AddSubMenu(NativeMenu menu, string endlabel)
{
NativeSubmenuItem item = AddSubMenu(menu);
item.AltTitle = endlabel;
return item;
}
public void Remove(NativeItem item)
{
if (!Items.Contains(item))
{
return;
}
Items.Remove(item);
if (SelectedIndex >= Items.Count)
{
SelectedIndex = Items.Count - 1;
}
else
{
UpdateItemList();
}
MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Removed));
UpdateItems();
}
public void Remove(Func<NativeItem, bool> pred)
{
for (int i = 0; i < Items.Count; i++)
{
NativeItem item = Items[i];
if (!pred(item))
{
continue;
}
Items.Remove(item);
MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Added));
}
if (SelectedIndex >= Items.Count)
{
SelectedIndex = Items.Count - 1;
}
else
{
UpdateItemList();
}
UpdateItems();
}
public void Clear()
{
List<NativeItem> items = new List<NativeItem>(Items);
Items.Clear();
foreach (NativeItem item in items)
{
MenuModified?.Invoke(this, new MenuModifiedEventArgs(item, ItemOperation.Added));
}
index = 0;
firstItem = 0;
UpdateItemList();
UpdateItems();
}
public bool Contains(NativeItem item) => Items.Contains(item);
public virtual void Process()
{
if (!visible)
{
if (!justClosed)
{
return;
}
#if FIVEM
API.SetInputExclusive(0, (int)Control.PhoneCancel);
API.SetInputExclusive(0, (int)Control.FrontendPause);
#elif ALTV
Alt.Natives.SetInputExclusive(0, (int)Control.CellPhoneCancel);
Alt.Natives.SetInputExclusive(0, (int)Control.FrontendPause);
#elif RAGEMP
Invoker.Invoke(Natives.SetInputExclusive, 0, (int)Control.PhoneCancel);
Invoker.Invoke(Natives.SetInputExclusive, 0, (int)Control.FrontendPause);
#elif RPH
NativeFunction.CallByHash<int>(0x351220255D64C155, 0, (int)Control.CellphoneCancel);
NativeFunction.CallByHash<int>(0x351220255D64C155, 0, (int)Control.FrontendPause);
#elif SHVDN3 || SHVDNC
Function.Call(Hash.SET_INPUT_EXCLUSIVE, 0, (int)Control.PhoneCancel);
Function.Call(Hash.SET_INPUT_EXCLUSIVE, 0, (int)Control.FrontendPause);
#endif
if (!Controls.IsPressed((Control)177 /*PhoneCancel*/) && !Controls.IsPressed(Control.FrontendPause))
{
justClosed = false;
}
return;
}
NativeItem selected = SelectedItem;
if (selected != null && descriptionText.Text != selected.Description)
{
UpdateDescription();
}
Draw();
ProcessControls();
Buttons.Draw();
}
public virtual void Recalculate()
{
// Store the current values of X and Y
PointF pos;
if (SafeZoneAware)
{
float x = 0;
switch (Alignment)
{
case Alignment.Left:
x = Offset.X;
break;
case Alignment.Right:
x = Offset.X - Width;
break;
}
pos = SafeZone.GetPositionAt(new PointF(x, Offset.Y), Alignment, GFXAlignment.Top);
}
else
{
float x = 0;
switch (Alignment)
{
case Alignment.Left:
x = Offset.X;
break;
case Alignment.Right:
x = 1f.ToXScaled() - Width - Offset.X;
break;
}
pos = new PointF(x, Offset.Y);
}
// If there is a banner and is a valid element
if (bannerImage != null && bannerImage is BaseElement bannerImageBase)
{
// Set the position and size of the banner
bannerImageBase.literalPosition = new PointF(pos.X, pos.Y);
bannerImageBase.literalSize = new SizeF(width, bannerImageBase.Size.Height);
bannerImageBase.Recalculate();
// If there is a text element, also set the position of it
if (BannerText != null)
{
BannerText.Position = new PointF(pos.X + 209, pos.Y + 22);
}
// Finally, increase the current position of Y based on the banner height
pos.Y += bannerImageBase.Size.Height;
}
// Time for the name background
// Set the position and size of it
nameImage.literalPosition = new PointF(pos.X, pos.Y);
nameImage.literalSize = new SizeF(width, nameHeight);
nameImage.Recalculate();
// If there is a text, also set the position of it
if (nameText != null)
{
nameText.Position = new PointF(pos.X + 6, pos.Y + 4.2f);
}
// Finally, increase the size based on the name height
// currentY += nameHeight;
// Set the size of the selection rectangle
selectedRect.Size = new SizeF(width, itemHeight);
// And set the word wrap of the description
descriptionText.WordWrap = width - posXDescTxt;
// Set the right size of the rotation
searchAreaRight = new PointF(1f.ToXScaled() - 30, 0);
// Then, continue with an item update
UpdateItems();
}
public void Back()
{
Visible = false;
if (Visible)
{
return;
}
justClosed = true;
if (Parent != null)
{
Parent.Visible = true;
}
}
[Obsolete("Set Visible to true instead.", true)]
public void Open() => Visible = true;
[Obsolete("Set Visible to false instead.", true)]
public void Close() => Visible = false;
public void Previous()
{
if (Items.Count == 0)
{
return;
}
int nextIndex = SelectedIndex;
while (true)
{
nextIndex -= 1;
if (nextIndex < 0)
{
nextIndex = Items.Count - 1;
}
if (Items[nextIndex] is NativeSeparatorItem)
{
continue;
}
if (nextIndex == SelectedIndex)
{
return;
}
SelectedIndex = nextIndex;
return;
}
}
public void Next()
{
if (Items.Count == 0)
{
return;
}
int nextIndex = SelectedIndex;
while (true)
{
nextIndex += 1;
if (nextIndex >= Items.Count)
{
nextIndex = 0;
}
if (Items[nextIndex] is NativeSeparatorItem)
{
continue;
}
if (nextIndex == SelectedIndex)
{
return;
}
SelectedIndex = nextIndex;
return;
}
}
#endregion
}
}