PKHeX/PKHeX.WinForms/Subforms/PKM Editors/Text.cs
Kurt 3317a8bfda Add trash view/edit to all Trainer forms
Make PID text entry uppercase across the program
Standardize RivalTrash => RivalNameTrash, same for Rival => RivalName
2026-03-14 22:28:00 -05:00

353 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using PKHeX.Core;
namespace PKHeX.WinForms;
public partial class TrashEditor : Form
{
private readonly IStringConverter Converter;
private readonly List<NumericUpDown> Bytes = [];
public string FinalString { get; private set; }
public byte[] FinalBytes { get; private set; }
private readonly byte[] Raw;
private bool editing;
private static TrashEditor Get<T>(TextBoxBase tb, T provider, Span<byte> trash = default) where T : IStringConverter, IGeneration, IContext
=> new(tb, provider, provider.Generation, provider.Context, trash);
public static void Show<T>(TextBoxBase tb, T provider, Span<byte> trash = default, bool readOnly = false)
where T : IStringConverter, IGeneration, IContext
{
using var form = Get(tb, provider, trash);
if (readOnly)
form.B_Save.Enabled = false;
form.ShowDialog();
tb.Text = form.FinalString;
form.FinalBytes.CopyTo(trash);
}
// workaround to being unable to translate this via reflection with the DevUtil scraper with ref structs
#nullable disable
// ReSharper disable once NotNullOrRequiredMemberIsNotInitialized
private TrashEditor()
{
InitializeComponent();
WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage);
}
#nullable enable
private TrashEditor(TextBoxBase tb, IStringConverter converter, byte generation, EntityContext context, Span<byte> raw = default) : this()
{
Converter = converter;
FinalString = tb.Text;
editing = true;
if (raw.Length != 0)
{
Raw = FinalBytes = raw.ToArray();
AddTrashEditing(raw.Length, generation, context);
}
else
{
Raw = FinalBytes = [];
}
var f = FontUtil.GetPKXFont();
AddCharEditing(f, context);
TB_Text.MaxLength = tb.MaxLength;
TB_Text.Text = tb.Text;
TB_Text.Font = f;
if (FLP_Characters.Controls.Count == 0)
{
FLP_Characters.Visible = false;
FLP_Hex.Height *= 2;
}
else if (FLP_Hex.Controls.Count == 0)
{
FLP_Characters.Location = FLP_Hex.Location;
FLP_Characters.Height *= 2;
}
editing = false;
CenterToParent();
B_ApplyTrash.MouseHover += (_, _) =>
{
var text = GetTrashString();
var data = SetString(text);
var display = $"{text} = {Environment.NewLine}{string.Join(' ', data.Select(z => $"{z:X2}"))}";
Tip.Show(display, B_ApplyTrash);
};
}
private void B_Cancel_Click(object sender, EventArgs e) => Close();
private void B_Save_Click(object sender, EventArgs e)
{
FinalString = TB_Text.Text;
if (FinalBytes.Length == 0)
FinalBytes = Raw;
Close();
}
private void AddCharEditing(Font f, EntityContext context)
{
var chars = GetChars(context);
if (chars.Length == 0)
return;
FLP_Characters.Visible = true;
foreach (ushort c in chars)
{
var l = GetLabel(((char)c).ToString());
l.Font = f;
l.AutoSize = false;
l.Size = new Size(20, 20);
l.Click += (_, _) => { if (TB_Text.Text.Length < TB_Text.MaxLength) TB_Text.AppendText(l.Text); };
FLP_Characters.Controls.Add(l);
Tip.SetToolTip(l, $"Insert {l.Text} (0x{c:X4})");
}
}
private void AddTrashEditing(int count, byte generation, EntityContext context)
{
FLP_Hex.Visible = true;
GB_Trash.Visible = true;
NUD_Generation.Value = generation;
for (int i = 0; i < count; i++)
{
var l = GetLabel($"${i:X2}");
l.Font = NUD_Generation.Font;
var n = GetNUD(min: 0, max: 255, hex: true);
n.Click += (_, _) =>
{
switch (ModifierKeys)
{
case Keys.Shift: n.Value = n.Maximum; break;
case Keys.Alt: n.Value = n.Minimum; break;
}
};
n.Value = Raw[i];
n.ValueChanged += (_, _) => UpdateNUD(n);
FLP_Hex.Controls.Add(l);
FLP_Hex.Controls.Add(n);
Bytes.Add(n);
if (i % 4 == 3)
FLP_Hex.SetFlowBreak(n, true);
}
TB_Text.TextChanged += (_, _) => UpdateString(TB_Text);
var source = GameInfo.Sources; // don't use FilteredSources here -- allow any species
CB_Species.InitializeBinding();
CB_Species.DataSource = new BindingSource(source.SpeciesDataSource, string.Empty);
CB_Language.InitializeBinding();
CB_Language.DataSource = GameInfo.LanguageDataSource(generation, context);
}
private void UpdateNUD(NumericUpDown nud)
{
if (editing)
return;
// build bytes
editing = true;
int index = Bytes.IndexOf(nud);
Raw[index] = (byte)nud.Value;
TB_Text.Text = GetString();
editing = false;
}
private void UpdateString(TextBox tb)
{
if (editing)
return;
editing = true;
// build bytes
ReadOnlySpan<byte> data = SetString(tb.Text);
if (data.Length > Raw.Length)
data = data[..Raw.Length];
data.CopyTo(Raw);
for (int i = 0; i < Raw.Length; i++)
Bytes[i].Value = Raw[i];
editing = false;
}
private void B_ApplyTrash_Click(object sender, EventArgs e)
{
string text = GetTrashString();
ReadOnlySpan<byte> data = SetString(text);
ReadOnlySpan<byte> current = SetString(TB_Text.Text);
if (data.Length <= current.Length)
{
WinFormsUtil.Alert("Trash byte layer is hidden by current text.",
$"Current Bytes: {current.Length}" + Environment.NewLine + $"Layer Bytes: {data.Length}");
return;
}
if (data.Length > Bytes.Count)
{
WinFormsUtil.Alert("Trash byte layer is too long to apply.");
return;
}
for (int i = current.Length; i < data.Length; i++)
Bytes[i].Value = data[i];
}
private string GetTrashString()
{
var species = (ushort)WinFormsUtil.GetIndex(CB_Species);
var language = WinFormsUtil.GetIndex(CB_Language);
var gen = (byte)NUD_Generation.Value;
string text = SpeciesName.GetSpeciesNameGeneration(species, language, gen);
if (string.IsNullOrEmpty(text)) // no result
text = CB_Species.Text;
return text;
}
private void B_ClearTrash_Click(object sender, EventArgs e)
{
byte[] current = SetString(TB_Text.Text);
for (int i = current.Length; i < Bytes.Count; i++)
Bytes[i].Value = 0;
}
private byte[] SetString(ReadOnlySpan<char> text)
{
Span<byte> temp = stackalloc byte[Raw.Length];
var written = Converter.SetString(temp, text, text.Length, StringConverterOption.None);
return temp[..written].ToArray();
}
private string GetString() => Converter.GetString(Raw);
// Helpers
private static Label GetLabel(string str) => new() { Text = str, AutoSize = false, Size = new Size(40, 24), TextAlign = ContentAlignment.MiddleRight };
private static NumericUpDown GetNUD(byte min, byte max, bool hex) => new()
{
Maximum = max,
Minimum = min,
Hexadecimal = hex,
Width = 40,
Padding = Padding.Empty,
Margin = Padding.Empty,
};
private static ReadOnlySpan<ushort> GetChars(EntityContext context) => context switch
{
EntityContext.Gen5 => SpecialCharsGen5,
EntityContext.Gen6 or EntityContext.Gen7 or EntityContext.Gen7b => SpecialCharsGen67,
_ when !context.IsEraPreSwitch => SpecialCharsGen8,
_ => [], // Undocumented
};
// Unicode codepoints for special characters, incorrectly starting at 0x2460 instead of 0xE0xx.
private static ReadOnlySpan<ushort> SpecialCharsGen5 =>
[
0x2460, // Full Neutral (Happy in Gen7)
0x2461, // Full Happy (Angry in Gen7)
0x2462, // Full Sad
0x2463, // Full Angry (Neutral in Gen7)
0x2464, // Full Right-up arrow
0x2465, // Full Right-down arrow
0x2466, // Full Zz
0x2467, // ×
0x2468, // ÷
// Skip 69-6B, can't be entered.
0x246C, // …
0x246D, // ♂
0x246E, // ♀
0x246F, // ♠
0x2470, // ♣
0x2471, // ♥
0x2472, // ♦
0x2473, // ★
0x2474, // ◎
0x2475, // ○
0x2476, // □
0x2477, // △
0x2478, // ◇
0x2479, // ♪
0x247A, // ☀
0x247B, // ☁
0x247C, // ☂
0x247D, // ☃
0x247E, // Half Neutral
0x247F, // Half Happy
0x2480, // Half Sad
0x2481, // Half Angry
0x2482, // Half Right-up arrow
0x2483, // Half Right-down arrow
0x2484, // Half Zz
];
private static ReadOnlySpan<ushort> SpecialCharsGen67 =>
[
0xE081, // Full Neutral (Happy in Gen7)
0xE082, // Full Happy (Angry in Gen7)
0xE083, // Full Sad
0xE084, // Full Angry (Neutral in Gen7)
0xE085, // Full Right-up arrow
0xE086, // Full Right-down arrow
0xE087, // Full Zz
0xE088, // ×
0xE089, // ÷
// Skip 8A-8C, can't be entered.
0xE08D, // …
0xE08E, // ♂
0xE08F, // ♀
0xE090, // ♠
0xE091, // ♣
0xE092, // ♥
0xE093, // ♦
0xE094, // ★
0xE095, // ◎
0xE096, // ○
0xE097, // □
0xE098, // △
0xE099, // ◇
0xE09A, // ♪
0xE09B, // ☀
0xE09C, // ☁
0xE09D, // ☂
0xE09E, // ☃
0xE09F, // Half Neutral
0xE0A0, // Half Happy
0xE0A1, // Half Sad
0xE0A2, // Half Angry
0xE0A3, // Half Right-up arrow
0xE0A4, // Half Right-down arrow
0xE0A5, // Half Zz
];
private static ReadOnlySpan<ushort> SpecialCharsGen8 =>
[
'…', // '\uE08D' -> '\u2026'
'♂', // '\uE08E' -> '\u2642'
'♀', // '\uE08F' -> '\u2640'
'♠', // '\uE090' -> '\u2660'
'♣', // '\uE091' -> '\u2663'
'♥', // '\uE092' -> '\u2665'
'♦', // '\uE093' -> '\u2666'
'★', // '\uE094' -> '\u2605'
'◎', // '\uE095' -> '\u25CE'
'○', // '\uE096' -> '\u25CB'
'□', // '\uE097' -> '\u25A1'
'△', // '\uE098' -> '\u25B3'
'◇', // '\uE099' -> '\u25C7'
'♪', // '\uE09A' -> '\u266A'
'☀', // '\uE09B' -> '\u2600'
'☁', // '\uE09C' -> '\u2601'
'☂', // '\uE09D' -> '\u2602'
'☃', // '\uE09E' -> '\u2603'
];
}