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); /// /// Centers the horizontally and vertically so that its center is the same as the 's center. /// /// /// 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); } /// /// Horizontally centers the to the 's horizontal center. /// 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() where T : Form => (T?)Application.OpenForms.Cast
().FirstOrDefault(form => form is T); public static T? FindFirstControlOfType(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(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(this Form parent) where T : Form { var form = FirstFormOfType(); if (form == null) return false; form.CenterToForm(parent); form.BringToFront(); return true; } #region Message Displays /// /// Displays a dialog showing the details of an error. /// /// User-friendly message about the error. /// Instance of the error's . /// The associated with the dialog. internal static DialogResult Error(string friendlyMessage, Exception exception) { System.Media.SystemSounds.Exclamation.Play(); return ErrorWindow.ShowErrorDialog(friendlyMessage, exception, true); } /// /// Displays a dialog showing the details of an error. /// /// User-friendly message about the error. /// The associated with the dialog. 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; } /// /// Gets the selected value of the input . If no value is selected, will return 0. /// /// ComboBox to retrieve value for. 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); } /// /// Initializes the to be bound to a provided list. /// /// Control to initialize binding public static void InitializeBinding(this ListControl control) { control.DisplayMember = nameof(ComboItem.Text); control.ValueMember = nameof(ComboItem.Value); } public static void SetValueClamped(this NumericUpDown nud, int value) => nud.Value = Math.Min(nud.Maximum, Math.Max(nud.Minimum, value)); public static void SetValueClamped(this NumericUpDown nud, uint value) => nud.Value = Math.Min(nud.Maximum, Math.Max(nud.Minimum, value)); public static void RemoveDropCB(object? sender, KeyEventArgs e) { if (sender == null) return; ((ComboBox)sender).DroppedDown = false; } /// /// Iterates the Control's child controls recursively to obtain all controls of the specified type. /// /// Type of control /// /// All children and subchildren contained by . public static IEnumerable GetAllControlsOfType(Control control) where T : Control { foreach (var c in control.Controls.Cast()) { if (c is T match) yield return match; foreach (var sub in GetAllControlsOfType(c)) yield return sub; } } /// /// Reads in custom extension types that allow the program to open more extensions. /// /// Extensions to add public static void AddSaveFileExtensions(IEnumerable 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 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; /// /// Opens a dialog to open a , file, or any other supported file. /// /// Misc extensions of files supported by the Save File. /// Output result path /// Result of whether or not a file is to be loaded from the output path. public static bool OpenSAVPKMDialog(IEnumerable 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; } /// /// Opens a dialog to save a file. /// /// file to be saved. /// Result of whether or not the file was saved. 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); } /// /// Opens a dialog to save a file. /// /// to be saved. /// Box the player will be greeted with when accessing the PC ingame. /// Result of whether or not the file was saved. 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; } } } /// /// Opens a dialog to save a file. /// /// to be saved. /// Game the gift originates from /// Result of whether or not the file was saved. 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; } /// /// Gets the File Dialog filter for a Mystery Gift I/O operation. /// /// Format specifier for the /// Game the format originated from/to 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|*.*"; } }