v1.2
This commit is contained in:
106
FSI.BT.Tools/SystemTrayMenu/Helpers/DgvMouseRow.cs
Normal file
106
FSI.BT.Tools/SystemTrayMenu/Helpers/DgvMouseRow.cs
Normal file
@@ -0,0 +1,106 @@
|
||||
// <copyright file="DgvMouseRow.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
public class DgvMouseRow : IDisposable
|
||||
{
|
||||
private readonly Timer timerRaiseRowMouseLeave = new();
|
||||
private DataGridView dgv;
|
||||
private DataGridViewCellEventArgs eventArgs;
|
||||
|
||||
internal DgvMouseRow()
|
||||
{
|
||||
timerRaiseRowMouseLeave.Interval = 200;
|
||||
timerRaiseRowMouseLeave.Tick += TimerRaiseRowMouseLeave_Tick;
|
||||
}
|
||||
|
||||
~DgvMouseRow() // the finalizer
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
internal event Action<object, DataGridViewCellEventArgs> RowMouseEnter;
|
||||
|
||||
internal event Action<object, DataGridViewCellEventArgs> RowMouseLeave;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
internal void CellMouseEnter(object sender, DataGridViewCellEventArgs newEventArgs)
|
||||
{
|
||||
DataGridView newDgv = (DataGridView)sender;
|
||||
|
||||
if (dgv != newDgv || newEventArgs.RowIndex != eventArgs.RowIndex)
|
||||
{
|
||||
if (timerRaiseRowMouseLeave.Enabled)
|
||||
{
|
||||
timerRaiseRowMouseLeave.Stop();
|
||||
TriggerRowMouseLeave();
|
||||
}
|
||||
|
||||
TriggerRowMouseEnter(newDgv, newEventArgs);
|
||||
}
|
||||
else
|
||||
{
|
||||
timerRaiseRowMouseLeave.Stop();
|
||||
}
|
||||
|
||||
dgv = newDgv;
|
||||
eventArgs = newEventArgs;
|
||||
}
|
||||
|
||||
internal void CellMouseLeave(object sender, DataGridViewCellEventArgs e)
|
||||
{
|
||||
timerRaiseRowMouseLeave.Start();
|
||||
}
|
||||
|
||||
internal void MouseLeave(object sender, EventArgs e)
|
||||
{
|
||||
if (timerRaiseRowMouseLeave.Enabled)
|
||||
{
|
||||
timerRaiseRowMouseLeave.Stop();
|
||||
TriggerRowMouseLeave();
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
timerRaiseRowMouseLeave.Tick -= TimerRaiseRowMouseLeave_Tick;
|
||||
timerRaiseRowMouseLeave.Dispose();
|
||||
dgv = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void TimerRaiseRowMouseLeave_Tick(object sender, EventArgs e)
|
||||
{
|
||||
timerRaiseRowMouseLeave.Stop();
|
||||
TriggerRowMouseLeave();
|
||||
}
|
||||
|
||||
private void TriggerRowMouseLeave()
|
||||
{
|
||||
if (dgv != null)
|
||||
{
|
||||
RowMouseLeave?.Invoke(dgv, eventArgs);
|
||||
}
|
||||
|
||||
dgv = null;
|
||||
eventArgs = null;
|
||||
}
|
||||
|
||||
private void TriggerRowMouseEnter(DataGridView dgv, DataGridViewCellEventArgs e)
|
||||
{
|
||||
RowMouseEnter?.Invoke(dgv, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
191
FSI.BT.Tools/SystemTrayMenu/Helpers/DragDropHelper.cs
Normal file
191
FSI.BT.Tools/SystemTrayMenu/Helpers/DragDropHelper.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
// <copyright file="DragDropHelper.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using FSI.BT.Tools.Global.Utilities;
|
||||
using FSI.BT.Tools.SystemTrayMenu.DataClasses;
|
||||
using FSI.BT.Tools.SystemTrayMenu.UserInterface;
|
||||
using FSI.BT.Tools.SystemTrayMenu.Utilities;
|
||||
|
||||
public static class DragDropHelper
|
||||
{
|
||||
public static void DragEnter(object sender, DragEventArgs e)
|
||||
{
|
||||
object data = e.Data.GetData("UniformResourceLocator");
|
||||
|
||||
if (data is MemoryStream memoryStream)
|
||||
{
|
||||
byte[] bytes = memoryStream.ToArray();
|
||||
Encoding encod = Encoding.ASCII;
|
||||
string url = encod.GetString(bytes);
|
||||
if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
e.Effect = DragDropEffects.Copy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void DragDrop(object sender, DragEventArgs e)
|
||||
{
|
||||
Menu menu = (Menu)sender;
|
||||
string path;
|
||||
if (menu != null)
|
||||
{
|
||||
RowData rowData = (RowData)menu.Tag;
|
||||
if (rowData != null)
|
||||
{
|
||||
path = rowData.ResolvedPath;
|
||||
}
|
||||
else
|
||||
{
|
||||
path = Config.Path;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
path = Config.Path;
|
||||
}
|
||||
|
||||
object data = e.Data.GetData("UniformResourceLocator");
|
||||
MemoryStream ms = data as MemoryStream;
|
||||
byte[] bytes = ms.ToArray();
|
||||
Encoding encod = Encoding.ASCII;
|
||||
string url = encod.GetString(bytes);
|
||||
|
||||
new Thread(CreateShortcutInBackground).Start();
|
||||
void CreateShortcutInBackground()
|
||||
{
|
||||
CreateShortcut(url.Replace("\0", string.Empty), path);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CreateShortcut(string url, string pathToStoreFile)
|
||||
{
|
||||
string title = GetUrlShortcutTitle(url);
|
||||
string fileNamePathShortcut = pathToStoreFile + "\\" + title.Trim() + ".url";
|
||||
WriteShortcut(url, null, fileNamePathShortcut);
|
||||
string pathIcon = DownloadUrlIcon(url);
|
||||
if (!string.IsNullOrEmpty(pathIcon))
|
||||
{
|
||||
WriteShortcut(url, pathIcon, fileNamePathShortcut);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetUrlShortcutTitle(string url)
|
||||
{
|
||||
string title = url
|
||||
.Replace("/", " ")
|
||||
.Replace("https", string.Empty)
|
||||
.Replace("http", string.Empty);
|
||||
string invalid =
|
||||
new string(Path.GetInvalidFileNameChars()) +
|
||||
new string(Path.GetInvalidPathChars());
|
||||
foreach (char character in invalid)
|
||||
{
|
||||
title = title.Replace(character.ToString(), string.Empty);
|
||||
}
|
||||
|
||||
title = Truncate(title, 128); // max 255
|
||||
return title;
|
||||
}
|
||||
|
||||
private static string Truncate(string value, int maxLength)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(value) &&
|
||||
value.Length > maxLength)
|
||||
{
|
||||
value = value[..maxLength];
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void WriteShortcut(string url, string pathIcon, string fileNamePathShortcut)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(fileNamePathShortcut))
|
||||
{
|
||||
File.Delete(fileNamePathShortcut);
|
||||
}
|
||||
|
||||
StreamWriter writer = new(fileNamePathShortcut);
|
||||
writer.WriteLine("[InternetShortcut]");
|
||||
writer.WriteLine($"URL={url.TrimEnd('\0')}");
|
||||
writer.WriteLine("IconIndex=0");
|
||||
writer.WriteLine($"HotKey=0");
|
||||
writer.WriteLine($"IDList=");
|
||||
if (!string.IsNullOrEmpty(pathIcon))
|
||||
{
|
||||
writer.WriteLine($"IconFile={pathIcon}");
|
||||
}
|
||||
|
||||
writer.Flush();
|
||||
writer.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn($"{nameof(WriteShortcut)} failed", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string DownloadUrlIcon(string url)
|
||||
{
|
||||
string pathIcon = string.Empty;
|
||||
string pathToStoreIcons = Global.Vars.SystemTrayMenuSettings.PathIcoDirectory;
|
||||
Uri uri = new(url);
|
||||
string hostname = uri.Host.ToString();
|
||||
string pathIconPng = Path.Combine(pathToStoreIcons, $"{hostname}.png");
|
||||
string urlGoogleIconDownload = @"http://www.google.com/s2/favicons?sz=32&domain=" + url;
|
||||
HttpClient client = new();
|
||||
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(pathToStoreIcons))
|
||||
{
|
||||
Directory.CreateDirectory(pathToStoreIcons);
|
||||
}
|
||||
|
||||
using HttpResponseMessage response = client.GetAsync(urlGoogleIconDownload).Result;
|
||||
using HttpContent content = response.Content;
|
||||
Stream stream = content.ReadAsStreamAsync().Result;
|
||||
using var fileStream = File.Create(pathIconPng);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
stream.CopyTo(fileStream);
|
||||
fileStream.Close();
|
||||
|
||||
pathIcon = Path.Combine(pathToStoreIcons, $"{hostname}.ico");
|
||||
if (!ImagingHelper.ConvertToIcon(pathIconPng, pathIcon, 32))
|
||||
{
|
||||
Log.Info("Failed to convert icon.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn($"{nameof(DownloadUrlIcon)} failed", ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (File.Exists(pathIconPng))
|
||||
{
|
||||
File.Delete(pathIconPng);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warn($"{nameof(DownloadUrlIcon)} failed to delete {pathIconPng}", ex);
|
||||
}
|
||||
|
||||
return pathIcon;
|
||||
}
|
||||
}
|
||||
}
|
||||
183
FSI.BT.Tools/SystemTrayMenu/Helpers/Fading.cs
Normal file
183
FSI.BT.Tools/SystemTrayMenu/Helpers/Fading.cs
Normal file
@@ -0,0 +1,183 @@
|
||||
// <copyright file="Fading.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using FSI.BT.Tools.SystemTrayMenu.Utilities;
|
||||
|
||||
public class Fading : IDisposable
|
||||
{
|
||||
private const int Interval100FPS = 10; // 100fps=>1s/100fps=~10ms
|
||||
|
||||
private const double StepIn = 0.20;
|
||||
private const double StepOut = 0.10;
|
||||
private const double Transparent = 0.80;
|
||||
private const double TransparentMinus = 0.60; // Transparent - StepIn
|
||||
private const double TransparentPlus = 0.85; // Transparent + StepOut
|
||||
private const double Shown = 1.00;
|
||||
private const double ShownMinus = 0.80; // Shown - StepIn
|
||||
|
||||
private readonly Timer timer = new();
|
||||
private FadingState state = FadingState.Idle;
|
||||
private double opacity;
|
||||
private bool visible;
|
||||
|
||||
internal Fading()
|
||||
{
|
||||
timer.Interval = Interval100FPS;
|
||||
timer.Tick += Timer_Tick;
|
||||
}
|
||||
|
||||
~Fading() // the finalizer
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
internal event Action Hide;
|
||||
|
||||
internal event Action Show;
|
||||
|
||||
internal event Action<double> ChangeOpacity;
|
||||
|
||||
internal enum FadingState
|
||||
{
|
||||
Idle,
|
||||
Show,
|
||||
ShowTransparent,
|
||||
Hide,
|
||||
}
|
||||
|
||||
internal bool IsHiding => state == FadingState.Hide;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
internal void Fade(FadingState state)
|
||||
{
|
||||
StartStopTimer(state);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
timer.Tick -= Timer_Tick;
|
||||
timer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private void StartStopTimer(FadingState newState)
|
||||
{
|
||||
if (newState == FadingState.Idle)
|
||||
{
|
||||
state = newState;
|
||||
timer.Stop();
|
||||
}
|
||||
else
|
||||
{
|
||||
state = newState;
|
||||
timer.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private void Timer_Tick(object sender, EventArgs e)
|
||||
{
|
||||
FadeStep();
|
||||
}
|
||||
|
||||
private void FadeStep()
|
||||
{
|
||||
switch (state)
|
||||
{
|
||||
case FadingState.Show:
|
||||
if (!visible)
|
||||
{
|
||||
visible = true;
|
||||
Show?.Invoke();
|
||||
opacity = 0;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
}
|
||||
else if (Global.Vars.SystemTrayMenuSettings.UseFading &&
|
||||
opacity < ShownMinus)
|
||||
{
|
||||
opacity += StepIn;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Global.Vars.SystemTrayMenuSettings.UseFading)
|
||||
{
|
||||
// #393 provoke a redraw for the CS_DROPSHADOW to work
|
||||
opacity = ShownMinus;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
}
|
||||
|
||||
opacity = Shown;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
StartStopTimer(FadingState.Idle);
|
||||
}
|
||||
|
||||
break;
|
||||
case FadingState.ShowTransparent:
|
||||
if (!visible)
|
||||
{
|
||||
visible = true;
|
||||
Show?.Invoke();
|
||||
opacity = 0;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
}
|
||||
else if (Global.Vars.SystemTrayMenuSettings.UseFading &&
|
||||
opacity < TransparentMinus)
|
||||
{
|
||||
opacity += StepIn;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
}
|
||||
else if (Global.Vars.SystemTrayMenuSettings.UseFading &&
|
||||
opacity > TransparentPlus)
|
||||
{
|
||||
opacity -= StepOut;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
}
|
||||
else
|
||||
{
|
||||
opacity = Transparent;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
StartStopTimer(FadingState.Idle);
|
||||
}
|
||||
|
||||
break;
|
||||
case FadingState.Hide:
|
||||
if (Global.Vars.SystemTrayMenuSettings.UseFading &&
|
||||
opacity > StepOut)
|
||||
{
|
||||
opacity -= StepOut;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
}
|
||||
else if (visible)
|
||||
{
|
||||
opacity = 0;
|
||||
ChangeOpacity?.Invoke(opacity);
|
||||
visible = false;
|
||||
Hide?.Invoke();
|
||||
StartStopTimer(FadingState.Idle);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartStopTimer(FadingState.Idle);
|
||||
}
|
||||
|
||||
break;
|
||||
case FadingState.Idle:
|
||||
default:
|
||||
StartStopTimer(FadingState.Idle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
147
FSI.BT.Tools/SystemTrayMenu/Helpers/ImagingHelper.cs
Normal file
147
FSI.BT.Tools/SystemTrayMenu/Helpers/ImagingHelper.cs
Normal file
@@ -0,0 +1,147 @@
|
||||
// <copyright file="ImagingHelper.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System.Drawing;
|
||||
using System.Drawing.Drawing2D;
|
||||
using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for imaging.
|
||||
/// </summary>
|
||||
public static class ImagingHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a PNG image to a icon (ico).
|
||||
/// </summary>
|
||||
/// <param name="input">The input stream.</param>
|
||||
/// <param name="output">The output stream.</param>
|
||||
/// <param name="size">The size (16x16 px by default).</param>
|
||||
/// <param name="preserveAspectRatio">Preserve the aspect ratio.</param>
|
||||
/// <returns>Wether or not the icon was succesfully generated.</returns>
|
||||
public static bool ConvertToIcon(Stream input, Stream output, int size = 16, bool preserveAspectRatio = false)
|
||||
{
|
||||
Bitmap inputBitmap = (Bitmap)Image.FromStream(input);
|
||||
if (inputBitmap != null)
|
||||
{
|
||||
int width, height;
|
||||
if (preserveAspectRatio)
|
||||
{
|
||||
width = size;
|
||||
height = inputBitmap.Height / inputBitmap.Width * size;
|
||||
}
|
||||
else
|
||||
{
|
||||
width = height = size;
|
||||
}
|
||||
|
||||
Bitmap newBitmap = new(inputBitmap, new Size(width, height));
|
||||
if (newBitmap != null)
|
||||
{
|
||||
// save the resized png into a memory stream for future use
|
||||
using MemoryStream memoryStream = new();
|
||||
newBitmap.Save(memoryStream, ImageFormat.Png);
|
||||
|
||||
BinaryWriter iconWriter = new(output);
|
||||
if (output != null && iconWriter != null)
|
||||
{
|
||||
// 0-1 reserved, 0
|
||||
iconWriter.Write((byte)0);
|
||||
iconWriter.Write((byte)0);
|
||||
|
||||
// 2-3 image type, 1 = icon, 2 = cursor
|
||||
iconWriter.Write((short)1);
|
||||
|
||||
// 4-5 number of images
|
||||
iconWriter.Write((short)1);
|
||||
|
||||
// image entry 1
|
||||
// 0 image width
|
||||
iconWriter.Write((byte)width);
|
||||
|
||||
// 1 image height
|
||||
iconWriter.Write((byte)height);
|
||||
|
||||
// 2 number of colors
|
||||
iconWriter.Write((byte)0);
|
||||
|
||||
// 3 reserved
|
||||
iconWriter.Write((byte)0);
|
||||
|
||||
// 4-5 color planes
|
||||
iconWriter.Write((short)0);
|
||||
|
||||
// 6-7 bits per pixel
|
||||
iconWriter.Write((short)32);
|
||||
|
||||
// 8-11 size of image data
|
||||
iconWriter.Write((int)memoryStream.Length);
|
||||
|
||||
// 12-15 offset of image data
|
||||
iconWriter.Write(6 + 16);
|
||||
|
||||
// write image data
|
||||
// png data must contain the whole png data file
|
||||
iconWriter.Write(memoryStream.ToArray());
|
||||
|
||||
iconWriter.Flush();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a PNG image to a icon (ico).
|
||||
/// </summary>
|
||||
/// <param name="inputPath">The input path.</param>
|
||||
/// <param name="outputPath">The output path.</param>
|
||||
/// <param name="size">The size (16x16 px by default).</param>
|
||||
/// <param name="preserveAspectRatio">Preserve the aspect ratio.</param>
|
||||
/// <returns>Wether or not the icon was succesfully generated.</returns>
|
||||
public static bool ConvertToIcon(string inputPath, string outputPath, int size = 16, bool preserveAspectRatio = false)
|
||||
{
|
||||
using FileStream inputStream = new(inputPath, FileMode.Open);
|
||||
using FileStream outputStream = new(outputPath, FileMode.OpenOrCreate);
|
||||
return ConvertToIcon(inputStream, outputStream, size, preserveAspectRatio);
|
||||
}
|
||||
|
||||
public static Image RotateImage(Image img, float rotationAngle)
|
||||
{
|
||||
// create an empty Bitmap image
|
||||
Bitmap bmp = new(img.Width, img.Height);
|
||||
|
||||
// turn the Bitmap into a Graphics object
|
||||
Graphics gfx = Graphics.FromImage(bmp);
|
||||
|
||||
// now we set the rotation point to the center of our image
|
||||
gfx.TranslateTransform(0.5f + ((float)bmp.Width / 2), 0.5f + ((float)bmp.Height / 2));
|
||||
|
||||
// now rotate the image
|
||||
gfx.RotateTransform(rotationAngle);
|
||||
|
||||
gfx.TranslateTransform(0.5f - ((float)bmp.Width / 2), 0.5f - ((float)bmp.Height / 2));
|
||||
|
||||
// set the InterpolationMode to HighQualityBicubic so to ensure a high
|
||||
// quality image once it is transformed to the specified size
|
||||
gfx.InterpolationMode = InterpolationMode.HighQualityBicubic;
|
||||
|
||||
// now draw our new image onto the graphics object
|
||||
gfx.DrawImage(img, new Point(0, 0));
|
||||
|
||||
// dispose of our Graphics object
|
||||
gfx.Dispose();
|
||||
|
||||
// return the image
|
||||
return bmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
215
FSI.BT.Tools/SystemTrayMenu/Helpers/JoystickHelper.cs
Normal file
215
FSI.BT.Tools/SystemTrayMenu/Helpers/JoystickHelper.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
// <copyright file="JoystickHelper.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helpers
|
||||
{
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.Metrics;
|
||||
using System.Reflection.Metadata;
|
||||
using System.Threading;
|
||||
using System.Windows.Forms;
|
||||
using SharpDX.DirectInput;
|
||||
|
||||
public class JoystickHelper : IDisposable
|
||||
{
|
||||
private readonly System.Timers.Timer timerReadJoystick = new();
|
||||
private readonly object lockRead = new();
|
||||
private Joystick joystick;
|
||||
private Keys pressingKey;
|
||||
private int pressingKeyCounter;
|
||||
private bool joystickHelperEnabled;
|
||||
|
||||
public JoystickHelper()
|
||||
{
|
||||
timerReadJoystick.Interval = 80;
|
||||
timerReadJoystick.Elapsed += ReadJoystickLoop;
|
||||
timerReadJoystick.Enabled = false;
|
||||
if (Global.Vars.SystemTrayMenuSettings.SupportGamepad)
|
||||
{
|
||||
timerReadJoystick.Start();
|
||||
}
|
||||
}
|
||||
|
||||
~JoystickHelper() // the finalizer
|
||||
{
|
||||
Dispose(false);
|
||||
}
|
||||
|
||||
public event Action<Keys> KeyPressed;
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
joystickHelperEnabled = true;
|
||||
}
|
||||
|
||||
public void Disable()
|
||||
{
|
||||
joystickHelperEnabled = false;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Dispose(true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
timerReadJoystick.Elapsed -= ReadJoystickLoop;
|
||||
timerReadJoystick?.Dispose();
|
||||
joystick?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static Keys ReadKeyFromState(JoystickUpdate state)
|
||||
{
|
||||
Keys keys = Keys.None;
|
||||
switch (state.Offset)
|
||||
{
|
||||
case JoystickOffset.PointOfViewControllers0:
|
||||
switch (state.Value)
|
||||
{
|
||||
case 0:
|
||||
keys = Keys.Up;
|
||||
break;
|
||||
case 9000:
|
||||
keys = Keys.Right;
|
||||
break;
|
||||
case 18000:
|
||||
keys = Keys.Down;
|
||||
break;
|
||||
case 27000:
|
||||
keys = Keys.Left;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
case JoystickOffset.Buttons0:
|
||||
if (state.Value == 128)
|
||||
{
|
||||
keys = Keys.Enter;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
private void ReadJoystickLoop(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
if (joystickHelperEnabled)
|
||||
{
|
||||
lock (lockRead)
|
||||
{
|
||||
timerReadJoystick.Stop();
|
||||
if (joystick == null)
|
||||
{
|
||||
Thread.Sleep(3000);
|
||||
InitializeJoystick();
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadJoystick();
|
||||
}
|
||||
|
||||
timerReadJoystick.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReadJoystick()
|
||||
{
|
||||
try
|
||||
{
|
||||
joystick.Poll();
|
||||
JoystickUpdate[] datas = joystick.GetBufferedData();
|
||||
foreach (JoystickUpdate state in datas)
|
||||
{
|
||||
if (state.Value < 0)
|
||||
{
|
||||
pressingKey = Keys.None;
|
||||
pressingKeyCounter = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
Keys key = ReadKeyFromState(state);
|
||||
if (key != Keys.None)
|
||||
{
|
||||
KeyPressed?.Invoke(key);
|
||||
if (state.Offset == JoystickOffset.PointOfViewControllers0)
|
||||
{
|
||||
pressingKeyCounter = 0;
|
||||
pressingKey = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pressingKey != Keys.None)
|
||||
{
|
||||
pressingKeyCounter += 1;
|
||||
if (pressingKeyCounter > 1)
|
||||
{
|
||||
KeyPressed?.Invoke(pressingKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
joystick?.Dispose();
|
||||
joystick = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeJoystick()
|
||||
{
|
||||
// Initialize DirectInput
|
||||
DirectInput directInput = new();
|
||||
|
||||
// Find a Joystick Guid
|
||||
Guid joystickGuid = Guid.Empty;
|
||||
|
||||
foreach (DeviceInstance deviceInstance in directInput.GetDevices(
|
||||
DeviceType.Gamepad,
|
||||
DeviceEnumerationFlags.AllDevices))
|
||||
{
|
||||
joystickGuid = deviceInstance.InstanceGuid;
|
||||
}
|
||||
|
||||
// If Gamepad not found, look for a Joystick
|
||||
if (joystickGuid == Guid.Empty)
|
||||
{
|
||||
foreach (DeviceInstance deviceInstance in directInput.GetDevices(
|
||||
DeviceType.Joystick,
|
||||
DeviceEnumerationFlags.AllDevices))
|
||||
{
|
||||
joystickGuid = deviceInstance.InstanceGuid;
|
||||
}
|
||||
}
|
||||
|
||||
// If Joystick found
|
||||
if (joystickGuid != Guid.Empty)
|
||||
{
|
||||
// Instantiate the joystick
|
||||
joystick = new Joystick(directInput, joystickGuid);
|
||||
|
||||
// Set BufferSize in order to use buffered data.
|
||||
joystick.Properties.BufferSize = 128;
|
||||
|
||||
var handle = Process.GetCurrentProcess().MainWindowHandle;
|
||||
joystick.SetCooperativeLevel(handle, CooperativeLevel.NonExclusive | CooperativeLevel.Background);
|
||||
|
||||
// Acquire the joystick
|
||||
joystick.Acquire();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
27
FSI.BT.Tools/SystemTrayMenu/Helpers/KeyPressedEventArgs.cs
Normal file
27
FSI.BT.Tools/SystemTrayMenu/Helpers/KeyPressedEventArgs.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// <copyright file="KeyPressedEventArgs.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
|
||||
/// <summary>
|
||||
/// Event Args for the event that is fired after the hot key has been pressed.
|
||||
/// </summary>
|
||||
internal class KeyPressedEventArgs : EventArgs
|
||||
{
|
||||
private readonly Keys key;
|
||||
|
||||
internal KeyPressedEventArgs(KeyboardHookModifierKeys modifier, Keys key)
|
||||
{
|
||||
Modifier = modifier;
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
internal KeyboardHookModifierKeys Modifier { get; }
|
||||
|
||||
internal Keys Key => key;
|
||||
}
|
||||
}
|
||||
167
FSI.BT.Tools/SystemTrayMenu/Helpers/KeyboardHook.cs
Normal file
167
FSI.BT.Tools/SystemTrayMenu/Helpers/KeyboardHook.cs
Normal file
@@ -0,0 +1,167 @@
|
||||
// <copyright file="KeyboardHook.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System;
|
||||
using System.Windows.Forms;
|
||||
using FSI.BT.Tools.Global.DllImports;
|
||||
using FSI.BT.Tools.Global.UserInterface.HotkeyTextboxControl;
|
||||
using FSI.BT.Tools.SystemTrayMenu.Utilities;
|
||||
|
||||
/// <summary>
|
||||
/// The enumeration of possible modifiers.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum KeyboardHookModifierKeys : uint
|
||||
{
|
||||
None = 0,
|
||||
Alt = 1,
|
||||
Control = 2,
|
||||
Shift = 4,
|
||||
Win = 8,
|
||||
}
|
||||
|
||||
public sealed class KeyboardHook : IDisposable
|
||||
{
|
||||
private readonly Window window = new();
|
||||
private int currentId;
|
||||
|
||||
public KeyboardHook()
|
||||
{
|
||||
// register the event of the inner native window.
|
||||
window.KeyPressed += Window_KeyPressed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A hot key has been pressed.
|
||||
/// </summary>
|
||||
internal event EventHandler<KeyPressedEventArgs> KeyPressed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// unregister all the registered hot keys.
|
||||
for (int i = currentId; i > 0; i--)
|
||||
{
|
||||
NativeMethods.User32UnregisterHotKey(window.Handle, i);
|
||||
}
|
||||
|
||||
// dispose the inner native window.
|
||||
window.KeyPressed -= Window_KeyPressed;
|
||||
window.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a hot key in the system.
|
||||
/// </summary>
|
||||
/// <param name="key">The key itself that is associated with the hot key.</param>
|
||||
internal void RegisterHotKey(Keys key)
|
||||
{
|
||||
uint keyModifiersNone = 0;
|
||||
RegisterHotKey(keyModifiersNone, key);
|
||||
}
|
||||
|
||||
internal void RegisterHotKey()
|
||||
{
|
||||
KeyboardHookModifierKeys modifiers = KeyboardHookModifierKeys.None;
|
||||
string modifiersString = Global.Vars.SystemTrayMenuSettings.HotKey;
|
||||
if (!string.IsNullOrEmpty(modifiersString))
|
||||
{
|
||||
if (modifiersString.ToUpperInvariant().Contains("ALT", StringComparison.InvariantCulture))
|
||||
{
|
||||
modifiers |= KeyboardHookModifierKeys.Alt;
|
||||
}
|
||||
|
||||
if (modifiersString.ToUpperInvariant().Contains("CTRL", StringComparison.InvariantCulture) ||
|
||||
modifiersString.ToUpperInvariant().Contains("STRG", StringComparison.InvariantCulture))
|
||||
{
|
||||
modifiers |= KeyboardHookModifierKeys.Control;
|
||||
}
|
||||
|
||||
if (modifiersString.ToUpperInvariant().Contains("SHIFT", StringComparison.InvariantCulture))
|
||||
{
|
||||
modifiers |= KeyboardHookModifierKeys.Shift;
|
||||
}
|
||||
|
||||
if (modifiersString.ToUpperInvariant().Contains("WIN", StringComparison.InvariantCulture))
|
||||
{
|
||||
modifiers |= KeyboardHookModifierKeys.Win;
|
||||
}
|
||||
}
|
||||
|
||||
RegisterHotKey(
|
||||
modifiers,
|
||||
HotkeyControl.HotkeyFromString(
|
||||
Global.Vars.SystemTrayMenuSettings.HotKey));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers a hot key in the system.
|
||||
/// </summary>
|
||||
/// <param name="modifier">The modifiers that are associated with the hot key.</param>
|
||||
/// <param name="key">The key itself that is associated with the hot key.</param>
|
||||
internal void RegisterHotKey(KeyboardHookModifierKeys modifier, Keys key)
|
||||
{
|
||||
RegisterHotKey((uint)modifier, key);
|
||||
}
|
||||
|
||||
private void Window_KeyPressed(object sender, KeyPressedEventArgs e)
|
||||
{
|
||||
KeyPressed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
private void RegisterHotKey(uint modifier, Keys key)
|
||||
{
|
||||
currentId += 1;
|
||||
|
||||
if (!NativeMethods.User32RegisterHotKey(
|
||||
window.Handle, currentId, modifier, (uint)key))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
Global.Utilities.Translator.GetText("Could not register the hot key."));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Represents the window that is used internally to get the messages.
|
||||
/// </summary>
|
||||
private class Window : NativeWindow, IDisposable
|
||||
{
|
||||
private const int WmHotkey = 0x0312;
|
||||
|
||||
public Window()
|
||||
{
|
||||
// create the handle for the window.
|
||||
CreateHandle(new CreateParams());
|
||||
}
|
||||
|
||||
public event EventHandler<KeyPressedEventArgs> KeyPressed;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DestroyHandle();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overridden to get the notifications.
|
||||
/// </summary>
|
||||
/// <param name="m">m.</param>
|
||||
protected override void WndProc(ref Message m)
|
||||
{
|
||||
base.WndProc(ref m);
|
||||
|
||||
// check if we got a hot key pressed.
|
||||
if (m.Msg == WmHotkey)
|
||||
{
|
||||
// get the keys.
|
||||
Keys key = (Keys)(((int)m.LParam >> 16) & 0xFFFF);
|
||||
KeyboardHookModifierKeys modifier = (KeyboardHookModifierKeys)((int)m.LParam & 0xFFFF);
|
||||
|
||||
// invoke the event to notify the parent.
|
||||
KeyPressed?.Invoke(this, new KeyPressedEventArgs(modifier, key));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
252
FSI.BT.Tools/SystemTrayMenu/Helpers/Updater/GitHubUpdate.cs
Normal file
252
FSI.BT.Tools/SystemTrayMenu/Helpers/Updater/GitHubUpdate.cs
Normal file
@@ -0,0 +1,252 @@
|
||||
//// <copyright file="GitHubUpdate.cs" company="PlaceholderCompany">
|
||||
//// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
//// </copyright>
|
||||
|
||||
//namespace FSI.BT.Tools.SystemTrayMenu.Helper.Updater
|
||||
//{
|
||||
// using System;
|
||||
// using System.Collections.Generic;
|
||||
// using System.Drawing;
|
||||
// using System.Net.Http;
|
||||
// using System.Reflection;
|
||||
// using System.Windows.Forms;
|
||||
// using FSI.BT.Tools.Global.Utilities;
|
||||
// using FSI.BT.Tools.SystemTrayMenu.Utilities;
|
||||
|
||||
// public class GitHubUpdate
|
||||
// {
|
||||
// private static List<Dictionary<string, object>> releases;
|
||||
// private static Form newVersionForm;
|
||||
|
||||
// public static void ActivateNewVersionFormOrCheckForUpdates(bool showWhenUpToDate)
|
||||
// {
|
||||
// if (newVersionForm != null)
|
||||
// {
|
||||
// newVersionForm.HandleInvoke(newVersionForm.Activate);
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// CheckForUpdates(showWhenUpToDate);
|
||||
// }
|
||||
// }
|
||||
|
||||
// private static void CheckForUpdates(bool showWhenUpToDate)
|
||||
// {
|
||||
// string urlGithubReleases = @"http://api.github.com/repos/Hofknecht/SystemTrayMenu/releases";
|
||||
// HttpClient client = new();
|
||||
|
||||
// // https://developer.github.com/v3/#user-agent-required
|
||||
// client.DefaultRequestHeaders.Add("User-Agent", "FSI.BT.Tools.SystemTrayMenu/" + Application.ProductVersion.ToString());
|
||||
|
||||
// // https://developer.github.com/v3/media/#request-specific-version
|
||||
// client.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3.text+json");
|
||||
|
||||
// try
|
||||
// {
|
||||
// using HttpResponseMessage response = client.GetAsync(urlGithubReleases).Result;
|
||||
// using HttpContent content = response.Content;
|
||||
// string responseString = content.ReadAsStringAsync().Result;
|
||||
// releases = responseString.FromJson<List<Dictionary<string, object>>>();
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Warn($"{nameof(CheckForUpdates)} failed", ex);
|
||||
// }
|
||||
|
||||
// if (releases == null)
|
||||
// {
|
||||
// Log.Info($"{nameof(CheckForUpdates)} failed.");
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// RemoveCurrentAndOlderVersions();
|
||||
// ShowNewVersionOrUpToDateDialog(showWhenUpToDate);
|
||||
// }
|
||||
|
||||
// newVersionForm?.Dispose();
|
||||
// newVersionForm = null;
|
||||
// }
|
||||
|
||||
// private static void RemoveCurrentAndOlderVersions()
|
||||
// {
|
||||
// int releasesCount = releases.Count;
|
||||
// Version versionCurrent = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
// for (int i = 0; i < releasesCount; i++)
|
||||
// {
|
||||
// string tagName = releases[i]["tag_name"].ToString();
|
||||
// Version versionGitHub = new(tagName.Replace("v", string.Empty));
|
||||
// if (versionGitHub.CompareTo(versionCurrent) < 1)
|
||||
// {
|
||||
// releases.RemoveRange(i, releasesCount - i);
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// private static void ShowNewVersionOrUpToDateDialog(bool showWhenUpToDate)
|
||||
// {
|
||||
// if (releases.Count > 0)
|
||||
// {
|
||||
// if (NewVersionDialog() == DialogResult.Yes)
|
||||
// {
|
||||
// Log.ProcessStart("https://github.com/Hofknecht/SystemTrayMenu/releases");
|
||||
// }
|
||||
// }
|
||||
// else if (showWhenUpToDate)
|
||||
// {
|
||||
// MessageBox.Show(Global.Utilities.Translator.GetText("You have the latest version of SystemTrayMenu!"));
|
||||
// }
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// Creates a window to show changelog of new available versions.
|
||||
// /// </summary>
|
||||
// /// <param name="LatestVersionTitle">Name of latest release.</param>
|
||||
// /// <param name="Changelog">Pathnotes.</param>
|
||||
// /// <returns>OK = OK, Yes = Website, else = Cancel.</returns>
|
||||
// private static DialogResult NewVersionDialog()
|
||||
// {
|
||||
// const int ClientPad = 15;
|
||||
// newVersionForm = new()
|
||||
// {
|
||||
// StartPosition = FormStartPosition.CenterScreen,
|
||||
// FormBorderStyle = FormBorderStyle.FixedDialog,
|
||||
// Icon = Config.GetAppIcon(),
|
||||
// ShowInTaskbar = false,
|
||||
// };
|
||||
// newVersionForm.FormBorderStyle = FormBorderStyle.Sizable;
|
||||
// newVersionForm.MaximizeBox = true;
|
||||
// newVersionForm.MinimizeBox = false;
|
||||
// newVersionForm.ClientSize = new Size(600, 400);
|
||||
// newVersionForm.MinimumSize = newVersionForm.ClientSize;
|
||||
// newVersionForm.Text = Global.Utilities.Translator.GetText("New version available!");
|
||||
|
||||
// Label label = new()
|
||||
// {
|
||||
// Size = new Size(newVersionForm.ClientSize.Width - ClientPad, 20),
|
||||
// Location = new Point(ClientPad, ClientPad),
|
||||
// Text = $"{Global.Utilities.Translator.GetText("Latest available version:")} {GetLatestVersionName()}",
|
||||
// };
|
||||
// newVersionForm.Controls.Add(label);
|
||||
|
||||
// Button buttonOK = new()
|
||||
// {
|
||||
// DialogResult = DialogResult.OK,
|
||||
// Name = "buttonOK",
|
||||
// };
|
||||
// buttonOK.Location = new Point(
|
||||
// newVersionForm.ClientSize.Width - buttonOK.Size.Width - ClientPad,
|
||||
// newVersionForm.ClientSize.Height - buttonOK.Size.Height - ClientPad);
|
||||
// buttonOK.MinimumSize = new Size(75, 23);
|
||||
// buttonOK.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
// buttonOK.Text = Global.Utilities.Translator.GetText("OK");
|
||||
// buttonOK.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
// buttonOK.AutoSize = true;
|
||||
// newVersionForm.Controls.Add(buttonOK);
|
||||
|
||||
// Button buttonGoToDownloadPage = new()
|
||||
// {
|
||||
// DialogResult = DialogResult.Yes,
|
||||
// Name = "buttonGoToDownloadPage",
|
||||
// };
|
||||
// buttonGoToDownloadPage.Location = new Point(
|
||||
// newVersionForm.ClientSize.Width - buttonGoToDownloadPage.Size.Width - ClientPad - buttonOK.Size.Width - ClientPad,
|
||||
// newVersionForm.ClientSize.Height - buttonGoToDownloadPage.Size.Height - ClientPad);
|
||||
// buttonGoToDownloadPage.MinimumSize = new Size(75, 23);
|
||||
// buttonGoToDownloadPage.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
|
||||
// buttonGoToDownloadPage.Text = Global.Utilities.Translator.GetText("Go to download page");
|
||||
// buttonGoToDownloadPage.AutoSizeMode = AutoSizeMode.GrowAndShrink;
|
||||
// buttonGoToDownloadPage.AutoSize = true;
|
||||
// newVersionForm.Controls.Add(buttonGoToDownloadPage);
|
||||
|
||||
// TextBox textBox = new()
|
||||
// {
|
||||
// Location = new Point(ClientPad, label.Location.Y + label.Size.Height + 5),
|
||||
// };
|
||||
// textBox.Size = new Size(
|
||||
// newVersionForm.ClientSize.Width - (ClientPad * 2),
|
||||
// buttonOK.Location.Y - ClientPad - textBox.Location.Y);
|
||||
// textBox.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right;
|
||||
// textBox.Multiline = true;
|
||||
// textBox.Text = GetChangelog();
|
||||
// textBox.ReadOnly = true;
|
||||
// textBox.ScrollBars = ScrollBars.Both;
|
||||
// textBox.BackColor = Color.FromKnownColor(KnownColor.Window);
|
||||
// textBox.ForeColor = Color.FromKnownColor(KnownColor.ControlText);
|
||||
// newVersionForm.Controls.Add(textBox);
|
||||
|
||||
// newVersionForm.AcceptButton = buttonOK;
|
||||
// return newVersionForm.ShowDialog();
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// Returns the latest release version name.
|
||||
// /// </summary>
|
||||
// /// <returns>Version name.</returns>
|
||||
// private static string GetLatestVersionName()
|
||||
// {
|
||||
// string result = "Unknown";
|
||||
|
||||
// if (releases == null)
|
||||
// {
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// result = releases[0]["tag_name"].ToString().Replace("v", string.Empty);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Warn($"{nameof(GetLatestVersionName)} failed", ex);
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// }
|
||||
|
||||
// /// <summary>
|
||||
// /// Returns the change log from current version up to the latest release version.
|
||||
// /// </summary>
|
||||
// /// <returns>Change log summary or error text.</returns>
|
||||
// private static string GetChangelog()
|
||||
// {
|
||||
// string result = string.Empty;
|
||||
// string errorstr = "An error occurred during update check!" + Environment.NewLine;
|
||||
|
||||
// if (releases == null)
|
||||
// {
|
||||
// return errorstr + "Could not receive changelog!";
|
||||
// }
|
||||
|
||||
// try
|
||||
// {
|
||||
// for (int i = 0; i < releases.Count; i++)
|
||||
// {
|
||||
// Dictionary<string, object> release = releases[i];
|
||||
|
||||
// result += release["name"].ToString()
|
||||
// + Environment.NewLine
|
||||
// + release["body_text"].ToString()
|
||||
// .Replace("\n\n", Environment.NewLine)
|
||||
// .Replace("\n \n", Environment.NewLine)
|
||||
// + Environment.NewLine + Environment.NewLine;
|
||||
// if (i < releases.Count)
|
||||
// {
|
||||
// result += "--------------------------------------------------" +
|
||||
// "-------------------------------------------------------"
|
||||
// + Environment.NewLine;
|
||||
// }
|
||||
// }
|
||||
|
||||
// result = result.Replace("\n", Environment.NewLine);
|
||||
// }
|
||||
// catch (Exception ex)
|
||||
// {
|
||||
// Log.Warn($"{nameof(GetChangelog)}", ex);
|
||||
// result = errorstr + ex.Message.ToString();
|
||||
// }
|
||||
|
||||
// return result;
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
480
FSI.BT.Tools/SystemTrayMenu/Helpers/Updater/JsonParser.cs
Normal file
480
FSI.BT.Tools/SystemTrayMenu/Helpers/Updater/JsonParser.cs
Normal file
@@ -0,0 +1,480 @@
|
||||
// <copyright file="JsonParser.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper.Updater
|
||||
{
|
||||
// Copyright (c) 2018 Alex Parker
|
||||
// Copyright (c) 2018-2019 Peter Kirmeier
|
||||
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
// this software and associated documentation files (the "Software"), to deal in
|
||||
// the Software without restriction, including without limitation the rights to
|
||||
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
// the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
|
||||
// The above copyright notice and this permission notice shall be included in all
|
||||
// copies or substantial portions of the Software.
|
||||
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
|
||||
// Really simple JSON parser in ~300 lines
|
||||
// - Attempts to parse JSON files with minimal GC allocation
|
||||
// - Nice and simple "[1,2,3]".FromJson<List<int>>() API
|
||||
// - Classes and structs can be parsed too!
|
||||
// class Foo { public int Value; }
|
||||
// "{\"Value\":10}".FromJson<Foo>()
|
||||
// - Can parse JSON without type information into Dictionary<string,object> and List<object> e.g.
|
||||
// "[1,2,3]".FromJson<object>().GetType() == typeof(List<object>)
|
||||
// "{\"Value\":10}".FromJson<object>().GetType() == typeof(Dictionary<string,object>)
|
||||
// - No JIT Emit support to support AOT compilation on iOS
|
||||
// - Attempts are made to NOT throw an exception if the JSON is corrupted or invalid: returns null instead.
|
||||
// - Only public fields and property setters on classes/structs will be written to
|
||||
//
|
||||
// Limitations:
|
||||
// - No JIT Emit support to parse structures quickly
|
||||
// - Limited to parsing <2GB JSON files (due to int.MaxValue)
|
||||
// - Parsing of abstract classes or interfaces is NOT supported and will throw an exception.
|
||||
public static class JSONParser
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static Stack<List<string>> splitArrayPool;
|
||||
[ThreadStatic]
|
||||
private static StringBuilder stringBuilder;
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, FieldInfo>> fieldInfoCache;
|
||||
[ThreadStatic]
|
||||
private static Dictionary<Type, Dictionary<string, PropertyInfo>> propertyInfoCache;
|
||||
|
||||
public static T FromJson<T>(this string json)
|
||||
{
|
||||
// Initialize, if needed, the ThreadStatic variables
|
||||
propertyInfoCache ??= new Dictionary<Type, Dictionary<string, PropertyInfo>>();
|
||||
|
||||
fieldInfoCache ??= new Dictionary<Type, Dictionary<string, FieldInfo>>();
|
||||
|
||||
stringBuilder ??= new StringBuilder();
|
||||
|
||||
splitArrayPool ??= new Stack<List<string>>();
|
||||
|
||||
// Remove all whitespace not within strings to make parsing simpler
|
||||
stringBuilder.Length = 0;
|
||||
for (int i = 0; i < json.Length; i++)
|
||||
{
|
||||
char c = json[i];
|
||||
if (c == '"')
|
||||
{
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (char.IsWhiteSpace(c))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
stringBuilder.Append(c);
|
||||
}
|
||||
|
||||
// Parse the thing!
|
||||
return (T)ParseValue(typeof(T), stringBuilder.ToString());
|
||||
}
|
||||
|
||||
internal static object ParseValue(Type type, string json)
|
||||
{
|
||||
if (type == typeof(string))
|
||||
{
|
||||
if (json.Length <= 2)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
StringBuilder parseStringBuilder = new(json.Length);
|
||||
for (int i = 1; i < json.Length - 1; ++i)
|
||||
{
|
||||
if (json[i] == '\\' && i + 1 < json.Length - 1)
|
||||
{
|
||||
int j = "\"\\nrtbf/".IndexOf(json[i + 1]);
|
||||
if (j >= 0)
|
||||
{
|
||||
parseStringBuilder.Append("\"\\\n\r\t\b\f/"[j]);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (json[i + 1] == 'u' && i + 5 < json.Length - 1)
|
||||
{
|
||||
if (uint.TryParse(json.AsSpan(i + 2, 4), System.Globalization.NumberStyles.AllowHexSpecifier, null, out uint c))
|
||||
{
|
||||
parseStringBuilder.Append((char)c);
|
||||
i += 5;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseStringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
return parseStringBuilder.ToString();
|
||||
}
|
||||
|
||||
if (type.IsPrimitive)
|
||||
{
|
||||
var result = Convert.ChangeType(json, type, System.Globalization.CultureInfo.InvariantCulture);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (type == typeof(decimal))
|
||||
{
|
||||
decimal.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out decimal result);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (json == "null")
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type.IsEnum)
|
||||
{
|
||||
if (json[0] == '"')
|
||||
{
|
||||
json = json[1..^1];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Enum.Parse(type, json, false);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (type.IsArray)
|
||||
{
|
||||
Type arrayType = type.GetElementType();
|
||||
if (json[0] != '[' || json[^1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
Array newArray = Array.CreateInstance(arrayType, elems.Count);
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
newArray.SetValue(ParseValue(arrayType, elems[i]), i);
|
||||
}
|
||||
|
||||
splitArrayPool.Push(elems);
|
||||
return newArray;
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>))
|
||||
{
|
||||
Type listType = type.GetGenericArguments()[0];
|
||||
if (json[0] != '[' || json[^1] != ']')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
List<string> elems = Split(json);
|
||||
var list = (IList)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count });
|
||||
for (int i = 0; i < elems.Count; i++)
|
||||
{
|
||||
list.Add(ParseValue(listType, elems[i]));
|
||||
}
|
||||
|
||||
splitArrayPool.Push(elems);
|
||||
return list;
|
||||
}
|
||||
|
||||
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
Type keyType, valueType;
|
||||
{
|
||||
Type[] args = type.GetGenericArguments();
|
||||
keyType = args[0];
|
||||
valueType = args[1];
|
||||
}
|
||||
|
||||
// Refuse to parse dictionary keys that aren't of type string
|
||||
if (keyType != typeof(string))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Must be a valid dictionary element
|
||||
if (json[0] != '{' || json[^1] != '}')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dictionary = (IDictionary)type.GetConstructor(new Type[] { typeof(int) }).Invoke(new object[] { elems.Count / 2 });
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string keyValue = elems[i][1..^1];
|
||||
object val = ParseValue(valueType, elems[i + 1]);
|
||||
dictionary.Add(keyValue, val);
|
||||
}
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
|
||||
if (type == typeof(object))
|
||||
{
|
||||
return ParseAnonymousValue(json);
|
||||
}
|
||||
|
||||
if (json[0] == '{' && json[^1] == '}')
|
||||
{
|
||||
return ParseObject(type, json);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static int AppendUntilStringEnd(bool appendEscapeCharacter, int startIdx, string json)
|
||||
{
|
||||
stringBuilder.Append(json[startIdx]);
|
||||
for (int i = startIdx + 1; i < json.Length; i++)
|
||||
{
|
||||
if (json[i] == '\\')
|
||||
{
|
||||
if (appendEscapeCharacter)
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i + 1]);
|
||||
i++; // Skip next character as it is escaped
|
||||
}
|
||||
else if (json[i] == '"')
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
return i;
|
||||
}
|
||||
else
|
||||
{
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return json.Length - 1;
|
||||
}
|
||||
|
||||
// Splits { <value>:<value>, <value>:<value> } and [ <value>, <value> ] into a list of <value> strings
|
||||
private static List<string> Split(string json)
|
||||
{
|
||||
List<string> splitArray = splitArrayPool.Count > 0 ? splitArrayPool.Pop() : new List<string>();
|
||||
splitArray.Clear();
|
||||
if (json.Length == 2)
|
||||
{
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
int parseDepth = 0;
|
||||
stringBuilder.Length = 0;
|
||||
for (int i = 1; i < json.Length - 1; i++)
|
||||
{
|
||||
switch (json[i])
|
||||
{
|
||||
case '[':
|
||||
case '{':
|
||||
parseDepth++;
|
||||
break;
|
||||
case ']':
|
||||
case '}':
|
||||
parseDepth--;
|
||||
break;
|
||||
case '"':
|
||||
i = AppendUntilStringEnd(true, i, json);
|
||||
continue;
|
||||
case ',':
|
||||
case ':':
|
||||
if (parseDepth == 0)
|
||||
{
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
stringBuilder.Length = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
stringBuilder.Append(json[i]);
|
||||
}
|
||||
|
||||
splitArray.Add(stringBuilder.ToString());
|
||||
|
||||
return splitArray;
|
||||
}
|
||||
|
||||
private static object ParseAnonymousValue(string json)
|
||||
{
|
||||
if (json.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (json[0] == '{' && json[^1] == '}')
|
||||
{
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var dict = new Dictionary<string, object>(elems.Count / 2);
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
dict.Add(elems[i][1..^1], ParseAnonymousValue(elems[i + 1]));
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
|
||||
if (json[0] == '[' && json[^1] == ']')
|
||||
{
|
||||
List<string> items = Split(json);
|
||||
var finalList = new List<object>(items.Count);
|
||||
for (int i = 0; i < items.Count; i++)
|
||||
{
|
||||
finalList.Add(ParseAnonymousValue(items[i]));
|
||||
}
|
||||
|
||||
return finalList;
|
||||
}
|
||||
|
||||
if (json[0] == '"' && json[^1] == '"')
|
||||
{
|
||||
return ParseValue(typeof(string), json); // fix https://github.com/zanders3/json/issues/29
|
||||
}
|
||||
|
||||
if (char.IsDigit(json[0]) || json[0] == '-')
|
||||
{
|
||||
if (json.Contains('.'))
|
||||
{
|
||||
double.TryParse(json, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double result);
|
||||
return result;
|
||||
}
|
||||
else
|
||||
{
|
||||
_ = int.TryParse(json, out int result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (json == "true")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (json == "false")
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// handles json == "null" as well as invalid JSON
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Dictionary<string, T> CreateMemberNameDictionary<T>(T[] members)
|
||||
where T : MemberInfo
|
||||
{
|
||||
Dictionary<string, T> nameToMember = new(StringComparer.OrdinalIgnoreCase);
|
||||
for (int i = 0; i < members.Length; i++)
|
||||
{
|
||||
/*T member = members[i];
|
||||
if (member.IsDefined(typeof(IgnoreDataMemberAttribute), true))
|
||||
continue;
|
||||
|
||||
string name = member.Name;
|
||||
if (member.IsDefined(typeof(DataMemberAttribute), true))
|
||||
{
|
||||
DataMemberAttribute dataMemberAttribute = (DataMemberAttribute)Attribute.GetCustomAttribute(member, typeof(DataMemberAttribute), true);
|
||||
if (!string.IsNullOrEmpty(dataMemberAttribute.Name))
|
||||
name = dataMemberAttribute.Name;
|
||||
}
|
||||
|
||||
nameToMember.Add(name, member);*/
|
||||
// The above code is not working with .Net framework 2.0, so we ignore these attributes for compatibility reasons:
|
||||
nameToMember.Add(members[i].Name, members[i]);
|
||||
}
|
||||
|
||||
return nameToMember;
|
||||
}
|
||||
|
||||
private static object ParseObject(Type type, string json)
|
||||
{
|
||||
object instance = FormatterServices.GetUninitializedObject(type);
|
||||
|
||||
// The list is split into key/value pairs only, this means the split must be divisible by 2 to be valid JSON
|
||||
List<string> elems = Split(json);
|
||||
if (elems.Count % 2 != 0)
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
||||
if (!fieldInfoCache.TryGetValue(type, out Dictionary<string, FieldInfo> nameToField))
|
||||
{
|
||||
nameToField = CreateMemberNameDictionary(type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
fieldInfoCache.Add(type, nameToField);
|
||||
}
|
||||
|
||||
if (!propertyInfoCache.TryGetValue(type, out Dictionary<string, PropertyInfo> nameToProperty))
|
||||
{
|
||||
nameToProperty = CreateMemberNameDictionary(type.GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy));
|
||||
propertyInfoCache.Add(type, nameToProperty);
|
||||
}
|
||||
|
||||
for (int i = 0; i < elems.Count; i += 2)
|
||||
{
|
||||
if (elems[i].Length <= 2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string key = elems[i][1..^1];
|
||||
string value = elems[i + 1];
|
||||
|
||||
if (nameToField.TryGetValue(key, out FieldInfo fieldInfo))
|
||||
{
|
||||
fieldInfo.SetValue(instance, ParseValue(fieldInfo.FieldType, value));
|
||||
}
|
||||
else if (nameToProperty.TryGetValue(key, out PropertyInfo propertyInfo))
|
||||
{
|
||||
propertyInfo.SetValue(instance, ParseValue(propertyInfo.PropertyType, value), null);
|
||||
}
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
16
FSI.BT.Tools/SystemTrayMenu/Helpers/WindowsExplorerSort.cs
Normal file
16
FSI.BT.Tools/SystemTrayMenu/Helpers/WindowsExplorerSort.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
// <copyright file="WindowsExplorerSort.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System.Collections.Generic;
|
||||
|
||||
internal class WindowsExplorerSort : IComparer<string>
|
||||
{
|
||||
public int Compare(string x, string y)
|
||||
{
|
||||
return Global.DllImports.NativeMethods.ShlwapiStrCmpLogicalW(x, y);
|
||||
}
|
||||
}
|
||||
}
|
||||
80
FSI.BT.Tools/SystemTrayMenu/Helpers/WindowsTaskbar.cs
Normal file
80
FSI.BT.Tools/SystemTrayMenu/Helpers/WindowsTaskbar.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// <copyright file="WindowsTaskbar.cs" company="PlaceholderCompany">
|
||||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace FSI.BT.Tools.SystemTrayMenu.Helper
|
||||
{
|
||||
using System;
|
||||
using System.Drawing;
|
||||
using System.Runtime.InteropServices;
|
||||
using static Global.DllImports.NativeMethods;
|
||||
|
||||
public enum TaskbarPosition
|
||||
{
|
||||
Unknown = -1,
|
||||
Left,
|
||||
Top,
|
||||
Right,
|
||||
Bottom,
|
||||
}
|
||||
|
||||
public sealed class WindowsTaskbar
|
||||
{
|
||||
private const string ClassName = "Shell_TrayWnd";
|
||||
|
||||
public WindowsTaskbar()
|
||||
{
|
||||
IntPtr taskbarHandle = User32FindWindow(ClassName, null);
|
||||
|
||||
APPBARDATA data = new()
|
||||
{
|
||||
cbSize = (uint)Marshal.SizeOf(typeof(APPBARDATA)),
|
||||
hWnd = taskbarHandle,
|
||||
};
|
||||
IntPtr result = Shell32SHAppBarMessage(ABM.GetTaskbarPos, ref data);
|
||||
if (result == IntPtr.Zero)
|
||||
{
|
||||
Bounds = new Rectangle(20, 20, 20, 20);
|
||||
}
|
||||
else
|
||||
{
|
||||
Position = (TaskbarPosition)data.uEdge;
|
||||
Bounds = Rectangle.FromLTRB(data.rc.left, data.rc.top, data.rc.right, data.rc.bottom);
|
||||
|
||||
data.cbSize = (uint)Marshal.SizeOf(typeof(APPBARDATA));
|
||||
result = Shell32SHAppBarMessage(ABM.GetState, ref data);
|
||||
int state = result.ToInt32();
|
||||
AlwaysOnTop = (state & ABS.AlwaysOnTop) == ABS.AlwaysOnTop;
|
||||
AutoHide = (state & ABS.Autohide) == ABS.Autohide;
|
||||
}
|
||||
}
|
||||
|
||||
public Rectangle Bounds
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public TaskbarPosition Position
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public Point Location => Bounds.Location;
|
||||
|
||||
public Size Size => Bounds.Size;
|
||||
|
||||
public bool AlwaysOnTop
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
|
||||
public bool AutoHide
|
||||
{
|
||||
get;
|
||||
private set;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user