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

luttje / Key2Joy / 6602636543

22 Oct 2023 08:16AM UTC coverage: 44.104% (-8.4%) from 52.519%
6602636543

Pull #50

github

web-flow
Merge cf342a7b3 into 14b7ce9a7
Pull Request #50: Add XInput in preparation for gamepad triggers + add xmldoc

764 of 2383 branches covered (0.0%)

Branch coverage included in aggregate %.

3060 of 3060 new or added lines in 106 files covered. (100.0%)

3896 of 8183 relevant lines covered (47.61%)

15812.68 hits per line

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

0.0
/Key2Joy.Gui/MainForm.cs
1
using System;
2
using System.Collections.Generic;
3
using System.Diagnostics;
4
using System.Drawing;
5
using System.IO;
6
using System.Linq;
7
using System.Media;
8
using System.Windows.Forms;
9
using BrightIdeasSoftware;
10
using CommandLine;
11
using CommonServiceLocator;
12
using Key2Joy.Config;
13
using Key2Joy.Contracts;
14
using Key2Joy.Contracts.Mapping;
15
using Key2Joy.Contracts.Util;
16
using Key2Joy.Gui.Properties;
17
using Key2Joy.Gui.Util;
18
using Key2Joy.LowLevelInput;
19
using Key2Joy.Mapping;
20
using Key2Joy.Mapping.Actions;
21
using Key2Joy.Mapping.Actions.Input;
22
using Key2Joy.Mapping.Actions.Logic;
23
using Key2Joy.Mapping.Triggers;
24

25
namespace Key2Joy.Gui;
26

27
public partial class MainForm : Form, IAcceptAppCommands, IHaveHandleAndInvoke
28
{
29
    private readonly IDictionary<string, CachedMappingGroup> cachedMappingGroups = new Dictionary<string, CachedMappingGroup>();
×
30
    private readonly ConfigState configState;
31

32
    private MappingProfile selectedProfile;
33

34
    public MainForm(bool shouldStartMinimized = false)
×
35
    {
36
        this.configState = ServiceLocator.Current
×
37
            .GetInstance<IConfigManager>()
×
38
            .GetConfigState();
×
39

40
        this.InitializeComponent();
×
41

42
        this.ApplyMinimizedStateIfNeeded(shouldStartMinimized);
×
43
        this.ConfigureStatusLabels();
×
44
        this.SetupNotificationIndicator();
×
45
        this.PopulateGroupImages();
×
46
        this.RegisterListViewEvents();
×
47

48
        this.ConfigureTriggerColumn();
×
49
        this.ConfigureActionColumn();
×
50
        this.ConfigureTooltips();
×
51
    }
×
52

53
    private void RefreshMappingGroupMenu()
54
    {
55
        var menu = this.groupMappingsByToolStripMenuItem.DropDown;
×
56
        var groupTypes = Enum.GetValues(typeof(ViewMappingGroupType));
×
57
        var configManager = ServiceLocator.Current.GetInstance<IConfigManager>();
×
58
        var current = configManager.GetConfigState().SelectedViewMappingGroupType;
×
59

60
        menu.Items.Clear();
×
61

62
        foreach (var groupType in groupTypes)
×
63
        {
64
            var item = new ToolStripMenuItem(groupType.ToString());
×
65
            item.Click += (s, e) =>
×
66
            {
×
67
                var selected = (ViewMappingGroupType)groupType;
×
68
                configManager.GetConfigState().SelectedViewMappingGroupType = selected;
×
69
                this.RefreshMappingsAfterGroupChange();
×
70
            };
×
71

72
            if (current == (ViewMappingGroupType)groupType)
×
73
            {
74
                item.Checked = true;
×
75
            }
76

77
            menu.Items.Add(item);
×
78
        }
79
    }
×
80

81
    /// <summary>
82
    /// Shows a notification banner at the top of the app.
83
    /// </summary>
84
    /// <param name="banner"></param>
85
    private void ShowNotification(NotificationBannerControl banner)
86
    {
87
        banner.Dock = DockStyle.Top;
×
88
        this.pnlNotificationsParent.Controls.Add(banner);
×
89
        this.pnlNotificationsParent.PerformLayout();
×
90
    }
×
91

92
    /// <summary>
93
    /// Refresh the listed mappings, their sorting and formatting.
94
    /// Call this after making a change to the mapped options.
95
    /// </summary>
96
    private void RefreshMappings()
97
        => this.olvMappings.SetObjects(this.selectedProfile.MappedOptions);
×
98

99
    private void RefreshMappingsAfterGroupChange()
100
    {
101
        this.RefreshMappings();
×
102
        this.RefreshMappingGroupMenu();
×
103
        this.FindAndDetachParentChildDifferentGroups();
×
104
    }
×
105

106
    private void ApplyMinimizedStateIfNeeded(bool shouldMinimize)
107
    {
108
        this.WindowState = shouldMinimize ? FormWindowState.Minimized : FormWindowState.Normal;
×
109
        this.ShowInTaskbar = !shouldMinimize;
×
110
    }
×
111

112
    private void ConfigureStatusLabels()
113
        => this.lblStatusActive.Visible = this.chkArmed.Checked;
×
114

115
    private void SetupNotificationIndicator()
116
    {
117
        var items = new MenuItem[]{
×
118
            new MenuItem("Show", (s, e) => {
×
119
                this.Show();
×
120
                this.BringToFront();
×
121

×
122
                if (this.WindowState == FormWindowState.Minimized) { this.WindowState = FormWindowState.Normal; } }),
×
123
            new MenuItem("Exit", this.ExitProgramToolStripMenuItem_Click)
×
124
        };
×
125

126
        this.ntfIndicator.ContextMenu = new ContextMenu(items);
×
127
    }
×
128

129
    private void PopulateGroupImages()
130
    {
131
        var allAttributes = ActionsRepository.GetAllActionAttributes();
×
132
        ImageList imageList = new();
×
133

134
        foreach (var attribute in allAttributes)
×
135
        {
136
            if (attribute.GroupImage != null && !imageList.Images.ContainsKey(attribute.GroupImage))
×
137
            {
138
                imageList.Images.Add(attribute.GroupImage, (Bitmap)Resources.ResourceManager.GetObject(attribute.GroupImage));
×
139
            }
140
        }
141

142
        this.olvMappings.GroupImageList = imageList;
×
143
    }
×
144

145
    private void RegisterListViewEvents()
146
    {
147
        this.olvColumnAction.GroupKeyGetter += this.OlvMappings_GroupKeyGetter;
×
148
        this.olvColumnAction.GroupKeyToTitleConverter += this.OlvMappings_GroupKeyToTitleConverter;
×
149

150
        this.olvMappings.BeforeCreatingGroups += this.OlvMappings_BeforeCreatingGroups;
×
151
        this.olvMappings.AboutToCreateGroups += this.OlvMappings_AboutToCreateGroups;
×
152

153
        this.olvMappings.CellClick += this.OlvMappings_CellClick;
×
154
        this.olvMappings.CellRightClick += this.OlvMappings_CellRightClick;
×
155
        this.olvMappings.FormatRow += this.OlvMappings_FormatRow;
×
156
        this.olvMappings.FormatCell += this.OlvMappings_FormatCell;
×
157
        this.olvMappings.KeyUp += this.OlvMappings_KeyUp;
×
158
    }
×
159

160
    private void ConfigureTriggerColumn()
161
        => this.olvColumnTrigger.AspectToStringConverter = delegate (object obj)
×
162
        {
×
163
            var trigger = obj as CoreTrigger;
×
164

×
165
            if (trigger == null)
×
166
            {
×
167
                return "(no trigger mapped)";
×
168
            }
×
169

×
170
            return trigger.GetNameDisplay().Ellipsize(64);
×
171
        };
×
172

173
    private void ConfigureActionColumn()
174
        => this.olvColumnAction.AspectToStringConverter = delegate (object obj)
×
175
        {
×
176
            var action = obj as CoreAction;
×
177

×
178
            if (action == null)
×
179
            {
×
180
                return "(no action mapped)";
×
181
            }
×
182

×
183
            return action.GetNameDisplay().Ellipsize(64);
×
184
        };
×
185

186
    private void ConfigureTooltips()
187
        => this.olvMappings.CellToolTipShowing += (s, e) =>
×
188
        {
×
189
            if (e.Model is not MappedOption mappedOption)
×
190
            {
×
191
                return;
×
192
            }
×
193

×
194
            var action = mappedOption.Action;
×
195
            var trigger = mappedOption.Trigger;
×
196
            var toolTipText = string.Empty;
×
197

×
198
            if (action.GetNameDisplay() != action.GetNameDisplay().Ellipsize(64))
×
199
            {
×
200
                toolTipText += $"Action: {action.GetNameDisplay()}\n";
×
201
            }
×
202
            else if (trigger.GetNameDisplay() != trigger.GetNameDisplay().Ellipsize(64))
×
203
            {
×
204
                toolTipText += $"Trigger: {trigger.GetNameDisplay()}\n";
×
205
            }
×
206

×
207
            if (!string.IsNullOrEmpty(toolTipText))
×
208
            {
×
209
                e.Text = toolTipText;
×
210
            }
×
211
        };
×
212

213
    private void RefreshColumnWidths()
214
    {
215
        this.olvColumnAction.MaximumWidth = this.olvMappings.Width - this.olvColumnTrigger.Width - 25;
×
216
        this.olvColumnAction.Width = Math.Max(this.olvColumnAction.Width, this.olvColumnAction.MaximumWidth);
×
217
    }
×
218

219
    private void SetSelectedProfile(MappingProfile profile)
220
    {
221
        this.selectedProfile = profile;
×
222
        this.configState.LastLoadedProfile = profile.FilePath;
×
223

224
        this.RefreshMappings();
×
225
        this.olvMappings.AutoResizeColumns(ColumnHeaderAutoResizeStyle.ColumnContent);
×
226
        this.olvMappings.Sort(this.olvColumnTrigger, SortOrder.Ascending);
×
227
        this.RefreshMappingsAfterGroupChange();
×
228

229
        this.UpdateSelectedProfileName();
×
230
    }
×
231

232
    private void UpdateSelectedProfileName() => this.txtProfileName.Text = this.selectedProfile.Name;
×
233

234
    private void SetStatusView(bool isEnabled)
235
    {
236
        this.chkArmed.CheckedChanged -= this.ChkEnabled_CheckedChanged;
×
237
        this.chkArmed.Checked = isEnabled;
×
238
        this.chkArmed.CheckedChanged += this.ChkEnabled_CheckedChanged;
×
239

240
        this.lblStatusActive.Visible = isEnabled;
×
241
        this.lblStatusInactive.Visible = !isEnabled;
×
242
    }
×
243

244
    private MappingProfile CreateNewProfile(string nameSuffix = default)
245
    {
246
        MappingProfile profile = new($"{this.txtProfileName.Text}{nameSuffix}", this.selectedProfile?.MappedOptions);
×
247

248
        this.SetSelectedProfile(profile);
×
249

250
        return profile;
×
251
    }
252

253
    private void EditMappedOption(MappedOption existingMappedOption = null)
254
    {
255
        this.chkArmed.Checked = false;
×
256
        MappingForm mappingForm = new(existingMappedOption);
×
257
        var result = mappingForm.ShowDialog();
×
258

259
        if (result == DialogResult.Cancel)
×
260
        {
261
            return;
×
262
        }
263

264
        if (existingMappedOption == null)
×
265
        {
266
            this.selectedProfile.AddMapping(mappingForm.MappedOption);
×
267
        }
268

269
        if (mappingForm.MappedOptionReverse != null)
×
270
        {
271
            this.selectedProfile.AddMapping(mappingForm.MappedOptionReverse);
×
272
        }
273

274
        this.selectedProfile.Save();
×
275
        this.RefreshMappings();
×
276
    }
×
277

278
    private void RemoveMappings(IList<MappedOption> mappedOptions)
279
    {
280
        var children = mappedOptions.SelectMany(x => x.Children).ToList();
×
281

282
        if (children.Any())
×
283
        {
284
            var introText = mappedOptions.Count == 1 ? "This mapped option has" : "These mapped options have a total of";
×
285
            var shouldRemove = MessageBox.Show(
×
286
                $"{introText} {children.Count} child mapping{(children.Count != 1 ? "s" : "")}. Do you want to remove them as well?",
×
287
                "Remove child mappings?",
×
288
                MessageBoxButtons.YesNo,
×
289
                MessageBoxIcon.Question) == DialogResult.Yes;
×
290

291
            if (shouldRemove)
×
292
            {
293
                foreach (var child in children)
×
294
                {
295
                    this.selectedProfile.RemoveMapping(child);
×
296
                }
297
            }
298
            else
299
            {
300
                // Otherwise remove their parent
301
                foreach (var child in children)
×
302
                {
303
                    child.SetParent(null);
×
304
                }
305
            }
306
        }
307

308
        foreach (var mappedOption in mappedOptions)
×
309
        {
310
            this.selectedProfile.RemoveMapping(mappedOption);
×
311
        }
312

313
        this.selectedProfile.Save();
×
314
        this.RefreshMappings();
×
315
    }
×
316

317
    private void RemoveSelectedMappings()
318
    {
319
        var selectedCount = this.olvMappings.SelectedItems.Count;
×
320

321
        if (selectedCount == 0)
×
322
        {
323
            return;
×
324
        }
325

326
        if (selectedCount > 1)
×
327
        {
328
            if (MessageBox.Show($"Are you sure you want to remove the {selectedCount} selected mappings?", $"Remove {selectedCount} Mappings", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No)
×
329
            {
330
                return;
×
331
            }
332
        }
333

334
        var mappedOptions = new List<MappedOption>();
×
335

336
        foreach (var item in this.olvMappings.SelectedObjects)
×
337
        {
338
            mappedOptions.Add(item as MappedOption);
×
339
        }
340

341
        this.RemoveMappings(mappedOptions);
×
342
        this.selectedProfile.Save();
×
343
    }
×
344

345
    private void MakeMappingParentless(MappedOption childOption)
346
    {
347
        childOption.SetParent(null);
×
348
        this.selectedProfile.Save();
×
349
        this.RefreshMappings();
×
350
    }
×
351

352
    /// <summary>
353
    /// Gets the group of the ObjectListView by it's row object.
354
    /// </summary>
355
    /// <param name="rowObject"></param>
356
    /// <returns></returns>
357
    private ListViewGroup GetByItem(object rowObject)
358
    {
359
        foreach (var item in this.olvMappings.Items)
×
360
        {
361
            var listViewItem = item as OLVListItem;
×
362

363
            if (listViewItem.RowObject == rowObject)
×
364
            {
365
                return listViewItem.Group;
×
366
            }
367
        }
368

369
        return null;
×
370
    }
×
371

372
    private void ChooseNewParent(MappedOption child, MappedOption targetParent)
373
    {
374
        var childGroup = this.GetByItem(child);
×
375
        var parentGroup = this.GetByItem(targetParent);
×
376

377
        if (childGroup != parentGroup)
×
378
        {
379
            MessageBox.Show(
×
380
                $"The child is in the '{childGroup.Header}' group and the parent is in the '{parentGroup.Header}' group. Cannot parent between groups. Consider disabling grouping in the configuration",
×
381
                "Cannot parent across groups",
×
382
                MessageBoxButtons.OK,
×
383
                MessageBoxIcon.Error
×
384
            );
×
385
            return;
×
386
        }
387

388
        child.SetParent(targetParent);
×
389
        this.selectedProfile.Save();
×
390
        this.RefreshMappings();
×
391
        SystemSounds.Beep.Play();
×
392

393
        return;
×
394
    }
395

396
    /// <summary>
397
    /// Call this after changing the group mode, so that parents and children do not
398
    /// exist across groups. (not supported by the <see cref="MappingGroupItemComparer"/>)
399
    /// </summary>
400
    private void FindAndDetachParentChildDifferentGroups()
401
    {
402
        var changedMappings = new List<MappedOption>();
×
403

404
        foreach (var mappedOption in this.selectedProfile.MappedOptions)
×
405
        {
406
            if (mappedOption.Parent != null)
×
407
            {
408
                var childGroup = this.GetByItem(mappedOption);
×
409
                var parentGroup = this.GetByItem(mappedOption.Parent);
×
410

411
                if (childGroup != parentGroup)
×
412
                {
413
                    changedMappings.Add(mappedOption);
×
414
                }
415
            }
416
        }
417

418
        if (changedMappings.Count > 0)
×
419
        {
420
            var mappingSummaryList = changedMappings.Select(x => $"- {x.ToString().Ellipsize(200)}").ToList();
×
421
            var mappingSummary = string.Join(Environment.NewLine, mappingSummaryList);
×
422

423
            this.RefreshMappings();
×
424
            var plural = changedMappings.Count > 1 ? "s" : "";
×
425
            var pluralWas = changedMappings.Count > 1 ? "were" : "was";
×
426
            var result = MessageBox.Show(
×
427
                $"Found {changedMappings.Count} parent/child mapping{plural} that {pluralWas} in different groups:\n{mappingSummary}\n\nThis can happen if you change grouping or if an invalid profile is loaded. To prevent weird sorting behaviour the mapping{plural} {pluralWas} detached from their parent.\n\nYou can still restore to the previous setup by switching to a compatible grouping type ('None' always works).\n\nSelect 'Cancel' if you want to switch to the grouping type 'None', or 'OK' to save the profile with the detached mapping{plural}.",
×
428
                $"Parent/child mapping{plural} detached",
×
429
                MessageBoxButtons.OKCancel,
×
430
                MessageBoxIcon.Warning
×
431
            );
×
432

433
            if (result == DialogResult.Cancel)
×
434
            {
435
                var configManager = ServiceLocator.Current.GetInstance<IConfigManager>();
×
436
                configManager.GetConfigState().SelectedViewMappingGroupType = ViewMappingGroupType.None;
×
437
                this.RefreshMappingsAfterGroupChange();
×
438
                return;
×
439
            }
440

441
            foreach (var mappedOption in changedMappings)
×
442
            {
443
                mappedOption.SetParent(null);
×
444
            }
445
        }
446
    }
×
447

448
    public bool RunAppCommand(AppCommand command)
449
    {
450
        switch (command)
451
        {
452
            case AppCommand.Abort:
453
                this.BeginInvoke(new MethodInvoker(delegate
×
454
                {
×
455
                    this.chkArmed.Checked = false;
×
456
                }));
×
457

458
                return true;
×
459

460
            case AppCommand.ResetScriptEnvironment:
461
                break;
462

463
            default:
464
                break;
465
        }
466

467
        return false;
×
468
    }
469

470
    private void MainForm_Load(object sender, EventArgs e)
471
    {
472
        var lastLoadedProfile = MappingProfile.RestoreLastLoaded();
×
473

474
        if (lastLoadedProfile != null)
×
475
        {
476
            this.SetSelectedProfile(lastLoadedProfile);
×
477
        }
478

479
        // Ensure the manager knows which window handle catches all inputs
480
        Key2JoyManager.Instance.SetHandlerWithInvoke(this);
×
481
        Key2JoyManager.Instance.StatusChanged += (s, ev) =>
×
482
        {
×
483
            this.SetStatusView(ev.IsEnabled);
×
484

×
485
            if (ev.Profile != null)
×
486
            {
×
487
                this.SetSelectedProfile(ev.Profile);
×
488
            }
×
489
        };
×
490

491
        this.RefreshColumnWidths();
×
492
    }
×
493

494
    private void BtnCreateMapping_Click(object sender, EventArgs e)
495
    {
496
        if (this.selectedProfile == null)
×
497
        {
498
            this.CreateNewProfile();
×
499
        }
500

501
        this.EditMappedOption();
×
502
    }
×
503

504
    private void OlvMappings_CellClick(object sender, CellClickEventArgs e)
505
    {
506
        if (e.ClickCount < 2 || e.Item == null)
×
507
        {
508
            return;
×
509
        }
510

511
        if (this.olvMappings.SelectedObject is not MappedOption mappedOption)
×
512
        {
513
            return;
×
514
        }
515

516
        this.EditMappedOption(mappedOption);
×
517
    }
×
518

519
    private CachedMappingGroup GetGroupOrCreateInCache(MappingAttribute attribute)
520
    {
521
        var uniqueId = attribute.GroupName + attribute.GroupImage;
×
522

523
        if (!this.cachedMappingGroups.TryGetValue(uniqueId, out var mapping))
×
524
        {
525
            this.cachedMappingGroups.Add(uniqueId, mapping = new CachedMappingGroup
×
526
            {
×
527
                Name = attribute.GroupName,
×
528
                Image = attribute.GroupImage,
×
529
            });
×
530
        }
531

532
        return mapping;
×
533
    }
534

535
    private object OlvMappings_GroupKeyGetter(object rowObject)
536
    {
537
        var option = (AbstractMappedOption)rowObject;
×
538

539
        MappingAttribute attribute = null;
×
540

541
        var configManager = ServiceLocator.Current.GetInstance<IConfigManager>();
×
542
        var mappingGroupType = configManager.GetConfigState().SelectedViewMappingGroupType;
×
543

544
        switch (mappingGroupType)
545
        {
546
            case ViewMappingGroupType.ByAction:
547
                attribute = ActionsRepository.GetAttributeForAction(option.Action);
×
548
                break;
×
549

550
            case ViewMappingGroupType.ByTrigger:
551
                attribute = TriggersRepository.GetAttributeForTrigger(option.Trigger);
×
552
                break;
553

554
            case ViewMappingGroupType.None:
555
            default:
556
                break;
557
        }
558

559
        if (attribute != null)
×
560
        {
561
            return this.GetGroupOrCreateInCache(attribute);
×
562
        }
563

564
        return null;
×
565
    }
566

567
    private string OlvMappings_GroupKeyToTitleConverter(object groupKey)
568
    {
569
        if (groupKey == null)
×
570
        {
571
            return null;
×
572
        }
573

574
        var groupAttribute = (CachedMappingGroup)groupKey;
×
575

576
        return groupAttribute.Name;
×
577
    }
578

579
    private void OlvMappings_BeforeCreatingGroups(object sender, CreateGroupsEventArgs e)
580
    {
581
        e.Parameters.GroupComparer = new MappingGroupComparer();
×
582
        e.Parameters.ItemComparer = new MappingGroupItemComparer(
×
583
            e.Parameters.PrimarySort,
×
584
            e.Parameters.PrimarySortOrder
×
585
        );
×
586
    }
×
587

588
    private void OlvMappings_AboutToCreateGroups(object sender, CreateGroupsEventArgs e)
589
    {
590
        foreach (var group in e.Groups)
×
591
        {
592
            if (group.Key == null)
×
593
            {
594
                continue;
595
            }
596

597
            var groupAttribute = (CachedMappingGroup)group.Key;
×
598
            group.TitleImage = groupAttribute.Image;
×
599
        }
600
    }
×
601

602
    private void OlvMappings_CellRightClick(object sender, CellRightClickEventArgs e)
603
    {
604
        var builder = new MappingContextMenuBuilder(this.olvMappings.SelectedItems);
×
605
        builder.SelectEditMapping += (s, e) => this.EditMappedOption(e.MappedOption);
×
606
        builder.SelectMakeMappingParentless += (s, e) => this.MakeMappingParentless(e.MappedOption);
×
607
        builder.SelectChooseNewParent += (s, e) => this.ChooseNewParent(e.MappedOption, e.NewParent);
×
608
        builder.SelectMultiEditMapping += this.Builder_SelectMultiEditMapping;
×
609
        builder.SelectRemoveMappings += (s, e) =>
×
610
        {
×
611
            this.RemoveSelectedMappings();
×
612
            this.selectedProfile.Save();
×
613
        };
×
614
        e.MenuStrip = builder.Build();
×
615
    }
×
616

617
    private void Builder_SelectMultiEditMapping(object sender, SelectMultiEditMappingEventArgs e)
618
    {
619
        // Apply, save and refresh
620
        foreach (var mappingAspect in e.MappingAspects)
×
621
        {
622
            e.Property.SetValue(mappingAspect, e.Value);
×
623
        }
624

625
        this.selectedProfile.Save();
×
626
        this.RefreshMappings();
×
627
    }
×
628

629
    private void OlvMappings_KeyUp(object sender, KeyEventArgs e)
630
    {
631
        if (e.KeyCode != Keys.Delete)
×
632
        {
633
            return;
×
634
        }
635

636
        this.RemoveSelectedMappings();
×
637
    }
×
638

639
    private void OlvMappings_FormatRow(object sender, FormatRowEventArgs e)
640
    {
641
        if (e.Model is not MappedOption mappedOption)
×
642
        {
643
            return;
×
644
        }
645

646
        if (mappedOption.IsChild)
×
647
        {
648
            e.Item.CellPadding = new Rectangle(
×
649
                (e.ListView.CellPadding?.Left ?? 0) + 10,
×
650
                e.ListView.CellPadding?.Top ?? 0,
×
651
                e.ListView.CellPadding?.Right ?? 0,
×
652
                e.ListView.CellPadding?.Bottom ?? 0
×
653
            );
×
654
            e.Item.ForeColor = Color.Gray;
×
655
        }
656
    }
×
657

658
    private void OlvMappings_FormatCell(object sender, FormatCellEventArgs e)
659
    {
660
        if (e.CellValue != null)
×
661
        {
662
            return;
×
663
        }
664

665
        e.SubItem.ForeColor = SystemColors.GrayText;
×
666
        e.SubItem.Font = new Font(e.SubItem.Font, FontStyle.Italic);
×
667
    }
×
668

669
    private void ChkEnabled_CheckedChanged(object sender, EventArgs e)
670
    {
671
        var isArmed = this.chkArmed.Checked;
×
672

673
        this.SetStatusView(isArmed);
×
674

675
        if (isArmed)
×
676
        {
677
            try
678
            {
679
                Key2JoyManager.Instance.ArmMappings(this.selectedProfile);
×
680
            }
×
681
            catch (MappingArmingFailedException ex)
×
682
            {
683
                this.chkArmed.Checked = false;
×
684
                MessageBox.Show(
×
685
                    this,
×
686
                    ex.Message,
×
687
                    "Error",
×
688
                    MessageBoxButtons.OK,
×
689
                    MessageBoxIcon.Error
×
690
                );
×
691
            }
×
692
        }
693
        else
694
        {
695
            Key2JoyManager.Instance.DisarmMappings();
×
696
        }
697

698
        this.deviceListControl.RefreshDevices();
×
699
    }
×
700

701
    private void TxtProfileName_TextChanged(object sender, EventArgs e)
702
    {
703
        this.selectedProfile.Name = this.txtProfileName.Text;
×
704
        this.selectedProfile.Save();
×
705
    }
×
706

707
    private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
708
    {
709
        if (e.CloseReason == CloseReason.UserClosing)
×
710
        {
711
            if (this.configState.ShouldCloseButtonMinimize)
×
712
            {
713
                e.Cancel = true;
×
714
                this.Hide();
×
715
                return;
×
716
            }
717
        }
718
    }
×
719

720
    private void NewProfileToolStripMenuItem_Click(object sender, EventArgs e) => this.CreateNewProfile(" - Copy");
×
721

722
    private void LoadProfileToolStripMenuItem_Click(object sender, EventArgs e)
723
    {
724
        // Load a profile from the file system
725
        OpenFileDialog dialog = new()
×
726
        {
×
727
            Filter = "Key2Joy Profiles|*" + MappingProfile.EXTENSION,
×
728
            Title = "Load Profile",
×
729
            InitialDirectory = MappingProfile.GetSaveDirectory(),
×
730
            RestoreDirectory = true,
×
731
            CheckFileExists = true,
×
732
            CheckPathExists = true,
×
733
            ShowReadOnly = false
×
734
        };
×
735

736
        if (dialog.ShowDialog() != DialogResult.OK)
×
737
        {
738
            return;
×
739
        }
740

741
        var profile = MappingProfile.Load(dialog.FileName);
×
742

743
        if (profile == null)
×
744
        {
745
            MessageBox.Show("The selected profile was corrupt! If you did not modify the profile file this could be a bug.\n\nPlease help us by reporting the bug on GitHub: https://github.com/luttje/Key2Joy.", "Failed to load profile!", MessageBoxButtons.OK, MessageBoxIcon.Error);
×
746
            return;
×
747
        }
748

749
        this.SetSelectedProfile(profile);
×
750
    }
×
751

752
    private void SaveProfileToolStripMenuItem_Click(object sender, EventArgs e)
753
        => MessageBox.Show("When you make changes to a profile, changes are automatically saved. This button is only here to explain that feature to you.", "Profile already saved!", MessageBoxButtons.OK, MessageBoxIcon.Information);
×
754

755
    private void OpenProfileFolderToolStripMenuItem_Click(object sender, EventArgs e)
756
    {
757
        if (this.selectedProfile == null)
×
758
        {
759
            Process.Start(MappingProfile.GetSaveDirectory());
×
760
            return;
×
761
        }
762

763
        var argument = "/select, \"" + this.selectedProfile.FilePath + "\"";
×
764
        Process.Start("explorer.exe", argument);
×
765
    }
×
766

767
    private void ExitProgramToolStripMenuItem_Click(object sender, EventArgs e) => Application.Exit();
×
768

769
    private void CreateNewMappingToolStripMenuItem_Click(object sender, EventArgs e) => this.BtnCreateMapping_Click(sender, e);
×
770

771
    private void GamePadPressAndReleaseToolStripMenuItem_Click(object sender, EventArgs e)
772
    {
773
        List<MappedOption> range = new();
×
774
        range.AddRange(GamePadButtonAction.GetAllButtonActions(PressState.Press));
×
775
        range.AddRange(GamePadButtonAction.GetAllButtonActions(PressState.Release));
×
776

777
        this.selectedProfile.AddMappingRange(range);
×
778
        this.selectedProfile.Save();
×
779
        this.RefreshMappings();
×
780
    }
×
781

782
    private void GamePadPressToolStripMenuItem_Click(object sender, EventArgs e)
783
    {
784
        var range = GamePadButtonAction.GetAllButtonActions(PressState.Press);
×
785
        this.selectedProfile.AddMappingRange(range);
×
786
        this.selectedProfile.Save();
×
787
        this.RefreshMappings();
×
788
    }
×
789

790
    private void GamePadReleaseToolStripMenuItem_Click(object sender, EventArgs e)
791
    {
792
        var range = GamePadButtonAction.GetAllButtonActions(PressState.Release);
×
793
        this.selectedProfile.AddMappingRange(range);
×
794
        this.selectedProfile.Save();
×
795
        this.RefreshMappings();
×
796
    }
×
797

798
    private void KeyboardPressAndReleaseToolStripMenuItem_Click(object sender, EventArgs e)
799
    {
800
        List<MappedOption> range = new();
×
801
        range.AddRange(KeyboardAction.GetAllButtonActions(PressState.Press));
×
802
        range.AddRange(KeyboardAction.GetAllButtonActions(PressState.Release));
×
803

804
        this.selectedProfile.AddMappingRange(range);
×
805
        this.selectedProfile.Save();
×
806
        this.RefreshMappings();
×
807
    }
×
808

809
    private void KeyboardPressToolStripMenuItem_Click(object sender, EventArgs e)
810
    {
811
        var range = KeyboardAction.GetAllButtonActions(PressState.Press);
×
812
        this.selectedProfile.AddMappingRange(range);
×
813
        this.selectedProfile.Save();
×
814
        this.RefreshMappings();
×
815
    }
×
816

817
    private void KeyboardReleaseToolStripMenuItem_Click(object sender, EventArgs e)
818
    {
819
        var range = KeyboardAction.GetAllButtonActions(PressState.Release);
×
820
        this.selectedProfile.AddMappingRange(range);
×
821
        this.selectedProfile.Save();
×
822
        this.RefreshMappings();
×
823
    }
×
824

825
    private void TestKeyboardToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://devicetests.com/keyboard-tester");
×
826

827
    private void TestMouseToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://devicetests.com/mouse-test");
×
828

829
    private void UserConfigurationsToolStripMenuItem_Click(object sender, EventArgs e)
830
    {
831
        new ConfigForm().ShowDialog();
×
832

833
        // Refresh the mapppings in case the user modified a group config
834
        this.RefreshMappingsAfterGroupChange();
×
835
    }
×
836

837
    private void ReportAProblemToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://github.com/luttje/Key2Joy/issues");
×
838

839
    private void ViewSourceCodeToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://github.com/luttje/Key2Joy");
×
840

841
    private void AboutToolStripMenuItem_Click(object sender, EventArgs e) => new AboutForm().ShowDialog();
×
842

843
    private void NtfIndicator_MouseDoubleClick(object sender, MouseEventArgs e)
844
    {
845
        this.WindowState = FormWindowState.Normal;
×
846
        this.ShowInTaskbar = true;
×
847
        this.Show();
×
848
    }
×
849

850
    private void CloseToolStripMenuItem_Click(object sender, EventArgs e) => this.Close();
×
851

852
    private void ViewLogFileToolStripMenuItem_Click(object sender, EventArgs e)
853
    {
854
        var logFile = Output.GetLogPath();
×
855

856
        if (!File.Exists(logFile))
×
857
        {
858
            MessageBox.Show("The log file does not exist yet. Please wait for the program to finish writing to it.", "Log file not found!", MessageBoxButtons.OK, MessageBoxIcon.Information);
×
859
            return;
×
860
        }
861

862
        Process.Start(logFile);
×
863
    }
×
864

865
    private void ViewEventViewerToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("eventvwr.exe", "/c:Application");
×
866

867
    private void DevicetestscomToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://devicetests.com/controller-tester");
×
868

869
    private void GamepadtestercomToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start("https://gamepad-tester.com/");
×
870

871
    private void OpenPluginsFolderToolStripMenuItem_Click(object sender, EventArgs e) => Process.Start(Program.Plugins.PluginsFolder);
×
872

873
    private void ManagePluginsToolStripMenuItem_Click(object sender, EventArgs e) => new PluginsForm().ShowDialog();
×
874

875
    private void GenerateReverseMappingsToolStripMenuItem_Click(object sender, EventArgs e)
876
    {
877
        var selectedCount = this.olvMappings.SelectedItems.Count;
×
878

879
        if (selectedCount == 0)
×
880
        {
881
            return;
×
882
        }
883

884
        if (selectedCount > 1
×
885
            && DialogUtilities.Confirm(
×
886
                $"Are you sure you want to create reverse mappings for all {selectedCount} selected mappings? Each type of action and trigger will configure their own useful reverse if possible.\n\n"
×
887
                + $"An example of an reverse mapping is how new 'Release' mappings will be created for each 'Press' and vice versa.",
×
888
                $"Generate {selectedCount} reverse mappings"
×
889
            ) == DialogResult.No)
×
890
        {
891
            return;
×
892
        }
893

894
        var selectedMappings = this.olvMappings.SelectedItems
×
895
            .Cast<OLVListItem>()
×
896
            .Select(item => (MappedOption)item.RowObject)
×
897
            .ToList();
×
898

899
        var newOptions = MappedOption.GenerateReverseMappings(selectedMappings);
×
900

901
        foreach (var option in newOptions)
×
902
        {
903
            this.selectedProfile.MappedOptions.Add(option);
×
904
        }
905

906
        this.selectedProfile.Save();
×
907
        this.RefreshMappings();
×
908
    }
×
909

910
    private void TxtFilter_TextChanged(object sender, EventArgs e)
911
        => this.olvMappings.ModelFilter = new ModelFilter(
×
912
            x =>
×
913
            {
×
914
                var mappedOption = (MappedOption)x;
×
915
                var filterText = this.txtFilter.Text;
×
916

×
917
                bool containsFilterText(string text)
×
918
                    => text.IndexOf(filterText, StringComparison.OrdinalIgnoreCase) > -1;
×
919

×
920
                var isMatch = containsFilterText(mappedOption.Action.ToString())
×
921
                           || containsFilterText(mappedOption.Trigger.ToString());
×
922

×
923
                return isMatch;
×
924
            }
×
925
        );
×
926

927
    private void MainForm_SizeChanged(object sender, EventArgs e)
928
        => this.RefreshColumnWidths();
×
929
}
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