mirror of
https://github.com/greenshot/greenshot
synced 2025-07-14 09:03:44 -07:00
Refactored code to use SafeHandle where possible, this should fix potential resource leaks and make the code more clear.
git-svn-id: http://svn.code.sf.net/p/greenshot/code/trunk@2429 7dccd23d-a4a3-4e1f-8c07-b4c1b4018ab4
This commit is contained in:
parent
9288fa8212
commit
201ee7082e
7 changed files with 365 additions and 233 deletions
|
@ -86,16 +86,13 @@ namespace Greenshot.Forms {
|
||||||
/// <param name="screenCoordinates">Point with the coordinates</param>
|
/// <param name="screenCoordinates">Point with the coordinates</param>
|
||||||
/// <returns>Color at the specified screenCoordinates</returns>
|
/// <returns>Color at the specified screenCoordinates</returns>
|
||||||
static private Color GetPixelColor(Point screenCoordinates) {
|
static private Color GetPixelColor(Point screenCoordinates) {
|
||||||
IntPtr hdc = User32.GetDC(IntPtr.Zero);
|
using (SafeWindowDCHandle screenDC = SafeWindowDCHandle.fromDesktop()) {
|
||||||
try {
|
try {
|
||||||
uint pixel = GDI32.GetPixel(hdc, screenCoordinates.X, screenCoordinates.Y);
|
uint pixel = GDI32.GetPixel(screenDC, screenCoordinates.X, screenCoordinates.Y);
|
||||||
Color color = Color.FromArgb(255, (int)(pixel & 0xFF), (int)(pixel & 0xFF00) >> 8, (int)(pixel & 0xFF0000) >> 16);
|
Color color = Color.FromArgb(255, (int)(pixel & 0xFF), (int)(pixel & 0xFF00) >> 8, (int)(pixel & 0xFF0000) >> 16);
|
||||||
return color;
|
return color;
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
return Color.Empty;
|
return Color.Empty;
|
||||||
} finally {
|
|
||||||
if (hdc != IntPtr.Zero) {
|
|
||||||
User32.ReleaseDC(IntPtr.Zero, hdc);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,9 +48,9 @@ namespace GreenshotPlugin.Controls {
|
||||||
get {
|
get {
|
||||||
if (vRefresh == 0) {
|
if (vRefresh == 0) {
|
||||||
// get te hDC of the desktop to get the VREFRESH
|
// get te hDC of the desktop to get the VREFRESH
|
||||||
IntPtr hDCDesktop = User32.GetWindowDC(User32.GetDesktopWindow());
|
using (SafeWindowDCHandle desktopHandle = SafeWindowDCHandle.fromDesktop()) {
|
||||||
vRefresh = GDI32.GetDeviceCaps(hDCDesktop, DeviceCaps.VREFRESH);
|
vRefresh = GDI32.GetDeviceCaps(desktopHandle, DeviceCaps.VREFRESH);
|
||||||
User32.ReleaseDC(hDCDesktop);
|
}
|
||||||
}
|
}
|
||||||
// A vertical refresh rate value of 0 or 1 represents the display hardware's default refresh rate.
|
// A vertical refresh rate value of 0 or 1 represents the display hardware's default refresh rate.
|
||||||
// As there is currently no know way to get the default, we guess it.
|
// As there is currently no know way to get the default, we guess it.
|
||||||
|
|
|
@ -493,7 +493,7 @@ EndSelection:<<<<<<<4
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int BITMAPFILEHEADER_LENGTH = 14;
|
private const int BITMAPFILEHEADER_LENGTH = 14;
|
||||||
public static void SetClipboardData(ISurface surface) {
|
public static void SetClipboardData(ISurface surface) {
|
||||||
DataObject ido = new DataObject();
|
DataObject dataObject = new DataObject();
|
||||||
|
|
||||||
// This will work for Office and most other applications
|
// This will work for Office and most other applications
|
||||||
//ido.SetData(DataFormats.Bitmap, true, image);
|
//ido.SetData(DataFormats.Bitmap, true, image);
|
||||||
|
@ -515,7 +515,7 @@ EndSelection:<<<<<<<4
|
||||||
ImageOutput.SaveToStream(imageToSave, null, pngStream, pngOutputSettings);
|
ImageOutput.SaveToStream(imageToSave, null, pngStream, pngOutputSettings);
|
||||||
pngStream.Seek(0, SeekOrigin.Begin);
|
pngStream.Seek(0, SeekOrigin.Begin);
|
||||||
// Set the PNG stream
|
// Set the PNG stream
|
||||||
ido.SetData(FORMAT_PNG, false, pngStream);
|
dataObject.SetData(FORMAT_PNG, false, pngStream);
|
||||||
}
|
}
|
||||||
} catch (Exception pngEX) {
|
} catch (Exception pngEX) {
|
||||||
LOG.Error("Error creating PNG for the Clipboard.", pngEX);
|
LOG.Error("Error creating PNG for the Clipboard.", pngEX);
|
||||||
|
@ -534,7 +534,7 @@ EndSelection:<<<<<<<4
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the DIB to the clipboard DataObject
|
// Set the DIB to the clipboard DataObject
|
||||||
ido.SetData(DataFormats.Dib, true, dibStream);
|
dataObject.SetData(DataFormats.Dib, true, dibStream);
|
||||||
}
|
}
|
||||||
} catch (Exception dibEx) {
|
} catch (Exception dibEx) {
|
||||||
LOG.Error("Error creating DIB for the Clipboard.", dibEx);
|
LOG.Error("Error creating DIB for the Clipboard.", dibEx);
|
||||||
|
@ -544,7 +544,7 @@ EndSelection:<<<<<<<4
|
||||||
if (config.ClipboardFormats.Contains(ClipboardFormat.HTML)) {
|
if (config.ClipboardFormats.Contains(ClipboardFormat.HTML)) {
|
||||||
string tmpFile = ImageOutput.SaveToTmpFile(surface, new SurfaceOutputSettings(OutputFormat.png, 100, false), null);
|
string tmpFile = ImageOutput.SaveToTmpFile(surface, new SurfaceOutputSettings(OutputFormat.png, 100, false), null);
|
||||||
string html = getHTMLString(surface, tmpFile);
|
string html = getHTMLString(surface, tmpFile);
|
||||||
ido.SetText(html, TextDataFormat.Html);
|
dataObject.SetText(html, TextDataFormat.Html);
|
||||||
} else if (config.ClipboardFormats.Contains(ClipboardFormat.HTMLDATAURL)) {
|
} else if (config.ClipboardFormats.Contains(ClipboardFormat.HTMLDATAURL)) {
|
||||||
string html;
|
string html;
|
||||||
using (MemoryStream tmpPNGStream = new MemoryStream()) {
|
using (MemoryStream tmpPNGStream = new MemoryStream()) {
|
||||||
|
@ -560,18 +560,18 @@ EndSelection:<<<<<<<4
|
||||||
}
|
}
|
||||||
html = getHTMLDataURLString(surface, tmpPNGStream);
|
html = getHTMLDataURLString(surface, tmpPNGStream);
|
||||||
}
|
}
|
||||||
ido.SetText(html, TextDataFormat.Html);
|
dataObject.SetText(html, TextDataFormat.Html);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
// we need to use the SetDataOject before the streams are closed otherwise the buffer will be gone!
|
// we need to use the SetDataOject before the streams are closed otherwise the buffer will be gone!
|
||||||
// Check if Bitmap is wanted
|
// Check if Bitmap is wanted
|
||||||
if (config.ClipboardFormats.Contains(ClipboardFormat.BITMAP)) {
|
if (config.ClipboardFormats.Contains(ClipboardFormat.BITMAP)) {
|
||||||
ido.SetImage(imageToSave);
|
dataObject.SetImage(imageToSave);
|
||||||
// Place the DataObject to the clipboard
|
// Place the DataObject to the clipboard
|
||||||
SetDataObject(ido, true);
|
SetDataObject(dataObject, true);
|
||||||
} else {
|
} else {
|
||||||
// Place the DataObject to the clipboard
|
// Place the DataObject to the clipboard
|
||||||
SetDataObject(ido, true);
|
SetDataObject(dataObject, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pngStream != null) {
|
if (pngStream != null) {
|
||||||
|
|
|
@ -428,6 +428,14 @@ namespace GreenshotPlugin.Core {
|
||||||
private static readonly log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(WindowCapture));
|
private static readonly log4net.ILog LOG = log4net.LogManager.GetLogger(typeof(WindowCapture));
|
||||||
private static CoreConfiguration conf = IniConfig.GetIniSection<CoreConfiguration>();
|
private static CoreConfiguration conf = IniConfig.GetIniSection<CoreConfiguration>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to cleanup the unmanged resource in the iconInfo for the CaptureCursor method
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hObject"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
[DllImport("gdi32", SetLastError = true)]
|
||||||
|
private static extern bool DeleteObject(IntPtr hObject);
|
||||||
|
|
||||||
private WindowCapture() {
|
private WindowCapture() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -517,10 +525,10 @@ namespace GreenshotPlugin.Core {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iconInfo.hbmMask != IntPtr.Zero) {
|
if (iconInfo.hbmMask != IntPtr.Zero) {
|
||||||
GDI32.DeleteObject(iconInfo.hbmMask);
|
DeleteObject(iconInfo.hbmMask);
|
||||||
}
|
}
|
||||||
if (iconInfo.hbmColor != IntPtr.Zero) {
|
if (iconInfo.hbmColor != IntPtr.Zero) {
|
||||||
GDI32.DeleteObject(iconInfo.hbmColor);
|
DeleteObject(iconInfo.hbmColor);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -641,26 +649,23 @@ namespace GreenshotPlugin.Core {
|
||||||
// capture.Image = capturedBitmap;
|
// capture.Image = capturedBitmap;
|
||||||
// capture.Location = captureBounds.Location;
|
// capture.Location = captureBounds.Location;
|
||||||
|
|
||||||
// "P/Invoke" Solution for capturing the screen
|
using (SafeWindowDCHandle desktopDCHandle = SafeWindowDCHandle.fromDesktop()) {
|
||||||
IntPtr hWndDesktop = User32.GetDesktopWindow();
|
if (desktopDCHandle.IsInvalid) {
|
||||||
// get te hDC of the target window
|
|
||||||
IntPtr hDCDesktop = User32.GetWindowDC(hWndDesktop);
|
|
||||||
|
|
||||||
// Make sure the last error is set to 0
|
|
||||||
Win32.SetLastError(0);
|
|
||||||
|
|
||||||
// create a device context we can copy to
|
|
||||||
IntPtr hDCDest = GDI32.CreateCompatibleDC(hDCDesktop);
|
|
||||||
// Check if the device context is there, if not throw an error with as much info as possible!
|
|
||||||
if (hDCDest == IntPtr.Zero) {
|
|
||||||
// Get Exception before the error is lost
|
// Get Exception before the error is lost
|
||||||
Exception exceptionToThrow = CreateCaptureException("CreateCompatibleDC", captureBounds);
|
Exception exceptionToThrow = CreateCaptureException("desktopDCHandle", captureBounds);
|
||||||
// Cleanup
|
|
||||||
User32.ReleaseDC(hWndDesktop, hDCDesktop);
|
|
||||||
// throw exception
|
// throw exception
|
||||||
throw exceptionToThrow;
|
throw exceptionToThrow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// create a device context we can copy to
|
||||||
|
using (SafeCompatibleDCHandle safeCompatibleDCHandle = GDI32.CreateCompatibleDC(desktopDCHandle)) {
|
||||||
|
// Check if the device context is there, if not throw an error with as much info as possible!
|
||||||
|
if (safeCompatibleDCHandle.IsInvalid) {
|
||||||
|
// Get Exception before the error is lost
|
||||||
|
Exception exceptionToThrow = CreateCaptureException("CreateCompatibleDC", captureBounds);
|
||||||
|
// throw exception
|
||||||
|
throw exceptionToThrow;
|
||||||
|
}
|
||||||
// Create BitmapInfoHeader for CreateDIBSection
|
// Create BitmapInfoHeader for CreateDIBSection
|
||||||
BitmapInfoHeader bmi = new BitmapInfoHeader(captureBounds.Width, captureBounds.Height, 24);
|
BitmapInfoHeader bmi = new BitmapInfoHeader(captureBounds.Width, captureBounds.Height, 24);
|
||||||
|
|
||||||
|
@ -669,32 +674,21 @@ namespace GreenshotPlugin.Core {
|
||||||
|
|
||||||
// create a bitmap we can copy it to, using GetDeviceCaps to get the width/height
|
// create a bitmap we can copy it to, using GetDeviceCaps to get the width/height
|
||||||
IntPtr bits0; // not used for our purposes. It returns a pointer to the raw bits that make up the bitmap.
|
IntPtr bits0; // not used for our purposes. It returns a pointer to the raw bits that make up the bitmap.
|
||||||
IntPtr hDIBSection = GDI32.CreateDIBSection(hDCDesktop, ref bmi, BitmapInfoHeader.DIB_RGB_COLORS, out bits0, IntPtr.Zero, 0);
|
using (SafeDibSectionHandle safeDibSectionHandle = GDI32.CreateDIBSection(desktopDCHandle, ref bmi, BitmapInfoHeader.DIB_RGB_COLORS, out bits0, IntPtr.Zero, 0)) {
|
||||||
|
if (safeDibSectionHandle.IsInvalid) {
|
||||||
if (hDIBSection == IntPtr.Zero) {
|
|
||||||
// Get Exception before the error is lost
|
// Get Exception before the error is lost
|
||||||
Exception exceptionToThrow = CreateCaptureException("CreateDIBSection", captureBounds);
|
Exception exceptionToThrow = CreateCaptureException("CreateDIBSection", captureBounds);
|
||||||
exceptionToThrow.Data.Add("hdcDest", hDCDest.ToInt32());
|
exceptionToThrow.Data.Add("hdcDest", safeCompatibleDCHandle.DangerousGetHandle().ToInt32());
|
||||||
exceptionToThrow.Data.Add("hdcSrc", hDCDesktop.ToInt32());
|
exceptionToThrow.Data.Add("hdcSrc", desktopDCHandle.DangerousGetHandle().ToInt32());
|
||||||
|
|
||||||
// clean up
|
|
||||||
GDI32.DeleteDC(hDCDest);
|
|
||||||
User32.ReleaseDC(hWndDesktop, hDCDesktop);
|
|
||||||
|
|
||||||
// Throw so people can report the problem
|
// Throw so people can report the problem
|
||||||
throw exceptionToThrow;
|
throw exceptionToThrow;
|
||||||
} else {
|
} else {
|
||||||
// select the bitmap object and store the old handle
|
// select the bitmap object and store the old handle
|
||||||
IntPtr hOldObject = GDI32.SelectObject(hDCDest, hDIBSection);
|
using (SafeSelectObjectHandle selectObject = safeCompatibleDCHandle.SelectObject(safeDibSectionHandle)) {
|
||||||
|
|
||||||
// bitblt over (make copy)
|
// bitblt over (make copy)
|
||||||
GDI32.BitBlt(hDCDest, 0, 0, captureBounds.Width, captureBounds.Height, hDCDesktop, captureBounds.X, captureBounds.Y, CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt);
|
GDI32.BitBlt(safeCompatibleDCHandle, 0, 0, captureBounds.Width, captureBounds.Height, desktopDCHandle, captureBounds.X, captureBounds.Y, CopyPixelOperation.SourceCopy | CopyPixelOperation.CaptureBlt);
|
||||||
|
}
|
||||||
// restore selection (old handle)
|
|
||||||
GDI32.SelectObject(hDCDest, hOldObject);
|
|
||||||
// clean up
|
|
||||||
GDI32.DeleteDC(hDCDest);
|
|
||||||
User32.ReleaseDC(hWndDesktop, hDCDesktop);
|
|
||||||
|
|
||||||
// get a .NET image object for it
|
// get a .NET image object for it
|
||||||
// A suggestion for the "A generic error occurred in GDI+." E_FAIL/0×80004005 error is to re-try...
|
// A suggestion for the "A generic error occurred in GDI+." E_FAIL/0×80004005 error is to re-try...
|
||||||
|
@ -723,7 +717,7 @@ namespace GreenshotPlugin.Core {
|
||||||
}
|
}
|
||||||
// Check if we need to have a transparent background, needed for offscreen content
|
// Check if we need to have a transparent background, needed for offscreen content
|
||||||
if (offscreenContent) {
|
if (offscreenContent) {
|
||||||
using (Bitmap tmpBitmap = Bitmap.FromHbitmap(hDIBSection)) {
|
using (Bitmap tmpBitmap = Bitmap.FromHbitmap(safeDibSectionHandle.DangerousGetHandle())) {
|
||||||
// Create a new bitmap which has a transparent background
|
// Create a new bitmap which has a transparent background
|
||||||
returnBitmap = ImageHelper.CreateEmpty(tmpBitmap.Width, tmpBitmap.Height, PixelFormat.Format32bppArgb, Color.Transparent, tmpBitmap.HorizontalResolution, tmpBitmap.VerticalResolution);
|
returnBitmap = ImageHelper.CreateEmpty(tmpBitmap.Width, tmpBitmap.Height, PixelFormat.Format32bppArgb, Color.Transparent, tmpBitmap.HorizontalResolution, tmpBitmap.VerticalResolution);
|
||||||
// Content will be copied here
|
// Content will be copied here
|
||||||
|
@ -740,7 +734,7 @@ namespace GreenshotPlugin.Core {
|
||||||
} else {
|
} else {
|
||||||
// All screens, which are inside the capture, are of equal size
|
// All screens, which are inside the capture, are of equal size
|
||||||
// assign image to Capture, the image will be disposed there..
|
// assign image to Capture, the image will be disposed there..
|
||||||
returnBitmap = Bitmap.FromHbitmap(hDIBSection);
|
returnBitmap = Bitmap.FromHbitmap(safeDibSectionHandle.DangerousGetHandle());
|
||||||
}
|
}
|
||||||
// We got through the capture without exception
|
// We got through the capture without exception
|
||||||
success = true;
|
success = true;
|
||||||
|
@ -754,9 +748,9 @@ namespace GreenshotPlugin.Core {
|
||||||
LOG.Error("Still couldn't create Bitmap!");
|
LOG.Error("Still couldn't create Bitmap!");
|
||||||
throw exception;
|
throw exception;
|
||||||
}
|
}
|
||||||
// free up the Bitmap object
|
}
|
||||||
GDI32.DeleteObject(hDIBSection);
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return returnBitmap;
|
return returnBitmap;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1224,15 +1224,15 @@ namespace GreenshotPlugin.Core {
|
||||||
/// Get the region for a window
|
/// Get the region for a window
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private Region GetRegion() {
|
private Region GetRegion() {
|
||||||
IntPtr windowRegionPtr = GDI32.CreateRectRgn(0,0,0,0);
|
using (SafeRegionHandle region = GDI32.CreateRectRgn(0, 0, 0, 0)) {
|
||||||
RegionResult result = User32.GetWindowRgn(Handle, windowRegionPtr);
|
if (!region.IsInvalid) {
|
||||||
Region returnRegion = null;
|
RegionResult result = User32.GetWindowRgn(Handle, region);
|
||||||
if (result != RegionResult.REGION_ERROR && result != RegionResult.REGION_NULLREGION) {
|
if (result != RegionResult.REGION_ERROR && result != RegionResult.REGION_NULLREGION) {
|
||||||
returnRegion = Region.FromHrgn(windowRegionPtr);
|
return Region.FromHrgn(region.DangerousGetHandle());
|
||||||
}
|
}
|
||||||
// Free the region object
|
}
|
||||||
GDI32.DeleteObject(windowRegionPtr);
|
}
|
||||||
return returnRegion;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanFreezeOrUnfreeze(string titleOrProcessname) {
|
private bool CanFreezeOrUnfreeze(string titleOrProcessname) {
|
||||||
|
|
|
@ -26,6 +26,13 @@ using Microsoft.Win32.SafeHandles;
|
||||||
|
|
||||||
namespace GreenshotPlugin.UnmanagedHelpers {
|
namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
public static class GDIExtensions {
|
public static class GDIExtensions {
|
||||||
|
/// <summary>
|
||||||
|
/// Check if all the corners of the rectangle are visible in the specified region.
|
||||||
|
/// Not a perfect check, but this currently a workaround for checking if a window is completely visible
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="region"></param>
|
||||||
|
/// <param name="rectangle"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public static bool AreRectangleCornersVisisble(this Region region, Rectangle rectangle) {
|
public static bool AreRectangleCornersVisisble(this Region region, Rectangle rectangle) {
|
||||||
Point topLeft = new Point(rectangle.X, rectangle.Y);
|
Point topLeft = new Point(rectangle.X, rectangle.Y);
|
||||||
Point topRight = new Point(rectangle.X + rectangle.Width, rectangle.Y);
|
Point topRight = new Point(rectangle.X + rectangle.Width, rectangle.Y);
|
||||||
|
@ -38,12 +45,36 @@ namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
|
|
||||||
return topLeftVisible && topRightVisible && bottomLeftVisible && bottomRightVisible;
|
return topLeftVisible && topRightVisible && bottomLeftVisible && bottomRightVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a SafeHandle for the GetHdc, so one can use using to automatically cleanup the devicecontext
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="graphics"></param>
|
||||||
|
/// <returns>SafeDeviceContextHandle</returns>
|
||||||
|
public static SafeDeviceContextHandle getSafeDeviceContext(this Graphics graphics) {
|
||||||
|
return SafeDeviceContextHandle.fromGraphics(graphics);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Abstract class SafeObjectHandle which contains all handles that are cleaned with DeleteObject
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SafeObjectHandle : SafeHandleZeroOrMinusOneIsInvalid {
|
||||||
|
[DllImport("gdi32", SetLastError = true)]
|
||||||
|
private static extern bool DeleteObject(IntPtr hObject);
|
||||||
|
|
||||||
|
protected SafeObjectHandle(bool ownsHandle) : base(ownsHandle) {
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ReleaseHandle() {
|
||||||
|
return DeleteObject(handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A hbitmap SafeHandle implementation
|
/// A hbitmap SafeHandle implementation
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class SafeHBitmapHandle : SafeHandleZeroOrMinusOneIsInvalid {
|
public class SafeHBitmapHandle : SafeObjectHandle {
|
||||||
[SecurityCritical]
|
[SecurityCritical]
|
||||||
private SafeHBitmapHandle() : base(true) {
|
private SafeHBitmapHandle() : base(true) {
|
||||||
}
|
}
|
||||||
|
@ -52,9 +83,117 @@ namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
public SafeHBitmapHandle(IntPtr preexistingHandle) : base(true) {
|
public SafeHBitmapHandle(IntPtr preexistingHandle) : base(true) {
|
||||||
SetHandle(preexistingHandle);
|
SetHandle(preexistingHandle);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A hRegion SafeHandle implementation
|
||||||
|
/// </summary>
|
||||||
|
public class SafeRegionHandle : SafeObjectHandle {
|
||||||
|
[SecurityCritical]
|
||||||
|
private SafeRegionHandle() : base(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
public SafeRegionHandle(IntPtr preexistingHandle) : base(true) {
|
||||||
|
SetHandle(preexistingHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A dibsection SafeHandle implementation
|
||||||
|
/// </summary>
|
||||||
|
public class SafeDibSectionHandle : SafeObjectHandle {
|
||||||
|
[SecurityCritical]
|
||||||
|
private SafeDibSectionHandle() : base(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
public SafeDibSectionHandle(IntPtr preexistingHandle) : base(true) {
|
||||||
|
SetHandle(preexistingHandle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A select object safehandle implementation
|
||||||
|
/// This impl will select the passed SafeHandle to the HDC and replace the returned value when disposing
|
||||||
|
/// </summary>
|
||||||
|
public class SafeSelectObjectHandle : SafeHandleZeroOrMinusOneIsInvalid {
|
||||||
|
[DllImport("gdi32", SetLastError = true)]
|
||||||
|
private static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);
|
||||||
|
|
||||||
|
private SafeHandle hdc;
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
private SafeSelectObjectHandle() : base(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
public SafeSelectObjectHandle(SafeDCHandle hdc, SafeHandle newHandle) : base(true) {
|
||||||
|
this.hdc = hdc;
|
||||||
|
SetHandle(SelectObject(hdc.DangerousGetHandle(), newHandle.DangerousGetHandle()));
|
||||||
|
}
|
||||||
|
|
||||||
protected override bool ReleaseHandle() {
|
protected override bool ReleaseHandle() {
|
||||||
return GDI32.DeleteObject(handle);
|
SelectObject(hdc.DangerousGetHandle(), handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class SafeDCHandle : SafeHandleZeroOrMinusOneIsInvalid {
|
||||||
|
protected SafeDCHandle(bool ownsHandle) : base(ownsHandle) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// A CompatibleDC SafeHandle implementation
|
||||||
|
/// </summary>
|
||||||
|
public class SafeCompatibleDCHandle : SafeDCHandle {
|
||||||
|
[DllImport("gdi32", SetLastError = true)]
|
||||||
|
private static extern bool DeleteDC(IntPtr hDC);
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
private SafeCompatibleDCHandle() : base(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
public SafeCompatibleDCHandle(IntPtr preexistingHandle) : base(true) {
|
||||||
|
SetHandle(preexistingHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SafeSelectObjectHandle SelectObject(SafeHandle newHandle) {
|
||||||
|
return new SafeSelectObjectHandle(this, newHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ReleaseHandle() {
|
||||||
|
return DeleteDC(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A DeviceContext SafeHandle implementation
|
||||||
|
/// </summary>
|
||||||
|
public class SafeDeviceContextHandle : SafeDCHandle {
|
||||||
|
private Graphics graphics = null;
|
||||||
|
[SecurityCritical]
|
||||||
|
private SafeDeviceContextHandle() : base(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
public SafeDeviceContextHandle(Graphics graphics, IntPtr preexistingHandle) : base(true) {
|
||||||
|
this.graphics = graphics;
|
||||||
|
SetHandle(preexistingHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ReleaseHandle() {
|
||||||
|
graphics.ReleaseHdc(handle);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SafeSelectObjectHandle SelectObject(SafeHandle newHandle) {
|
||||||
|
return new SafeSelectObjectHandle(this, newHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SafeDeviceContextHandle fromGraphics(Graphics graphics) {
|
||||||
|
return new SafeDeviceContextHandle(graphics, graphics.GetHdc());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,78 +202,53 @@ namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class GDI32 {
|
public static class GDI32 {
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern bool BitBlt(IntPtr hObject,int nXDest,int nYDest, int nWidth,int nHeight,IntPtr hObjectSource, int nXSrc,int nYSrc, CopyPixelOperation dwRop);
|
public static extern bool BitBlt(SafeHandle hObject, int nXDest, int nYDest, int nWidth, int nHeight, SafeHandle hdcSrc, int nXSrc, int nYSrc, CopyPixelOperation dwRop);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern bool StretchBlt(IntPtr hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, CopyPixelOperation dwRop );
|
public static extern bool StretchBlt(SafeHandle hdcDest, int nXOriginDest, int nYOriginDest, int nWidthDest, int nHeightDest, SafeHandle hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc, CopyPixelOperation dwRop);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern IntPtr CreateCompatibleDC(IntPtr hDC);
|
public static extern SafeCompatibleDCHandle CreateCompatibleDC(SafeHandle hDC);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern bool DeleteDC(IntPtr hDC);
|
public static extern IntPtr SelectObject(SafeHandle hDC, SafeHandle hObject);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern bool DeleteObject(IntPtr hObject);
|
public static extern SafeDibSectionHandle CreateDIBSection(SafeHandle hdc, ref BitmapInfoHeader bmi, uint Usage, out IntPtr bits, IntPtr hSection, uint dwOffset);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern IntPtr SelectObject(IntPtr hDC,IntPtr hObject);
|
public static extern SafeRegionHandle CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern IntPtr CreateDIBSection(IntPtr hdc, ref BitmapInfoHeader bmi, uint Usage, out IntPtr bits, IntPtr hSection, uint dwOffset);
|
public static extern uint GetPixel(SafeHandle hdc, int nXPos, int nYPos);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
[DllImport("gdi32", SetLastError=true)]
|
||||||
public static extern IntPtr CreateRectRgn(int nLeftRect, int nTopRect, int nRightRect, int nBottomRect);
|
public static extern int GetDeviceCaps(SafeHandle hdc, DeviceCaps nIndex);
|
||||||
[DllImport("gdi32", SetLastError=true)]
|
|
||||||
public static extern int GetClipBox(IntPtr hdc, out RECT lprc);
|
|
||||||
[DllImport("gdi32", SetLastError = true)]
|
|
||||||
public static extern uint GetPixel(IntPtr hdc, int nXPos, int nYPos);
|
|
||||||
[DllImport("gdi32", SetLastError = true)]
|
|
||||||
public static extern IntPtr CreateRoundRectRgn(int x1, int y1, int x2, int y2, int cx, int cy);
|
|
||||||
[DllImport("gdi32", SetLastError = true)]
|
|
||||||
public static extern int GetDeviceCaps(IntPtr hdc, DeviceCaps nIndex);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
/// StretchBlt extension for the graphics object
|
||||||
/// Doesn't work?
|
/// Doesn't work?
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="target"></param>
|
/// <param name="target"></param>
|
||||||
/// <param name="source"></param>
|
/// <param name="source"></param>
|
||||||
public static void StretchBlt(this Graphics target, Bitmap sourceBitmap, Rectangle source, Rectangle destination) {
|
public static void StretchBlt(this Graphics target, Bitmap sourceBitmap, Rectangle source, Rectangle destination) {
|
||||||
IntPtr hDCSrc = IntPtr.Zero;
|
using (SafeDeviceContextHandle targetDC = target.getSafeDeviceContext()) {
|
||||||
IntPtr hDCDest = IntPtr.Zero;
|
using (SafeCompatibleDCHandle safeCompatibleDCHandle = CreateCompatibleDC(targetDC)) {
|
||||||
try {
|
|
||||||
hDCDest = target.GetHdc();
|
|
||||||
hDCSrc = CreateCompatibleDC(hDCDest);
|
|
||||||
using (SafeHBitmapHandle hBitmapHandle = new SafeHBitmapHandle(sourceBitmap.GetHbitmap())) {
|
using (SafeHBitmapHandle hBitmapHandle = new SafeHBitmapHandle(sourceBitmap.GetHbitmap())) {
|
||||||
IntPtr pOrig = SelectObject(hDCSrc, hBitmapHandle.DangerousGetHandle());
|
using (SafeSelectObjectHandle selectObject = safeCompatibleDCHandle.SelectObject(hBitmapHandle)) {
|
||||||
StretchBlt(hDCDest, destination.X, destination.Y, destination.Width, destination.Height, hDCSrc, source.Left, source.Top, source.Width, source.Height, CopyPixelOperation.SourceCopy);
|
StretchBlt(targetDC, destination.X, destination.Y, destination.Width, destination.Height, safeCompatibleDCHandle, source.Left, source.Top, source.Width, source.Height, CopyPixelOperation.SourceCopy);
|
||||||
IntPtr pNew = SelectObject(hDCDest, pOrig);
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (hDCSrc != IntPtr.Zero) {
|
|
||||||
DeleteDC(hDCSrc);
|
|
||||||
}
|
}
|
||||||
if (hDCDest != IntPtr.Zero) {
|
|
||||||
target.ReleaseHdc(hDCDest);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
///
|
/// Bitblt extension for the graphics object
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="target"></param>
|
/// <param name="target"></param>
|
||||||
/// <param name="source"></param>
|
/// <param name="source"></param>
|
||||||
public static void BitBlt(this Graphics target, Bitmap sourceBitmap, Rectangle source, Point destination) {
|
public static void BitBlt(this Graphics target, Bitmap sourceBitmap, Rectangle source, Point destination) {
|
||||||
IntPtr hDCSrc = IntPtr.Zero;
|
using (SafeDeviceContextHandle targetDC = target.getSafeDeviceContext()) {
|
||||||
IntPtr hDCDest = IntPtr.Zero;
|
using (SafeCompatibleDCHandle safeCompatibleDCHandle = CreateCompatibleDC(targetDC)) {
|
||||||
try {
|
|
||||||
hDCDest = target.GetHdc();
|
|
||||||
hDCSrc = CreateCompatibleDC(hDCDest);
|
|
||||||
using (SafeHBitmapHandle hBitmapHandle = new SafeHBitmapHandle(sourceBitmap.GetHbitmap())) {
|
using (SafeHBitmapHandle hBitmapHandle = new SafeHBitmapHandle(sourceBitmap.GetHbitmap())) {
|
||||||
IntPtr pOrig = SelectObject(hDCSrc, hBitmapHandle.DangerousGetHandle());
|
using (SafeSelectObjectHandle selectObject = safeCompatibleDCHandle.SelectObject(hBitmapHandle)) {
|
||||||
BitBlt(hDCDest, destination.X, destination.Y, source.Width, source.Height, hDCSrc, source.Left, source.Top, CopyPixelOperation.SourceCopy);
|
BitBlt(safeCompatibleDCHandle, destination.X, destination.Y, source.Width, source.Height, safeCompatibleDCHandle, source.Left, source.Top, CopyPixelOperation.SourceCopy);
|
||||||
IntPtr pNew = SelectObject(hDCDest, pOrig);
|
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
if (hDCSrc != IntPtr.Zero) {
|
|
||||||
DeleteDC(hDCSrc);
|
|
||||||
}
|
}
|
||||||
if (hDCDest != IntPtr.Zero) {
|
|
||||||
target.ReleaseHdc(hDCDest);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
using Microsoft.Win32.SafeHandles;
|
using Microsoft.Win32.SafeHandles;
|
||||||
|
using System.Security;
|
||||||
|
|
||||||
namespace GreenshotPlugin.UnmanagedHelpers {
|
namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -36,38 +37,10 @@ namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public delegate int EnumWindowsProc(IntPtr hwnd, int lParam);
|
public delegate int EnumWindowsProc(IntPtr hwnd, int lParam);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Used with SetWinEventHook
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hWinEventHook"></param>
|
|
||||||
/// <param name="eventType"></param>
|
|
||||||
/// <param name="hwnd"></param>
|
|
||||||
/// <param name="idObject"></param>
|
|
||||||
/// <param name="idChild"></param>
|
|
||||||
/// <param name="dwEventThread"></param>
|
|
||||||
/// <param name="dwmsEventTime"></param>
|
|
||||||
public delegate void WinEventDelegate(IntPtr hWinEventHook, WinEvent eventType, IntPtr hwnd, EventObjects idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// A SafeHandle class implementation for the hIcon
|
|
||||||
/// </summary>
|
|
||||||
public class SafeIconHandle : SafeHandleZeroOrMinusOneIsInvalid {
|
|
||||||
private SafeIconHandle(): base(true) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public SafeIconHandle(IntPtr hIcon) : base(true) {
|
|
||||||
this.SetHandle(hIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool ReleaseHandle() {
|
|
||||||
return User32.DestroyIcon(this.handle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// User32 Wrappers
|
/// User32 Wrappers
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class User32 {
|
public static class User32 {
|
||||||
public const int SC_RESTORE = 0xF120;
|
public const int SC_RESTORE = 0xF120;
|
||||||
public const int SC_CLOSE = 0xF060;
|
public const int SC_CLOSE = 0xF060;
|
||||||
public const int SC_MAXIMIZE = 0xF030;
|
public const int SC_MAXIMIZE = 0xF030;
|
||||||
|
@ -166,11 +139,7 @@ namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
[DllImport("user32", SetLastError=true, EntryPoint = "PostMessageA")]
|
[DllImport("user32", SetLastError=true, EntryPoint = "PostMessageA")]
|
||||||
public static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
|
public static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
|
||||||
[DllImport("user32", SetLastError = true)]
|
[DllImport("user32", SetLastError = true)]
|
||||||
public static extern RegionResult GetWindowRgn(IntPtr hWnd, IntPtr hRgn);
|
public static extern RegionResult GetWindowRgn(IntPtr hWnd, SafeHandle hRgn);
|
||||||
[DllImport("user32", SetLastError = true)]
|
|
||||||
public static extern IntPtr GetWindowDC(IntPtr hWnd);
|
|
||||||
[DllImport("user32", SetLastError = true)]
|
|
||||||
public static extern IntPtr ReleaseDC(IntPtr hWnd,IntPtr hDC);
|
|
||||||
[DllImport("user32", SetLastError = true)]
|
[DllImport("user32", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, WindowPos uFlags);
|
public static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, WindowPos uFlags);
|
||||||
|
@ -236,8 +205,6 @@ namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
[DllImport("user32", SetLastError = true)]
|
[DllImport("user32", SetLastError = true)]
|
||||||
public static extern bool GetIconInfo(SafeIconHandle iconHandle, out IconInfo iconInfo);
|
public static extern bool GetIconInfo(SafeIconHandle iconHandle, out IconInfo iconInfo);
|
||||||
[DllImport("user32", SetLastError = true)]
|
[DllImport("user32", SetLastError = true)]
|
||||||
public static extern bool DrawIcon(IntPtr hDC, int X, int Y, IntPtr hIcon);
|
|
||||||
[DllImport("user32", SetLastError = true)]
|
|
||||||
public static extern IntPtr SetCapture(IntPtr hWnd);
|
public static extern IntPtr SetCapture(IntPtr hWnd);
|
||||||
[DllImport("user32", SetLastError = true)]
|
[DllImport("user32", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
@ -306,4 +273,64 @@ namespace GreenshotPlugin.UnmanagedHelpers {
|
||||||
return exceptionToThrow;
|
return exceptionToThrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used with SetWinEventHook
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hWinEventHook"></param>
|
||||||
|
/// <param name="eventType"></param>
|
||||||
|
/// <param name="hwnd"></param>
|
||||||
|
/// <param name="idObject"></param>
|
||||||
|
/// <param name="idChild"></param>
|
||||||
|
/// <param name="dwEventThread"></param>
|
||||||
|
/// <param name="dwmsEventTime"></param>
|
||||||
|
public delegate void WinEventDelegate(IntPtr hWinEventHook, WinEvent eventType, IntPtr hwnd, EventObjects idObject, int idChild, uint dwEventThread, uint dwmsEventTime);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A SafeHandle class implementation for the hIcon
|
||||||
|
/// </summary>
|
||||||
|
public class SafeIconHandle : SafeHandleZeroOrMinusOneIsInvalid {
|
||||||
|
private SafeIconHandle() : base(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public SafeIconHandle(IntPtr hIcon) : base(true) {
|
||||||
|
this.SetHandle(hIcon);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ReleaseHandle() {
|
||||||
|
return User32.DestroyIcon(this.handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A WindowDC SafeHandle implementation
|
||||||
|
/// </summary>
|
||||||
|
public class SafeWindowDCHandle : SafeHandleZeroOrMinusOneIsInvalid {
|
||||||
|
[DllImport("user32", SetLastError = true)]
|
||||||
|
private static extern IntPtr GetWindowDC(IntPtr hWnd);
|
||||||
|
[DllImport("user32", SetLastError = true)]
|
||||||
|
private static extern bool ReleaseDC(IntPtr hWnd, IntPtr hDC);
|
||||||
|
|
||||||
|
private IntPtr hWnd;
|
||||||
|
[SecurityCritical]
|
||||||
|
private SafeWindowDCHandle() : base(true) {
|
||||||
|
}
|
||||||
|
|
||||||
|
[SecurityCritical]
|
||||||
|
public SafeWindowDCHandle(IntPtr hWnd, IntPtr preexistingHandle) : base(true) {
|
||||||
|
this.hWnd = hWnd;
|
||||||
|
SetHandle(preexistingHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool ReleaseHandle() {
|
||||||
|
bool returnValue = ReleaseDC(hWnd, handle);
|
||||||
|
return returnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static SafeWindowDCHandle fromDesktop() {
|
||||||
|
IntPtr hWndDesktop = User32.GetDesktopWindow();
|
||||||
|
IntPtr hDCDesktop = GetWindowDC(hWndDesktop);
|
||||||
|
return new SafeWindowDCHandle(hWndDesktop, hDCDesktop);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue