|
| 1 | +using MaximizeToVirtualDesktop.Interop; |
| 2 | + |
| 3 | +namespace MaximizeToVirtualDesktop; |
| 4 | + |
| 5 | +/// <summary> |
| 6 | +/// Modal settings dialog for configuring hotkeys and maximize-button behavior. |
| 7 | +/// Call <see cref="ApplyToSettings"/> after <c>ShowDialog</c> returns <c>DialogResult.OK</c>. |
| 8 | +/// </summary> |
| 9 | +internal sealed class SettingsDialog : Form |
| 10 | +{ |
| 11 | + private readonly AppSettings _settings; |
| 12 | + |
| 13 | + private readonly CheckBox _chkHotkeyCtrl, _chkHotkeyAlt, _chkHotkeyShift, _chkHotkeyWin; |
| 14 | + private readonly ComboBox _cmbHotkeyKey; |
| 15 | + |
| 16 | + private readonly CheckBox _chkPinCtrl, _chkPinAlt, _chkPinShift, _chkPinWin; |
| 17 | + private readonly ComboBox _cmbPinKey; |
| 18 | + |
| 19 | + private readonly CheckBox _chkInvertShiftClick; |
| 20 | + |
| 21 | + // (display name, VK code) pairs for the key combo boxes |
| 22 | + private static readonly (string Name, uint Vk)[] SupportedKeys = |
| 23 | + [ |
| 24 | + .. Enumerable.Range('A', 26).Select(c => (((char)c).ToString(), (uint)c)), |
| 25 | + .. Enumerable.Range('0', 10).Select(c => (((char)c).ToString(), (uint)c)), |
| 26 | + .. Enumerable.Range(1, 12).Select(i => ($"F{i}", (uint)(0x70 + i - 1))), |
| 27 | + ]; |
| 28 | + |
| 29 | + public SettingsDialog(AppSettings settings) |
| 30 | + { |
| 31 | + _settings = settings; |
| 32 | + |
| 33 | + SuspendLayout(); |
| 34 | + |
| 35 | + AutoScaleDimensions = new SizeF(96F, 96F); |
| 36 | + AutoScaleMode = AutoScaleMode.Dpi; |
| 37 | + |
| 38 | + Text = "Settings — Maximize to Virtual Desktop"; |
| 39 | + StartPosition = FormStartPosition.CenterScreen; |
| 40 | + FormBorderStyle = FormBorderStyle.FixedDialog; |
| 41 | + MaximizeBox = false; |
| 42 | + MinimizeBox = false; |
| 43 | + ShowInTaskbar = false; |
| 44 | + Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath); |
| 45 | + |
| 46 | + int margin = 14; |
| 47 | + int grpW = 460; |
| 48 | + int y = margin + 4; |
| 49 | + |
| 50 | + // Maximize hotkey group |
| 51 | + var grpMaximize = new GroupBox { Text = "Maximize Hotkey", Location = new Point(margin, y), Size = new Size(grpW, 72) }; |
| 52 | + Controls.Add(grpMaximize); |
| 53 | + (_chkHotkeyCtrl, _chkHotkeyAlt, _chkHotkeyShift, _chkHotkeyWin, _cmbHotkeyKey) = |
| 54 | + AddHotkeyRow(grpMaximize, settings.HotkeyModifiers, settings.HotkeyKey); |
| 55 | + y += grpMaximize.Height + 12; |
| 56 | + |
| 57 | + // Pin hotkey group |
| 58 | + var grpPin = new GroupBox { Text = "Pin Hotkey", Location = new Point(margin, y), Size = new Size(grpW, 72) }; |
| 59 | + Controls.Add(grpPin); |
| 60 | + (_chkPinCtrl, _chkPinAlt, _chkPinShift, _chkPinWin, _cmbPinKey) = |
| 61 | + AddHotkeyRow(grpPin, settings.PinHotkeyModifiers, settings.PinHotkeyKey); |
| 62 | + y += grpPin.Height + 12; |
| 63 | + |
| 64 | + // Behavior group |
| 65 | + var grpBehavior = new GroupBox { Text = "Behavior", Location = new Point(margin, y), Size = new Size(grpW, 80) }; |
| 66 | + Controls.Add(grpBehavior); |
| 67 | + _chkInvertShiftClick = new CheckBox |
| 68 | + { |
| 69 | + Text = "Always maximize to virtual desktop on click\r\n" + |
| 70 | + "(Shift+Click performs a normal maximize instead)", |
| 71 | + AutoSize = true, |
| 72 | + Checked = settings.InvertShiftClick, |
| 73 | + Location = new Point(10, 28), |
| 74 | + }; |
| 75 | + grpBehavior.Controls.Add(_chkInvertShiftClick); |
| 76 | + y += grpBehavior.Height + 16; |
| 77 | + |
| 78 | + // Buttons — uniform height, Reset Defaults right-aligned |
| 79 | + int btnW = 90; |
| 80 | + int btnResetW = 130; |
| 81 | + int btnH = 30; |
| 82 | + var btnOk = new Button |
| 83 | + { |
| 84 | + Text = "OK", |
| 85 | + DialogResult = DialogResult.OK, |
| 86 | + Location = new Point(margin, y), |
| 87 | + Size = new Size(btnW, btnH), |
| 88 | + FlatStyle = FlatStyle.System, |
| 89 | + }; |
| 90 | + var btnCancel = new Button |
| 91 | + { |
| 92 | + Text = "Cancel", |
| 93 | + DialogResult = DialogResult.Cancel, |
| 94 | + Location = new Point(margin + btnW + 8, y), |
| 95 | + Size = new Size(btnW, btnH), |
| 96 | + FlatStyle = FlatStyle.System, |
| 97 | + }; |
| 98 | + var btnReset = new Button |
| 99 | + { |
| 100 | + Text = "Reset Defaults", |
| 101 | + Location = new Point(margin + grpW - btnResetW, y), |
| 102 | + Size = new Size(btnResetW, btnH), |
| 103 | + FlatStyle = FlatStyle.System, |
| 104 | + }; |
| 105 | + btnReset.Click += (_, _) => ResetDefaults(); |
| 106 | + btnOk.Click += (_, e) => |
| 107 | + { |
| 108 | + var error = ValidateHotkeys(); |
| 109 | + if (error != null) |
| 110 | + { |
| 111 | + MessageBox.Show(error, "Invalid Settings", MessageBoxButtons.OK, MessageBoxIcon.Warning); |
| 112 | + DialogResult = DialogResult.None; |
| 113 | + } |
| 114 | + }; |
| 115 | + |
| 116 | + Controls.AddRange(new Control[] { btnOk, btnCancel, btnReset }); |
| 117 | + AcceptButton = btnOk; |
| 118 | + CancelButton = btnCancel; |
| 119 | + |
| 120 | + ClientSize = new Size(margin + grpW + margin, y + btnH + margin); |
| 121 | + |
| 122 | + ResumeLayout(false); |
| 123 | + PerformLayout(); |
| 124 | + } |
| 125 | + |
| 126 | + private static (CheckBox ctrl, CheckBox alt, CheckBox shift, CheckBox win, ComboBox key) |
| 127 | + AddHotkeyRow(GroupBox grp, uint modifiers, uint vk) |
| 128 | + { |
| 129 | + // FlowLayoutPanel inside the group box handles DPI scaling for the row |
| 130 | + var row = new FlowLayoutPanel |
| 131 | + { |
| 132 | + FlowDirection = FlowDirection.LeftToRight, |
| 133 | + AutoSize = true, |
| 134 | + AutoSizeMode = AutoSizeMode.GrowAndShrink, |
| 135 | + WrapContents = false, |
| 136 | + Location = new Point(6, 26), |
| 137 | + }; |
| 138 | + |
| 139 | + var chkCtrl = new CheckBox { Text = "Ctrl", Checked = (modifiers & NativeMethods.MOD_CONTROL) != 0, AutoSize = true, Margin = new Padding(2, 2, 4, 0) }; |
| 140 | + var chkAlt = new CheckBox { Text = "Alt", Checked = (modifiers & NativeMethods.MOD_ALT) != 0, AutoSize = true, Margin = new Padding(2, 2, 4, 0) }; |
| 141 | + var chkShift = new CheckBox { Text = "Shift", Checked = (modifiers & NativeMethods.MOD_SHIFT) != 0, AutoSize = true, Margin = new Padding(2, 2, 4, 0) }; |
| 142 | + var chkWin = new CheckBox { Text = "Win", Checked = (modifiers & NativeMethods.MOD_WIN) != 0, AutoSize = true, Margin = new Padding(2, 2, 10, 0) }; |
| 143 | + var lblKey = new Label { Text = "Key:", AutoSize = true, Margin = new Padding(2, 5, 2, 0) }; |
| 144 | + var cmb = new ComboBox |
| 145 | + { |
| 146 | + DropDownStyle = ComboBoxStyle.DropDownList, |
| 147 | + Width = 60, |
| 148 | + Margin = new Padding(2, 2, 0, 0), |
| 149 | + }; |
| 150 | + |
| 151 | + foreach (var (name, _) in SupportedKeys) |
| 152 | + cmb.Items.Add(name); |
| 153 | + |
| 154 | + var idx = Array.FindIndex(SupportedKeys, k => k.Vk == vk); |
| 155 | + cmb.SelectedIndex = Math.Max(0, idx); |
| 156 | + |
| 157 | + row.Controls.AddRange(new Control[] { chkCtrl, chkAlt, chkShift, chkWin, lblKey, cmb }); |
| 158 | + grp.Controls.Add(row); |
| 159 | + return (chkCtrl, chkAlt, chkShift, chkWin, cmb); |
| 160 | + } |
| 161 | + |
| 162 | + private void ResetDefaults() |
| 163 | + { |
| 164 | + _chkHotkeyCtrl.Checked = true; |
| 165 | + _chkHotkeyAlt.Checked = true; |
| 166 | + _chkHotkeyShift.Checked = true; |
| 167 | + _chkHotkeyWin.Checked = false; |
| 168 | + _cmbHotkeyKey.SelectedIndex = Array.FindIndex(SupportedKeys, k => k.Vk == NativeMethods.VK_X); |
| 169 | + |
| 170 | + _chkPinCtrl.Checked = true; |
| 171 | + _chkPinAlt.Checked = true; |
| 172 | + _chkPinShift.Checked = true; |
| 173 | + _chkPinWin.Checked = false; |
| 174 | + _cmbPinKey.SelectedIndex = Array.FindIndex(SupportedKeys, k => k.Vk == NativeMethods.VK_P); |
| 175 | + |
| 176 | + _chkInvertShiftClick.Checked = false; |
| 177 | + } |
| 178 | + |
| 179 | + /// <summary>Returns an error message if the current hotkey configuration is invalid, or null if valid.</summary> |
| 180 | + private string? ValidateHotkeys() |
| 181 | + { |
| 182 | + uint hotkeyMod = BuildModifiers(_chkHotkeyCtrl, _chkHotkeyAlt, _chkHotkeyShift, _chkHotkeyWin); |
| 183 | + uint pinMod = BuildModifiers(_chkPinCtrl, _chkPinAlt, _chkPinShift, _chkPinWin); |
| 184 | + |
| 185 | + if (hotkeyMod == 0) |
| 186 | + return "Maximize hotkey needs at least one modifier (Ctrl, Alt, Shift, or Win)."; |
| 187 | + if (pinMod == 0) |
| 188 | + return "Pin hotkey needs at least one modifier (Ctrl, Alt, Shift, or Win)."; |
| 189 | + if (_cmbHotkeyKey.SelectedIndex < 0) |
| 190 | + return "Please select a key for the maximize hotkey."; |
| 191 | + if (_cmbPinKey.SelectedIndex < 0) |
| 192 | + return "Please select a key for the pin hotkey."; |
| 193 | + |
| 194 | + uint hotkeyVk = SupportedKeys[_cmbHotkeyKey.SelectedIndex].Vk; |
| 195 | + uint pinVk = SupportedKeys[_cmbPinKey.SelectedIndex].Vk; |
| 196 | + |
| 197 | + if (hotkeyMod == pinMod && hotkeyVk == pinVk) |
| 198 | + return "Maximize and Pin hotkeys cannot be the same combination."; |
| 199 | + |
| 200 | + return null; |
| 201 | + } |
| 202 | + |
| 203 | + /// <summary>Writes the dialog's current values back into the <see cref="AppSettings"/> object.</summary> |
| 204 | + public void ApplyToSettings() |
| 205 | + { |
| 206 | + _settings.HotkeyModifiers = BuildModifiers(_chkHotkeyCtrl, _chkHotkeyAlt, _chkHotkeyShift, _chkHotkeyWin); |
| 207 | + _settings.HotkeyKey = _cmbHotkeyKey.SelectedIndex >= 0 ? SupportedKeys[_cmbHotkeyKey.SelectedIndex].Vk : NativeMethods.VK_X; |
| 208 | + _settings.PinHotkeyModifiers = BuildModifiers(_chkPinCtrl, _chkPinAlt, _chkPinShift, _chkPinWin); |
| 209 | + _settings.PinHotkeyKey = _cmbPinKey.SelectedIndex >= 0 ? SupportedKeys[_cmbPinKey.SelectedIndex].Vk : NativeMethods.VK_P; |
| 210 | + _settings.InvertShiftClick = _chkInvertShiftClick.Checked; |
| 211 | + } |
| 212 | + |
| 213 | + private static uint BuildModifiers(CheckBox ctrl, CheckBox alt, CheckBox shift, CheckBox win) |
| 214 | + { |
| 215 | + uint m = 0; |
| 216 | + if (ctrl.Checked) m |= NativeMethods.MOD_CONTROL; |
| 217 | + if (alt.Checked) m |= NativeMethods.MOD_ALT; |
| 218 | + if (shift.Checked) m |= NativeMethods.MOD_SHIFT; |
| 219 | + if (win.Checked) m |= NativeMethods.MOD_WIN; |
| 220 | + return m; |
| 221 | + } |
| 222 | +} |
0 commit comments