forked from KSP-CKAN/CKAN
/
Util.cs
344 lines (311 loc) · 13.4 KB
/
Util.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
using System;
using System.Collections.Generic;
using System.Linq;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using Timer = System.Windows.Forms.Timer;
#if NET5_0_OR_GREATER
using System.Runtime.Versioning;
#endif
using log4net;
namespace CKAN.GUI
{
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public static class Util
{
/// <summary>
/// Invokes an action on the UI thread, or directly if we're
/// on the UI thread.
/// </summary>
public static void Invoke<T>(T obj, Action action) where T : Control
{
if (obj.InvokeRequired) // if we're not in the UI thread
{
// enqueue call on UI thread and wait for it to return
obj.Invoke(new MethodInvoker(action));
}
else
{
// we're on the UI thread, execute directly
action();
}
}
// utility helper to deal with multi-threading and UI
// async version, doesn't wait for UI thread
// use with caution, when not sure use blocking Invoke()
public static void AsyncInvoke<T>(T obj, Action action) where T : Control
{
if (obj.InvokeRequired) // if we're not in the UI thread
{
// enqueue call on UI thread and continue
obj.BeginInvoke(new MethodInvoker(action));
}
else
{
// we're on the UI thread, execute directly
action();
}
}
// hides the console window on windows
// useful when running the GUI
[DllImport("kernel32.dll")]
private static extern IntPtr GetConsoleWindow();
[DllImport("user32.dll")]
private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
public static void HideConsoleWindow()
{
if (Platform.IsWindows)
{
ShowWindow(GetConsoleWindow(), 0);
}
}
/// <summary>
/// Returns true if the string could be a valid http address.
/// DOES NOT ACTUALLY CHECK IF IT EXISTS, just the format.
/// </summary>
public static bool CheckURLValid(string source)
=> Uri.TryCreate(source, UriKind.Absolute, out Uri uri_result)
&& (uri_result.Scheme == Uri.UriSchemeHttp
|| uri_result.Scheme == Uri.UriSchemeHttps);
/// <summary>
/// Open a URL, unless it's "N/A"
/// </summary>
/// <param name="url">The URL</param>
public static void OpenLinkFromLinkLabel(string url)
{
if (url == Properties.Resources.ModInfoNSlashA)
{
return;
}
TryOpenWebPage(url);
}
/// <summary>
/// Tries to open an url using the default application.
/// If it fails, it tries again by prepending each prefix before the url before it gives up.
/// </summary>
public static bool TryOpenWebPage(string url, IEnumerable<string> prefixes = null)
{
// Default prefixes to try if not provided
if (prefixes == null)
{
prefixes = new string[] { "https://", "http://" };
}
foreach (string fullUrl in new string[] { url }
.Concat(prefixes.Select(p => p + url).Where(CheckURLValid)))
{
if (Utilities.ProcessStartURL(fullUrl))
{
return true;
}
}
return false;
}
/// <summary>
/// React to the user clicking a mouse button on a link.
/// Opens the URL in browser on left click, presents a
/// right click menu on right click.
/// </summary>
/// <param name="url">The link's URL</param>
/// <param name="e">The click event</param>
public static void HandleLinkClicked(string url, LinkLabelLinkClickedEventArgs e)
{
switch (e.Button)
{
case MouseButtons.Left:
OpenLinkFromLinkLabel(url);
break;
case MouseButtons.Right:
LinkContextMenu(url);
break;
}
}
/// <summary>
/// Show a context menu when the user right clicks a link
/// </summary>
/// <param name="url">The URL of the link</param>
public static void LinkContextMenu(string url)
{
ToolStripMenuItem copyLink = new ToolStripMenuItem(Properties.Resources.UtilCopyLink);
copyLink.Click += new EventHandler((sender, ev) => Clipboard.SetText(url));
ContextMenuStrip menu = new ContextMenuStrip();
if (Platform.IsMono)
{
menu.Renderer = new FlatToolStripRenderer();
}
menu.Items.Add(copyLink);
menu.Show(Cursor.Position);
}
/// <summary>
/// Find a screen that the given box overlaps
/// </summary>
/// <param name="location">Upper left corner of box</param>
/// <param name="size">Width and height of box</param>
/// <returns>
/// The first screen that overlaps the box if any, otherwise null
/// </returns>
public static Screen FindScreen(Point location, Size size)
{
var rect = new Rectangle(location, size);
return Screen.AllScreens.FirstOrDefault(sc => sc.WorkingArea.IntersectsWith(rect));
}
/// <summary>
/// Adjust position of a box so it fits entirely on one screen
/// </summary>
/// <param name="location">Top left corner of box</param>
/// <param name="size">Width and height of box</param>
/// <returns>
/// Original location if already fully on-screen, otherwise
/// a position representing sliding it onto the screen
/// </returns>
public static Point ClampedLocation(Point location, Size size, Screen screen = null)
{
if (screen == null)
{
log.DebugFormat("Looking for screen of {0}, {1}", location, size);
screen = FindScreen(location, size);
}
if (screen != null)
{
log.DebugFormat("Found screen: {0}", screen.WorkingArea);
// Slide the whole rectangle fully onto the screen
if (location.X < screen.WorkingArea.Left)
{
location.X = screen.WorkingArea.Left;
}
if (location.Y < screen.WorkingArea.Top)
{
location.Y = screen.WorkingArea.Top;
}
if (location.X + size.Width > screen.WorkingArea.Right)
{
location.X = screen.WorkingArea.Right - size.Width;
}
if (location.Y + size.Height > screen.WorkingArea.Bottom)
{
location.Y = screen.WorkingArea.Bottom - size.Height;
}
log.DebugFormat("Clamped location: {0}", location);
}
return location;
}
/// <summary>
/// Adjust position of a box so it fits on one screen with a margin around it
/// </summary>
/// <param name="location">Top left corner of box</param>
/// <param name="size">Width and height of box</param>
/// <param name="topLeftMargin">Size of space between window and top left edge of screen</param>
/// <param name="bottomRightMargin">Size of space between window and bottom right edge of screen</param>
/// <returns>
/// Original location if already fully on-screen plus margins, otherwise
/// a position representing sliding it onto the screen
/// </returns>
public static Point ClampedLocationWithMargins(Point location, Size size, Size topLeftMargin, Size bottomRightMargin, Screen screen = null)
{
// Imagine drawing a larger box around the window, the size of the desired margin.
// We pass that box to ClampedLocation to make sure it fits on screen,
// then place our window at an offset within the box
return ClampedLocation(location - topLeftMargin, size + topLeftMargin + bottomRightMargin, screen) + topLeftMargin;
}
/// <summary>
/// Coalesce multiple events from a busy event source into single delayed reactions
///
/// See: https://www.freecodecamp.org/news/javascript-debounce-example/
///
/// Additional convenience features:
/// - Ability to do something immediately unconditionally
/// - Execute immediately if a condition is met
/// - Pass the events to the functions
/// </summary>
/// <param name="startFunc">Called immediately when the event is fired, for fast parts of the handling</param>
/// <param name="immediateFunc">If this returns true for an event, truncate the delay and fire doneFunc immediately</param>
/// <param name="abortFunc">If this returns true for an event, ignore it completely (e.g. for setting text box contents programmatically)</param>
/// <param name="doneFunc">Called after timeoutMs milliseconds, or immediately if immediateFunc returns true</param>
/// <param name="timeoutMs">Number of milliseconds between the last event and when to call doneFunc</param>
/// <typeparam name="EventT">Event type handled</typeparam>
/// <returns>A new event handler that wraps the given functions using the timer</returns>
public static EventHandler<EventT> Debounce<EventT>(
EventHandler<EventT> startFunc,
Func<object, EventT, bool> immediateFunc,
Func<object, EventT, bool> abortFunc,
EventHandler<EventT> doneFunc,
int timeoutMs = 500)
{
// Store the most recent event we received
object receivedFrom = null;
EventT received = default;
// Set up the timer that will track the delay
Timer timer = new Timer() { Interval = timeoutMs };
timer.Tick += (sender, evt) =>
{
timer.Stop();
doneFunc(receivedFrom, received);
};
return (object sender, EventT evt) =>
{
if (!abortFunc(sender, evt))
{
timer.Stop();
startFunc(sender, evt);
if (immediateFunc(sender, evt))
{
doneFunc(sender, evt);
receivedFrom = null;
received = default;
}
else
{
receivedFrom = sender;
received = evt;
timer.Start();
}
}
};
}
public static Color BlendColors(Color[] colors)
=> colors.Length < 1 ? Color.Empty
: colors.Length == 1 ? colors[0]
: colors.Aggregate((back, fore) => fore.AlphaBlendWith(1f / colors.Length, back));
public static Color AlphaBlendWith(this Color c1, float alpha, Color c2)
=> AddColors(c1.MultiplyBy(alpha),
c2.MultiplyBy(1f - alpha));
private static Color MultiplyBy(this Color c, float f)
=> Color.FromArgb((int)(f * c.R),
(int)(f * c.G),
(int)(f * c.B));
private static Color AddColors(Color a, Color b)
=> Color.FromArgb(a.R + b.R,
a.G + b.G,
a.B + b.B);
/// <summary>
/// Simple syntactic sugar around Graphics.MeasureString
/// </summary>
/// <param name="g">The graphics context</param>
/// <param name="font">The font to be used for the text</param>
/// <param name="text">String to measure size of</param>
/// <param name="maxWidth">Number of pixels allowed horizontally</param>
/// <returns>
/// Number of pixels needed vertically to fit the string
/// </returns>
public static int StringHeight(Graphics g, string text, Font font, int maxWidth)
=> (int)g.MeasureString(text, font, (int)(maxWidth / XScale(g))).Height;
/// <summary>
/// Calculate how much vertical space is needed to display a label's text
/// </summary>
/// <param name="g">The graphics context</param>
/// <param name="lbl">The label</param>
/// <returns>
/// Number of pixels needed vertically to show the label's full text
/// </returns>
public static int LabelStringHeight(Graphics g, Label lbl)
=> (int)(YScale(g) * (lbl.Margin.Vertical + lbl.Padding.Vertical
+ StringHeight(g, lbl.Text, lbl.Font,
(lbl.Width - lbl.Margin.Horizontal
- lbl.Padding.Horizontal))));
private static float XScale(Graphics g) => g.DpiX / 96f;
private static float YScale(Graphics g) => g.DpiY / 96f;
private static readonly ILog log = LogManager.GetLogger(typeof(Util));
}
}