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

KSP-CKAN / CKAN / 15432034930

04 Jun 2025 02:19AM UTC coverage: 30.322% (+0.04%) from 30.28%
15432034930

Pull #4386

github

web-flow
Merge f8b59bcd0 into 4cf303cc8
Pull Request #4386: Mod list multi-select

4063 of 14340 branches covered (28.33%)

Branch coverage included in aggregate %.

55 of 170 new or added lines in 12 files covered. (32.35%)

59 existing lines in 5 files now uncovered.

13712 of 44281 relevant lines covered (30.97%)

0.63 hits per line

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

0.11
/GUI/Main/Main.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Drawing;
4
using System.Globalization;
5
using System.IO;
6
using System.Linq;
7
using System.Threading;
8
using System.Windows.Forms;
9
using Timer = System.Windows.Forms.Timer;
10
#if NET5_0_OR_GREATER
11
using System.Runtime.Versioning;
12
#endif
13

14
using log4net;
15
using Autofac;
16

17
using CKAN.IO;
18
using CKAN.Extensions;
19
using CKAN.Versioning;
20
using CKAN.GUI.Attributes;
21
using CKAN.Configuration;
22

23
// Don't warn if we use our own obsolete properties
24
#pragma warning disable 0618
25

26
namespace CKAN.GUI
27
{
28
    #if NET5_0_OR_GREATER
29
    [SupportedOSPlatform("windows")]
30
    #endif
31
    public partial class Main : Form, IMessageFilter
32
    {
33
        private static readonly ILog log = LogManager.GetLogger(typeof(Main));
2✔
34

35
        // Stuff we set in the constructor and never change
36
        public readonly IUser currentUser;
37
        public readonly GameInstanceManager Manager;
38
        public GameInstance? CurrentInstance => Manager.CurrentInstance;
×
39
        private readonly RepositoryDataManager repoData;
40
        private readonly string? userAgent;
41
        private readonly AutoUpdate updater = new AutoUpdate();
×
42
        public bool Waiting => Wait.Busy;
×
43

44
        // Stuff we set when the game instance changes
45
        public GUIConfiguration? configuration;
46
        public PluginController? pluginController;
47

48
        private readonly TabController tabController;
49
        private string? focusIdent;
50

51
        private bool needRegistrySave = false;
×
52

53
        [Obsolete("Main.Instance is a global singleton. Find a better way to access this object.")]
54
        public static Main? Instance { get; private set; }
55

56
        /// <summary>
57
        /// Set up the main form's core properties quickly.
58
        /// This should have NO UI interactions!
59
        /// A constructor just initializes the object, it doesn't ask the user questions.
60
        /// That's the job of OnLoad or OnShown.
61
        /// </summary>
62
        /// <param name="cmdlineArgs">The strings from the command line that launched us</param>
63
        /// <param name="mgr">Game instance manager created by the cmdline handler</param>
64
        public Main(string[]             cmdlineArgs,
×
65
                    GameInstanceManager? mgr,
66
                    string?              userAgent)
67
        {
×
68
            log.Info("Starting the GUI");
×
69
            if (//cmdlineArgs is [_, string focusIdent, ..]
×
70
                cmdlineArgs.Length > 1
71
                && cmdlineArgs[1] is string focusIdent)
72
            {
×
73
                if (//focusIdent is ['/', '/', .. var rest]
×
74
                    focusIdent.Length > 2)
75
                {
×
76
                    focusIdent = focusIdent[2..];
×
77
                }
×
78
                else if (//focusIdent is ['c', 'k', 'a', 'n', ':', '/', '/', .. var rest2]
×
79
                    focusIdent.StartsWith("ckan://"))
80
                {
×
81
                    focusIdent = focusIdent[7..];
×
82
                }
×
83
                if (//focusIdent is [.. var start, '/']
×
84
                    focusIdent.EndsWith("/"))
85
                {
×
86
                    focusIdent = focusIdent[..^1];
×
87
                }
×
88
            }
×
89

90
            var mainConfig = ServiceLocator.Container.Resolve<IConfiguration>();
×
91

92
            // If the language is not set yet in the config, try to save the current language.
93
            // If it isn't supported, it'll still be null afterwards. Doesn't matter, .NET handles the resource selection.
94
            // Once the user chooses a language in the settings, the string will be no longer null, and we can change
95
            // CKAN's language here before any GUI components are initialized.
96
            if (string.IsNullOrEmpty(mainConfig.Language))
×
97
            {
×
98
                string runtimeLanguage = Thread.CurrentThread.CurrentUICulture.IetfLanguageTag;
×
99
                mainConfig.Language = runtimeLanguage;
×
100
            }
×
101
            else
102
            {
×
103
                CultureInfo.DefaultThreadCurrentUICulture = Thread.CurrentThread.CurrentUICulture = new CultureInfo(mainConfig.Language);
×
104
            }
×
105

106
            Application.AddMessageFilter(this);
×
107

108
            InitializeComponent();
×
109
            // React when the user clicks a tag or filter link in mod info
110
            ModInfo.OnChangeFilter += ManageMods.Filter;
×
111
            ModInfo.ModuleDoubleClicked += ManageMods.ResetFilterAndSelectModOnList;
×
112
            repoData = ServiceLocator.Container.Resolve<RepositoryDataManager>();
×
113
            this.userAgent = userAgent;
×
114

115
            Instance = this;
×
116

117
            // Replace mono's broken, ugly toolstrip renderer
118
            if (Platform.IsMono)
×
119
            {
×
120
                MainMenu.Renderer = new FlatToolStripRenderer();
×
121
                FileToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer();
×
122
                settingsToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer();
×
123
                helpToolStripMenuItem.DropDown.Renderer = new FlatToolStripRenderer();
×
124
                minimizedContextMenuStrip.Renderer = new FlatToolStripRenderer();
×
125
            }
×
126

127
            // Initialize all user interaction dialogs.
128
            RecreateDialogs();
×
129

130
            // WinForms on Mac OS X has a nasty bug where the UI thread hogs the CPU,
131
            // making our download speeds really slow unless you move the mouse while
132
            // downloading. Yielding periodically addresses that.
133
            // https://bugzilla.novell.com/show_bug.cgi?id=663433
134
            if (Platform.IsMac)
×
135
            {
×
136
                var timer = new Timer
×
137
                {
138
                    Interval = 2
139
                };
140
                timer.Tick += (sender, e) => Thread.Yield();
×
141
                timer.Start();
×
142
            }
×
143

144
            // Set the window name and class for X11
145
            if (Platform.IsX11)
×
146
            {
×
147
                HandleCreated += (sender, e) => X11.SetWMClass(Meta.ProductName,
×
148
                                                               Meta.ProductName, Handle);
149
            }
×
150

151
            currentUser = new GUIUser(this, Wait, StatusLabel, StatusProgress);
×
152
            if (mgr != null)
×
153
            {
×
154
                // With a working GUI, assign a GUIUser to the GameInstanceManager to replace the ConsoleUser
155
                mgr.User = currentUser;
×
156
                Manager = mgr;
×
157
            }
×
158
            else
159
            {
×
160
                Manager = new GameInstanceManager(currentUser);
×
161
            }
×
162

163
            Manager.CacheChanged += OnCacheChanged;
×
164
            OnCacheChanged(null);
×
165
            Manager.InstanceChanged += Manager_InstanceChanged;
×
166

167
            tabController = new TabController(MainTabControl);
×
168
            tabController.ShowTab(ManageModsTabPage.Name);
×
169

170
            // Disable the modinfo controls until a mod has been choosen. This has an effect if the modlist is empty.
171
            ActiveModInfo = null;
×
172
        }
×
173

174
        protected override void OnLoad(EventArgs e)
175
        {
×
176
            // Try to get an instance
177
            if (CurrentInstance == null)
×
178
            {
×
179
                // Maybe we can find an instance automatically (e.g., portable, only, default)
180
                Manager.GetPreferredInstance();
×
181
            }
×
182

183
            // We need a config object to get the window geometry, but we don't need the registry lock yet
184
            configuration = GUIConfigForInstance(
×
185
                Manager.SteamLibrary,
186
                // Find the most recently used instance if no default instance
187
                CurrentInstance ?? InstanceWithNewestGUIConfig(Manager.Instances.Values));
188

189
            // This must happen before Shown, and it depends on the configuration
190
            SetStartPosition();
×
191
            Size = configuration.WindowSize;
×
192
            WindowState = configuration.IsWindowMaximised ? FormWindowState.Maximized : FormWindowState.Normal;
×
193

NEW
194
            URLHandlers.RegisterURLHandler(configuration, CurrentInstance, currentUser);
×
195

196
            Util.Invoke(this, () => Text = $"CKAN {Meta.GetVersion()}");
×
197

198
            splitContainer1.SplitterDistance = configuration.PanelPosition;
×
199

200
            log.Info("GUI started");
×
201
            base.OnLoad(e);
×
202
        }
×
203

204
        private static GUIConfiguration GUIConfigForInstance(SteamLibrary steamLib, GameInstance? inst)
UNCOV
205
            => inst == null ? new GUIConfiguration()
×
206
                            : GUIConfiguration.LoadOrCreateConfiguration(inst, steamLib);
207

208
        private static GameInstance? InstanceWithNewestGUIConfig(IEnumerable<GameInstance> instances)
209
            => instances.Where(inst => inst.Valid)
×
210
                        .OrderByDescending(GUIConfiguration.LastWriteTime)
211
                        .ThenBy(inst => inst.Name)
×
212
                        .FirstOrDefault();
213

214
        /// <summary>
215
        /// Form.Visible says true even when the form hasn't shown yet.
216
        /// This value will tell the truth.
217
        /// </summary>
218
        public bool actuallyVisible { get; private set; } = false;
×
219

220
        protected override void OnShown(EventArgs e)
221
        {
×
222
            actuallyVisible = true;
×
223

224
            tabController.RenameTab(WaitTabPage.Name, Properties.Resources.MainLoadingGameInstance);
×
225
            ShowWaitDialog();
×
226
            DisableMainWindow();
×
227
            Wait.StartWaiting(
×
228
                (sender, evt) =>
229
                {
×
230
                    if (evt != null)
×
231
                    {
×
232
                        currentUser.RaiseMessage(Properties.Resources.MainModListLoadingRegistry);
×
233
                        // Make sure we have a lockable instance
234
                        do
235
                        {
×
236
                            if (CurrentInstance == null && !InstancePromptAtStart())
×
237
                            {
×
238
                                // User cancelled, give up
239
                                evt.Result = false;
×
240
                                return;
×
241
                            }
242
                            for (RegistryManager? regMgr = null;
×
243
                                 CurrentInstance != null && regMgr == null;)
×
244
                            {
×
245
                                // We now have a tentative instance. Check if it's locked.
246
                                try
247
                                {
×
248
                                    // This will throw RegistryInUseKraken if locked by another process
249
                                    regMgr = RegistryManager.Instance(CurrentInstance, repoData);
×
250
                                    // Tell the user their registry was reset if it was corrupted
251
                                    if (regMgr.previousCorruptedMessage != null
×
252
                                        && regMgr.previousCorruptedPath != null)
253
                                    {
×
254
                                        errorDialog.ShowErrorDialog(this,
×
255
                                            Properties.Resources.MainCorruptedRegistry,
256
                                            regMgr.previousCorruptedPath, regMgr.previousCorruptedMessage,
257
                                            Path.Combine(Path.GetDirectoryName(regMgr.previousCorruptedPath) ?? "", regMgr.LatestInstalledExportFilename()));
258
                                        regMgr.previousCorruptedMessage = null;
×
259
                                        regMgr.previousCorruptedPath    = null;
×
260
                                        // But the instance is actually fine because a new registry was just created
261
                                    }
×
262
                                }
×
263
                                catch (RegistryInUseKraken kraken)
×
264
                                {
×
265
                                    if (YesNoDialog(
×
266
                                        kraken.ToString(),
267
                                        Properties.Resources.MainDeleteLockfileYes,
268
                                        Properties.Resources.MainDeleteLockfileNo))
269
                                    {
×
270
                                        // Delete it
271
                                        File.Delete(kraken.lockfilePath);
×
272
                                        // Loop back around to re-acquire the lock
273
                                    }
×
274
                                    else
275
                                    {
×
276
                                        // Couldn't get the lock, there is no current instance
277
                                        Manager.SetCurrentInstance((GameInstance?)null);
×
278
                                        if (Manager.Instances.Values.All(inst => !inst.Valid || inst.IsMaybeLocked))
×
279
                                        {
×
280
                                            // Everything's invalid or locked, give up
281
                                            evt.Result = false;
×
282
                                            return;
×
283
                                        }
284
                                    }
×
285
                                }
×
286
                                catch (RegistryVersionNotSupportedKraken kraken)
×
287
                                {
×
288
                                    currentUser.RaiseError("{0}", kraken.Message);
×
289
                                    if (CheckForCKANUpdate())
×
290
                                    {
×
291
                                        UpdateCKAN();
×
292
                                    }
×
293
                                    else
294
                                    {
×
295
                                        Manager.SetCurrentInstance((GameInstance?)null);
×
296
                                        if (Manager.Instances.Values.All(inst => !inst.Valid || inst.IsMaybeLocked))
×
297
                                        {
×
298
                                            // Everything's invalid or locked, give up
299
                                            evt.Result = false;
×
300
                                            return;
×
301
                                        }
302
                                    }
×
303
                                }
×
304
                            }
×
305
                        } while (CurrentInstance == null);
×
306
                        // We can only reach this point if CurrentInstance is not null
307
                        // AND we acquired the lock for it successfully
308
                        evt.Result = true;
×
309
                    }
×
310
                },
×
311
                (sender, evt) =>
312
                {
×
313
                    if (evt != null)
×
314
                    {
×
315
                        // Application.Exit doesn't work if the window is disabled!
316
                        EnableMainWindow();
×
317
                        if (evt.Error != null)
×
318
                        {
×
319
                            currentUser.RaiseError("{0}", evt.Error.Message);
×
320
                        }
×
321
                        else if (evt.Result is bool b && b)
×
322
                        {
×
323
                            HideWaitDialog();
×
324
                            CheckTrayState();
×
325
                            Console.CancelKeyPress += (sender2, evt2) =>
×
326
                            {
×
327
                                // Hide tray icon on Ctrl-C
328
                                minimizeNotifyIcon.Visible = false;
×
329
                            };
×
330
                            InitRefreshTimer();
×
331
                            CurrentInstanceUpdated();
×
332
                        }
×
333
                        else
334
                        {
×
335
                            Application.Exit();
×
336
                        }
×
337
                    }
×
338
                },
×
339
                false,
340
                null);
341

342
            base.OnShown(e);
×
343
        }
×
344

345
        private bool InstancePromptAtStart()
346
        {
×
347
            bool gotInstance = false;
×
348
            Util.Invoke(this, () =>
×
349
            {
×
350
                var result = new ManageGameInstancesDialog(Manager, !actuallyVisible, currentUser).ShowDialog(this);
×
351
                gotInstance = result == DialogResult.OK;
×
352
            });
×
353
            return gotInstance;
×
354
        }
×
355

356
        private void StabilityToleranceConfig_Changed(string? identifier, ReleaseStatus? relStat)
357
        {
×
358
            // null represents the overall setting, for which we'll refresh when the settings dialog closes
359
            if (identifier != null)
×
360
            {
×
361
                RefreshModList(false);
×
362
            }
×
363
        }
×
364

365
        private void UpdateStatusBar()
366
        {
×
367
            if (CurrentInstance != null)
×
368
            {
×
369
                StatusInstanceLabel.Text = string.Format(
×
370
                    CurrentInstance.playTime != null
371
                    && CurrentInstance.playTime.Time > TimeSpan.Zero
372
                        ? Properties.Resources.StatusInstanceLabelTextWithPlayTime
373
                        : Properties.Resources.StatusInstanceLabelText,
374
                    CurrentInstance.Name,
375
                    CurrentInstance.game.ShortName,
376
                    CurrentInstance.Version()?.ToString(),
377
                    CurrentInstance.playTime?.ToString() ?? "");
378
            }
×
379
        }
×
380

381
        private void Manager_InstanceChanged(GameInstance? previous, GameInstance? current)
382
        {
×
NEW
383
            if (previous != null)
×
384
            {
×
NEW
385
                if (needRegistrySave)
×
386
                {
×
NEW
387
                    using (var transaction = CkanTransaction.CreateTransactionScope())
×
NEW
388
                    {
×
389
                        // Save registry
NEW
390
                        RegistryManager.Instance(previous, repoData).Save(false);
×
NEW
391
                        transaction.Complete();
×
NEW
392
                        needRegistrySave = false;
×
NEW
393
                    }
×
394
                }
×
NEW
395
                configuration?.Save(previous);
×
396
            }
×
NEW
397
            configuration = GUIConfigForInstance(Manager.SteamLibrary, current);
×
UNCOV
398
        }
×
399

400
        /// <summary>
401
        /// React to switching to a new game instance
402
        /// </summary>
403
        /// <param name="allowRepoUpdate">true if a repo update is allowed if needed (e.g. on initial load), false otherwise</param>
404
        private void CurrentInstanceUpdated()
405
        {
×
NEW
406
            if (CurrentInstance == null || configuration == null)
×
407
            {
×
408
                return;
×
409
            }
410
            CurrentInstance.StabilityToleranceConfig.Changed += StabilityToleranceConfig_Changed;
×
411
            // This will throw RegistryInUseKraken if locked by another process
412
            var regMgr = RegistryManager.Instance(CurrentInstance, repoData);
×
413
            log.Debug("Current instance updated, scanning");
×
414

415
            Util.Invoke(this, () =>
×
416
            {
×
417
                Text = $"{Meta.ProductName} {Meta.GetVersion()} - {CurrentInstance.game.ShortName} {CurrentInstance.Version()}    --    {Platform.FormatPath(CurrentInstance.GameDir())}";
×
418
                minimizeNotifyIcon.Text = $"{Meta.ProductName} - {CurrentInstance.Name}";
×
419
                UpdateStatusBar();
×
420
            });
×
421

422
            if (CurrentInstance.CompatibleVersionsAreFromDifferentGameVersion)
×
423
            {
×
424
                new CompatibleGameVersionsDialog(CurrentInstance, !actuallyVisible)
×
425
                    .ShowDialog(this);
426
            }
×
427

428
            var registry = regMgr.registry;
×
429
            if (regMgr.previousCorruptedMessage != null
×
430
                && regMgr.previousCorruptedPath != null)
431
            {
×
432
                errorDialog.ShowErrorDialog(this,
×
433
                    Properties.Resources.MainCorruptedRegistry,
434
                    regMgr.previousCorruptedPath, regMgr.previousCorruptedMessage,
435
                    Path.Combine(Path.GetDirectoryName(regMgr.previousCorruptedPath) ?? "", regMgr.LatestInstalledExportFilename()));
436
                regMgr.previousCorruptedMessage = null;
×
437
                regMgr.previousCorruptedPath = null;
×
438
            }
×
439

440
            var pluginsPath = Path.Combine(CurrentInstance.CkanDir(), "Plugins");
×
UNCOV
441
            if (!Directory.Exists(pluginsPath))
×
442
            {
×
443
                Directory.CreateDirectory(pluginsPath);
×
444
            }
×
445
            pluginController = new PluginController(pluginsPath, true);
×
446

447
            CurrentInstance.game.RebuildSubdirectories(CurrentInstance.GameDir());
×
448

449
            ManageMods.InstanceUpdated();
×
450

451
            AutoUpdatePrompts(ServiceLocator.Container
×
452
                                            .Resolve<IConfiguration>(),
453
                              configuration);
454

455
            if (configuration.CheckForUpdatesOnLaunch && CheckForCKANUpdate())
×
456
            {
×
457
                UpdateCKAN();
×
458
            }
×
459
            else
460
            {
×
461
                if (configuration.RefreshOnStartup)
×
462
                {
×
463
                    UpdateRepo(refreshWithoutChanges: true);
×
464
                }
×
465
                else
466
                {
×
467
                    RefreshModList(registry.Repositories.Count > 0);
×
468
                }
×
469
            }
×
470
        }
×
471

472
        /// <summary>
473
        /// Open the user guide when the user presses F1
474
        /// </summary>
475
        protected override void OnHelpRequested(HelpEventArgs evt)
476
        {
×
477
            evt.Handled = Util.TryOpenWebPage(HelpURLs.UserGuide);
×
478
        }
×
479

480
        protected override void OnFormClosed(FormClosedEventArgs e)
481
        {
×
482
            if (CurrentInstance != null)
×
483
            {
×
484
                RegistryManager.DisposeInstance(CurrentInstance);
×
485
            }
×
486

487
            // Stop all running play time timers
488
            foreach (var inst in Manager.Instances.Values)
×
489
            {
×
490
                if (inst.Valid)
×
491
                {
×
492
                    inst.playTime?.Stop(inst.CkanDir());
×
493
                }
×
494
            }
×
495
            Application.RemoveMessageFilter(this);
×
496
            actuallyVisible = false;
×
497
            base.OnFormClosed(e);
×
498
        }
×
499

500
        private void SetStartPosition()
501
        {
×
502
            if (configuration != null)
×
503
            {
×
504
                var screen = Util.FindScreen(configuration.WindowLoc, configuration.WindowSize);
×
505
                if (screen == null)
×
506
                {
×
507
                    // Start at center of screen if we have an invalid location saved in the config
508
                    // (such as -32000,-32000, which Windows uses when you're minimized)
509
                    StartPosition = FormStartPosition.CenterScreen;
×
510
                }
×
511
                else if (configuration.WindowLoc.X == -1 && configuration.WindowLoc.Y == -1)
×
512
                {
×
513
                    // Center on screen for first launch
514
                    StartPosition = FormStartPosition.CenterScreen;
×
515
                }
×
516
                else if (Platform.IsMac)
×
517
                {
×
518
                    // Make sure there's room at the top for the MacOSX menu bar
519
                    Location = Util.ClampedLocationWithMargins(
×
520
                        configuration.WindowLoc, configuration.WindowSize,
521
                        new Size(0, 30), new Size(0, 0),
522
                        screen
523
                    );
524
                }
×
525
                else
526
                {
×
527
                    // Just make sure it's fully on screen
528
                    Location = Util.ClampedLocation(configuration.WindowLoc, configuration.WindowSize, screen);
×
529
                }
×
530
            }
×
531
        }
×
532

533
        protected override void OnFormClosing(FormClosingEventArgs e)
534
        {
×
535
            // Only close the window, when the user has access to the "Exit" of the menu.
536
            if (!MainMenu.Enabled)
×
537
            {
×
538
                e.Cancel = true;
×
539
                return;
×
540
            }
541

542
            if (!ManageMods.AllowClose())
×
543
            {
×
544
                e.Cancel = true;
×
545
                return;
×
546
            }
547

548
            if (configuration != null)
×
549
            {
×
550
                // Copy window location to app settings
551
                configuration.WindowLoc = WindowState == FormWindowState.Normal ? Location : RestoreBounds.Location;
×
552

553
                // Copy window size to app settings if not maximized
554
                configuration.WindowSize = WindowState == FormWindowState.Normal ? Size : RestoreBounds.Size;
×
555

556
                //copy window maximized state to app settings
557
                configuration.IsWindowMaximised = WindowState == FormWindowState.Maximized;
×
558

559
                // Copy panel position to app settings
560
                configuration.PanelPosition = splitContainer1.SplitterDistance;
×
561

562
                // Save settings
NEW
563
                if (CurrentInstance != null)
×
NEW
564
                {
×
NEW
565
                    configuration.Save(CurrentInstance);
×
NEW
566
                }
×
UNCOV
567
            }
×
568

569
            if (needRegistrySave && CurrentInstance != null)
×
570
            {
×
571
                using (var transaction = CkanTransaction.CreateTransactionScope())
×
572
                {
×
573
                    // Save registry
574
                    RegistryManager.Instance(CurrentInstance, repoData).Save(false);
×
575
                    transaction.Complete();
×
576
                    needRegistrySave = false;
×
577
                }
×
578
            }
×
579

580
            // Remove the tray icon
581
            minimizeNotifyIcon.Visible = false;
×
582
            minimizeNotifyIcon.Dispose();
×
583

584
            base.OnFormClosing(e);
×
585
        }
×
586

587
        // https://learn.microsoft.com/en-us/windows/win32/winmsg/window-notifications
588
        private const int WM_PARENTNOTIFY = 0x210;
589

590
        protected override void WndProc(ref Message m)
591
        {
×
592
            base.WndProc(ref m);
×
593
            if (Platform.IsWindows)
×
594
            {
×
595
                switch (m.Msg)
×
596
                {
597
                    // Windows sends us this when you click outside the search dropdown
598
                    // Mono closes the dropdown automatically
599
                    case WM_PARENTNOTIFY:
600
                        ManageMods?.CloseSearch(
×
601
                            PointToScreen(new Point(m.LParam.ToInt32() & 0xffff,
602
                                                    m.LParam.ToInt32() >> 16)));
603
                        break;
×
604
                }
605
            }
×
606
        }
×
607

608
        protected override void OnMove(EventArgs e)
609
        {
×
610
            base.OnMove(e);
×
611
            ManageMods?.ParentMoved();
×
612
        }
×
613

614
        private IEnumerable<T> VisibleControls<T>()
615
            => (MainTabControl?.SelectedTab
×
616
                              ?.Controls
617
                               .OfType<T>()
618
                              ?? Enumerable.Empty<T>())
619
                  .Concat(!splitContainer1.Panel2Collapsed
620
                          && ModInfo is T t
621
                              ? Enumerable.Repeat(t, 1)
622
                              : Enumerable.Empty<T>());
623

624
        protected override void OnKeyDown(KeyEventArgs e)
625
        {
×
626
            switch (e.KeyData)
×
627
            {
628
                case Keys.Control | Keys.F:
629
                    VisibleControls<ISearchableControl>()
×
630
                        .FirstOrDefault()
631
                        ?.FocusSearch(false);
632
                    e.Handled = true;
×
633
                    break;
×
634
                case Keys.Control | Keys.Shift | Keys.F:
635
                    VisibleControls<ISearchableControl>()
×
636
                        .FirstOrDefault()
637
                        ?.FocusSearch(true);
638
                    e.Handled = true;
×
639
                    break;
×
640
                default:
641
                    base.OnKeyDown(e);
×
642
                    break;
×
643
            }
644
        }
×
645

646
        private void SetupDefaultSearch()
647
        {
×
648
            if (CurrentInstance != null && configuration != null)
×
649
            {
×
650
                var registry = RegistryManager.Instance(CurrentInstance, repoData).registry;
×
651
                var def = configuration.DefaultSearches;
×
652
                if (def == null || def.Count < 1)
×
653
                {
×
654
                    // Fall back to old setting
655
                    ManageMods.Filter(
×
656
                        ModList.FilterToSavedSearch(
657
                            CurrentInstance,
658
                            (GUIModFilter)configuration.ActiveFilter,
659
                            configuration.TagFilter == null
660
                                ? null
661
                                : registry.Tags.GetOrDefault(configuration.TagFilter),
662
                            ModuleLabelList.ModuleLabels.LabelsFor(CurrentInstance.Name)
663
                                .FirstOrDefault(l => l.Name == configuration.CustomLabelFilter)),
×
664
                        false);
665
                    // Clear the old filter so it doesn't get pulled forward again
666
                    configuration.ActiveFilter = (int)GUIModFilter.All;
×
667
                }
×
668
                else
669
                {
×
670
                    var searches = def.Select(s => ModSearch.Parse(CurrentInstance, s))
×
671
                                      .OfType<ModSearch>()
672
                                      .ToList();
673
                    ManageMods.SetSearches(searches);
×
674
                }
×
675
            }
×
676
        }
×
677

678
        private void EditCommandLines()
679
        {
×
680
            if (CurrentInstance != null && configuration != null)
×
681
            {
×
682
                var dialog = new GameCommandLineOptionsDialog();
×
683
                var defaults = CurrentInstance.game.DefaultCommandLines(Manager.SteamLibrary,
×
684
                                                                        new DirectoryInfo(CurrentInstance.GameDir()));
685
                if (dialog.ShowGameCommandLineOptionsDialog(this, configuration.CommandLines, defaults) == DialogResult.OK)
×
686
                {
×
687
                    configuration.CommandLines = dialog.Results;
×
688
                }
×
689
            }
×
690
        }
×
691

692
        private void InstallFromCkanFiles(string[] files)
693
        {
×
694
            if (CurrentInstance == null)
×
695
            {
×
696
                return;
×
697
            }
698
            // We'll need to make some registry changes to do this.
699
            var registry_manager = RegistryManager.Instance(CurrentInstance, repoData);
×
700
            var stabilityTolerance = CurrentInstance.StabilityToleranceConfig;
×
701
            var crit = CurrentInstance.VersionCriteria();
×
702

703
            var installed = registry_manager.registry.InstalledModules.Select(inst => inst.Module).ToList();
×
704
            var toInstall = new List<CkanModule>();
×
705
            foreach (string path in files)
×
706
            {
×
707
                CkanModule module;
708

709
                try
710
                {
×
711
                    module = CkanModule.FromFile(path);
×
712
                    if (module.IsMetapackage && module.depends != null)
×
713
                    {
×
714
                        // Add metapackage dependencies to the changeset so we can skip compat checks for them
715
                        toInstall.AddRange(module.depends
×
716
                            .Where(rel => !rel.MatchesAny(installed, null, null))
×
717
                            .Select(rel =>
718
                                // If there's a compatible match, return it
719
                                // Metapackages aren't intending to prompt users to choose providing mods
720
                                rel.ExactMatch(registry_manager.registry, stabilityTolerance, crit, installed, toInstall)
×
721
                                // Otherwise look for incompatible
722
                                ?? rel.ExactMatch(registry_manager.registry, stabilityTolerance, null, installed, toInstall))
723
                            .OfType<CkanModule>());
724
                    }
×
725
                    toInstall.Add(module);
×
726
                }
×
727
                catch (Kraken kraken)
×
728
                {
×
729
                    currentUser.RaiseError("{0}", kraken.InnerException == null
×
730
                        ? kraken.Message
731
                        : $"{kraken.Message}: {kraken.InnerException.Message}");
732

733
                    continue;
×
734
                }
735
                catch (Exception ex)
×
736
                {
×
737
                    currentUser.RaiseError("{0}", ex.Message);
×
738
                    continue;
×
739
                }
740

741
                if (module.IsDLC)
×
742
                {
×
743
                    currentUser.RaiseError(Properties.Resources.MainCantInstallDLC, module);
×
744
                    continue;
×
745
                }
746
            }
×
747

748
            var modpacks = toInstall.Where(m => m.IsMetapackage)
×
749
                                    .ToArray();
750
            if (modpacks.Any())
×
751
            {
×
752
                CkanModule.GetMinMaxVersions(modpacks,
×
753
                                             out _, out _,
754
                                             out GameVersion? minGame, out GameVersion? maxGame);
755
                var filesRange = new GameVersionRange(minGame ?? GameVersion.Any,
×
756
                                                      maxGame ?? GameVersion.Any);
757
                var instRanges = crit.Versions.Select(gv => gv.ToVersionRange())
×
758
                                              .ToList();
759
                var missing = CurrentInstance.game
×
760
                                             .KnownVersions
761
                                             .Where(gv => filesRange.Contains(gv)
×
762
                                                          && !instRanges.Any(ir => ir.Contains(gv)))
×
763
                                             // Use broad Major.Minor group for each specific version
764
                                             .Select(gv => new GameVersion(gv.Major, gv.Minor))
×
765
                                             .Distinct()
766
                                             .ToList();
767
                if (missing.Count != 0
×
768
                    && YesNoDialog(string.Format(Properties.Resources.MetapackageAddCompatibilityPrompt,
769
                                                 filesRange.ToSummaryString(CurrentInstance.game),
770
                                                 crit.ToSummaryString(CurrentInstance.game)),
771
                                   Properties.Resources.MetapackageAddCompatibilityYes,
772
                                   Properties.Resources.MetapackageAddCompatibilityNo))
773
                {
×
774
                    CurrentInstance.SetCompatibleVersions(crit.Versions
×
775
                                                              .Concat(missing)
776
                                                              .ToList());
777
                    crit = CurrentInstance.VersionCriteria();
×
778
                }
×
779
            }
×
780

781
            // Get all recursively incompatible module identifiers (quickly)
782
            var allIncompat = registry_manager.registry.IncompatibleModules(stabilityTolerance, crit)
×
783
                .Select(mod => mod.identifier)
×
784
                .ToHashSet();
785
            // Get incompatible mods we're installing
786
            var myIncompat = toInstall.Where(mod => allIncompat.Contains(mod.identifier)).ToList();
×
787
            if (myIncompat.Count == 0
×
788
                // Confirm installation of incompatible like the Versions tab does
789
                || YesNoDialog(string.Format(Properties.Resources.ModpackInstallIncompatiblePrompt,
790
                                             string.Join(Environment.NewLine, myIncompat),
791
                                             crit.ToSummaryString(CurrentInstance.game)),
792
                               Properties.Resources.AllModVersionsInstallYes,
793
                               Properties.Resources.AllModVersionsInstallNo))
794
            {
×
795
                UpdateChangesDialog(toInstall.Select(m => new ModChange(m, GUIModChangeType.Install,
×
796
                                                                        ServiceLocator.Container.Resolve<IConfiguration>()))
797
                                             .ToList(),
798
                                    null);
799
                tabController.ShowTab(ChangesetTabPage.Name, 1);
×
800
            }
×
801
        }
×
802

803
        private const int WM_XBUTTONDOWN = 0x20b;
804
        private const int MK_XBUTTON1    = 0x20;
805
        private const int MK_XBUTTON2    = 0x40;
806

807
        public bool PreFilterMessage(ref Message m)
808
        {
×
809
            switch (m.Msg)
×
810
            {
811
                case WM_XBUTTONDOWN:
812
                    switch (m.WParam.ToInt32() & 0xffff)
×
813
                    {
814
                        case MK_XBUTTON1:
815
                            ManageMods.NavGoBackward();
×
816
                            break;
×
817

818
                        case MK_XBUTTON2:
819
                            ManageMods.NavGoForward();
×
820
                            break;
×
821
                    }
822
                    break;
×
823
            }
824
            return false;
×
825
        }
×
826

827
        private void ManageMods_OnSelectedModuleChanged(GUIMod m)
828
        {
×
829
            if (MainTabControl.SelectedTab == ManageModsTabPage)
×
830
            {
×
831
                ActiveModInfo = m;
×
832
            }
×
833
        }
×
834

835
        private GUIMod? ActiveModInfo
836
        {
837
            set {
×
838
                if (value?.ToModule() == null)
×
839
                {
×
840
                    splitContainer1.Panel2Collapsed = true;
×
841
                }
×
842
                else
843
                {
×
844
                    if (splitContainer1.Panel2Collapsed)
×
845
                    {
×
846
                        splitContainer1.Panel2Collapsed = false;
×
847
                    }
×
848
                }
×
849
                ModInfo.SelectedModule = value;
×
850
            }
×
851
        }
852

853
        private void ShowSelectionModInfo(CkanModule? module)
854
        {
×
855
            if (CurrentInstance != null && configuration != null)
×
856
            {
×
857
                ActiveModInfo = module == null ? null : new GUIMod(
×
858
                    module,
859
                    repoData,
860
                    RegistryManager.Instance(CurrentInstance, repoData).registry,
861
                    CurrentInstance.StabilityToleranceConfig,
862
                    CurrentInstance.VersionCriteria(),
863
                    null,
864
                    configuration.HideEpochs,
865
                    configuration.HideV);
866
            }
×
867
        }
×
868

869
        private void ShowSelectionModInfo(ListView.SelectedListViewItemCollection selection)
870
        {
×
871
            ShowSelectionModInfo(selection?.OfType<ListViewItem>()
×
872
                                           .FirstOrDefault()?.Tag as CkanModule);
873
        }
×
874

875
        private void ManageMods_OnChangeSetChanged(List<ModChange> changeset, Dictionary<GUIMod, string> conflicts)
876
        {
×
877
            if (changeset != null && changeset.Count != 0)
×
878
            {
×
879
                tabController.ShowTab(ChangesetTabPage.Name, 1, false);
×
880
                UpdateChangesDialog(
×
881
                    changeset,
882
                    conflicts.ToDictionary(item => item.Key.ToCkanModule(),
×
883
                                           item => item.Value));
×
884
                AuditRecommendationsToolStripMenuItem.Enabled = false;
×
885
            }
×
886
            else
887
            {
×
888
                tabController.HideTab(ChangesetTabPage.Name);
×
889
                AuditRecommendationsToolStripMenuItem.Enabled = true;
×
890
            }
×
891
        }
×
892

893
        private void ManageMods_OnRegistryChanged()
894
        {
×
895
            needRegistrySave = true;
×
896
        }
×
897

898
        private void ManageMods_RaiseMessage(string message)
899
        {
×
900
            currentUser.RaiseMessage("{0}", message);
×
901
        }
×
902

903
        private void ManageMods_RaiseError(string error)
904
        {
×
905
            currentUser.RaiseError("{0}", error);
×
906
        }
×
907

908
        private void ManageMods_SetStatusBar(string message)
909
        {
×
910
            StatusLabel.ToolTipText = StatusLabel.Text = message;
×
911
        }
×
912

913
        private void ManageMods_ClearStatusBar()
914
        {
×
915
            StatusLabel.ToolTipText = StatusLabel.Text = "";
×
916
        }
×
917

918
        private void MainTabControl_OnSelectedIndexChanged(object? sender, EventArgs? e)
919
        {
×
920
            switch (MainTabControl.SelectedTab?.Name)
×
921
            {
922
                case "ManageModsTabPage":
923
                    ActiveModInfo = ManageMods.SelectedModule;
×
924
                    ManageMods.ModGrid.Focus();
×
925
                    break;
×
926

927
                case "ChangesetTabPage":
928
                    ShowSelectionModInfo(Changeset.SelectedItem);
×
929
                    break;
×
930

931
                case "ChooseRecommendedModsTabPage":
932
                    ShowSelectionModInfo(ChooseRecommendedMods.SelectedItems);
×
933
                    break;
×
934

935
                case "ChooseProvidedModsTabPage":
936
                    ShowSelectionModInfo(ChooseProvidedMods.SelectedItems);
×
937
                    break;
×
938

939
                default:
940
                    ShowSelectionModInfo(null as CkanModule);
×
941
                    break;
×
942
            }
943
        }
×
944

945
        private void Main_Resize(object? sender, EventArgs? e)
946
        {
×
947
            UpdateTrayState();
×
948
        }
×
949

950
        private void LaunchGame(string command)
951
        {
×
952
            if (CurrentInstance != null)
×
953
            {
×
954
                var registry = RegistryManager.Instance(CurrentInstance, repoData).registry;
×
955
                var suppressedIdentifiers = CurrentInstance.GetSuppressedCompatWarningIdentifiers;
×
956
                var incomp = registry.IncompatibleInstalled(CurrentInstance.VersionCriteria())
×
957
                    .Where(m => !m.Module.IsDLC && !suppressedIdentifiers.Contains(m.identifier))
×
958
                    .ToList();
959
                if (incomp.Count != 0 && CurrentInstance.Version() is GameVersion gv)
×
960
                {
×
961
                    // Warn that it might not be safe to run game with incompatible modules installed
962
                    string incompatDescrip = incomp
×
963
                        .Select(m => $"{m.Module} ({m.Module.CompatibleGameVersions(CurrentInstance.game)})")
×
964
                        .Aggregate((a, b) => $"{a}{Environment.NewLine}{b}");
×
965
                    var result = SuppressableYesNoDialog(
×
966
                        string.Format(Properties.Resources.MainLaunchWithIncompatible,
967
                                      incompatDescrip),
968
                        string.Format(Properties.Resources.MainLaunchDontShow,
969
                                      CurrentInstance.game.ShortName,
970
                                      gv.WithoutBuild),
971
                        Properties.Resources.MainLaunch,
972
                        Properties.Resources.MainGoBack);
973
                    if (result.Item1 != DialogResult.Yes)
×
974
                    {
×
975
                        return;
×
976
                    }
977
                    else if (result.Item2)
×
978
                    {
×
979
                        CurrentInstance.AddSuppressedCompatWarningIdentifiers(
×
980
                            incomp.Select(m => m.identifier)
×
981
                                  .ToHashSet());
982
                    }
×
983
                }
×
984

985
                CurrentInstance.PlayGame(command,
×
986
                                         () =>
987
                                         {
×
988
                                             ManageMods.OnGameExit();
×
989
                                             UpdateStatusBar();
×
990
                                         });
×
991
            }
×
992
        }
×
993

994
        // This is used by Reinstall
995
        private void ManageMods_StartChangeSet(List<ModChange> changeset, Dictionary<GUIMod, string> conflicts)
996
        {
×
997
            UpdateChangesDialog(changeset,
×
998
                                conflicts?.ToDictionary(item => item.Key.ToCkanModule(),
×
999
                                                        item => item.Value));
×
1000
            tabController.ShowTab(ChangesetTabPage.Name, 1);
×
1001
        }
×
1002

1003
        private void RefreshModList(bool allowAutoUpdate, Dictionary<string, bool>? oldModules = null)
1004
        {
×
1005
            tabController.RenameTab(WaitTabPage.Name, Properties.Resources.MainModListWaitTitle);
×
1006
            ShowWaitDialog();
×
1007
            DisableMainWindow();
×
1008
            ActiveModInfo = null;
×
1009
            Wait.StartWaiting(
×
1010
                ManageMods.Update,
1011
                (sender, e) =>
1012
                {
×
1013
                    if (e != null)
×
1014
                    {
×
1015
                        if (allowAutoUpdate && e.Result is bool b && !b)
×
1016
                        {
×
1017
                            UpdateRepo();
×
1018
                        }
×
1019
                        else
1020
                        {
×
1021
                            UpdateTrayInfo();
×
1022
                            HideWaitDialog();
×
1023
                            EnableMainWindow();
×
1024
                            SetupDefaultSearch();
×
1025
                            if (focusIdent != null)
×
1026
                            {
×
1027
                                log.Debug("Attempting to select mod from startup parameters");
×
1028
                                ManageMods.FocusMod(focusIdent, true, true);
×
1029
                                // Only do it the first time
1030
                                focusIdent = null;
×
1031
                            }
×
1032
                        }
×
1033
                    }
×
1034
                },
×
1035
                false,
1036
                oldModules);
1037
        }
×
1038

1039
        [ForbidGUICalls]
1040
        private void EnableMainWindow()
1041
        {
×
1042
            Util.Invoke(this, () =>
×
1043
            {
×
1044
                Enabled = true;
×
1045
                MainMenu.Enabled = true;
×
1046
                tabController.SetTabLock(false);
×
1047
                /* Windows (7 & 8 only?) bug #1548 has extra facets.
1048
                 * parent.childcontrol.Enabled = false seems to disable the parent,
1049
                 * if childcontrol had focus. Depending on optimization steps,
1050
                 * parent.childcontrol.Enabled = true does not necessarily
1051
                 * re-enable the parent.*/
1052
                Focus();
×
1053
            });
×
1054
        }
×
1055

1056
        [ForbidGUICalls]
1057
        private void DisableMainWindow()
1058
        {
×
1059
            Util.Invoke(this, () =>
×
1060
            {
×
1061
                MainMenu.Enabled = false;
×
1062
                tabController.SetTabLock(true);
×
1063
            });
×
1064
        }
×
1065

1066
    }
1067
}
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