PKHeX/PKHeX.WinForms/Subforms/SAV_Database.cs
Kurt cd19d1d700 Fix folder list backup fetch
Introduces an option to disable reading .bak / "backup" files; trickle the changes up to all surface users so that SAV_FolderList can correctly load all .bak saves into its Backup tab.

The "ignore backups" was added in the latest feature push to ignore "backup" from being detected as the latest save.
2022-02-05 09:49:47 -08:00

706 lines
26 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using PKHeX.Core;
using PKHeX.Core.Searching;
using PKHeX.Drawing.PokeSprite;
using PKHeX.WinForms.Controls;
using PKHeX.WinForms.Properties;
using static PKHeX.Core.MessageStrings;
namespace PKHeX.WinForms
{
public partial class SAV_Database : Form
{
private readonly SaveFile SAV;
private readonly SAVEditor BoxView;
private readonly PKMEditor PKME_Tabs;
public SAV_Database(PKMEditor f1, SAVEditor saveditor)
{
InitializeComponent();
WinFormsUtil.TranslateInterface(this, Main.CurrentLanguage);
SAV = saveditor.SAV;
BoxView = saveditor;
PKME_Tabs = f1;
// Preset Filters to only show PKM available for loaded save
CB_FormatComparator.SelectedIndex = 3; // <=
var grid = DatabasePokeGrid;
var smallWidth = grid.Width;
var smallHeight = grid.Height;
grid.InitializeGrid(6, 11, SpriteUtil.Spriter);
grid.SetBackground(Resources.box_wp_clean);
var newWidth = grid.Width;
var newHeight = grid.Height;
var wdelta = newWidth - smallWidth;
if (wdelta != 0)
Width += wdelta;
var hdelta = newHeight - smallHeight;
if (hdelta != 0)
Height += hdelta;
PKXBOXES = grid.Entries.ToArray();
// Enable Scrolling when hovered over
foreach (var slot in PKXBOXES)
{
// Enable Click
slot.MouseClick += (sender, e) =>
{
if (sender == null)
return;
switch (ModifierKeys)
{
case Keys.Control: ClickView(sender, e); break;
case Keys.Alt: ClickDelete(sender, e); break;
case Keys.Shift: ClickSet(sender, e); break;
}
};
slot.ContextMenuStrip = mnu;
if (Main.Settings.Hover.HoverSlotShowText)
slot.MouseEnter += (o, args) => ShowHoverTextForSlot(slot, args);
}
Counter = L_Count.Text;
Viewed = L_Viewed.Text;
L_Viewed.Text = string.Empty; // invisible for now
PopulateComboBoxes();
// Load Data
B_Search.Enabled = false;
L_Count.Text = "Loading...";
var task = new Task(LoadDatabase);
task.ContinueWith(z =>
{
if (!z.IsFaulted)
return;
Invoke((MethodInvoker)(() => L_Count.Text = "Failed."));
if (z.Exception == null)
return;
WinFormsUtil.Error("Loading database failed.", z.Exception.InnerException ?? new Exception(z.Exception.Message));
});
task.Start();
Menu_SearchSettings.DropDown.Closing += (sender, e) =>
{
if (e.CloseReason == ToolStripDropDownCloseReason.ItemClicked)
e.Cancel = true;
};
CB_Format.Items[0] = MsgAny;
CenterToParent();
}
private readonly PictureBox[] PKXBOXES;
private readonly string DatabasePath = Main.DatabasePath;
private List<SlotCache> Results = new();
private List<SlotCache> RawDB = new();
private int slotSelected = -1; // = null;
private Image? slotColor;
private const int RES_MAX = 66;
private const int RES_MIN = 6;
private readonly string Counter;
private readonly string Viewed;
private const int MAXFORMAT = PKX.Generation;
private readonly SummaryPreviewer ShowSet = new();
// Important Events
private void ClickView(object sender, EventArgs e)
{
var pb = WinFormsUtil.GetUnderlyingControl<PictureBox>(sender);
int index = Array.IndexOf(PKXBOXES, pb);
if (!GetShiftedIndex(ref index))
{
System.Media.SystemSounds.Exclamation.Play();
return;
}
if (sender == mnu)
mnu.Hide();
slotSelected = index;
slotColor = SpriteUtil.Spriter.View;
FillPKXBoxes(SCR_Box.Value);
L_Viewed.Text = string.Format(Viewed, Results[index].Identify());
PKME_Tabs.PopulateFields(Results[index].Entity, false);
}
private void ClickDelete(object sender, EventArgs e)
{
var pb = WinFormsUtil.GetUnderlyingControl<PictureBox>(sender);
int index = Array.IndexOf(PKXBOXES, pb);
if (!GetShiftedIndex(ref index))
{
System.Media.SystemSounds.Exclamation.Play();
return;
}
var entry = Results[index];
var pk = entry.Entity;
if (entry.Source is SlotInfoFile f)
{
// Data from Database: Delete file from disk
var path = f.Path;
if (File.Exists(path))
File.Delete(path);
}
else if (entry.Source is SlotInfoBox(var box, var slot) && entry.SAV == SAV)
{
// Data from Box: Delete from save file
var change = new SlotInfoBox(box, slot);
var pkSAV = change.Read(SAV);
if (!pkSAV.DecryptedBoxData.SequenceEqual(pk.DecryptedBoxData)) // data still exists in SAV, unmodified
{
WinFormsUtil.Error(MsgDBDeleteFailModified, MsgDBDeleteFailWarning);
return;
}
BoxView.EditEnv.Slots.Delete(change);
}
else
{
WinFormsUtil.Error(MsgDBDeleteFailBackup, MsgDBDeleteFailWarning);
return;
}
// Remove from database.
RawDB.Remove(entry);
Results.Remove(entry);
// Refresh database view.
L_Count.Text = string.Format(Counter, Results.Count);
slotSelected = -1;
FillPKXBoxes(SCR_Box.Value);
System.Media.SystemSounds.Asterisk.Play();
}
private void ClickSet(object sender, EventArgs e)
{
// Don't care what slot was clicked, just add it to the database
if (!PKME_Tabs.EditsComplete)
return;
PKM pk = PKME_Tabs.PreparePKM();
Directory.CreateDirectory(DatabasePath);
string path = Path.Combine(DatabasePath, Util.CleanFileName(pk.FileName));
if (File.Exists(path))
{
WinFormsUtil.Alert(MsgDBAddFailExistsFile);
return;
}
File.WriteAllBytes(path, pk.DecryptedBoxData);
var info = new SlotInfoFile(path);
var entry = new SlotCache(info, pk);
Results.Add(entry);
// Refresh database view.
L_Count.Text = string.Format(Counter, Results.Count);
slotSelected = Results.Count - 1;
slotColor = SpriteUtil.Spriter.Set;
if ((SCR_Box.Maximum+1)*6 < Results.Count)
SCR_Box.Maximum++;
SCR_Box.Value = Math.Max(0, SCR_Box.Maximum - (PKXBOXES.Length/6) + 1);
FillPKXBoxes(SCR_Box.Value);
WinFormsUtil.Alert(MsgDBAddFromTabsSuccess);
}
private bool GetShiftedIndex(ref int index)
{
if ((uint)index >= RES_MAX)
return false;
index += SCR_Box.Value * RES_MIN;
return index < Results.Count;
}
private void PopulateComboBoxes()
{
// Set the Text
CB_HeldItem.InitializeBinding();
CB_Species.InitializeBinding();
CB_Ability.InitializeBinding();
CB_Nature.InitializeBinding();
CB_GameOrigin.InitializeBinding();
CB_HPType.InitializeBinding();
var Any = new ComboItem(MsgAny, -1);
var DS_Species = new List<ComboItem>(GameInfo.SpeciesDataSource);
DS_Species.RemoveAt(0); DS_Species.Insert(0, Any); CB_Species.DataSource = DS_Species;
var DS_Item = new List<ComboItem>(GameInfo.ItemDataSource);
DS_Item.Insert(0, Any); CB_HeldItem.DataSource = DS_Item;
var DS_Nature = new List<ComboItem>(GameInfo.NatureDataSource);
DS_Nature.Insert(0, Any); CB_Nature.DataSource = DS_Nature;
var DS_Ability = new List<ComboItem>(GameInfo.AbilityDataSource);
DS_Ability.Insert(0, Any); CB_Ability.DataSource = DS_Ability;
var DS_Version = new List<ComboItem>(GameInfo.VersionDataSource);
DS_Version.Insert(0, Any); CB_GameOrigin.DataSource = DS_Version;
string[] hptypes = new string[GameInfo.Strings.types.Length - 2]; Array.Copy(GameInfo.Strings.types, 1, hptypes, 0, hptypes.Length);
var DS_Type = Util.GetCBList(hptypes);
DS_Type.Insert(0, Any); CB_HPType.DataSource = DS_Type;
// Set the Move ComboBoxes too..
var DS_Move = new List<ComboItem>(GameInfo.MoveDataSource);
DS_Move.RemoveAt(0); DS_Move.Insert(0, Any);
{
foreach (ComboBox cb in new[] { CB_Move1, CB_Move2, CB_Move3, CB_Move4 })
{
cb.InitializeBinding();
cb.DataSource = new BindingSource(DS_Move, null);
}
}
// Trigger a Reset
ResetFilters(this, EventArgs.Empty);
}
private void ResetFilters(object sender, EventArgs e)
{
CHK_Shiny.Checked = CHK_IsEgg.Checked = true;
CHK_Shiny.CheckState = CHK_IsEgg.CheckState = CheckState.Indeterminate;
MT_ESV.Text = string.Empty;
CB_HeldItem.SelectedIndex = 0;
CB_Species.SelectedIndex = 0;
CB_Ability.SelectedIndex = 0;
CB_Nature.SelectedIndex = 0;
CB_HPType.SelectedIndex = 0;
CB_Level.SelectedIndex = 0;
TB_Level.Text = string.Empty;
CB_EVTrain.SelectedIndex = 0;
CB_IV.SelectedIndex = 0;
CB_Move1.SelectedIndex = CB_Move2.SelectedIndex = CB_Move3.SelectedIndex = CB_Move4.SelectedIndex = 0;
CB_GameOrigin.SelectedIndex = 0;
CB_Generation.SelectedIndex = 0;
MT_ESV.Visible = L_ESV.Visible = false;
RTB_Instructions.Clear();
if (sender != this)
System.Media.SystemSounds.Asterisk.Play();
}
private void GenerateDBReport(object sender, EventArgs e)
{
if (WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDBCreateReportPrompt, MsgDBCreateReportWarning) != DialogResult.Yes)
return;
if (this.OpenWindowExists<ReportGrid>())
return;
ReportGrid reportGrid = new();
reportGrid.Show();
reportGrid.PopulateData(Results);
}
private sealed class SearchFolderDetail
{
public string Path { get; }
public bool IgnoreBackupFiles { get; }
public SearchFolderDetail(string path, bool ignoreBackupFiles)
{
Path = path;
IgnoreBackupFiles = ignoreBackupFiles;
}
}
private void LoadDatabase()
{
var settings = Main.Settings;
var otherPaths = new List<SearchFolderDetail>();
if (settings.EntityDb.SearchExtraSaves)
otherPaths.AddRange(settings.Backup.OtherBackupPaths.Where(Directory.Exists).Select(z => new SearchFolderDetail(z, true)));
if (settings.EntityDb.SearchBackups)
otherPaths.Add(new SearchFolderDetail(Main.BackupPath, false));
RawDB = LoadPKMSaves(DatabasePath, SAV, otherPaths, settings.EntityDb.SearchExtraSavesDeep);
// Load stats for pkm who do not have any
foreach (var entry in RawDB)
{
var pk = entry.Entity;
pk.ForcePartyData();
}
try
{
while (!IsHandleCreated) { }
BeginInvoke(new MethodInvoker(() => SetResults(RawDB)));
}
catch { /* Window Closed? */ }
}
private static List<SlotCache> LoadPKMSaves(string pkmdb, SaveFile SAV, List<SearchFolderDetail> otherPaths, bool otherDeep)
{
var dbTemp = new ConcurrentBag<SlotCache>();
var extensions = new HashSet<string>(PKM.Extensions.Select(z => $".{z}"));
var files = Directory.EnumerateFiles(pkmdb, "*", SearchOption.AllDirectories);
Parallel.ForEach(files, file => SlotInfoLoader.AddFromLocalFile(file, dbTemp, SAV, extensions));
foreach (var folder in otherPaths)
{
if (!SaveUtil.GetSavesFromFolder(folder.Path, otherDeep, out IEnumerable<string> paths, folder.IgnoreBackupFiles))
continue;
Parallel.ForEach(paths, file => TryAddPKMsFromSaveFilePath(dbTemp, file));
}
// Fetch from save file
SlotInfoLoader.AddFromSaveFile(SAV, dbTemp);
var result = new List<SlotCache>(dbTemp);
result.RemoveAll(z => !z.IsDataValid());
if (Main.Settings.EntityDb.FilterUnavailableSpecies)
{
static bool IsPresentInGameSWSH(ISpeciesForm pk) => pk is PK8 || ((PersonalInfoSWSH)PersonalTable.SWSH.GetFormEntry(pk.Species, pk.Form)).IsPresentInGame;
static bool IsPresentInGameBDSP(ISpeciesForm pk) => pk is PB8;//|| ((PersonalInfoBDSP)PersonalTable.BDSP.GetFormEntry(pk.Species, pk.Form)).IsPresentInGame;
static bool IsPresentInGamePLA (ISpeciesForm pk) => pk is PA8;//|| ((PersonalInfoLA)PersonalTable.LA.GetFormEntry(pk.Species, pk.Form)).IsPresentInGame;
if (SAV is SAV8SWSH)
result.RemoveAll(z => !IsPresentInGameSWSH(z.Entity));
else if (SAV is SAV8BS)
result.RemoveAll(z => !IsPresentInGameBDSP(z.Entity));
else if (SAV is SAV8LA)
result.RemoveAll(z => !IsPresentInGamePLA(z.Entity));
}
var sort = Main.Settings.EntityDb.InitialSortMode;
if (sort is DatabaseSortMode.SlotIdentity)
result.Sort();
else if (sort is DatabaseSortMode.SpeciesForm)
result.Sort((first, second) => first.CompareToSpeciesForm(second));
// Finalize the Database
return result;
}
private static void TryAddPKMsFromSaveFilePath(ConcurrentBag<SlotCache> dbTemp, string file)
{
var sav = SaveUtil.GetVariantSAV(file);
if (sav == null)
{
Debug.WriteLine("Unable to load SaveFile: " + file);
return;
}
SlotInfoLoader.AddFromSaveFile(sav, dbTemp);
}
// IO Usage
private void OpenDB(object sender, EventArgs e)
{
if (Directory.Exists(DatabasePath))
Process.Start("explorer.exe", DatabasePath);
}
private void Menu_Export_Click(object sender, EventArgs e)
{
if (Results.Count == 0)
{ WinFormsUtil.Alert(MsgDBCreateReportFail); return; }
if (DialogResult.Yes != WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDBExportResultsPrompt))
return;
using var fbd = new FolderBrowserDialog();
if (DialogResult.OK != fbd.ShowDialog())
return;
string path = fbd.SelectedPath;
Directory.CreateDirectory(path);
foreach (var pkm in Results.Select(z => z.Entity))
File.WriteAllBytes(Path.Combine(path, Util.CleanFileName(pkm.FileName)), pkm.DecryptedPartyData);
}
private void Menu_Import_Click(object sender, EventArgs e)
{
if (!BoxView.GetBulkImportSettings(out var clearAll, out var overwrite, out var noSetb))
return;
int box = BoxView.Box.CurrentBox;
int ctr = SAV.LoadBoxes(Results.Select(z => z.Entity), out var result, box, clearAll, overwrite, noSetb);
if (ctr <= 0)
return;
BoxView.SetPKMBoxes();
BoxView.UpdateBoxViewers();
WinFormsUtil.Alert(result);
}
// View Updates
private IEnumerable<SlotCache> SearchDatabase()
{
var settings = GetSearchSettings();
IEnumerable<SlotCache> res = RawDB;
// pre-filter based on the file path (if specified)
if (!Menu_SearchBoxes.Checked)
res = res.Where(z => z.SAV != SAV);
if (!Menu_SearchDatabase.Checked)
res = res.Where(z => !IsIndividualFilePKMDB(z));
if (!Menu_SearchBackups.Checked)
res = res.Where(z => !IsBackupSaveFile(z));
// return filtered results
return settings.Search(res);
}
private SearchSettings GetSearchSettings()
{
var settings = new SearchSettings
{
Format = MAXFORMAT - CB_Format.SelectedIndex + 1, // 0->(n-1) => 1->n
SearchFormat = (SearchComparison)CB_FormatComparator.SelectedIndex,
Generation = CB_Generation.SelectedIndex,
Version = WinFormsUtil.GetIndex(CB_GameOrigin),
HiddenPowerType = WinFormsUtil.GetIndex(CB_HPType),
Species = WinFormsUtil.GetIndex(CB_Species),
Ability = WinFormsUtil.GetIndex(CB_Ability),
Nature = WinFormsUtil.GetIndex(CB_Nature),
Item = WinFormsUtil.GetIndex(CB_HeldItem),
BatchInstructions = RTB_Instructions.Lines,
Level = int.TryParse(TB_Level.Text, out var lvl) ? lvl : null,
SearchLevel = (SearchComparison)CB_Level.SelectedIndex,
EVType = CB_EVTrain.SelectedIndex,
IVType = CB_IV.SelectedIndex,
};
settings.AddMove(WinFormsUtil.GetIndex(CB_Move1));
settings.AddMove(WinFormsUtil.GetIndex(CB_Move2));
settings.AddMove(WinFormsUtil.GetIndex(CB_Move3));
settings.AddMove(WinFormsUtil.GetIndex(CB_Move4));
if (CHK_Shiny.CheckState != CheckState.Indeterminate)
settings.SearchShiny = CHK_Shiny.CheckState == CheckState.Checked;
if (CHK_IsEgg.CheckState != CheckState.Indeterminate)
{
settings.SearchEgg = CHK_IsEgg.CheckState == CheckState.Checked;
if (int.TryParse(MT_ESV.Text, out int esv))
settings.ESV = esv;
}
if (Menu_SearchLegal.Checked != Menu_SearchIllegal.Checked)
settings.SearchLegal = Menu_SearchLegal.Checked;
if (Menu_SearchClones.Checked)
{
settings.SearchClones = ModifierKeys switch
{
Keys.Control => CloneDetectionMethod.HashPID,
_ => CloneDetectionMethod.HashDetails,
};
}
return settings;
}
private async void B_Search_Click(object sender, EventArgs e)
{
B_Search.Enabled = false;
var search = SearchDatabase();
bool legalSearch = Menu_SearchLegal.Checked ^ Menu_SearchIllegal.Checked;
bool wordFilter = ParseSettings.CheckWordFilter;
if (wordFilter && legalSearch && WinFormsUtil.Prompt(MessageBoxButtons.YesNo, MsgDBSearchLegalityWordfilter) == DialogResult.No)
ParseSettings.CheckWordFilter = false;
var results = await Task.Run(() => search.ToList()).ConfigureAwait(true);
ParseSettings.CheckWordFilter = wordFilter;
if (results.Count == 0)
{
if (!Menu_SearchBoxes.Checked && !Menu_SearchDatabase.Checked && !Menu_SearchBackups.Checked)
WinFormsUtil.Alert(MsgDBSearchFail, MsgDBSearchNone);
else
WinFormsUtil.Alert(MsgDBSearchNone);
}
SetResults(results); // updates Count Label as well.
System.Media.SystemSounds.Asterisk.Play();
B_Search.Enabled = true;
}
private void UpdateScroll(object sender, ScrollEventArgs e)
{
if (e.OldValue != e.NewValue)
FillPKXBoxes(e.NewValue);
}
private void SetResults(List<SlotCache> res)
{
Results = res;
ShowSet.Clear();
SCR_Box.Maximum = (int)Math.Ceiling((decimal)Results.Count / RES_MIN);
if (SCR_Box.Maximum > 0) SCR_Box.Maximum--;
slotSelected = -1; // reset the slot last viewed
SCR_Box.Value = 0;
FillPKXBoxes(0);
L_Count.Text = string.Format(Counter, Results.Count);
B_Search.Enabled = true;
}
private void FillPKXBoxes(int start)
{
if (Results.Count == 0)
{
for (int i = 0; i < RES_MAX; i++)
{
PKXBOXES[i].Image = null;
PKXBOXES[i].BackgroundImage = null;
}
return;
}
int begin = start*RES_MIN;
int end = Math.Min(RES_MAX, Results.Count - begin);
for (int i = 0; i < end; i++)
PKXBOXES[i].Image = Results[i + begin].Entity.Sprite(SAV, -1, -1, true);
for (int i = end; i < RES_MAX; i++)
PKXBOXES[i].Image = null;
for (int i = 0; i < RES_MAX; i++)
PKXBOXES[i].BackgroundImage = SpriteUtil.Spriter.Transparent;
if (slotSelected != -1 && slotSelected >= begin && slotSelected < begin + RES_MAX)
PKXBOXES[slotSelected - begin].BackgroundImage = slotColor ?? SpriteUtil.Spriter.View;
}
// Misc Update Methods
private void ToggleESV(object sender, EventArgs e)
{
L_ESV.Visible = MT_ESV.Visible = CHK_IsEgg.CheckState == CheckState.Checked;
}
private void ChangeLevel(object sender, EventArgs e)
{
if (CB_Level.SelectedIndex == 0)
TB_Level.Text = string.Empty;
}
private void ChangeGame(object sender, EventArgs e)
{
if (CB_GameOrigin.SelectedIndex != 0)
CB_Generation.SelectedIndex = 0;
}
private void ChangeGeneration(object sender, EventArgs e)
{
if (CB_Generation.SelectedIndex != 0)
CB_GameOrigin.SelectedIndex = 0;
}
private void Menu_Exit_Click(object sender, EventArgs e) => Close();
protected override void OnMouseWheel(MouseEventArgs e)
{
if (!DatabasePokeGrid.RectangleToScreen(DatabasePokeGrid.ClientRectangle).Contains(MousePosition))
return;
int oldval = SCR_Box.Value;
int newval = oldval + (e.Delta < 0 ? 1 : -1);
if (newval >= SCR_Box.Minimum && SCR_Box.Maximum >= newval)
FillPKXBoxes(SCR_Box.Value = newval);
}
private void ChangeFormatFilter(object sender, EventArgs e)
{
if (CB_FormatComparator.SelectedIndex == 0)
{
CB_Format.Visible = false; // !any
CB_Format.SelectedIndex = 0;
}
else
{
CB_Format.Visible = true;
int index = MAXFORMAT - SAV.Generation + 1;
CB_Format.SelectedIndex = index < CB_Format.Items.Count ? index : 0; // SAV generation (offset by 1 for "Any")
}
}
private void Menu_DeleteClones_Click(object sender, EventArgs e)
{
var dr = WinFormsUtil.Prompt(MessageBoxButtons.YesNo,
MsgDBDeleteCloneWarning + Environment.NewLine +
MsgDBDeleteCloneAdvice, MsgContinue);
if (dr != DialogResult.Yes)
return;
var deleted = 0;
var db = RawDB.Where(IsIndividualFilePKMDB)
.OrderByDescending(GetRevisedTime);
var hasher = SearchUtil.GetCloneDetectMethod(CloneDetectionMethod.HashDetails);
var duplicates = SearchUtil.GetExtraClones(db, z => hasher(z.Entity));
foreach (var entry in duplicates)
{
var src = entry.Source;
var path = ((SlotInfoFile)src).Path;
if (!File.Exists(path))
continue;
try { File.Delete(path); ++deleted; }
catch (Exception ex) { WinFormsUtil.Error(MsgDBDeleteCloneFail + Environment.NewLine + ex.Message + Environment.NewLine + path); }
}
if (deleted == 0)
{ WinFormsUtil.Alert(MsgDBDeleteCloneNone); return; }
WinFormsUtil.Alert(string.Format(MsgFileDeleteCount, deleted), MsgWindowClose);
Close();
}
private static DateTime GetRevisedTime(SlotCache arg)
{
var src = arg.Source;
if (src is not SlotInfoFile f)
return DateTime.Now;
return File.GetLastWriteTimeUtc(f.Path);
}
private bool IsBackupSaveFile(SlotCache pk)
{
return pk.SAV is not FakeSaveFile && pk.SAV != SAV;
}
private bool IsIndividualFilePKMDB(SlotCache pk)
{
return pk.Source is SlotInfoFile f && f.Path.StartsWith(DatabasePath + Path.DirectorySeparatorChar, StringComparison.Ordinal);
}
private void L_Viewed_MouseEnter(object sender, EventArgs e) => hover.SetToolTip(L_Viewed, L_Viewed.Text);
private void ShowHoverTextForSlot(object sender, EventArgs e)
{
var pb = (PictureBox)sender;
int index = Array.IndexOf(PKXBOXES, pb);
if (!GetShiftedIndex(ref index))
return;
ShowSet.Show(pb, Results[index].Entity);
}
}
}