mirror of
https://github.com/kwsch/PKHeX.git
synced 2026-03-21 17:48:28 -05:00
Handle handle leaks on dragdrop cursor icon
Also pass the tooltips to the components container so that they dispose of anything if needed too. A user had a long-running script/session that drag-dropped a few thousand times, which exhausted the Windows GDI handle limit (10,000 per process).
This commit is contained in:
parent
c7f02bcc20
commit
5b42ff746d
|
|
@ -136,7 +136,7 @@ public int GetAverageColor()
|
|||
}
|
||||
}
|
||||
|
||||
private static Span<byte> GetSpan(IntPtr ptr, int length)
|
||||
private static Span<byte> GetSpan(nint ptr, int length)
|
||||
=> MemoryMarshal.CreateSpan(ref Unsafe.AddByteOffset(ref Unsafe.NullRef<byte>(), ptr), length);
|
||||
|
||||
public static Bitmap LayerImage(Bitmap baseLayer, Bitmap overLayer, int x, int y, double transparency)
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ public SAVEditor()
|
|||
|
||||
SL_Extra.ViewIndex = -2;
|
||||
menu = new ContextMenuSAV { Manager = M };
|
||||
components!.Add(menu);
|
||||
InitializeEvents();
|
||||
}
|
||||
|
||||
|
|
@ -1497,7 +1498,8 @@ private async void TabMouseMove(object sender, MouseEventArgs e)
|
|||
{
|
||||
using var img = new Bitmap(Box.Width, Box.Height);
|
||||
Box.DrawToBitmap(img, new Rectangle(0, 0, Box.Width, Box.Height));
|
||||
using var cursor = Cursor = new Cursor(img.GetHicon());
|
||||
using var dragCursor = new BitmapCursor(img);
|
||||
Cursor = dragCursor.Cursor;
|
||||
await File.WriteAllBytesAsync(newFile, bin).ConfigureAwait(true);
|
||||
DoDragDrop(new DataObject(DataFormats.FileDrop, new[] { newFile }), DragDropEffects.Copy);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public void QueryContinueDrag(object? sender, QueryContinueDragEventArgs e)
|
|||
|
||||
public void DragEnter(object? sender, DragEventArgs e)
|
||||
{
|
||||
if (sender is null)
|
||||
if (sender is not Control c)
|
||||
return;
|
||||
if ((e.AllowedEffect & DragDropEffects.Copy) != 0) // external file
|
||||
e.Effect = DragDropEffects.Copy;
|
||||
|
|
@ -93,7 +93,7 @@ public void DragEnter(object? sender, DragEventArgs e)
|
|||
e.Effect = DragDropEffects.Move;
|
||||
|
||||
if (Drag.Info.IsDragDropInProgress)
|
||||
Drag.SetCursor(((Control)sender).FindForm(), Drag.Info.Cursor);
|
||||
Drag.SetCursor(c, Drag.Info.Cursor);
|
||||
}
|
||||
|
||||
private static SlotViewInfo<T> GetSlotInfo<T>(T pb) where T : Control
|
||||
|
|
@ -156,7 +156,7 @@ private void HandleMovePKM(PictureBox pb, bool encrypt)
|
|||
// drop finished, clean up
|
||||
Drag.Info.Source = null;
|
||||
Drag.Reset();
|
||||
Drag.ResetCursor(pb.FindForm());
|
||||
Drag.ResetCursor(pb);
|
||||
|
||||
// Browser apps need time to load data since the file isn't moved to a location on the user's local storage.
|
||||
// Tested 10ms -> too quick, 100ms was fine. 500ms should be safe?
|
||||
|
|
@ -210,7 +210,7 @@ private bool TryMakeDragDropPKM(PictureBox pb, ReadOnlySpan<byte> data, string n
|
|||
ArgumentNullException.ThrowIfNull(img);
|
||||
File.WriteAllBytes(newfile, data);
|
||||
|
||||
Drag.SetCursor(pb.FindForm(), new Cursor(img.GetHicon()));
|
||||
Drag.SetOwnedCursor(pb, img);
|
||||
Hover.Stop();
|
||||
pb.Image = null;
|
||||
pb.BackgroundImage = SpriteUtil.Spriter.Drag;
|
||||
|
|
@ -223,7 +223,7 @@ private bool TryMakeDragDropPKM(PictureBox pb, ReadOnlySpan<byte> data, string n
|
|||
{
|
||||
pb.Image = img;
|
||||
pb.BackgroundImage = LastSlot.OriginalBackground;
|
||||
Drag.ResetCursor(pb.FindForm());
|
||||
Drag.ResetCursor(pb);
|
||||
return external;
|
||||
}
|
||||
|
||||
|
|
@ -347,7 +347,7 @@ private bool TrySetPKMDestination(PictureBox pb, DropModifier mod)
|
|||
// Copy from temp to destination slot.
|
||||
var type = info.IsDragSwap ? SlotTouchType.Swap : SlotTouchType.Set;
|
||||
Env.Slots.Set(info.Destination!.Slot, pk, type);
|
||||
Drag.ResetCursor(pb.FindForm());
|
||||
Drag.ResetCursor(pb);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -381,6 +381,7 @@ public void SwapBoxes(int index, int other, SaveFile SAV)
|
|||
|
||||
public void Dispose()
|
||||
{
|
||||
Drag.Dispose();
|
||||
Hover.Dispose();
|
||||
SE.Dispose();
|
||||
LastSlot.OriginalBackground?.Dispose();
|
||||
|
|
|
|||
38
PKHeX.WinForms/Controls/Slots/BitmapCursor.cs
Normal file
38
PKHeX.WinForms/Controls/Slots/BitmapCursor.cs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows.Forms;
|
||||
|
||||
namespace PKHeX.WinForms.Controls;
|
||||
|
||||
/// <summary>
|
||||
/// Managed <see cref="Cursor"/> implementation that acquires the cursor appearance from a <see cref="Bitmap"/> and properly disposes of the underlying icon handle when done.
|
||||
/// </summary>
|
||||
public sealed class BitmapCursor : IDisposable
|
||||
{
|
||||
public Cursor Cursor { get; }
|
||||
private nint IconHandle;
|
||||
|
||||
public BitmapCursor(Bitmap bitmap)
|
||||
{
|
||||
IconHandle = bitmap.GetHicon(); // creates a handle we need to dispose of.
|
||||
Cursor = new Cursor(IconHandle);
|
||||
}
|
||||
|
||||
// Need to use DestroyIcon as Cursor does not take ownership of the icon handle and will not destroy it when disposed.
|
||||
|
||||
#pragma warning disable SYSLIB1054
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool DestroyIcon(nint hIcon);
|
||||
#pragma warning restore SYSLIB1054
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Cursor.Dispose();
|
||||
if (IconHandle == nint.Zero)
|
||||
return;
|
||||
_ = DestroyIcon(IconHandle);
|
||||
IconHandle = nint.Zero;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using System;
|
||||
using System.Drawing;
|
||||
using System.Windows.Forms;
|
||||
|
||||
|
|
@ -6,7 +7,7 @@ namespace PKHeX.WinForms.Controls;
|
|||
/// <summary>
|
||||
/// Manages drag-and-drop operations for slot controls.
|
||||
/// </summary>
|
||||
public sealed class DragManager
|
||||
public sealed class DragManager : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the current slot change information for drag-and-drop operations.
|
||||
|
|
@ -17,6 +18,7 @@ public sealed class DragManager
|
|||
/// Occurs when an external drag-and-drop operation is requested.
|
||||
/// </summary>
|
||||
public event DragEventHandler? RequestExternalDragDrop;
|
||||
private BitmapCursor? OwnedCursor;
|
||||
|
||||
/// <summary>
|
||||
/// Requests a drag-and-drop operation.
|
||||
|
|
@ -30,17 +32,26 @@ public sealed class DragManager
|
|||
/// </summary>
|
||||
/// <param name="f">The form to set the cursor for.</param>
|
||||
/// <param name="z">The cursor to set.</param>
|
||||
public void SetCursor(Form? f, Cursor? z)
|
||||
public void SetCursor(Control f, Cursor? z)
|
||||
{
|
||||
f?.Cursor = z;
|
||||
if (OwnedCursor is not null && !ReferenceEquals(OwnedCursor.Cursor, z))
|
||||
DisposeOwnedCursor();
|
||||
f.Cursor = z;
|
||||
Info.Cursor = z;
|
||||
}
|
||||
|
||||
public void SetOwnedCursor(Control f, Bitmap bitmap)
|
||||
{
|
||||
DisposeOwnedCursor();
|
||||
OwnedCursor = new BitmapCursor(bitmap);
|
||||
SetCursor(f, OwnedCursor.Cursor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the cursor for the specified form to the default cursor.
|
||||
/// </summary>
|
||||
/// <param name="sender">The form to reset the cursor for.</param>
|
||||
public void ResetCursor(Form? sender)
|
||||
public void ResetCursor(Control sender)
|
||||
{
|
||||
SetCursor(sender, Cursors.Default);
|
||||
}
|
||||
|
|
@ -50,13 +61,28 @@ public void ResetCursor(Form? sender)
|
|||
/// </summary>
|
||||
public void Initialize()
|
||||
{
|
||||
DisposeOwnedCursor();
|
||||
Info = new SlotChangeInfo<Cursor, PictureBox>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the drag manager's slot change info.
|
||||
/// </summary>
|
||||
public void Reset() => Info.Reset();
|
||||
public void Reset()
|
||||
{
|
||||
DisposeOwnedCursor();
|
||||
Info.Reset();
|
||||
}
|
||||
|
||||
private void DisposeOwnedCursor()
|
||||
{
|
||||
if (OwnedCursor is null)
|
||||
return;
|
||||
OwnedCursor.Dispose();
|
||||
OwnedCursor = null;
|
||||
}
|
||||
|
||||
public void Dispose() => DisposeOwnedCursor();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the mouse down position for drag detection.
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ private void FormLoadAddEvents()
|
|||
mnu.RequestEditorSaveAs += MainMenuSave;
|
||||
dragout.ContextMenuStrip = mnu.mnuL;
|
||||
C_SAV.menu.RequestEditorLegality = DisplayLegalityReport;
|
||||
components.Add(mnu);
|
||||
}
|
||||
|
||||
public void LoadInitialFiles(StartupArguments args)
|
||||
|
|
@ -1239,7 +1240,7 @@ private async void Dragout_MouseDown(object sender, MouseEventArgs e)
|
|||
|
||||
var pb = (PictureBox)sender;
|
||||
if (pb.Image is Bitmap img)
|
||||
C_SAV.M.Drag.Info.Cursor = Cursor = new Cursor(img.GetHicon());
|
||||
C_SAV.M.Drag.SetOwnedCursor(pb, img);
|
||||
|
||||
DoDragDrop(new DataObject(DataFormats.FileDrop, new[] { newfile }), DragDropEffects.Copy);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ protected override void Dispose(bool disposing)
|
|||
/// </summary>
|
||||
private void InitializeComponent()
|
||||
{
|
||||
components = new System.ComponentModel.Container();
|
||||
Tip = new System.Windows.Forms.ToolTip(components);
|
||||
TB_Text = new System.Windows.Forms.TextBox();
|
||||
CB_Species = new System.Windows.Forms.ComboBox();
|
||||
B_Cancel = new System.Windows.Forms.Button();
|
||||
|
|
@ -47,6 +49,12 @@ private void InitializeComponent()
|
|||
((System.ComponentModel.ISupportInitialize)NUD_Generation).BeginInit();
|
||||
SuspendLayout();
|
||||
//
|
||||
// Tip
|
||||
//
|
||||
Tip.AutoPopDelay = 32767;
|
||||
Tip.InitialDelay = 200;
|
||||
Tip.IsBalloon = false;
|
||||
//
|
||||
// TB_Text
|
||||
//
|
||||
TB_Text.Location = new System.Drawing.Point(114, 14);
|
||||
|
|
@ -262,5 +270,6 @@ private void InitializeComponent()
|
|||
private System.Windows.Forms.Label L_Generation;
|
||||
private System.Windows.Forms.Button B_ClearTrash;
|
||||
private System.Windows.Forms.Label L_String;
|
||||
private System.Windows.Forms.ToolTip Tip;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ namespace PKHeX.WinForms;
|
|||
public partial class TrashEditor : Form
|
||||
{
|
||||
private readonly IStringConverter Converter;
|
||||
private readonly ToolTip Tip = new() { InitialDelay = 200, IsBalloon = false, AutoPopDelay = 32_767 };
|
||||
private readonly List<NumericUpDown> Bytes = [];
|
||||
public string FinalString { get; private set; }
|
||||
public byte[] FinalBytes { get; private set; }
|
||||
|
|
@ -93,8 +92,7 @@ private void AddCharEditing(Font f, EntityContext context)
|
|||
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);
|
||||
var tt = new ToolTip();
|
||||
tt.SetToolTip(l, $"Insert {l.Text} (0x{c:X4})");
|
||||
Tip.SetToolTip(l, $"Insert {l.Text} (0x{c:X4})");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ private void GetContextMenu()
|
|||
ContextMenuStrip mnu = new();
|
||||
mnu.Items.Add(mnuHide);
|
||||
mnu.Items.Add(mnuRestore);
|
||||
components ??= new System.ComponentModel.Container();
|
||||
components.Add(mnu);
|
||||
|
||||
dgData.ContextMenuStrip = mnu;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ public SAV_FolderList(Action<SaveFile> openSaveFile)
|
|||
var drives = Environment.GetLogicalDrives();
|
||||
Paths = GetPathList(drives, backups);
|
||||
|
||||
components ??= new System.ComponentModel.Container();
|
||||
dgDataRecent.ContextMenuStrip = GetContextMenu(dgDataRecent);
|
||||
dgDataBackup.ContextMenuStrip = GetContextMenu(dgDataBackup);
|
||||
dgDataRecent.Sorted += (_, _) => GetFilterText(dgDataRecent);
|
||||
|
|
@ -111,7 +112,7 @@ private void AddButton(string name, string path)
|
|||
};
|
||||
FLP_Buttons.Controls.Add(button);
|
||||
|
||||
var hover = new ToolTip {AutoPopDelay = 30_000};
|
||||
var hover = new ToolTip(components) {AutoPopDelay = 30_000};
|
||||
button.MouseHover += (_, _) => hover.Show(path, button);
|
||||
}
|
||||
|
||||
|
|
@ -202,6 +203,7 @@ private ContextMenuStrip GetContextMenu(DataGridView dgv)
|
|||
mnu.Items.Add(mnuOpen);
|
||||
mnu.Items.Add(mnuBrowseAt);
|
||||
mnu.Items.Add(mnuDelete);
|
||||
components.Add(mnu);
|
||||
return mnu;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -290,7 +290,7 @@ private static void ApplyWorkFilter(TableLayoutPanel panel, IReadOnlyList<(strin
|
|||
panel.ResumeLayout();
|
||||
}
|
||||
|
||||
private TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
private static TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
{
|
||||
var box = new TextBox
|
||||
{
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ private static void ApplyConstFilter(TableLayoutPanel panel, IReadOnlyList<(stri
|
|||
panel.ResumeLayout();
|
||||
}
|
||||
|
||||
private TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
private static TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
{
|
||||
var box = new TextBox
|
||||
{
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ private static void ApplyConstFilter(TableLayoutPanel panel, IReadOnlyList<(stri
|
|||
panel.ResumeLayout();
|
||||
}
|
||||
|
||||
private TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
private static TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
{
|
||||
var box = new TextBox
|
||||
{
|
||||
|
|
|
|||
|
|
@ -250,7 +250,7 @@ private static void ApplyWorkFilter(TableLayoutPanel panel, IReadOnlyList<(strin
|
|||
panel.ResumeLayout();
|
||||
}
|
||||
|
||||
private TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
private static TextBox CreateSearchBox(Action<string> applyFilter)
|
||||
{
|
||||
var box = new TextBox
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user