using System.Diagnostics; using DataModel.Database; using Processor; using UI_WinForms.Resources; namespace UI_WinForms.Components; public sealed partial class Setting : UserControl { private readonly record struct SettingUIData( Bitmap Icon, Func SettingName, Func SettingExplanation, Func<(string URL, string TextPattern)> SettingExplanationLink, Func SetupDataControl, bool ChangeNeedsRestart ); private readonly SettingUIData data; private bool needRestart = false; public Setting() { this.InitializeComponent(); } public bool NeedRestart => this.needRestart; private Setting(SettingUIData settingMetaData) { this.InitializeComponent(); if(this.DesignMode) return; this.Dock = DockStyle.Top; this.data = settingMetaData; this.labelIcon.Image = settingMetaData.Icon; this.labelSettingName.Text = settingMetaData.SettingName(); this.labelExplanation.Text = settingMetaData.SettingExplanation(); // // Handle up to one link in the explanation: // var link = settingMetaData.SettingExplanationLink(); var startIdx = this.labelExplanation.Text.IndexOf(link.TextPattern, StringComparison.InvariantCulture); this.labelExplanation.Links.Clear(); if (!string.IsNullOrWhiteSpace(link.TextPattern) && !string.IsNullOrWhiteSpace(link.URL) && startIdx > -1) this.labelExplanation.Links.Add(startIdx, link.TextPattern.Length, link.URL); this.labelExplanation.LinkClicked += (_, args) => { if(args.Link.LinkData is string url && !string.IsNullOrWhiteSpace(url) && url.ToLowerInvariant().StartsWith("https://", StringComparison.InvariantCulture)) Process.Start(new ProcessStartInfo(url) { UseShellExecute = true }); }; var dataControl = settingMetaData.SetupDataControl(this.ChangeTrigger); this.tableLayout.Controls.Add(dataControl, 2, 0); // Ensure, that this data control is vertical centered by calculating the needed margin, considering the outer size of the table layout: var margin = (this.tableLayout.GetRowHeights().First() - dataControl.Height) / 2f; dataControl.Margin = new Padding(0, (int) margin, 0, (int)margin); // Calculate the needed height of the explanation label & centering of the data control when the parent window is resized: this.tableLayout.Resize += (sender, args) => { // Adjust the height of the parent controls (table & user control): this.tableLayout.Height = Math.Max((int)this.labelExplanation.CreateGraphics().MeasureString(this.labelExplanation.Text, this.labelExplanation.Font, new SizeF(this.labelExplanation.Width, 1000)).Height, 66); this.Height = this.tableLayout.Height + this.tableLayout.Margin.Vertical; // Ensure, that this data control is vertical centered by calculating the needed margin, considering the outer size of the table layout: var margin = (this.tableLayout.GetRowHeights().First() - dataControl.Height) / 2f; dataControl.Margin = new Padding(0, (int) margin, 0, (int)margin); }; } private void ChangeTrigger() { if(this.data.ChangeNeedsRestart) this.needRestart = true; } private void UpdateExplanation() => this.labelExplanation.Text = this.data.SettingExplanation(); private static async Task ShowDeepLModeSettingAsync() { var currentSetting = await AppSettings.GetDeepLMode(); var settingData = new SettingUIData( Icon: Icons.deepl_logo_icon_170284, SettingName: () => "DeepL Service", ChangeNeedsRestart: true, SettingExplanation: () => "DeepL is a translation service that offers a wide range of translation services. This setting allows you to choose between the free and pro version of DeepL.", SettingExplanationLink: () => ("https://www.deepl.com/", "DeepL"), SetupDataControl: (changeTrigger) => { var dropdown = new ComboBox(); dropdown.Items.Add("Disabled"); dropdown.Items.Add("Free version"); dropdown.Items.Add("Pro version"); dropdown.SelectedIndex = currentSetting switch { SettingDeepLMode.DISABLED => 0, SettingDeepLMode.USE_FREE_ACCOUNT => 1, SettingDeepLMode.USE_PRO_ACCOUNT => 2, _ => 0, }; // Setup the change event handler: dropdown.SelectedValueChanged += async (sender, args) => await AppSettings.SetDeepLMode(dropdown.SelectedIndex switch { 0 => SettingDeepLMode.DISABLED, 1 => SettingDeepLMode.USE_FREE_ACCOUNT, 2 => SettingDeepLMode.USE_PRO_ACCOUNT, _ => SettingDeepLMode.DISABLED, }); // Fires the change trigger: dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); // Apply the desired layout: dropdown.Dock = DockStyle.Fill; dropdown.DropDownStyle = ComboBoxStyle.DropDownList; return dropdown; } ); return new Setting(settingData); } private static async Task ShowDeepLAPIKeySettingAsync() { var currentSetting = await AppSettings.GetDeepLAPIKey(); var settingData = new SettingUIData( Icon: Icons.icons8_key_512, SettingName: () => "DeepL API Key", ChangeNeedsRestart: true, SettingExplanation: () => "The API key is required to use the DeepL translation service. You can find your API key on the DeepL website.", SettingExplanationLink: () => ("https://www.deepl.com/", "DeepL"), SetupDataControl: (changeTrigger) => { var textbox = new TextBox(); textbox.Text = currentSetting; textbox.TextChanged += async (sender, args) => await AppSettings.SetDeepLAPIKey(textbox.Text); textbox.TextChanged += (sender, args) => changeTrigger(); textbox.Dock = DockStyle.Fill; return textbox; } ); return new Setting(settingData); } private static async Task ShowDeepLUsageSettingAsync() { var currentUsage = await Processor.DeepL.GetUsage(); var percent = currentUsage.Enabled ? currentUsage.CharacterCount / (float)currentUsage.CharacterLimit : 0; // Local function to show & update the explanation text: string GetUsageText() => currentUsage.Enabled ? $"You used {currentUsage.CharacterCount:###,###,###,##0} characters out of {currentUsage.CharacterLimit:###,###,###,##0} ({percent:P2})." : currentUsage.AuthIssue ? "Was not able to authorize with DeepL. Please check your API key." : "DeepL is disabled or the API key is not set."; var settingData = new SettingUIData( Icon: Icons.icons8_increase_512, SettingName: () => "DeepL Usage", ChangeNeedsRestart: false, SettingExplanation: GetUsageText, SettingExplanationLink: () => (string.Empty, string.Empty), SetupDataControl: (changeTrigger) => { var progressbar = new ProgressBar(); progressbar.Maximum = 100; progressbar.Margin = new Padding(0, 16, 0, 16); progressbar.Dock = DockStyle.Fill; progressbar.Style = ProgressBarStyle.Continuous; progressbar.Value = percent switch { < 0 => 0, > 1 => 100, _ => (int)(percent * 100) }; var reloadButton = new Button(); reloadButton.Text = string.Empty; reloadButton.Image = Icons.icons8_reload_512; reloadButton.FlatStyle = FlatStyle.Flat; reloadButton.FlatAppearance.BorderSize = 0; reloadButton.BackColor = Color.Empty; reloadButton.UseVisualStyleBackColor = true; reloadButton.Size = new Size(60, 60); reloadButton.Click += async (sender, args) => { var usage = await Processor.DeepL.GetUsage(); // Update the outer variables: percent = usage.Enabled ? usage.CharacterCount / (float)usage.CharacterLimit : 0; currentUsage = usage; // Update the progress bar: progressbar.Value = percent switch { < 0 => 0, > 1 => 100, _ => (int)(percent * 100) }; // Update the explanation text. Therefore, we need to get the setting object through the chain of parents: var setting = (Setting) ((Control) sender).Parent.Parent.Parent; setting.UpdateExplanation(); }; // Setup the layout: var layout = new TableLayoutPanel(); layout.ColumnCount = 2; layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100)); layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 66)); layout.RowCount = 1; layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100)); layout.Controls.Add(progressbar, 0, 0); layout.Controls.Add(reloadButton, 1, 0); layout.Dock = DockStyle.Fill; return layout; } ); var setting = new Setting(settingData); return setting; } private static async Task ShowDeepLActionSettingAsync() { var currentSetting = await AppSettings.GetDeepLAction(); var settingData = new SettingUIData( Icon: Icons.icons8_play_512__2_, SettingName: () => "DeepL Operation", ChangeNeedsRestart: true, SettingExplanation: () => "Should the missing translations be automatically completed by DeepL? This can lead to higher costs. By default, DeepL is only applied manually.", SettingExplanationLink: () => ("https://www.deepl.com/", "DeepL"), SetupDataControl: (changeTrigger) => { // We set up a combo box with the available actions: var dropdown = new ComboBox(); dropdown.Items.Add("Manual"); dropdown.Items.Add("Automatic"); dropdown.SelectedIndex = currentSetting switch { SettingDeepLAction.MANUAL => 0, SettingDeepLAction.AUTOMATIC_ALL => 1, _ => 0, }; // Setup the change event handler: dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); dropdown.SelectedValueChanged += async (sender, args) => await AppSettings.SetDeepLAction(dropdown.SelectedIndex switch { 0 => SettingDeepLAction.MANUAL, 1 => SettingDeepLAction.AUTOMATIC_ALL, _ => SettingDeepLAction.MANUAL, }); // Apply the desired layout: dropdown.Dock = DockStyle.Fill; dropdown.DropDownStyle = ComboBoxStyle.DropDownList; return dropdown; } ); return new Setting(settingData); } internal readonly record struct ComboBoxItem(string DisplayText, int CultureIndex) { public override string ToString() => this.DisplayText; } private static async Task ShowDeepLSourceCultureSettingAsync() { var currentSourceCultureIndex = await AppSettings.GetDeepLSourceCultureIndex(); // We load the corresponding culture for that index. As dropdown items, we show // all other available cultures: var allCultures = await AppSettings.GetCultureInfos(); // Attention: We have to store the culture's index, because the index is not // continuous and can change when the user adds or removes a culture! var settingData = new SettingUIData( Icon: Icons.icons8_chat_bubble_512, SettingName: () => "DeepL Source Culture", ChangeNeedsRestart: true, SettingExplanation: () => "The source culture is used to translate the missing translations.", SettingExplanationLink: () => (string.Empty, string.Empty), SetupDataControl: (changeTrigger) => { var dropdown = new ComboBox(); var currentCultureDropdownIndex = 0; for (var n = 0; n < allCultures.Count; n++) { var cultureInfo = allCultures[n]; if(cultureInfo.Index == currentSourceCultureIndex) currentCultureDropdownIndex = n; dropdown.Items.Add(new ComboBoxItem($"{cultureInfo.Index}.: {cultureInfo.Code}", cultureInfo.Index)); } dropdown.SelectedIndex = currentCultureDropdownIndex; // Setup the change event handler: dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); dropdown.SelectedValueChanged += async (sender, args) => { if(dropdown.SelectedItem is ComboBoxItem selectedItem) await AppSettings.SetDeepLSourceCultureIndex(selectedItem.CultureIndex); }; // Apply the desired layout: dropdown.Dock = DockStyle.Fill; dropdown.DropDownStyle = ComboBoxStyle.DropDownList; return dropdown; } ); return new Setting(settingData); } private static IEnumerable> ShowCultureSettingsAsync() { var isFirstCulture = true; // We need this flag to distinguish the first task from the others. var cultureIndices = new List(new []{ -1 }); while (cultureIndices.Count > 0) { var innerLoopIndex = cultureIndices.Last(); // needed to avoid closure issues. yield return Task.Run(async () => { var localCultureIndex = innerLoopIndex; // Get a list of culture indices. Thus, we know the number of cultures. We cannot do this in the outer loop, // because we cannot await there. The AppSettings is caching the answer, though. The list of indices is ordered // ascending. var localCultureIndices = await AppSettings.GetCultureIndices(); // Update the number of cultures in the outer loop for the first call: if(isFirstCulture) { localCultureIndex = localCultureIndices.Last(); innerLoopIndex = localCultureIndices.Last(); cultureIndices.Clear(); cultureIndices.AddRange(localCultureIndices); isFirstCulture = false; } // Get the current culture code: var currentCultureCode = await AppSettings.GetCultureCode(localCultureIndex); // Construct the setting: return new Setting(new() { Icon = Icons.icons8_chat_bubble_512, SettingName = () => $"{localCultureIndex}. Culture", ChangeNeedsRestart = true, SettingExplanation = () => "The culture according to RFC 4646: First comes the ISO 639-1 language code in lower case, followed by a hyphen, followed by the ISO 3166-1 alpha-2 country code in upper case. Example: en-US for English in the USA, de-DE for German in Germany.", SettingExplanationLink = () => ("https://lonewolfonline.net/list-net-culture-country-codes/", "according to RFC 4646"), SetupDataControl = (changeTrigger) => { var textbox = new TextBox(); textbox.Text = currentCultureCode; textbox.TextChanged += async (sender, args) => { await AppSettings.SetCultureCode(localCultureIndex, textbox.Text); changeTrigger(); }; textbox.Dock = DockStyle.Fill; return textbox; } }); }); cultureIndices.Remove(innerLoopIndex); } } private static async Task ShowGeneratorModeSettingAsync() { var currentSetting = await AppSettings.GetGeneratorMode(); var settingData = new SettingUIData( Icon: Icons.icons8_code_512, SettingName: () => "Generator Mode", ChangeNeedsRestart: false, SettingExplanation: () => "The generator mode determines how the translation files are generated.", SettingExplanationLink: () => (string.Empty, string.Empty), SetupDataControl: (changeTrigger) => { // We set up a combo box with the available actions: var dropdown = new ComboBox(); dropdown.Items.Add("Automatic generation"); dropdown.Items.Add("Manual generation"); dropdown.SelectedIndex = currentSetting switch { SettingGeneratorMode.AUTOMATIC => 0, SettingGeneratorMode.MANUAL => 1, _ => 0, }; // Setup the change event handler: dropdown.SelectedValueChanged += (sender, args) => changeTrigger(); dropdown.SelectedValueChanged += async (sender, args) => await AppSettings.SetGeneratorMode(dropdown.SelectedIndex switch { 0 => SettingGeneratorMode.AUTOMATIC, 1 => SettingGeneratorMode.MANUAL, _ => SettingGeneratorMode.AUTOMATIC, }); // Apply the desired layout: dropdown.Dock = DockStyle.Fill; dropdown.DropDownStyle = ComboBoxStyle.DropDownList; return dropdown; } ); return new Setting(settingData); } private static async Task ShowGeneratorDotnetEnabledSettingAsync() { var currentSetting = await AppSettings.GetGeneratorDotnetEnabled(); var settingData = new SettingUIData( Icon: Icons.icons8_code_512, SettingName: () => "Generator: .NET", ChangeNeedsRestart: false, SettingExplanation: () => "When enabled, .NET translation files are generated. Requires a .NET 6 or newer project.", SettingExplanationLink: () => (string.Empty, string.Empty), SetupDataControl: (changeTrigger) => { // Set up an checkbox: var checkbox = new CheckBox(); checkbox.Checked = currentSetting; checkbox.CheckedChanged += (sender, args) => changeTrigger(); checkbox.CheckedChanged += async (sender, args) => await AppSettings.SetGeneratorDotnetEnabled(checkbox.Checked); checkbox.Text = "Enable .NET Generator"; // Apply the desired layout: checkbox.Dock = DockStyle.Fill; return checkbox; } ); return new Setting(settingData); } private static async Task ShowGeneratorDotnetDestinationPathSettingAsync() { var currentSetting = await AppSettings.GetGeneratorDotnetDestinationPath(); var settingData = new SettingUIData( Icon: Icons.icons8_code_512, SettingName: () => "Generator: .NET Destination Path", ChangeNeedsRestart: false, SettingExplanation: () => "The destination path for the .NET translation files. You might use environment variables like %USERPROFILE%.", SettingExplanationLink: () => (string.Empty, string.Empty), SetupDataControl: (changeTrigger) => { // Set up a horizontal layout: var layout = new TableLayoutPanel(); layout.ColumnCount = 2; layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 66F)); layout.RowCount = 1; layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); layout.Dock = DockStyle.Fill; // Set up a textbox: var textbox = new TextBox(); textbox.Text = currentSetting; textbox.TextChanged += (sender, args) => changeTrigger(); textbox.TextChanged += async (sender, args) => await AppSettings.SetGeneratorDotnetDestinationPath(textbox.Text); textbox.Dock = DockStyle.Fill; textbox.Margin = new Padding(0, 13, 0, 13); layout.Controls.Add(textbox, 0, 0); // Set up a button: var button = new Button(); button.Text = string.Empty; button.Image = Icons.icons8_folder_tree_512; button.FlatStyle = FlatStyle.Flat; button.FlatAppearance.BorderSize = 0; button.BackColor = Color.Empty; button.UseVisualStyleBackColor = true; button.Size = new Size(60, 60); button.Click += (sender, args) => { var dialog = new FolderBrowserDialog(); dialog.SelectedPath = textbox.Text; dialog.InitialDirectory = textbox.Text; dialog.Description = "Select the destination path for the .NET translation files."; dialog.ShowNewFolderButton = true; if (dialog.ShowDialog() == DialogResult.OK) textbox.Text = dialog.SelectedPath; }; button.Dock = DockStyle.Fill; layout.Controls.Add(button, 1, 0); return layout; } ); return new Setting(settingData); } public static IEnumerable> GetAllSettings() { yield return ShowGeneratorDotnetDestinationPathSettingAsync(); yield return ShowGeneratorDotnetEnabledSettingAsync(); yield return ShowGeneratorModeSettingAsync(); yield return ShowDeepLSourceCultureSettingAsync(); foreach (var setting in ShowCultureSettingsAsync()) { yield return setting; } yield return ShowDeepLUsageSettingAsync(); yield return ShowDeepLActionSettingAsync(); yield return ShowDeepLAPIKeySettingAsync(); yield return ShowDeepLModeSettingAsync(); } }