MainForm.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729
  1. using System;
  2. using System.Diagnostics;
  3. using System.Windows.Forms;
  4. using SteamAuth;
  5. using System.Collections.Generic;
  6. using System.Text.RegularExpressions;
  7. using System.Net;
  8. using Newtonsoft.Json;
  9. using System.Threading;
  10. using System.Drawing;
  11. using System.Linq;
  12. namespace Steam_Desktop_Authenticator
  13. {
  14. public partial class MainForm : Form
  15. {
  16. private SteamGuardAccount currentAccount = null;
  17. private SteamGuardAccount[] allAccounts;
  18. private List<string> updatedSessions = new List<string>();
  19. private Manifest manifest;
  20. private static SemaphoreSlim confirmationsSemaphore = new SemaphoreSlim(1, 1);
  21. private long steamTime = 0;
  22. private long currentSteamChunk = 0;
  23. private string passKey = null;
  24. private bool startSilent = false;
  25. // Forms
  26. private TradePopupForm popupFrm = new TradePopupForm();
  27. public MainForm()
  28. {
  29. InitializeComponent();
  30. }
  31. public void SetEncryptionKey(string key)
  32. {
  33. passKey = key;
  34. }
  35. public void StartSilent(bool silent)
  36. {
  37. startSilent = silent;
  38. }
  39. // Form event handlers
  40. private void MainForm_Shown(object sender, EventArgs e)
  41. {
  42. this.labelVersion.Text = String.Format("v{0}", Application.ProductVersion);
  43. try
  44. {
  45. this.manifest = Manifest.GetManifest();
  46. }
  47. catch (ManifestParseException)
  48. {
  49. MessageBox.Show("Unable to read your settings. Try restating SDA.", "Steam Desktop Authenticator", MessageBoxButtons.OK, MessageBoxIcon.Error);
  50. this.Close();
  51. }
  52. // Make sure we don't show that welcome dialog again
  53. this.manifest.FirstRun = false;
  54. this.manifest.Save();
  55. // Tick first time manually to sync time
  56. timerSteamGuard_Tick(new object(), EventArgs.Empty);
  57. if (manifest.Encrypted)
  58. {
  59. if (passKey == null)
  60. {
  61. passKey = manifest.PromptForPassKey();
  62. if (passKey == null)
  63. {
  64. Application.Exit();
  65. }
  66. }
  67. btnManageEncryption.Text = "Manage Encryption";
  68. }
  69. else
  70. {
  71. btnManageEncryption.Text = "Setup Encryption";
  72. }
  73. btnManageEncryption.Enabled = manifest.Entries.Count > 0;
  74. loadSettings();
  75. loadAccountsList();
  76. checkForUpdates();
  77. if (startSilent)
  78. {
  79. this.WindowState = FormWindowState.Minimized;
  80. }
  81. }
  82. private void MainForm_Load(object sender, EventArgs e)
  83. {
  84. trayIcon.Icon = this.Icon;
  85. }
  86. private void MainForm_Resize(object sender, EventArgs e)
  87. {
  88. if (this.WindowState == FormWindowState.Minimized)
  89. {
  90. this.Hide();
  91. }
  92. }
  93. private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
  94. {
  95. Application.Exit();
  96. }
  97. // UI Button handlers
  98. private void btnSteamLogin_Click(object sender, EventArgs e)
  99. {
  100. var loginForm = new LoginForm();
  101. loginForm.ShowDialog();
  102. this.loadAccountsList();
  103. }
  104. private void btnTradeConfirmations_Click(object sender, EventArgs e)
  105. {
  106. if (currentAccount == null) return;
  107. string oText = btnTradeConfirmations.Text;
  108. btnTradeConfirmations.Text = "Loading...";
  109. btnTradeConfirmations.Text = oText;
  110. ConfirmationFormWeb confirms = new ConfirmationFormWeb(currentAccount);
  111. confirms.Show();
  112. }
  113. private void btnManageEncryption_Click(object sender, EventArgs e)
  114. {
  115. if (manifest.Encrypted)
  116. {
  117. InputForm currentPassKeyForm = new InputForm("Enter current passkey", true);
  118. currentPassKeyForm.ShowDialog();
  119. if (currentPassKeyForm.Canceled)
  120. {
  121. return;
  122. }
  123. string curPassKey = currentPassKeyForm.txtBox.Text;
  124. InputForm changePassKeyForm = new InputForm("Enter new passkey, or leave blank to remove encryption.");
  125. changePassKeyForm.ShowDialog();
  126. if (changePassKeyForm.Canceled && !string.IsNullOrEmpty(changePassKeyForm.txtBox.Text))
  127. {
  128. return;
  129. }
  130. InputForm changePassKeyForm2 = new InputForm("Confirm new passkey, or leave blank to remove encryption.");
  131. changePassKeyForm2.ShowDialog();
  132. if (changePassKeyForm2.Canceled && !string.IsNullOrEmpty(changePassKeyForm.txtBox.Text))
  133. {
  134. return;
  135. }
  136. string newPassKey = changePassKeyForm.txtBox.Text;
  137. string confirmPassKey = changePassKeyForm2.txtBox.Text;
  138. if (newPassKey != confirmPassKey)
  139. {
  140. MessageBox.Show("Passkeys do not match.");
  141. return;
  142. }
  143. if (newPassKey.Length == 0)
  144. {
  145. newPassKey = null;
  146. }
  147. string action = newPassKey == null ? "remove" : "change";
  148. if (!manifest.ChangeEncryptionKey(curPassKey, newPassKey))
  149. {
  150. MessageBox.Show("Unable to " + action + " passkey.");
  151. }
  152. else
  153. {
  154. MessageBox.Show("Passkey successfully " + action + "d.");
  155. this.loadAccountsList();
  156. }
  157. }
  158. else
  159. {
  160. passKey = manifest.PromptSetupPassKey();
  161. this.loadAccountsList();
  162. }
  163. }
  164. private void labelUpdate_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
  165. {
  166. if (newVersion == null || currentVersion == null)
  167. {
  168. checkForUpdates();
  169. }
  170. else
  171. {
  172. compareVersions();
  173. }
  174. }
  175. private void btnCopy_Click(object sender, EventArgs e)
  176. {
  177. CopyLoginToken();
  178. }
  179. // Tool strip menu handlers
  180. private void menuQuit_Click(object sender, EventArgs e)
  181. {
  182. Application.Exit();
  183. }
  184. private void menuRemoveAccountFromManifest_Click(object sender, EventArgs e)
  185. {
  186. if (manifest.Encrypted)
  187. {
  188. MessageBox.Show("You cannot remove accounts from the manifest file while it is encrypted.", "Remove from manifest", MessageBoxButtons.OK, MessageBoxIcon.Error);
  189. }
  190. else
  191. {
  192. DialogResult res = MessageBox.Show("This will remove the selected account from the manifest file.\nUse this to move a maFile to another computer.\nThis will NOT delete your maFile.", "Remove from manifest", MessageBoxButtons.OKCancel);
  193. if (res == DialogResult.OK)
  194. {
  195. manifest.RemoveAccount(currentAccount, false);
  196. MessageBox.Show("Account removed from manifest.\nYou can now move its maFile to another computer and import it using the File menu.", "Remove from manifest");
  197. loadAccountsList();
  198. }
  199. }
  200. }
  201. private void menuLoginAgain_Click(object sender, EventArgs e)
  202. {
  203. this.PromptRefreshLogin(currentAccount);
  204. }
  205. private void menuImportAccount_Click(object sender, EventArgs e)
  206. {
  207. ImportAccountForm currentImport_maFile_Form = new ImportAccountForm();
  208. currentImport_maFile_Form.ShowDialog();
  209. loadAccountsList();
  210. }
  211. private void menuSettings_Click(object sender, EventArgs e)
  212. {
  213. new SettingsForm().ShowDialog();
  214. manifest = Manifest.GetManifest(true);
  215. loadSettings();
  216. }
  217. private async void menuDeactivateAuthenticator_Click(object sender, EventArgs e)
  218. {
  219. if (currentAccount == null) return;
  220. // Check for a valid refresh token first
  221. if (currentAccount.Session.IsRefreshTokenExpired())
  222. {
  223. MessageBox.Show("Your session has expired. Use the login again button under the selected account menu.", "Deactivate Authenticator", MessageBoxButtons.OK, MessageBoxIcon.Error);
  224. return;
  225. }
  226. // Check for a valid access token, refresh it if needed
  227. if (currentAccount.Session.IsAccessTokenExpired())
  228. {
  229. try
  230. {
  231. await currentAccount.Session.RefreshAccessToken();
  232. }
  233. catch (Exception ex)
  234. {
  235. MessageBox.Show(ex.Message, "Deactivate Authenticator Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  236. return;
  237. }
  238. }
  239. DialogResult res = MessageBox.Show("Would you like to remove Steam Guard completely?\nYes - Remove Steam Guard completely.\nNo - Switch back to Email authentication.", "Deactivate Authenticator: " + currentAccount.AccountName, MessageBoxButtons.YesNoCancel);
  240. int scheme = 0;
  241. if (res == DialogResult.Yes)
  242. {
  243. scheme = 2;
  244. }
  245. else if (res == DialogResult.No)
  246. {
  247. scheme = 1;
  248. }
  249. else if (res == DialogResult.Cancel)
  250. {
  251. scheme = 0;
  252. }
  253. if (scheme != 0)
  254. {
  255. string confCode = currentAccount.GenerateSteamGuardCode();
  256. InputForm confirmationDialog = new InputForm(String.Format("Removing Steam Guard from {0}. Enter this confirmation code: {1}", currentAccount.AccountName, confCode));
  257. confirmationDialog.ShowDialog();
  258. if (confirmationDialog.Canceled)
  259. {
  260. return;
  261. }
  262. string enteredCode = confirmationDialog.txtBox.Text.ToUpper();
  263. if (enteredCode != confCode)
  264. {
  265. MessageBox.Show("Confirmation codes do not match. Steam Guard not removed.");
  266. return;
  267. }
  268. bool success = await currentAccount.DeactivateAuthenticator(scheme);
  269. if (success)
  270. {
  271. MessageBox.Show(String.Format("Steam Guard {0}. maFile will be deleted after hitting okay. If you need to make a backup, now's the time.", (scheme == 2 ? "removed completely" : "switched to emails")));
  272. this.manifest.RemoveAccount(currentAccount);
  273. this.loadAccountsList();
  274. }
  275. else
  276. {
  277. MessageBox.Show("Steam Guard failed to deactivate.");
  278. }
  279. }
  280. else
  281. {
  282. MessageBox.Show("Steam Guard was not removed. No action was taken.");
  283. }
  284. }
  285. // Tray menu handlers
  286. private void trayIcon_MouseDoubleClick(object sender, MouseEventArgs e)
  287. {
  288. trayRestore_Click(sender, EventArgs.Empty);
  289. }
  290. private void trayRestore_Click(object sender, EventArgs e)
  291. {
  292. this.Show();
  293. this.WindowState = FormWindowState.Normal;
  294. }
  295. private void trayQuit_Click(object sender, EventArgs e)
  296. {
  297. Application.Exit();
  298. }
  299. private void trayTradeConfirmations_Click(object sender, EventArgs e)
  300. {
  301. btnTradeConfirmations_Click(sender, e);
  302. }
  303. private void trayCopySteamGuard_Click(object sender, EventArgs e)
  304. {
  305. if (txtLoginToken.Text != "")
  306. {
  307. Clipboard.SetText(txtLoginToken.Text);
  308. }
  309. }
  310. private void trayAccountList_SelectedIndexChanged(object sender, EventArgs e)
  311. {
  312. listAccounts.SelectedIndex = trayAccountList.SelectedIndex;
  313. }
  314. // Misc UI handlers
  315. private void listAccounts_SelectedValueChanged(object sender, EventArgs e)
  316. {
  317. for (int i = 0; i < allAccounts.Length; i++)
  318. {
  319. // Check if index is out of bounds first
  320. if (i < 0 || listAccounts.SelectedIndex < 0)
  321. continue;
  322. SteamGuardAccount account = allAccounts[i];
  323. if (account.AccountName == (string)listAccounts.Items[listAccounts.SelectedIndex])
  324. {
  325. trayAccountList.Text = account.AccountName;
  326. currentAccount = account;
  327. loadAccountInfo();
  328. break;
  329. }
  330. }
  331. }
  332. private void txtAccSearch_TextChanged(object sender, EventArgs e)
  333. {
  334. List<string> names = new List<string>(getAllNames());
  335. names = names.FindAll(new Predicate<string>(IsFilter));
  336. listAccounts.Items.Clear();
  337. listAccounts.Items.AddRange(names.ToArray());
  338. trayAccountList.Items.Clear();
  339. trayAccountList.Items.AddRange(names.ToArray());
  340. }
  341. // Timers
  342. private async void timerSteamGuard_Tick(object sender, EventArgs e)
  343. {
  344. lblStatus.Text = "Aligning time with Steam...";
  345. steamTime = await TimeAligner.GetSteamTimeAsync();
  346. lblStatus.Text = "";
  347. currentSteamChunk = steamTime / 30L;
  348. int secondsUntilChange = (int)(steamTime - (currentSteamChunk * 30L));
  349. loadAccountInfo();
  350. if (currentAccount != null)
  351. {
  352. pbTimeout.Value = 30 - secondsUntilChange;
  353. }
  354. }
  355. private async void timerTradesPopup_Tick(object sender, EventArgs e)
  356. {
  357. if (currentAccount == null || popupFrm.Visible) return;
  358. if (!confirmationsSemaphore.Wait(0))
  359. {
  360. return; //Only one thread may access this critical section at once. Mutex is a bad choice here because it'll cause a pileup of threads.
  361. }
  362. List<Confirmation> confs = new List<Confirmation>();
  363. Dictionary<SteamGuardAccount, List<Confirmation>> autoAcceptConfirmations = new Dictionary<SteamGuardAccount, List<Confirmation>>();
  364. SteamGuardAccount[] accs =
  365. manifest.CheckAllAccounts ? allAccounts : new SteamGuardAccount[] { currentAccount };
  366. try
  367. {
  368. lblStatus.Text = "Checking confirmations...";
  369. foreach (var acc in accs)
  370. {
  371. // Check for a valid refresh token first
  372. if (acc.Session.IsRefreshTokenExpired())
  373. {
  374. MessageBox.Show("Your session for account " + acc.AccountName + " has expired. You will be prompted to login again.", "Trade Confirmations", MessageBoxButtons.OK, MessageBoxIcon.Error);
  375. PromptRefreshLogin(acc);
  376. break;
  377. }
  378. // Check for a valid access token, refresh it if needed
  379. if (acc.Session.IsAccessTokenExpired())
  380. {
  381. try
  382. {
  383. lblStatus.Text = "Refreshing session...";
  384. await acc.Session.RefreshAccessToken();
  385. lblStatus.Text = "Checking confirmations...";
  386. }
  387. catch (Exception ex)
  388. {
  389. MessageBox.Show(ex.Message, "Steam Login Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  390. break;
  391. }
  392. }
  393. try
  394. {
  395. Confirmation[] tmp = await acc.FetchConfirmationsAsync();
  396. foreach (var conf in tmp)
  397. {
  398. if ((conf.ConfType == Confirmation.EMobileConfirmationType.MarketListing && manifest.AutoConfirmMarketTransactions) ||
  399. (conf.ConfType == Confirmation.EMobileConfirmationType.Trade && manifest.AutoConfirmTrades))
  400. {
  401. if (!autoAcceptConfirmations.ContainsKey(acc))
  402. autoAcceptConfirmations[acc] = new List<Confirmation>();
  403. autoAcceptConfirmations[acc].Add(conf);
  404. }
  405. else
  406. confs.Add(conf);
  407. }
  408. }
  409. catch (Exception)
  410. {
  411. }
  412. }
  413. lblStatus.Text = "";
  414. if (confs.Count > 0)
  415. {
  416. popupFrm.Confirmations = confs.ToArray();
  417. popupFrm.Popup();
  418. }
  419. if (autoAcceptConfirmations.Count > 0)
  420. {
  421. foreach (var acc in autoAcceptConfirmations.Keys)
  422. {
  423. var confirmations = autoAcceptConfirmations[acc].ToArray();
  424. await acc.AcceptMultipleConfirmations(confirmations);
  425. }
  426. }
  427. }
  428. catch (SteamGuardAccount.WGTokenInvalidException)
  429. {
  430. lblStatus.Text = "";
  431. }
  432. confirmationsSemaphore.Release();
  433. }
  434. // Other methods
  435. private void CopyLoginToken()
  436. {
  437. string text = txtLoginToken.Text;
  438. if (String.IsNullOrEmpty(text))
  439. return;
  440. Clipboard.SetText(text);
  441. }
  442. /// <summary>
  443. /// Display a login form to the user to refresh their OAuth Token
  444. /// </summary>
  445. /// <param name="account">The account to refresh</param>
  446. private void PromptRefreshLogin(SteamGuardAccount account)
  447. {
  448. var loginForm = new LoginForm(LoginForm.LoginType.Refresh, account);
  449. loginForm.ShowDialog();
  450. }
  451. /// <summary>
  452. /// Load UI with the current account info, this is run every second
  453. /// </summary>
  454. private void loadAccountInfo()
  455. {
  456. if (currentAccount != null && steamTime != 0)
  457. {
  458. popupFrm.Account = currentAccount;
  459. txtLoginToken.Text = currentAccount.GenerateSteamGuardCodeForTime(steamTime);
  460. groupAccount.Text = "Account: " + currentAccount.AccountName;
  461. }
  462. }
  463. /// <summary>
  464. /// Decrypts files and populates list UI with accounts
  465. /// </summary>
  466. private void loadAccountsList()
  467. {
  468. currentAccount = null;
  469. listAccounts.Items.Clear();
  470. listAccounts.SelectedIndex = -1;
  471. trayAccountList.Items.Clear();
  472. trayAccountList.SelectedIndex = -1;
  473. allAccounts = manifest.GetAllAccounts(passKey);
  474. if (allAccounts.Length > 0)
  475. {
  476. for (int i = 0; i < allAccounts.Length; i++)
  477. {
  478. SteamGuardAccount account = allAccounts[i];
  479. listAccounts.Items.Add(account.AccountName);
  480. trayAccountList.Items.Add(account.AccountName);
  481. }
  482. listAccounts.SelectedIndex = 0;
  483. trayAccountList.SelectedIndex = 0;
  484. listAccounts.Sorted = true;
  485. trayAccountList.Sorted = true;
  486. }
  487. menuDeactivateAuthenticator.Enabled = btnTradeConfirmations.Enabled = allAccounts.Length > 0;
  488. }
  489. private void listAccounts_KeyDown(object sender, KeyEventArgs e)
  490. {
  491. if (e.Control)
  492. {
  493. if (e.KeyCode == Keys.Up || e.KeyCode == Keys.Down)
  494. {
  495. int to = listAccounts.SelectedIndex - (e.KeyCode == Keys.Up ? 1 : -1);
  496. manifest.MoveEntry(listAccounts.SelectedIndex, to);
  497. loadAccountsList();
  498. }
  499. return;
  500. }
  501. if (!IsKeyAChar(e.KeyCode) && !IsKeyADigit(e.KeyCode))
  502. {
  503. return;
  504. }
  505. txtAccSearch.Focus();
  506. txtAccSearch.Text = e.KeyCode.ToString();
  507. txtAccSearch.SelectionStart = 1;
  508. }
  509. private static bool IsKeyAChar(Keys key)
  510. {
  511. return key >= Keys.A && key <= Keys.Z;
  512. }
  513. private static bool IsKeyADigit(Keys key)
  514. {
  515. return (key >= Keys.D0 && key <= Keys.D9) || (key >= Keys.NumPad0 && key <= Keys.NumPad9);
  516. }
  517. private bool IsFilter(string f)
  518. {
  519. if (txtAccSearch.Text.StartsWith("~"))
  520. {
  521. try
  522. {
  523. return Regex.IsMatch(f, txtAccSearch.Text);
  524. }
  525. catch (Exception)
  526. {
  527. return true;
  528. }
  529. }
  530. else
  531. {
  532. return f.Contains(txtAccSearch.Text.ToLower());
  533. }
  534. }
  535. private string[] getAllNames()
  536. {
  537. string[] itemArray = new string[allAccounts.Length];
  538. for (int i = 0; i < itemArray.Length; i++)
  539. {
  540. itemArray[i] = allAccounts[i].AccountName;
  541. }
  542. return itemArray;
  543. }
  544. private void loadSettings()
  545. {
  546. timerTradesPopup.Enabled = manifest.PeriodicChecking;
  547. timerTradesPopup.Interval = manifest.PeriodicCheckingInterval * 1000;
  548. }
  549. // Logic for version checking
  550. private Version newVersion = null;
  551. private Version currentVersion = null;
  552. private WebClient updateClient = null;
  553. private string updateUrl = null;
  554. private bool startupUpdateCheck = true;
  555. private void checkForUpdates()
  556. {
  557. if (updateClient != null) return;
  558. updateClient = new WebClient();
  559. updateClient.DownloadStringCompleted += UpdateClient_DownloadStringCompleted;
  560. updateClient.Headers.Add("Content-Type", "application/json");
  561. updateClient.Headers.Add("User-Agent", "Steam Desktop Authenticator");
  562. updateClient.DownloadStringAsync(new Uri("https://api.github.com/repos/Jessecar96/SteamDesktopAuthenticator/releases/latest"));
  563. }
  564. private void compareVersions()
  565. {
  566. if (newVersion > currentVersion)
  567. {
  568. labelUpdate.Text = "Download new version"; // Show the user a new version is available if they press no
  569. DialogResult updateDialog = MessageBox.Show(String.Format("A new version is available! Would you like to download it now?\nYou will update from version {0} to {1}", Application.ProductVersion, newVersion.ToString()), "New Version", MessageBoxButtons.YesNo);
  570. if (updateDialog == DialogResult.Yes)
  571. {
  572. Process.Start(updateUrl);
  573. }
  574. }
  575. else
  576. {
  577. if (!startupUpdateCheck)
  578. {
  579. MessageBox.Show(String.Format("You are using the latest version: {0}", Application.ProductVersion));
  580. }
  581. }
  582. newVersion = null; // Check the api again next time they check for updates
  583. updateClient = null; // Set to null to indicate it's done checking
  584. startupUpdateCheck = false; // Set when it's done checking on startup
  585. }
  586. private void UpdateClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
  587. {
  588. try
  589. {
  590. dynamic resultObject = JsonConvert.DeserializeObject(e.Result);
  591. newVersion = new Version(resultObject.tag_name.Value);
  592. currentVersion = new Version(Application.ProductVersion);
  593. updateUrl = resultObject.assets.First.browser_download_url.Value;
  594. compareVersions();
  595. }
  596. catch (Exception)
  597. {
  598. MessageBox.Show("Failed to check for updates.");
  599. }
  600. }
  601. private void MainForm_KeyDown(object sender, KeyEventArgs e)
  602. {
  603. if (e.KeyCode == Keys.C && e.Modifiers == Keys.Control)
  604. {
  605. CopyLoginToken();
  606. }
  607. }
  608. private void panelButtons_SizeChanged(object sender, EventArgs e)
  609. {
  610. int totButtons = panelButtons.Controls.OfType<Button>().Count();
  611. Point curPos = new Point(0, 0);
  612. foreach (Button but in panelButtons.Controls.OfType<Button>())
  613. {
  614. but.Width = panelButtons.Width / totButtons;
  615. but.Location = curPos;
  616. curPos = new Point(curPos.X + but.Width, 0);
  617. }
  618. }
  619. }
  620. }