using System; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using PKHeX.Core; using PKHeX.Drawing.Misc; using PKHeX.Drawing.PokeSprite; namespace PKHeX.WinForms.Controls; public sealed partial class PokePreview : Form { private static readonly Image[] GenderImages = [ Properties.Resources.gender_0, Properties.Resources.gender_1, Properties.Resources.gender_2, ]; private readonly List TextLinesPre = []; private readonly List MoveLines = []; private readonly List TextLinesHint = []; private readonly List TextLinesEncounter = []; private string HeaderName = string.Empty; private Image? HeaderBall; private Image? HeaderGender; private const int Border = 1; private const int HeaderTopPadding = 4; private const int HeaderBottomPadding = 4; private const int HeaderLeftPadding = 4; private const int HeaderRightPadding = 4; private const int HeaderIconGap = 2; private const int BodyTopPadding = 2; private const int BodyBottomPadding = 2; private const int BodyLeftPadding = 4; private const int BodyRightPadding = 4; private const int TextSectionTopPadding = 2; private const int TextSectionBottomPadding = 2; private const int TextLineSpacing = 1; private const int MoveIconTextGap = 2; private const int MoveSectionTopPadding = 4; private const int MoveSectionBottomPadding = 8; private const int IconSize = 24; private static Color IllegalTextColor => WinFormsUtil.ColorWarn; public PokePreview() { InitializeComponent(); SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true); UpdateStyles(); } public void Populate(PKM pk, in BattleTemplateExportSettings settings, in LegalityLocalizationContext ctx) { var main = Main.Settings; HeaderName = GetNameTitle(pk, settings); if (pk.Format > 2) HeaderBall = GetBallImage(pk); else HeaderBall = null; if (pk.Format != 1 || main.EntityEditor.ShowGenderGen1) HeaderGender = GetGenderImage(pk); else HeaderGender = null; TextLinesPre.Clear(); MoveLines.Clear(); TextLinesHint.Clear(); TextLinesEncounter.Clear(); var hover = Main.Settings.Hover; var (before, mid, after) = GetBeforeAndAfter(pk, ctx, settings); AppendTextSection(TextLinesPre, before, hover.PreviewShowPaste, ForeColor); BuildMoves(pk, ctx.Analysis, settings); AppendTextSection(TextLinesHint, mid, hover.HoverSlotShowLegalityHint, IllegalTextColor); AppendTextSection(TextLinesEncounter, after, hover.HoverSlotShowEncounter, ForeColor); ApplySize(); Invalidate(); } private void BuildMoves(PKM pk, LegalityAnalysis la, in BattleTemplateExportSettings settings) { if (pk.MoveCount == 0) return; var context = pk.Context; var strings = settings.Localization.Strings; var names = strings.movelist; var checks = la.Info.Moves; AppendMoveLine(pk, strings, names, context, pk.Move1, checks[0].Valid); AppendMoveLine(pk, strings, names, context, pk.Move2, checks[1].Valid); AppendMoveLine(pk, strings, names, context, pk.Move3, checks[2].Valid); AppendMoveLine(pk, strings, names, context, pk.Move4, checks[3].Valid); } private void AppendMoveLine(PKM pk, GameStrings strings, ReadOnlySpan names, EntityContext context, ushort move, bool valid) { if (move == 0 || move >= names.Length) return; byte type = MoveInfo.GetType(move, context); var name = names[move]; if (move == (int)PKHeX.Core.Move.HiddenPower && pk.Context is not EntityContext.Gen8a) { if (HiddenPower.TryGetTypeIndex(pk.HPType, out type)) name = $"{name} ({strings.types[type]}) [{pk.HPPower}]"; } var image = TypeSpriteUtil.GetTypeSpriteIconSmall(type); var color = valid ? ForeColor : IllegalTextColor; MoveLines.Add(new RenderMoveLine(name, image, color)); } private static void AppendTextSection(List list, ReadOnlySpan text, bool visible, Color color) { if (!visible || text.Length == 0) return; int i = 0; var lines = text.EnumerateLines(); foreach (var line in lines) { var topPadding = i == 0 ? TextSectionTopPadding : TextLineSpacing; const int bottomPadding = 0; list.Add(new RenderTextLine(line.ToString(), color, topPadding, bottomPadding)); i++; } var last = list[^1]; list[^1] = last with { BottomPadding = TextSectionBottomPadding }; } private void ApplySize() { var width = GetPreferredWidth(); var height = GetPreferredHeight(); var size = new Size(width, height); if (Size != size) Size = size; } private int GetPreferredWidth() { var width = 0; var nameSize = MeasureSize(HeaderName, Font); var headerWidth = Border + HeaderLeftPadding + nameSize.Width + HeaderRightPadding + Border; if (HeaderBall != null) headerWidth += IconSize + HeaderIconGap; if (HeaderGender != null) headerWidth += IconSize + HeaderIconGap; width = Math.Max(width, headerWidth); foreach (var move in MoveLines) { var size = MeasureSize(move.Text, Font); var lineWidth = Border + BodyLeftPadding + IconSize + MoveIconTextGap + size.Width + BodyRightPadding + Border; width = Math.Max(width, lineWidth); } foreach (var line in TextLinesPre) { var size = MeasureSize(line.Text, Font); var lineWidth = Border + BodyLeftPadding + size.Width + BodyRightPadding + Border; width = Math.Max(width, lineWidth); } foreach (var line in TextLinesHint) { var size = MeasureSize(line.Text, Font); var lineWidth = Border + BodyLeftPadding + size.Width + BodyRightPadding + Border; width = Math.Max(width, lineWidth); } foreach (var line in TextLinesEncounter) { var size = MeasureSize(line.Text, Font); var lineWidth = Border + BodyLeftPadding + size.Width + BodyRightPadding + Border; width = Math.Max(width, lineWidth); } return width; } private int GetPreferredHeight() { var height = Border; height += HeaderTopPadding + IconSize + HeaderBottomPadding; height += Border; height += BodyTopPadding; if (MoveLines.Count != 0) { height += MoveSectionTopPadding; height += MoveLines.Count * IconSize; height += MoveSectionBottomPadding; } foreach (var line in TextLinesPre) { height += line.TopPadding; var textHeight = Math.Max(Font.Height, MeasureSize(line.Text, Font).Height); height += textHeight; height += line.BottomPadding; } foreach (var line in TextLinesHint) { height += line.TopPadding; var textHeight = Math.Max(Font.Height, MeasureSize(line.Text, Font).Height); height += textHeight; height += line.BottomPadding; } foreach (var line in TextLinesEncounter) { height += line.TopPadding; var textHeight = Math.Max(Font.Height, MeasureSize(line.Text, Font).Height); height += textHeight; height += line.BottomPadding; } height += BodyBottomPadding; height += Border; return height; } protected override void OnPaint(PaintEventArgs e) { base.OnPaint(e); var g = e.Graphics; g.Clear(BackColor); var outer = new Rectangle(0, 0, Width - 1, Height - 1); g.DrawRectangle(SystemPens.ControlDark, outer); const int headerTop = Border; const int headerHeight = HeaderTopPadding + IconSize + HeaderBottomPadding; const int headerBottom = headerTop + headerHeight; g.DrawLine(SystemPens.ControlDark, Border, headerBottom, Width - Border - 1, headerBottom); DrawHeader(g, headerTop); var y = headerBottom + BodyTopPadding; y = DrawTextLines(TextLinesPre, g, y); y = DrawMoves(g, y); y = DrawTextLines(TextLinesHint, g, y); if (TextLinesEncounter.Count != 0) { g.DrawLine(SystemPens.ControlDark, Border, y, Width - Border - 1, y); y += Border; _ = DrawTextLines(TextLinesEncounter, g, y); } } private void DrawHeader(Graphics g, int headerTop) { var y = headerTop + HeaderTopPadding; var x = Border + HeaderLeftPadding; if (HeaderBall is not null) { g.DrawImage(HeaderBall, new Rectangle(x, y, IconSize, IconSize)); x += IconSize + HeaderIconGap; } var textRect = new Rectangle(x, y, Math.Max(0, Width - x - Border - HeaderRightPadding - IconSize - HeaderIconGap), IconSize); TextRenderer.DrawText(g, HeaderName, Font, textRect, ForeColor, TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis | TextFormatFlags.NoPadding); // Gender displayed at right edge. if (HeaderGender is not null) { var genderX = Width - Border - HeaderRightPadding - IconSize; g.DrawImage(HeaderGender, new Rectangle(genderX, y, IconSize, IconSize)); } } private int DrawMoves(Graphics g, int y) { if (MoveLines.Count == 0) return y; const int x = Border + BodyLeftPadding; y += MoveSectionTopPadding; foreach (var line in MoveLines) { if (line.Icon is not null) g.DrawImage(line.Icon, new Rectangle(x, y, IconSize, IconSize)); var textRect = new Rectangle(x + IconSize + MoveIconTextGap, y, Math.Max(0, Width - (x + IconSize + MoveIconTextGap) - Border - BodyRightPadding), IconSize); TextRenderer.DrawText(g, line.Text, Font, textRect, line.Color, TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.NoPadding); y += IconSize; } y += MoveSectionBottomPadding; return y; } private int DrawTextLines(List list, Graphics g, int y) { const int x = Border + BodyLeftPadding; var textWidth = Math.Max(0, Width - x - Border - BodyRightPadding); foreach (var line in list) { y += line.TopPadding; var height = Math.Max(Font.Height, MeasureSize(line.Text, Font).Height); var rect = new Rectangle(x, y, textWidth, height); TextRenderer.DrawText(g, line.Text, Font, rect, line.Color, TextFormatFlags.Left | TextFormatFlags.NoPadding); y += height + line.BottomPadding; } return y; } public static Size MeasureSize(ReadOnlySpan text, Font font) { const TextFormatFlags flags = TextFormatFlags.LeftAndRightPadding | TextFormatFlags.VerticalCenter; return TextRenderer.MeasureText(text, font, new Size(), flags); } private static string GetNameTitle(PKM pk, in BattleTemplateExportSettings settings) { var nick = pk.Nickname; var strings = settings.Localization.Strings; var all = strings.Species; var species = pk.Species; if (species >= all.Count) return nick; var expect = all[species]; if (settings.IsTokenInExport(BattleTemplateToken.Nickname)) return expect; if (nick.Equals(expect, StringComparison.OrdinalIgnoreCase)) return nick; return $"{nick} ({expect})"; } private static Bitmap GetBallImage(PKM pk) { var ball = (byte)Ball.Poke; if (pk.Format >= 3) ball = pk.Ball; return SpriteUtil.GetBallSprite(ball); } private static Image? GetGenderImage(PKM pk) { if (pk.Format == 1) return null; var gender = pk.Gender; if (gender >= GenderImages.Length) gender = 2; return GenderImages[gender]; } private static (string Before, string Middle, string After) GetBeforeAndAfter(PKM pk, in LegalityLocalizationContext la, in BattleTemplateExportSettings settings) { var order = settings.Order; // Bifurcate the order into two sections; split via Moves. var moveIndex = settings.GetTokenIndex(BattleTemplateToken.Moves); var before = moveIndex == -1 ? order : order[..moveIndex]; var after = moveIndex == -1 ? default : order[(moveIndex + 1)..]; if (before.Length > 0 && before[0] == BattleTemplateToken.FirstLine) before = before[1..]; // remove first line token; trust that users don't randomly move it lower in the list. var start = SummaryPreviewer.GetPreviewText(pk, settings with { Order = before }); var end = SummaryPreviewer.GetPreviewText(pk, settings with { Order = after }); if (settings.IsTokenInExport(BattleTemplateToken.IVs, before)) TryAppendOtherStats(pk, ref start, settings); else if (settings.IsTokenInExport(BattleTemplateToken.IVs, after)) TryAppendOtherStats(pk, ref end, settings); var mid = ""; if (Main.Settings.Hover.HoverSlotShowLegalityHint) mid = SummaryPreviewer.AppendLegalityHint(la, mid); if (Main.Settings.Hover.HoverSlotShowEncounter) end = SummaryPreviewer.AppendEncounterInfo(la, end); return (start, mid, end); } private static void TryAppendOtherStats(PKM pk, ref string line, in BattleTemplateExportSettings settings) { if (pk is IGanbaru g) AppendGanbaru(g, ref line, settings); if (pk is IAwakened a) AppendAwakening(a, ref line, settings); } private static void AppendGanbaru(IGanbaru g, ref string line, in BattleTemplateExportSettings settings) { Span stats = stackalloc byte[6]; g.GetGVs(stats); TryAppend(stats, ref line, settings, BattleTemplateToken.GVs); } private static void AppendAwakening(IAwakened a, ref string line, in BattleTemplateExportSettings settings) { Span stats = stackalloc byte[6]; a.GetAVs(stats); TryAppend(stats, ref line, settings, BattleTemplateToken.AVs); } private static void TryAppend(ReadOnlySpan stats, ref string line, BattleTemplateExportSettings settings, BattleTemplateToken token) where T : unmanaged, IEquatable { var localization = settings.Localization; var statNames = localization.Config.GetStatDisplay(settings.StatsOther); var value = ShowdownSet.GetStringStats(stats, default, statNames); if (value.Length == 0) return; var result = localization.Config.Push(token, value); line += Environment.NewLine + result; } /// Prevent stealing focus from the form that shows this. protected override bool ShowWithoutActivation => true; private const int WS_EX_TOPMOST = 0x00000008; private const int WS_EX_TOOLWINDOW = 0x00000080; private const int WS_EX_NOACTIVATE = 0x08000000; protected override CreateParams CreateParams { get { CreateParams createParams = base.CreateParams; createParams.ExStyle |= WS_EX_TOPMOST | WS_EX_NOACTIVATE | WS_EX_TOOLWINDOW; return createParams; } } private readonly record struct RenderMoveLine(string Text, Image? Icon, Color Color); private readonly record struct RenderTextLine(string Text, Color Color, int TopPadding, int BottomPadding); /// /// Moves the form to the specified screen coordinates without resizing or changing its z-order. /// /// /// This method updates the form's position using the Win32 SetWindowPos API with flags that minimize redraws and prevent activation or z-order changes. /// Use this method when you need to reposition the form efficiently without affecting its size or focus. /// /// The new horizontal position, in pixels, of the form's upper-left corner relative to the screen. /// The new vertical position, in pixels, of the form's upper-left corner relative to the screen. public void MoveForm(int x, int y) { const uint SWP_NOSIZE = 0x0001; const uint SWP_NOZORDER = 0x0004; const uint SWP_NOREDRAW = 0x0008; const uint SWP_NOACTIVATE = 0x0010; const uint SWP_NOSENDCHANGING = 0x0400; const uint SWP_ASYNCWINDOWPOS = 0x4000; const uint flags = SWP_NOZORDER | SWP_NOSIZE | SWP_NOREDRAW | SWP_NOACTIVATE | SWP_NOSENDCHANGING | SWP_ASYNCWINDOWPOS; const int HWND_TOPMOST = -1; SetWindowPos(Handle, HWND_TOPMOST, x, y, 0, 0, flags); return; [System.Runtime.InteropServices.DllImport("user32.dll")] static extern bool SetWindowPos(nint hWnd, nint hWndInsertAfter, int x, int y, int cx, int cy, uint uFlags); } }