PKHeX/PKHeX.WinForms/Util/WinFormsUtil.cs
Kurt 41578132cf Minor tweaks
Add Get/Set Relearn method to PKM.cs
Alpha Mastered move now indicates incomplete text entry (like moves/relearn)
Split up legal move indication helper class, use DI to allow other implementations of DataSource to be returned (pkhex mobile?).
Remove unused gender refresh method (see previous added UserControl commit)
Add helper method to center control within control
2022-05-02 18:11:31 -07:00

456 lines
17 KiB
C#

using PKHeX.Core;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using static PKHeX.Core.MessageStrings;
using Exception = System.Exception;
namespace PKHeX.WinForms
{
public static class WinFormsUtil
{
internal static void TranslateInterface(Control form, string lang) => form.TranslateInterface(lang);
/// <summary>
/// Centers the <see cref="child"/> horizontally and vertically so that its center is the same as the <see cref="parent"/>'s center.
/// </summary>
/// <param name="child"></param>
/// <param name="parent"></param>
internal static void CenterToForm(this Control child, Control? parent)
{
if (parent == null)
return;
int x = parent.Location.X + ((parent.Width - child.Width) / 2);
int y = parent.Location.Y + ((parent.Height - child.Height) / 2);
child.Location = new Point(Math.Max(x, 0), Math.Max(y, 0));
}
internal static void CenterWithin(this Control child, Control parent)
{
child.Location = new Point((parent.Width - child.Width) / 2, child.Location.Y);
}
/// <summary>
/// Horizontally centers the <see cref="child"/> to the <see cref="parent"/>'s horizontal center.
/// </summary>
internal static void HorizontallyCenter(this Control child, Control parent)
{
int x = ((parent.Width - child.Width) / 2);
child.Location = new Point(x, child.Location.Y);
}
public static T? FirstFormOfType<T>() where T : Form => (T?)Application.OpenForms.Cast<Form>().FirstOrDefault(form => form is T);
public static T? FindFirstControlOfType<T>(Control aParent) where T : class
{
while (true)
{
if (aParent is T t)
return t;
if (aParent.Parent != null)
aParent = aParent.Parent;
else
return null;
}
}
public static T? GetUnderlyingControl<T>(object sender) where T : class
{
while (true)
{
switch (sender)
{
case T p:
return p;
case ToolStripItem t:
sender = t.Owner;
continue;
case ContextMenuStrip c:
sender = c.SourceControl;
continue;
default:
return default;
}
}
}
public static bool OpenWindowExists<T>(this Form parent) where T : Form
{
var form = FirstFormOfType<T>();
if (form == null)
return false;
form.CenterToForm(parent);
form.BringToFront();
return true;
}
#region Message Displays
/// <summary>
/// Displays a dialog showing the details of an error.
/// </summary>
/// <param name="friendlyMessage">User-friendly message about the error.</param>
/// <param name="exception">Instance of the error's <see cref="Exception"/>.</param>
/// <returns>The <see cref="DialogResult"/> associated with the dialog.</returns>
internal static DialogResult Error(string friendlyMessage, Exception exception)
{
System.Media.SystemSounds.Exclamation.Play();
return ErrorWindow.ShowErrorDialog(friendlyMessage, exception, true);
}
/// <summary>
/// Displays a dialog showing the details of an error.
/// </summary>
/// <param name="lines">User-friendly message about the error.</param>
/// <returns>The <see cref="DialogResult"/> associated with the dialog.</returns>
internal static DialogResult Error(params string[] lines)
{
System.Media.SystemSounds.Hand.Play();
string msg = string.Join(Environment.NewLine + Environment.NewLine, lines);
return MessageBox.Show(msg, "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
internal static DialogResult Alert(params string[] lines) => Alert(true, lines);
internal static DialogResult Alert(bool sound, params string[] lines)
{
if (sound)
System.Media.SystemSounds.Asterisk.Play();
string msg = string.Join(Environment.NewLine + Environment.NewLine, lines);
return MessageBox.Show(msg, "Alert", MessageBoxButtons.OK, sound ? MessageBoxIcon.Information : MessageBoxIcon.None);
}
internal static DialogResult Prompt(MessageBoxButtons btn, params string[] lines)
{
System.Media.SystemSounds.Asterisk.Play();
string msg = string.Join(Environment.NewLine + Environment.NewLine, lines);
return MessageBox.Show(msg, "Prompt", btn, MessageBoxIcon.Question);
}
#endregion
internal static bool SetClipboardText(string text)
{
try
{
Clipboard.SetText(text);
return true;
}
catch (ExternalException x)
{
Error(MsgClipboardFailWrite, x);
}
// Clipboard might be locked sometimes
catch
{
Error(MsgClipboardFailWrite);
}
return false;
}
/// <summary>
/// Gets the selected value of the input <see cref="cb"/>. If no value is selected, will return 0.
/// </summary>
/// <param name="cb">ComboBox to retrieve value for.</param>
internal static int GetIndex(ListControl cb)
{
return (int)(cb.SelectedValue ?? 0);
}
public static void PanelScroll(object? sender, ScrollEventArgs e)
{
if (sender is not ScrollableControl p || e.NewValue < 0)
return;
switch (e.ScrollOrientation)
{
case ScrollOrientation.HorizontalScroll:
p.HorizontalScroll.Value = Clamp(e.NewValue, p.HorizontalScroll);
break;
case ScrollOrientation.VerticalScroll:
p.VerticalScroll.Value = Clamp(e.NewValue, p.VerticalScroll);
break;
default:
throw new IndexOutOfRangeException(nameof(e.ScrollOrientation));
}
static int Clamp(int value, ScrollProperties prop) => Math.Max(prop.Minimum, Math.Min(prop.Maximum, value));
}
public static void DoubleBuffered(this DataGridView dgv, bool setting)
{
Type dgvType = dgv.GetType();
var pi = dgvType.GetProperty("DoubleBuffered", BindingFlags.Instance | BindingFlags.NonPublic);
if (pi == null)
throw new Exception(nameof(dgv));
pi.SetValue(dgv, setting, null);
}
/// <summary>
/// Initializes the <see cref="control"/> to be bound to a provided <see cref="ComboItem"/> list.
/// </summary>
/// <param name="control">Control to initialize binding</param>
public static void InitializeBinding(this ListControl control)
{
control.DisplayMember = nameof(ComboItem.Text);
control.ValueMember = nameof(ComboItem.Value);
}
public static void RemoveDropCB(object? sender, KeyEventArgs e)
{
if (sender == null)
return;
((ComboBox)sender).DroppedDown = false;
}
/// <summary>
/// Iterates the Control's child controls recursively to obtain all controls of the specified type.
/// </summary>
/// <typeparam name="T">Type of control</typeparam>
/// <param name="control"></param>
/// <returns>All children and subchildren contained by <see cref="control"/>.</returns>
public static IEnumerable<Control> GetAllControlsOfType<T>(Control control) where T : Control
{
foreach (var c in control.Controls.Cast<Control>())
{
if (c is T match)
yield return match;
foreach (var sub in GetAllControlsOfType<T>(c))
yield return sub;
}
}
/// <summary>
/// Reads in custom extension types that allow the program to open more extensions.
/// </summary>
/// <param name="exts">Extensions to add</param>
public static void AddSaveFileExtensions(IEnumerable<string> exts)
{
// Only add new (unique) extensions
var dest = CustomSaveExtensions;
foreach (var ext in exts)
{
if (!dest.Contains(ext))
dest.Add(ext);
}
}
private static readonly List<string> CustomSaveExtensions = new()
{
// THESE ARE SAVE FILE EXTENSION TYPES. SAVE STATE (RAM SNAPSHOT) EXTENSIONS DO NOT GO HERE.
"sav", // standard
"dat", // VC data
"gci", // Dolphin GameCubeImage
"dsv", // DeSmuME
"srm", // RetroArch save files
"fla", // flashcard
"SaveRAM", // BizHawk
};
public static bool IsFileExtensionSAV(string file) => CustomSaveExtensions.Contains(Path.GetExtension(file));
private static string ExtraSaveExtensions => ";" + string.Join(";", CustomSaveExtensions.Select(z => $"*.{z}"));
public static bool DetectSaveFileOnFileOpen { private get; set; } = true;
/// <summary>
/// Opens a dialog to open a <see cref="SaveFile"/>, <see cref="PKM"/> file, or any other supported file.
/// </summary>
/// <param name="extensions">Misc extensions of <see cref="PKM"/> files supported by the Save File.</param>
/// <param name="path">Output result path</param>
/// <returns>Result of whether or not a file is to be loaded from the output path.</returns>
public static bool OpenSAVPKMDialog(IEnumerable<string> extensions, out string? path)
{
string supported = string.Join(";", extensions.Select(s => $"*.{s}").Concat(new[] { "*.pkm" }));
using var ofd = new OpenFileDialog
{
Filter = "All Files|*.*" +
$"|Supported Files (*.*)|main;*.bin;{supported};*.bak" + ExtraSaveExtensions +
"|Save Files (*.sav)|main" + ExtraSaveExtensions +
"|Decrypted PKM File (*.pkm)|" + supported +
"|Binary File|*.bin" +
"|Backup File|*.bak",
};
// Detect main
SaveFile? sav = null;
if (DetectSaveFileOnFileOpen)
{
try
{
sav = SaveFinder.FindMostRecentSaveFile();
}
catch (Exception ex)
{
Error(ex.Message);
}
}
if (sav != null)
ofd.FileName = sav.Metadata.FileName;
if (ofd.ShowDialog() != DialogResult.OK)
{
path = null;
return false;
}
path = ofd.FileName;
return true;
}
/// <summary>
/// Opens a dialog to save a <see cref="PKM"/> file.
/// </summary>
/// <param name="pk"><see cref="PKM"/> file to be saved.</param>
/// <returns>Result of whether or not the file was saved.</returns>
public static bool SavePKMDialog(PKM pk)
{
string pkx = pk.Extension;
bool allowEncrypted = pk.Format >= 3 && pkx[0] == 'p';
var genericFilter = $"Decrypted PKM File|*.{pkx}" +
(allowEncrypted ? $"|Encrypted PKM File|*.e{pkx[1..]}" : string.Empty) +
"|Binary File|*.bin" +
"|All Files|*.*";
using var sfd = new SaveFileDialog
{
Filter = genericFilter,
DefaultExt = pkx,
FileName = Util.CleanFileName(pk.FileName),
};
if (sfd.ShowDialog() != DialogResult.OK)
return false;
SavePKM(pk, sfd.FileName, pkx);
return true;
}
private static void SavePKM(PKM pk, string path, string pkx)
{
SaveBackup(path);
string ext = Path.GetExtension(path);
var data = $".{pkx}" == ext ? pk.DecryptedPartyData : pk.EncryptedPartyData;
File.WriteAllBytes(path, data);
}
private static void SaveBackup(string path)
{
if (!File.Exists(path))
return;
// File already exists, save a .bak
string bakpath = $"{path}.bak";
if (!File.Exists(bakpath))
File.Move(path, bakpath);
}
/// <summary>
/// Opens a dialog to save a <see cref="SaveFile"/> file.
/// </summary>
/// <param name="sav"><see cref="SaveFile"/> to be saved.</param>
/// <param name="currentBox">Box the player will be greeted with when accessing the PC ingame.</param>
/// <returns>Result of whether or not the file was saved.</returns>
public static bool ExportSAVDialog(SaveFile sav, int currentBox = 0)
{
using var sfd = new SaveFileDialog
{
Filter = sav.Metadata.Filter,
FileName = sav.Metadata.FileName,
FilterIndex = 1000, // default to last, All Files
RestoreDirectory = true,
};
if (Directory.Exists(sav.Metadata.FileFolder))
sfd.InitialDirectory = sav.Metadata.FileFolder;
if (sfd.ShowDialog() != DialogResult.OK)
return false;
// Set box now that we're saving
if (sav.HasBox)
sav.CurrentBox = currentBox;
var path = sfd.FileName;
if (path == null)
throw new NullReferenceException(nameof(sfd.FileName));
ExportSAV(sav, path);
return true;
}
private static void ExportSAV(SaveFile sav, string path)
{
var ext = Path.GetExtension(path).ToLowerInvariant();
var flags = sav.Metadata.GetSuggestedFlags(ext);
try
{
File.WriteAllBytes(path, sav.Write(flags));
sav.State.Edited = false;
sav.Metadata.SetExtraInfo(path);
Alert(MsgSaveExportSuccessPath, path);
}
catch (Exception x)
{
switch (x)
{
case UnauthorizedAccessException:
case FileNotFoundException:
case IOException:
Error(MsgFileWriteFail + Environment.NewLine + x.Message, MsgFileWriteProtectedAdvice);
break;
default:
throw;
}
}
}
/// <summary>
/// Opens a dialog to save a <see cref="MysteryGift"/> file.
/// </summary>
/// <param name="gift"><see cref="MysteryGift"/> to be saved.</param>
/// <param name="origin">Game the gift originates from</param>
/// <returns>Result of whether or not the file was saved.</returns>
public static bool ExportMGDialog(DataMysteryGift gift, GameVersion origin)
{
using var sfd = new SaveFileDialog
{
Filter = GetMysterGiftFilter(gift.Generation, origin),
FileName = Util.CleanFileName(gift.FileName),
};
if (sfd.ShowDialog() != DialogResult.OK)
return false;
string path = sfd.FileName;
SaveBackup(path);
File.WriteAllBytes(path, gift.Write());
return true;
}
/// <summary>
/// Gets the File Dialog filter for a Mystery Gift I/O operation.
/// </summary>
/// <param name="format">Format specifier for the </param>
/// <param name="origin">Game the format originated from/to</param>
public static string GetMysterGiftFilter(int format, GameVersion origin) => format switch
{
4 => "Gen4 Mystery Gift|*.pgt;*.pcd;*.wc4" + all,
5 => "Gen5 Mystery Gift|*.pgf" + all,
6 => "Gen6 Mystery Gift|*.wc6;*.wc6full" + all,
7 => GameVersion.GG.Contains(origin)
? "Beluga Gift Record|*.wr7" + all
: "Gen7 Mystery Gift|*.wc7;*.wc7full" + all,
8 when GameVersion.BDSP.Contains(origin) => "BD/SP Gift|*.wb8" + all,
8 when GameVersion.PLA.Contains(origin) => "Legends: Arceus Gift|*.wa8" + all,
8 => "Gen8 Mystery Gift|*.wc8" + all,
_ => string.Empty,
};
private const string all = "|All Files|*.*";
}
}