• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

KSP-CKAN / CKAN / 17179122983

23 Aug 2025 07:06PM UTC coverage: 58.488% (+0.004%) from 58.484%
17179122983

push

github

HebaruSan
Merge #4420 More tests and fixes

4748 of 8436 branches covered (56.28%)

Branch coverage included in aggregate %.

70 of 132 new or added lines in 17 files covered. (53.03%)

446 existing lines in 11 files now uncovered.

10050 of 16865 relevant lines covered (59.59%)

1.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

8.98
/GUI/Util.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Linq;
4
using System.Drawing;
5
using System.Windows.Forms;
6
using System.Runtime.InteropServices;
7
using Timer = System.Windows.Forms.Timer;
8
#if NET5_0_OR_GREATER
9
using System.Runtime.Versioning;
10
#endif
11

12
using log4net;
13

14
namespace CKAN.GUI
15
{
16
    #if NET5_0_OR_GREATER
17
    [SupportedOSPlatform("windows")]
18
    #endif
19
    public static class Util
20
    {
21
        #region Threading
22

23
        /// <summary>
24
        /// Invokes an action on the UI thread, or directly if we're
25
        /// on the UI thread.
26
        /// </summary>
27
        public static void Invoke<T>(T obj, Action action) where T : Control
28
        {
×
29
            if (obj.InvokeRequired) // if we're not in the UI thread
×
30
            {
×
31
                // enqueue call on UI thread and wait for it to return
32
                obj.Invoke(new MethodInvoker(action));
×
33
            }
×
34
            else
35
            {
×
36
                // we're on the UI thread, execute directly
37
                action();
×
38
            }
×
39
        }
×
40

41
        // utility helper to deal with multi-threading and UI
42
        // async version, doesn't wait for UI thread
43
        // use with caution, when not sure use blocking Invoke()
44
        public static void AsyncInvoke<T>(T obj, Action action) where T : Control
45
        {
×
46
            if (obj.InvokeRequired) // if we're not in the UI thread
×
47
            {
×
48
                // enqueue call on UI thread and continue
49
                obj.BeginInvoke(new MethodInvoker(action));
×
50
            }
×
51
            else
52
            {
×
53
                // we're on the UI thread, execute directly
54
                action();
×
55
            }
×
56
        }
×
57

58
        /// <summary>
59
        /// Coalesce multiple events from a busy event source into single delayed reactions
60
        ///
61
        /// See: https://www.freecodecamp.org/news/javascript-debounce-example/
62
        ///
63
        /// Additional convenience features:
64
        ///   - Ability to do something immediately unconditionally
65
        ///   - Execute immediately if a condition is met
66
        ///   - Pass the events to the functions
67
        /// </summary>
68
        /// <param name="startFunc">Called immediately when the event is fired, for fast parts of the handling</param>
69
        /// <param name="immediateFunc">If this returns true for an event, truncate the delay and fire doneFunc immediately</param>
70
        /// <param name="abortFunc">If this returns true for an event, ignore it completely (e.g. for setting text box contents programmatically)</param>
71
        /// <param name="doneFunc">Called after timeoutMs milliseconds, or immediately if immediateFunc returns true</param>
72
        /// <param name="timeoutMs">Number of milliseconds between the last event and when to call doneFunc</param>
73
        /// <typeparam name="EventT">Event type handled</typeparam>
74
        /// <returns>A new event handler that wraps the given functions using the timer</returns>
75
        public static EventHandler<EventT> Debounce<EventT>(
76
            EventHandler<EventT>        startFunc,
77
            Func<object?, EventT, bool> immediateFunc,
78
            Func<object?, EventT, bool> abortFunc,
79
            EventHandler<EventT?>       doneFunc,
80
            int timeoutMs = 500)
81
        {
×
82
            // Store the most recent event we received
83
            object? receivedFrom = null;
×
84
            EventT? received     = default;
×
85

86
            // Set up the timer that will track the delay
87
            Timer timer = new Timer() { Interval = timeoutMs };
×
88
            timer.Tick += (sender, evt) =>
×
89
            {
×
90
                timer.Stop();
×
91
                doneFunc(receivedFrom, received);
×
92
            };
×
93

94
            return (sender, evt) =>
×
95
            {
×
96
                if (!abortFunc(sender, evt))
×
97
                {
×
98
                    timer.Stop();
×
99
                    startFunc(sender, evt);
×
100
                    if (immediateFunc(sender, evt))
×
101
                    {
×
102
                        doneFunc(sender, evt);
×
103
                        receivedFrom = null;
×
104
                        received     = default;
×
105
                    }
×
106
                    else
107
                    {
×
108
                        receivedFrom = sender;
×
109
                        received     = evt;
×
110
                        timer.Start();
×
111
                    }
×
112
                }
×
113
            };
×
114
        }
×
115

116
        #endregion
117

118
        #region Link handling
119

120
        /// <summary>
121
        /// Returns true if the string could be a valid http address.
122
        /// DOES NOT ACTUALLY CHECK IF IT EXISTS, just the format.
123
        /// </summary>
124
        public static bool CheckURLValid(string source)
125
            => Uri.TryCreate(source, UriKind.Absolute, out Uri? uri_result)
2✔
126
                && (uri_result.Scheme == Uri.UriSchemeHttp
127
                 || uri_result.Scheme == Uri.UriSchemeHttps);
128

129
        /// <summary>
130
        /// Open a URL, unless it's "N/A"
131
        /// </summary>
132
        /// <param name="url">The URL</param>
133
        public static void OpenLinkFromLinkLabel(string url)
134
        {
×
135
            if (url == Properties.Resources.ModInfoNSlashA)
×
136
            {
×
137
                return;
×
138
            }
139

140
            TryOpenWebPage(url);
×
141
        }
×
142

143
        /// <summary>
144
        /// Tries to open an url using the default application.
145
        /// If it fails, it tries again by prepending each prefix before the url before it gives up.
146
        /// </summary>
147
        public static bool TryOpenWebPage(string url, IEnumerable<string>? prefixes = null)
148
        {
×
149
            // Default prefixes to try if not provided
150
            prefixes ??= new string[] { "https://", "http://" };
×
151

152
            foreach (string fullUrl in new string[] { url }
×
153
                .Concat(prefixes.Select(p => p + url).Where(CheckURLValid)))
×
154
            {
×
155
                if (Utilities.ProcessStartURL(fullUrl))
×
156
                {
×
157
                    return true;
×
158
                }
159
            }
×
160
            return false;
×
161
        }
×
162

163
        /// <summary>
164
        /// React to the user clicking a mouse button on a link.
165
        /// Opens the URL in browser on left click, presents a
166
        /// right click menu on right click.
167
        /// </summary>
168
        /// <param name="url">The link's URL</param>
169
        /// <param name="e">The click event</param>
170
        public static void HandleLinkClicked(string url, LinkLabelLinkClickedEventArgs? e)
171
        {
×
172
            switch (e?.Button)
×
173
            {
174
                case MouseButtons.Left:
175
                    OpenLinkFromLinkLabel(url);
×
176
                    if (e.Link != null)
×
177
                    {
×
178
                        e.Link.Visited = true;
×
179
                    }
×
180
                    break;
×
181

182
                case MouseButtons.Right:
183
                    LinkContextMenu(url);
×
184
                    break;
×
185
            }
186
        }
×
187

188
        /// <summary>
189
        /// Show a link right-click menu under a control,
190
        /// meant for keyboard access
191
        /// </summary>
192
        /// <param name="url">The URL of the link</param>
193
        /// <param name="c">The menu will be shown below the bottom of this control</param>
194
        public static void LinkContextMenu(string url, Control c)
195
            => LinkContextMenu(url, c.PointToScreen(new Point(0, c.Height)));
×
196

197
        /// <summary>
198
        /// Show a context menu when the user right clicks a link
199
        /// </summary>
200
        /// <param name="url">The URL of the link</param>
201
        /// <param name="where">Screen coordinates for the menu</param>
202
        public static void LinkContextMenu(string url, Point? where = null)
203
        {
×
204
            var copyLink = new ToolStripMenuItem(Properties.Resources.UtilCopyLink);
×
205
            copyLink.Click += (sender, ev) => Clipboard.SetText(url);
×
206

207
            var menu = new ContextMenuStrip();
×
208
            if (Platform.IsMono)
×
209
            {
×
210
                menu.Renderer = new FlatToolStripRenderer();
×
211
            }
×
212
            menu.Items.Add(copyLink);
×
213
            menu.Show(where ?? Cursor.Position);
×
214
        }
×
215

216
        #endregion
217

218
        #region Window positioning
219

220
        /// <summary>
221
        /// Find a screen that the given box overlaps
222
        /// </summary>
223
        /// <param name="location">Upper left corner of box</param>
224
        /// <param name="size">Width and height of box</param>
225
        /// <returns>
226
        /// The first screen that overlaps the box if any, otherwise null
227
        /// </returns>
228
        public static Screen? FindScreen(Point location, Size size)
229
        {
×
230
            var rect = new Rectangle(location, size);
×
231
            return Screen.AllScreens.FirstOrDefault(sc => sc.WorkingArea.IntersectsWith(rect));
×
232
        }
×
233

234
        /// <summary>
235
        /// Adjust position of a box so it fits entirely on one screen
236
        /// </summary>
237
        /// <param name="location">Top left corner of box</param>
238
        /// <param name="size">Width and height of box</param>
239
        /// <param name="screen">The screen to which to clamp, null for any screen</param>
240
        /// <returns>
241
        /// Original location if already fully on-screen, otherwise
242
        /// a position representing sliding it onto the screen
243
        /// </returns>
244
        public static Point ClampedLocation(Point location, Size size, Screen? screen = null)
UNCOV
245
        {
×
246
            if (screen == null)
×
247
            {
×
248
                log.DebugFormat("Looking for screen of {0}, {1}", location, size);
×
249
                screen = FindScreen(location, size);
×
250
            }
×
251
            if (screen != null)
×
252
            {
×
253
                log.DebugFormat("Found screen: {0}", screen.WorkingArea);
×
254
                // Slide the whole rectangle fully onto the screen
UNCOV
255
                if (location.X < screen.WorkingArea.Left)
×
256
                {
×
257
                    location.X = screen.WorkingArea.Left;
×
258
                }
×
259

UNCOV
260
                if (location.Y < screen.WorkingArea.Top)
×
261
                {
×
262
                    location.Y = screen.WorkingArea.Top;
×
263
                }
×
264

UNCOV
265
                if (location.X + size.Width > screen.WorkingArea.Right)
×
266
                {
×
267
                    location.X = screen.WorkingArea.Right - size.Width;
×
268
                }
×
269

UNCOV
270
                if (location.Y + size.Height > screen.WorkingArea.Bottom)
×
271
                {
×
272
                    location.Y = screen.WorkingArea.Bottom - size.Height;
×
273
                }
×
274

UNCOV
275
                log.DebugFormat("Clamped location: {0}", location);
×
276
            }
×
277
            return location;
×
278
        }
×
279

280
        /// <summary>
281
        /// Adjust position of a box so it fits on one screen with a margin around it
282
        /// </summary>
283
        /// <param name="location">Top left corner of box</param>
284
        /// <param name="size">Width and height of box</param>
285
        /// <param name="topLeftMargin">Size of space between window and top left edge of screen</param>
286
        /// <param name="bottomRightMargin">Size of space between window and bottom right edge of screen</param>
287
        /// <param name="screen">The screen to which to clamp, null for any screen</param>
288
        /// <returns>
289
        /// Original location if already fully on-screen plus margins, otherwise
290
        /// a position representing sliding it onto the screen
291
        /// </returns>
292
        public static Point ClampedLocationWithMargins(Point location, Size size, Size topLeftMargin, Size bottomRightMargin, Screen? screen = null)
UNCOV
293
        {
×
294
            // Imagine drawing a larger box around the window, the size of the desired margin.
295
            // We pass that box to ClampedLocation to make sure it fits on screen,
296
            // then place our window at an offset within the box
UNCOV
297
            return ClampedLocation(location - topLeftMargin, size + topLeftMargin + bottomRightMargin, screen) + topLeftMargin;
×
UNCOV
298
        }
×
299

300
        #endregion
301

302
        #region Color manipulation
303

304
        public static Color BlendColors(Color[] colors)
305
            => colors.Length <  1 ? Color.Empty
2!
306
             //: colors is [var c] ? c
307
             : colors.Length == 1 && colors[0] is var c ? c
UNCOV
308
             : Color.FromArgb(colors.Sum(c => c.A) / colors.Length,
×
UNCOV
309
                              colors.Sum(c => c.R) / colors.Length,
×
310
                              colors.Sum(c => c.G) / colors.Length,
×
311
                              colors.Sum(c => c.B) / colors.Length);
×
312

313
        public static Color AlphaBlendWith(this Color c1, float alpha, Color c2)
314
            => AddColors(c1.MultiplyBy(alpha),
2✔
315
                         c2.MultiplyBy(1f - alpha));
316

317
        private static Color MultiplyBy(this Color c, float f)
318
            => Color.FromArgb((int)(f * c.A),
2✔
319
                              (int)(f * c.R),
320
                              (int)(f * c.G),
321
                              (int)(f * c.B));
322

323
        private static Color AddColors(Color a, Color b)
324
            => Color.FromArgb(a.A + b.A,
2✔
325
                              a.R + b.R,
326
                              a.G + b.G,
327
                              a.B + b.B);
328

329
        public static Color? ForeColorForBackColor(this Color backColor)
330
            => backColor == Color.Transparent || backColor == Color.Empty ? null
2✔
331
             : backColor.GetLuminance() >= luminanceThreshold             ? Color.Black
332
             :                                                              Color.White;
333

334
        /// <summary>
335
        /// Below this is considered "dark," above is considered "light"
336
        /// </summary>
337
        private const double luminanceThreshold = 0.179128785;
338

339
        /// <summary>
340
        /// https://www.w3.org/WAI/GL/wiki/Relative_luminance
341
        /// </summary>
342
        /// <param name="color">The color for which to calculate the relative luminance</param>
343
        /// <returns>Relative luminance (basically a better version of brightness) of the color</returns>
344
        public static double GetLuminance(this Color color)
345
            => GetLuminance(color.R, color.G, color.B);
2✔
346

347
        private static double GetLuminance(byte R, byte G, byte B)
348
            => GetLuminance(LuminanceTransform(R),
2✔
349
                            LuminanceTransform(G),
350
                            LuminanceTransform(B));
351

352
        private static double GetLuminance(double Rs, double Gs, double Bs)
353
            => (0.2126 * Rs) + (0.7152 * Gs) + (0.0722 * Bs);
2✔
354

355
        private static double LuminanceTransform(byte value)
356
            => LuminanceTransform(value / 255.0);
2✔
357

358
        private static double LuminanceTransform(double value)
359
            => value <= 0.03928 ? value / 12.92
2✔
360
                                : Math.Pow((value + 0.055) / 1.055, 2.4);
361

362
        #endregion
363

364
        #region Bitmap manipulation
365

366
        public static Bitmap LerpBitmaps(Bitmap a, Bitmap b,
367
                                         float amount)
UNCOV
368
            => amount <= 0 ? a
×
369
             : amount >= 1 ? b
370
             : MergeBitmaps(a, b,
371
                            // Note pixA and pixB are swapped because our blend function
372
                            // pretends 'amount' is the first argument's alpha channel,
373
                            // so 0 -> third param by itself
UNCOV
374
                            (pixA, pixB) => AlphaBlendWith(pixB, amount, pixA));
×
375

376
        private static Bitmap MergeBitmaps(Bitmap a, Bitmap b,
377
                                           Func<Color, Color, Color> how)
UNCOV
378
        {
×
UNCOV
379
            var c = new Bitmap(a);
×
380
            foreach (var y in Enumerable.Range(0, c.Height))
×
381
            {
×
382
                foreach (var x in Enumerable.Range(0, c.Width))
×
383
                {
×
384
                    c.SetPixel(x, y, how(a.GetPixel(x, y),
×
385
                                         b.GetPixel(x, y)));
386
                }
×
UNCOV
387
            }
×
388
            return c;
×
389
        }
×
390

391
        #endregion
392

393
        #region Text sizing
394

395
        /// <summary>
396
        /// Simple syntactic sugar around Graphics.MeasureString
397
        /// </summary>
398
        /// <param name="g">The graphics context</param>
399
        /// <param name="font">The font to be used for the text</param>
400
        /// <param name="text">String to measure size of</param>
401
        /// <param name="maxWidth">Number of pixels allowed horizontally</param>
402
        /// <returns>
403
        /// Number of pixels needed vertically to fit the string
404
        /// </returns>
405
        public static int StringHeight(Graphics g, string text, Font font, int maxWidth)
UNCOV
406
            => (int)g.MeasureString(text, font, (int)(maxWidth / XScale(g))).Height;
×
407

408
        /// <summary>
409
        /// Calculate how much vertical space is needed to display a label's text
410
        /// </summary>
411
        /// <param name="g">The graphics context</param>
412
        /// <param name="lbl">The label</param>
413
        /// <returns>
414
        /// Number of pixels needed vertically to show the label's full text
415
        /// </returns>
416
        public static int LabelStringHeight(Graphics g, Label lbl)
UNCOV
417
            => (int)(YScale(g) * (lbl.Margin.Vertical + lbl.Padding.Vertical
×
418
                                  + StringHeight(g, lbl.Text, lbl.Font,
419
                                                 (lbl.Width - lbl.Margin.Horizontal
420
                                                            - lbl.Padding.Horizontal))));
421

422
        #endregion
423

424
        // Hides the console window on Windows
425
        // useful when running the GUI
426
        [DllImport("kernel32.dll", SetLastError=true)]
427
        private static extern int FreeConsole();
428

429
        public static void HideConsoleWindow()
UNCOV
430
        {
×
UNCOV
431
            if (Platform.IsWindows)
×
432
            {
×
433
                FreeConsole();
×
434
            }
×
435
        }
×
436

437
        private static float XScale(Graphics g) => g.DpiX / 96f;
×
UNCOV
438
        private static float YScale(Graphics g) => g.DpiY / 96f;
×
439

440
        private static readonly ILog log = LogManager.GetLogger(typeof(Util));
2✔
441
    }
442
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc