PKHeX/PKHeX.WinForms/Controls/Slots/SummaryPreviewer.cs
Kurt 0e097b1fc6 Minor slot hover performance improvements
Skip repaint on cursor moving the hover window
Cache reference to the slot interaction types and "nothing" slot image
Dispose of slot sprites when updating with a new one
If scrolling box/group, auto-update hover with the newly displayed slot's content instead of hiding
2026-03-19 17:25:06 -05:00

211 lines
7.2 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
namespace PKHeX.WinForms.Controls;
public sealed class SummaryPreviewer
{
private readonly ToolTip ShowSet = new() { InitialDelay = 200, IsBalloon = false, AutoPopDelay = 32_767 };
private readonly CryPlayer Cry = new();
private readonly PokePreview Previewer = new();
private CancellationTokenSource _source = new();
private static HoverSettings Settings => Main.Settings.Hover;
public void Show(Control pb, PKM pk, StorageSlotType type = StorageSlotType.None)
{
if (pk.Species == 0)
{
Clear();
return;
}
var programLanguage = Language.GetLanguageValue(Main.Settings.Startup.Language);
var cfg = Main.Settings.BattleTemplate;
var settings = cfg.Hover.GetSettings(programLanguage, pk.Context);
var localize = LegalityLocalizationSet.GetLocalization(programLanguage);
var la = new LegalityAnalysis(pk, type);
var ctx = LegalityLocalizationContext.Create(la, localize);
if (Settings.HoverSlotShowPreview && Control.ModifierKeys != Keys.Alt)
{
UpdatePreview(pb, pk, settings, ctx);
}
else if (Settings.HoverSlotShowText)
{
var text = GetPreviewText(pk, settings);
if (!settings.Order.Contains(BattleTemplateToken.FirstLine))
{
var insert = GetPreviewText(pk, settings with { Order = [BattleTemplateToken.FirstLine] });
text = insert + Environment.NewLine + text;
}
if (Settings.HoverSlotShowEncounter)
text = AppendEncounterInfo(ctx, text);
ShowSet.SetToolTip(pb, text);
}
if (Settings.HoverSlotPlayCry)
Cry.PlayCry(pk, pk.Context);
}
private void UpdatePreview(Control pb, PKM pk, in BattleTemplateExportSettings settings, in LegalityLocalizationContext ctx)
{
_source.Cancel();
_source.Dispose(); // Properly dispose the previous CancellationTokenSource
_source = new();
var wasVisible = Previewer.Visible;
UpdatePreviewPosition(new());
Previewer.Populate(pk, settings, ctx);
if (!wasVisible)
ShowInactiveTopmost(Previewer);
}
private const int SW_SHOWNOACTIVATE = 4;
private const int HWND_TOPMOST = -1;
private const uint SWP_NOACTIVATE = 0x0010;
#pragma warning disable SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
[System.Runtime.InteropServices.DllImport("user32.dll", EntryPoint = "SetWindowPos")]
private static extern bool SetWindowPos(
int hWnd, // Window handle
int hWndInsertAfter, // Placement-order handle
int X, // Horizontal position
int Y, // Vertical position
int cx, // Width
int cy, // Height
uint uFlags); // Window positioning flags
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern bool ShowWindow(nint hWnd, int nCmdShow);
#pragma warning restore SYSLIB1054 // Use 'LibraryImportAttribute' instead of 'DllImportAttribute' to generate P/Invoke marshalling code at compile time
public static void ShowInactiveTopmost(Form frm)
{
try
{
ShowWindow(frm.Handle, SW_SHOWNOACTIVATE);
SetWindowPos(frm.Handle.ToInt32(), HWND_TOPMOST,
frm.Left, frm.Top, frm.Width, frm.Height,
SWP_NOACTIVATE);
}
catch
{
// error handling
}
}
public void UpdatePreviewPosition(Point location)
{
var cLoc = Cursor.Position;
var shift = Settings.PreviewCursorShift;
cLoc.Offset(shift);
Previewer.MoveForm(cLoc.X, cLoc.Y);
}
public void Show(Control pb, IEncounterInfo enc)
{
if (enc.Species == 0)
{
Clear();
return;
}
if (Settings.HoverSlotShowText)
ShowSet.SetToolTip(pb, GetPreviewText(enc, Settings.HoverSlotShowEncounterVerbose));
if (Settings.HoverSlotPlayCry)
Cry.PlayCry(enc, enc.Context);
}
public void Clear()
{
try
{
var token = _source.Token;
Task.Run(async () =>
{
if (!Previewer.IsHandleCreated)
return; // not shown ever
// Give a little bit of fade-out delay
await Task.Delay(50, CancellationToken.None).ConfigureAwait(false);
if (!token.IsCancellationRequested)
await Previewer.InvokeAsync(Previewer.Hide, CancellationToken.None).ConfigureAwait(false);
}, CancellationToken.None).ConfigureAwait(false);
}
catch
{
// Ignore.
}
ShowSet.RemoveAll();
Cry.Stop();
}
public static string GetPreviewText(PKM pk, BattleTemplateExportSettings settings) => ShowdownParsing.GetLocalizedPreviewText(pk, settings);
public static string AppendEncounterInfo(LegalityLocalizationContext la, string text)
{
var result = new List<string>(8);
if (text.Length != 0) // add a blank line between the set and the encounter info if isn't already a blank line
{
result.Add(text);
result.Add(string.Empty);
}
LegalityFormatting.AddEncounterInfo(la, result);
return string.Join(Environment.NewLine, result);
}
private static string GetPreviewText(IEncounterInfo enc, bool verbose = false)
{
var lines = enc.GetTextLines(verbose, Main.CurrentLanguage);
return string.Join(Environment.NewLine, lines);
}
public static string AppendLegalityHint(in LegalityLocalizationContext la, string line)
{
// Get the first illegal check result, and append the localization of it as the hint.
// If all legal, return the input string unchanged.
var analysis = la.Analysis;
if (analysis.Valid)
return line;
foreach (var chk in analysis.Results)
{
if (chk.Valid)
continue;
var hint = la.Humanize(chk, verbose: true);
return Join(line, hint);
}
for (var i = 0; i < analysis.Info.Moves.Length; i++)
{
var chk = analysis.Info.Moves[i];
if (chk.Valid)
continue;
var hint = la.FormatMove(chk, i, la.Analysis.Info.Entity.Context);
return Join(line, hint);
}
for (var i = 0; i < analysis.Info.Relearn.Length; i++)
{
var chk = analysis.Info.Relearn[i];
if (chk.Valid)
continue;
var hint = la.FormatMove(chk, i, la.Analysis.Info.Entity.Context);
return Join(line, hint);
}
return line;
static string Join(string line, string hint)
{
if (hint.Length > 67)
hint = hint[..67] + "...";
return string.IsNullOrEmpty(line) ? hint : $"{line}{Environment.NewLine}{hint}";
}
}
}