mirror of
https://github.com/Ryujinx/Opentk.git
synced 2025-01-23 18:20:58 +00:00
[X11] Implemented XI2 keyboard input
This commit is contained in:
parent
2eb88d9788
commit
7d1bec58cc
|
@ -34,10 +34,12 @@ using OpenTK.Input;
|
||||||
|
|
||||||
namespace OpenTK.Platform.X11
|
namespace OpenTK.Platform.X11
|
||||||
{
|
{
|
||||||
sealed class XI2Mouse : IMouseDriver2, IDisposable
|
sealed class XI2Mouse : IKeyboardDriver2, IMouseDriver2, IDisposable
|
||||||
{
|
{
|
||||||
const XEventName ExitEvent = XEventName.LASTEvent + 1;
|
const XEventName ExitEvent = XEventName.LASTEvent + 1;
|
||||||
|
readonly object Sync = new object();
|
||||||
readonly Thread ProcessingThread;
|
readonly Thread ProcessingThread;
|
||||||
|
readonly X11KeyMap KeyMap;
|
||||||
bool disposed;
|
bool disposed;
|
||||||
|
|
||||||
class XIMouse
|
class XIMouse
|
||||||
|
@ -48,6 +50,14 @@ namespace OpenTK.Platform.X11
|
||||||
public XIScrollClassInfo ScrollY = new XIScrollClassInfo { number = -1 };
|
public XIScrollClassInfo ScrollY = new XIScrollClassInfo { number = -1 };
|
||||||
public XIValuatorClassInfo MotionX = new XIValuatorClassInfo { number = -1 };
|
public XIValuatorClassInfo MotionX = new XIValuatorClassInfo { number = -1 };
|
||||||
public XIValuatorClassInfo MotionY = new XIValuatorClassInfo { number = -1 };
|
public XIValuatorClassInfo MotionY = new XIValuatorClassInfo { number = -1 };
|
||||||
|
public string Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XIKeyboard
|
||||||
|
{
|
||||||
|
public KeyboardState State;
|
||||||
|
public XIDeviceInfo DeviceInfo;
|
||||||
|
public string Name;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atoms
|
// Atoms
|
||||||
|
@ -66,9 +76,12 @@ namespace OpenTK.Platform.X11
|
||||||
//static readonly IntPtr RelVertWheel;
|
//static readonly IntPtr RelVertWheel;
|
||||||
|
|
||||||
long cursor_x, cursor_y; // For GetCursorState()
|
long cursor_x, cursor_y; // For GetCursorState()
|
||||||
List<XIMouse> devices = new List<XIMouse>(); // List of connected mice
|
List<XIMouse> devices = new List<XIMouse>(); // list of connected mice
|
||||||
Dictionary<int, int> rawids = new Dictionary<int, int>(); // maps hardware device ids to XIMouse ids
|
Dictionary<int, int> rawids = new Dictionary<int, int>(); // maps hardware device ids to XIMouse ids
|
||||||
|
|
||||||
|
List<XIKeyboard> keyboards = new List<XIKeyboard>(); // list of connected keybords
|
||||||
|
Dictionary<int, int> keyboard_ids = new Dictionary<int, int>(); // maps hardware device ids to XIKeyboard ids
|
||||||
|
|
||||||
internal readonly X11WindowInfo window;
|
internal readonly X11WindowInfo window;
|
||||||
internal static int XIOpCode { get; private set; }
|
internal static int XIOpCode { get; private set; }
|
||||||
internal static int XIVersion { get; private set; }
|
internal static int XIVersion { get; private set; }
|
||||||
|
@ -108,7 +121,6 @@ namespace OpenTK.Platform.X11
|
||||||
|
|
||||||
public XI2Mouse()
|
public XI2Mouse()
|
||||||
{
|
{
|
||||||
Debug.WriteLine("Using XI2Mouse.");
|
|
||||||
window = new X11WindowInfo();
|
window = new X11WindowInfo();
|
||||||
|
|
||||||
window.Display = Functions.XOpenDisplay(IntPtr.Zero);
|
window.Display = Functions.XOpenDisplay(IntPtr.Zero);
|
||||||
|
@ -117,6 +129,8 @@ namespace OpenTK.Platform.X11
|
||||||
window.Screen = Functions.XDefaultScreen(window.Display);
|
window.Screen = Functions.XDefaultScreen(window.Display);
|
||||||
window.RootWindow = Functions.XRootWindow(window.Display, window.Screen);
|
window.RootWindow = Functions.XRootWindow(window.Display, window.Screen);
|
||||||
window.Handle = window.RootWindow;
|
window.Handle = window.RootWindow;
|
||||||
|
|
||||||
|
KeyMap = new X11KeyMap(window.Display);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSupported(window.Display))
|
if (!IsSupported(window.Display))
|
||||||
|
@ -124,6 +138,8 @@ namespace OpenTK.Platform.X11
|
||||||
|
|
||||||
using (new XLock(window.Display))
|
using (new XLock(window.Display))
|
||||||
using (XIEventMask mask = new XIEventMask(1,
|
using (XIEventMask mask = new XIEventMask(1,
|
||||||
|
XIEventMasks.RawKeyPressMask |
|
||||||
|
XIEventMasks.RawKeyReleaseMask |
|
||||||
XIEventMasks.RawButtonPressMask |
|
XIEventMasks.RawButtonPressMask |
|
||||||
XIEventMasks.RawButtonReleaseMask |
|
XIEventMasks.RawButtonReleaseMask |
|
||||||
XIEventMasks.RawMotionMask |
|
XIEventMasks.RawMotionMask |
|
||||||
|
@ -132,9 +148,8 @@ namespace OpenTK.Platform.X11
|
||||||
(XIEventMasks)(1 << (int)ExitEvent)))
|
(XIEventMasks)(1 << (int)ExitEvent)))
|
||||||
{
|
{
|
||||||
XI.SelectEvents(window.Display, window.Handle, mask);
|
XI.SelectEvents(window.Display, window.Handle, mask);
|
||||||
}
|
|
||||||
|
|
||||||
UpdateDevices();
|
UpdateDevices();
|
||||||
|
}
|
||||||
|
|
||||||
ProcessingThread = new Thread(ProcessEvents);
|
ProcessingThread = new Thread(ProcessEvents);
|
||||||
ProcessingThread.IsBackground = true;
|
ProcessingThread.IsBackground = true;
|
||||||
|
@ -181,27 +196,152 @@ namespace OpenTK.Platform.X11
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region IKeyboardDriver2 Members
|
||||||
|
|
||||||
|
KeyboardState IKeyboardDriver2.GetState()
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
KeyboardState state = new KeyboardState();
|
||||||
|
foreach (XIKeyboard k in keyboards)
|
||||||
|
{
|
||||||
|
state.MergeBits(k.State);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
KeyboardState IKeyboardDriver2.GetState(int index)
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < keyboards.Count)
|
||||||
|
{
|
||||||
|
return keyboards[index].State;
|
||||||
|
}
|
||||||
|
return new KeyboardState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string IKeyboardDriver2.GetDeviceName(int index)
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < keyboards.Count)
|
||||||
|
{
|
||||||
|
return keyboards[index].Name;
|
||||||
|
}
|
||||||
|
return String.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region IMouseDriver2 Members
|
||||||
|
|
||||||
|
MouseState IMouseDriver2.GetState()
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
MouseState master = new MouseState();
|
||||||
|
foreach (var d in devices)
|
||||||
|
{
|
||||||
|
master.MergeBits(d.State);
|
||||||
|
}
|
||||||
|
return master;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseState IMouseDriver2.GetState(int index)
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
if (index >= 0 && index < devices.Count)
|
||||||
|
{
|
||||||
|
return devices[index].State;
|
||||||
|
}
|
||||||
|
return new MouseState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseState IMouseDriver2.GetCursorState()
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
MouseState master = (this as IMouseDriver2).GetState();
|
||||||
|
master.X = (int)Interlocked.Read(ref cursor_x);
|
||||||
|
master.Y = (int)Interlocked.Read(ref cursor_y);
|
||||||
|
return master;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IMouseDriver2.SetPosition(double x, double y)
|
||||||
|
{
|
||||||
|
// Note: we cannot use window.Display here, because
|
||||||
|
// that will deadlock the input thread, which is
|
||||||
|
// blocking inside XIfEvent
|
||||||
|
using (new XLock(API.DefaultDisplay))
|
||||||
|
{
|
||||||
|
Functions.XWarpPointer(API.DefaultDisplay,
|
||||||
|
IntPtr.Zero, window.RootWindow, 0, 0, 0, 0, (int)x, (int)y);
|
||||||
|
Functions.XFlush(API.DefaultDisplay);
|
||||||
|
Interlocked.Exchange(ref cursor_x, (long)x);
|
||||||
|
Interlocked.Exchange(ref cursor_y, (long)y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Private Members
|
||||||
|
|
||||||
void UpdateDevices()
|
void UpdateDevices()
|
||||||
{
|
{
|
||||||
|
lock (Sync)
|
||||||
|
{
|
||||||
|
devices.Clear();
|
||||||
|
keyboards.Clear();
|
||||||
|
|
||||||
int count;
|
int count;
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
XIDeviceInfo* list = (XIDeviceInfo*)XI.QueryDevice(window.Display, 1, out count);
|
XIDeviceInfo* list = (XIDeviceInfo*)XI.QueryDevice(window.Display,
|
||||||
|
XI.XIAllDevices, out count);
|
||||||
|
|
||||||
Debug.Print("Refreshing mouse device list");
|
Debug.Print("Refreshing input device list");
|
||||||
Debug.Print("{0} mouse devices detected", count);
|
Debug.Print("{0} input devices detected", count);
|
||||||
|
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (devices.Count <= i)
|
switch ((list + i)->use)
|
||||||
{
|
{
|
||||||
devices.Add(new XIMouse());
|
case XIDeviceType.MasterKeyboard:
|
||||||
|
//case XIDeviceType.SlaveKeyboard:
|
||||||
|
{
|
||||||
|
XIKeyboard k = new XIKeyboard();
|
||||||
|
k.DeviceInfo = *(list + i);
|
||||||
|
k.State.SetIsConnected(k.DeviceInfo.enabled);
|
||||||
|
k.Name = Marshal.PtrToStringAnsi(k.DeviceInfo.name);
|
||||||
|
int id = k.DeviceInfo.deviceid;
|
||||||
|
if (!keyboard_ids.ContainsKey(id))
|
||||||
|
{
|
||||||
|
keyboard_ids.Add(k.DeviceInfo.deviceid, 0);
|
||||||
}
|
}
|
||||||
XIMouse d = devices[i];
|
keyboard_ids[id] = keyboards.Count;
|
||||||
|
keyboards.Add(k);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case XIDeviceType.MasterPointer:
|
||||||
|
//case XIDeviceType.SlavePointer:
|
||||||
|
case XIDeviceType.FloatingSlave:
|
||||||
|
{
|
||||||
|
XIMouse d = new XIMouse();
|
||||||
d.DeviceInfo = *(list + i);
|
d.DeviceInfo = *(list + i);
|
||||||
|
|
||||||
d.State.SetIsConnected(d.DeviceInfo.enabled);
|
d.State.SetIsConnected(d.DeviceInfo.enabled);
|
||||||
Debug.Print("Device {0} is {1} and has:",
|
d.Name = Marshal.PtrToStringAnsi(d.DeviceInfo.name);
|
||||||
i, d.DeviceInfo.enabled ? "enabled" : "disabled");
|
Debug.Print("Device {0} \"{1}\" is {2} and has:",
|
||||||
|
i, d.Name, d.DeviceInfo.enabled ? "enabled" : "disabled");
|
||||||
|
|
||||||
// Decode the XIDeviceInfo to axes, buttons and scroll types
|
// Decode the XIDeviceInfo to axes, buttons and scroll types
|
||||||
for (int j = 0; j < d.DeviceInfo.num_classes; j++)
|
for (int j = 0; j < d.DeviceInfo.num_classes; j++)
|
||||||
|
@ -257,58 +397,22 @@ namespace OpenTK.Platform.X11
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map the hardware device id to the current XIMouse id
|
// Map the hardware device id to the current XIMouse id
|
||||||
if (!rawids.ContainsKey(d.DeviceInfo.deviceid))
|
int id = d.DeviceInfo.deviceid;
|
||||||
|
if (!rawids.ContainsKey(id))
|
||||||
{
|
{
|
||||||
rawids.Add(d.DeviceInfo.deviceid, 0);
|
rawids.Add(id, 0);
|
||||||
|
}
|
||||||
|
rawids[id] = devices.Count;
|
||||||
|
devices.Add(d);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
rawids[d.DeviceInfo.deviceid] = i;
|
|
||||||
}
|
}
|
||||||
XI.FreeDeviceInfo((IntPtr)list);
|
XI.FreeDeviceInfo((IntPtr)list);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
#region IMouseDriver2 Members
|
|
||||||
|
|
||||||
public MouseState GetState()
|
|
||||||
{
|
|
||||||
MouseState master = new MouseState();
|
|
||||||
foreach (var d in devices)
|
|
||||||
{
|
|
||||||
master.MergeBits(d.State);
|
|
||||||
}
|
|
||||||
return master;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MouseState GetState(int index)
|
|
||||||
{
|
|
||||||
if (devices.Count > index)
|
|
||||||
return devices[index].State;
|
|
||||||
else
|
|
||||||
return new MouseState();
|
|
||||||
}
|
|
||||||
|
|
||||||
public MouseState GetCursorState()
|
|
||||||
{
|
|
||||||
MouseState master = GetState();
|
|
||||||
master.X = (int)Interlocked.Read(ref cursor_x);
|
|
||||||
master.Y = (int)Interlocked.Read(ref cursor_y);
|
|
||||||
return master;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetPosition(double x, double y)
|
|
||||||
{
|
|
||||||
using (new XLock(API.DefaultDisplay))
|
|
||||||
{
|
|
||||||
Functions.XWarpPointer(API.DefaultDisplay,
|
|
||||||
IntPtr.Zero, window.RootWindow, 0, 0, 0, 0, (int)x, (int)y);
|
|
||||||
Interlocked.Exchange(ref cursor_x, (long)x);
|
|
||||||
Interlocked.Exchange(ref cursor_y, (long)y);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
void ProcessEvents()
|
void ProcessEvents()
|
||||||
{
|
{
|
||||||
while (!disposed)
|
while (!disposed)
|
||||||
|
@ -341,6 +445,8 @@ namespace OpenTK.Platform.X11
|
||||||
// Nothing to do
|
// Nothing to do
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case XIEventType.RawKeyPress:
|
||||||
|
case XIEventType.RawKeyRelease:
|
||||||
case XIEventType.RawMotion:
|
case XIEventType.RawMotion:
|
||||||
case XIEventType.RawButtonPress:
|
case XIEventType.RawButtonPress:
|
||||||
case XIEventType.RawButtonRelease:
|
case XIEventType.RawButtonRelease:
|
||||||
|
@ -359,59 +465,73 @@ namespace OpenTK.Platform.X11
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProcessRawEvent(ref XGenericEventCookie cookie)
|
void ProcessRawEvent(ref XGenericEventCookie cookie)
|
||||||
|
{
|
||||||
|
lock (Sync)
|
||||||
{
|
{
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
XIRawEvent raw = *(XIRawEvent*)cookie.data;
|
XIRawEvent raw = *(XIRawEvent*)cookie.data;
|
||||||
|
XIMouse mouse;
|
||||||
if (!rawids.ContainsKey(raw.deviceid))
|
XIKeyboard keyboard;
|
||||||
{
|
|
||||||
Debug.Print("Unknown mouse device {0} encountered, ignoring.", raw.deviceid);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var d = devices[rawids[raw.deviceid]];
|
|
||||||
|
|
||||||
switch (raw.evtype)
|
switch (raw.evtype)
|
||||||
{
|
{
|
||||||
case XIEventType.RawMotion:
|
case XIEventType.RawMotion:
|
||||||
ProcessRawMotion(d, ref raw);
|
if (GetMouseDevice(raw.deviceid, out mouse))
|
||||||
|
{
|
||||||
|
ProcessRawMotion(mouse, ref raw);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case XIEventType.RawButtonPress:
|
case XIEventType.RawButtonPress:
|
||||||
switch (raw.detail)
|
case XIEventType.RawButtonRelease:
|
||||||
|
if (GetMouseDevice(raw.deviceid, out mouse))
|
||||||
{
|
{
|
||||||
case 1: d.State.EnableBit((int)MouseButton.Left); break;
|
float dx, dy;
|
||||||
case 2: d.State.EnableBit((int)MouseButton.Middle); break;
|
MouseButton button = X11KeyMap.TranslateButton(raw.detail, out dx, out dy);
|
||||||
case 3: d.State.EnableBit((int)MouseButton.Right); break;
|
mouse.State[button] = raw.evtype == XIEventType.RawButtonPress;
|
||||||
case 8: d.State.EnableBit((int)MouseButton.Button1); break;
|
|
||||||
case 9: d.State.EnableBit((int)MouseButton.Button2); break;
|
|
||||||
case 10: d.State.EnableBit((int)MouseButton.Button3); break;
|
|
||||||
case 11: d.State.EnableBit((int)MouseButton.Button4); break;
|
|
||||||
case 12: d.State.EnableBit((int)MouseButton.Button5); break;
|
|
||||||
case 13: d.State.EnableBit((int)MouseButton.Button6); break;
|
|
||||||
case 14: d.State.EnableBit((int)MouseButton.Button7); break;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case XIEventType.RawButtonRelease:
|
case XIEventType.RawKeyPress:
|
||||||
switch (raw.detail)
|
case XIEventType.RawKeyRelease:
|
||||||
|
if (GetKeyboardDevice(raw.deviceid, out keyboard))
|
||||||
{
|
{
|
||||||
case 1: d.State.DisableBit((int)MouseButton.Left); break;
|
Key key;
|
||||||
case 2: d.State.DisableBit((int)MouseButton.Middle); break;
|
if (KeyMap.TranslateKey(raw.detail, out key))
|
||||||
case 3: d.State.DisableBit((int)MouseButton.Right); break;
|
{
|
||||||
case 8: d.State.DisableBit((int)MouseButton.Button1); break;
|
keyboard.State[key] = raw.evtype == XIEventType.RawKeyPress;
|
||||||
case 9: d.State.DisableBit((int)MouseButton.Button2); break;
|
}
|
||||||
case 10: d.State.DisableBit((int)MouseButton.Button3); break;
|
|
||||||
case 11: d.State.DisableBit((int)MouseButton.Button4); break;
|
|
||||||
case 12: d.State.DisableBit((int)MouseButton.Button5); break;
|
|
||||||
case 13: d.State.DisableBit((int)MouseButton.Button6); break;
|
|
||||||
case 14: d.State.DisableBit((int)MouseButton.Button7); break;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetMouseDevice(int deviceid, out XIMouse mouse)
|
||||||
|
{
|
||||||
|
if (!rawids.ContainsKey(deviceid))
|
||||||
|
{
|
||||||
|
Debug.Print("Unknown mouse device {0} encountered, ignoring.", deviceid);
|
||||||
|
mouse = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
mouse = devices[rawids[deviceid]];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool GetKeyboardDevice(int deviceid, out XIKeyboard keyboard)
|
||||||
|
{
|
||||||
|
if (!keyboard_ids.ContainsKey(deviceid))
|
||||||
|
{
|
||||||
|
Debug.Print("Unknown keyboard device {0} encountered, ignoring.", deviceid);
|
||||||
|
keyboard = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
keyboard = keyboards[keyboard_ids[deviceid]];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
unsafe static void ProcessRawMotion(XIMouse d, ref XIRawEvent raw)
|
unsafe static void ProcessRawMotion(XIMouse d, ref XIRawEvent raw)
|
||||||
{
|
{
|
||||||
|
@ -453,6 +573,8 @@ namespace OpenTK.Platform.X11
|
||||||
bool valid = false;
|
bool valid = false;
|
||||||
if ((long)e.GenericEventCookie.extension == arg.ToInt64())
|
if ((long)e.GenericEventCookie.extension == arg.ToInt64())
|
||||||
{
|
{
|
||||||
|
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawKeyPress;
|
||||||
|
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawKeyRelease;
|
||||||
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawMotion;
|
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawMotion;
|
||||||
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawButtonPress;
|
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawButtonPress;
|
||||||
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawButtonRelease;
|
valid |= e.GenericEventCookie.evtype == (int)XIEventType.RawButtonRelease;
|
||||||
|
@ -470,6 +592,8 @@ namespace OpenTK.Platform.X11
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region IDisposable Members
|
#region IDisposable Members
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|
Loading…
Reference in a new issue