using System; using System.Collections.Generic; using System.Drawing; using System.Linq; using System.Net; using System.Net.Http; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Windows.Forms; using System.Xml; using TradeIdeas.TIProData; using TradeIdeas.TIProData.Interfaces; using TradeIdeas.XML; // TODO when I hit save it does one extra syntax check. 7/20/2021 I cannot reproduce this. namespace TradeIdeas.TIProGUI.FormulaEditor { public partial class FormulaEditor : Form, IFont, ISnapToGrid, ISaveLayout { private readonly IConnectionMaster ConnectionMaster; /// /// This is a convenient thing to put into a ComboBox's Items list. /// class Selectable { /// /// Display this to the user. /// public readonly string User; /// /// This is what we use everywhere else. /// In particular, this is how we store the value when we /// talk with the server. /// public readonly string Internal; public Selectable(string User, string Internal) { this.User = User; this.Internal = Internal; System.Diagnostics.Debug.Assert((User != null) && (Internal != null)); } public override string ToString() { return User; } } /// /// A description of a single formula. /// /// This corresponds to a single record in the user_filters table. /// class FormulaInfo { /// /// E.g. "U8" /// public readonly string InternalCode; /// /// E.g. "3 * [Price]". CRLFs work as expected. /// public string Source; /// /// The name of the field that will appear in column headers, on the config window, etc. /// public string Description; /// /// E.g. "$" or "%". This is often displayed near the description. /// We often have 2 or more fields with the same description but different units. /// public string Units; public string Format; public string Graphics; public FormulaInfo(string internalCode) { InternalCode = internalCode; Source = ""; Description = ""; Units = ""; Format = ""; Graphics = ""; } public FormulaInfo(XmlElement element) { InternalCode = element.Property("internal_code"); Source = element.Property("source"); Description = element.Property("description"); Units = element.Property("units"); Format = element.Property("format"); Graphics = element.Property("graphics"); } /// /// After a call to clear() we expect ToString() to return InternalCode and nothing else. /// public void Clear() { Source = ""; Description = ""; Units = ""; } /// /// A good value to display in a ComboBox. /// /// public override string ToString() { string result = InternalCode; if (Description != "") { result += " — " + Description; if (Units != "") { result += " (" + Units + ")"; } } return result; } } class CompilerResult { public readonly bool Valid; public readonly bool TopList; public readonly string ErrorMessage; public readonly int ErrorLocation; public CompilerResult(XmlElement element) { Valid = element.Property("valid", false); TopList = element.Property("top-list", false); ErrorMessage = element.Property("error-message", ""); ErrorLocation = element.Property("error-location", -1); // Add some additional logic to provide additional error info // such as: 417 - Expectation Failed. if (!Valid && string.IsNullOrWhiteSpace(ErrorMessage)) { var errorMessage = "Unknown Error."; if (null != element && !string.IsNullOrWhiteSpace(element.InnerText)) errorMessage = element.InnerText; ErrorMessage = errorMessage; } } }; /// /// This is a quick and dirty API. Want to do something better. /// Ideally avoid HTTP. /// /// This is how we load, save, delete and verify out formulas. /// private class TempData { private static readonly HttpClient _client = new HttpClient(); /// /// Ask the server for all of the user's formulas. /// /// Required to contact the server /// One item for every formula the user has saved. public static async Task> GetFullList(ILoginManager loginManager) { try { var url = "https://www.trade-ideas.com/FormulaEditorAPI/List.php?internal_code=*&username="; url += WebUtility.UrlEncode(loginManager.Username); url += "&password="; url += WebUtility.UrlEncode(loginManager.Password); string responseBody = await _client.GetStringAsync(url); var document = new XmlDocument(); document.LoadXml(responseBody); var result = new List(); foreach (var element in document.DocumentElement.Enum()) { result.Add(new FormulaInfo(element)); } return result; } catch (Exception e) { string debugView = e.ToString(); // Return an empty list. return new List(); } } /// /// Ask the server to review the source code. /// This only provides advice. It does not make any changes. /// The web page ties this function to the save function. /// This program gives constant syntax advice to the user, but /// only saves when the user requests it. /// /// The formula to be checked. This can include CRLF. /// The result of the check. public static async Task SyntaxCheck(string source) { try { // Changed to use PostAsync. string url = "https://www.trade-ideas.com/FormulaEditorAPI/Check.php"; // We were getting 417 Expectation Failed on some requests and this next line seemed to fix it. // https://stackoverflow.com/a/45035032/35229 _client.DefaultRequestHeaders.ExpectContinue = false; var content = new FormUrlEncodedContent(new[] { new KeyValuePair("source", source) }); var response = await _client.PostAsync(url, content); var contents = await response.Content.ReadAsStringAsync(); var document = new XmlDocument(); document.LoadXml(contents); return new CompilerResult(document.DocumentElement); } catch (Exception e) { string debugView = e.ToString(); // Return an empty list. return new CompilerResult(new XmlDocument().DocumentElement); } } /// /// Saves this formula to the server. /// /// Required to contact the server. /// /// This returns a task. You can await the task if you want to know when this request was completed. public static async Task Save(ILoginManager loginManager, FormulaInfo formulaToSave) { try { // Changed to use PostAsync. var url = "https://www.trade-ideas.com/FormulaEditorAPI/Save.php"; var content = new FormUrlEncodedContent(new[] { new KeyValuePair("username", loginManager.Username), new KeyValuePair("password", loginManager.Password), new KeyValuePair("internal_code", formulaToSave.InternalCode), new KeyValuePair("source", formulaToSave.Source), new KeyValuePair("description", formulaToSave.Description), new KeyValuePair("units", formulaToSave.Units), new KeyValuePair("format", formulaToSave.Format), new KeyValuePair("graphics", formulaToSave.Graphics) }); var response = await _client.PostAsync(url, content); } catch (Exception e) { string debugView = e.ToString(); } } /// /// Ask the server to delete this formula. /// /// Required to contact the server. /// Only the internal code is used. internal static void Delete(ILoginManager loginManager, FormulaInfo formulaToSave) { try { var url = new StringBuilder("https://www.trade-ideas.com//FormulaEditorAPI/Remove.php?username="); url.Append(WebUtility.UrlEncode(loginManager.Username)); url.Append("&password="); url.Append(WebUtility.UrlEncode(loginManager.Password)); url.Append("&internal_code="); url.Append(WebUtility.UrlEncode(formulaToSave.InternalCode)); _client.GetStringAsync(url.ToString()); } catch (HttpRequestException e) { string debugView = e.ToString(); } } } private FontManager _fontManager; private const float DEFAULT_FONT_SIZE = 8.25F; private bool _suspendSyntaxChecking = false; private readonly Bitmap _editImage; private readonly Bitmap _saveImage; public FormulaEditor(IConnectionMaster connectionMaster) { InitializeComponent(); _editImage = Properties.Resources.Edit; _saveImage = Properties.Resources.Save; WindowIconCache.SetIcon(this); _fontManager = new FontManager(this, DEFAULT_FONT_SIZE); _fontManager.selectTheFont(); Font = GuiEnvironment.FontSettings; StartPosition = FormStartPosition.Manual; SetSnapToGrid(GuiEnvironment.SnapToGrid); ConnectionMaster = connectionMaster; TradeIdeas.TIProGUI.GuiEnvironment.RecordUseCase("FormulaEditor.NewWindow", ConnectionMaster.SendManager); InitFromServerData(); formatComboBox.Items.Add(new Selectable("Price", "p")); formatComboBox.Items.Add(new Selectable("1", "0")); formatComboBox.Items.Add(new Selectable("1.0", "1")); formatComboBox.Items.Add(new Selectable("1.00", "2")); formatComboBox.Items.Add(new Selectable("1.000", "3")); formatComboBox.Items.Add(new Selectable("1.0000", "4")); formatComboBox.Items.Add(new Selectable("1.00000", "5")); formatComboBox.SelectedIndex = 0; graphicsComboBox.Items.Add(new Selectable("None", "")); graphicsComboBox.Items.Add(new Selectable("Position in Range", "%R")); graphicsComboBox.Items.Add(new Selectable("Triangles", "RBCone")); graphicsComboBox.SelectedIndex = 0; sourceDelayTextBox.Delay = 50; // 50ms } /// /// Maps from the InternalCode to the FormulaInfo. /// This includes all formulas that might be saved to the server, not just the ones currently on the server. /// I.e. This *always* includes all 100, U0 - U99. /// private readonly Dictionary Formulas = new Dictionary(); /// /// Compares two formulas by the internal code, the "U" character is removed and the remaining code is compared as a numeric value /// /// /// /// private int formulasComparer(FormulaInfo formula1, FormulaInfo formula2) { try { if (!int.TryParse(formula1.InternalCode.Remove(0, 1), out int numericalCode1)) return 0; if (!int.TryParse(formula2.InternalCode.Remove(0, 1), out int numericalCode2)) return 0; if (numericalCode1 > numericalCode2) return 1; else if (numericalCode1 == numericalCode2) return 0; else return -1; } catch (Exception) { return 0; } } /// /// Do some one time initialization. Load the latest data from the server. /// public async void InitFromServerData() { List < FormulaInfo > formulas = await TempData.GetFullList(ConnectionMaster.LoginManager); formulas.Sort(formulasComparer); foreach (FormulaInfo formula in formulas) { loadComboBox.Items.Add(formula); Formulas[formula.InternalCode] = formula; } for (int i = 0; i < 99; i++) { string internalCode = "U" + i; if (!Formulas.ContainsKey(internalCode)) { FormulaInfo formula = new FormulaInfo(internalCode); createNewComboBox.Items.Add(formula); Formulas.Add(internalCode, formula); } } if (createNewComboBox.Items.Count > 0) { // This will implicitly call SetFormulaToSave() on the selected item. createNewComboBox.SelectedIndex = 0; SetInternalCodeFromFree(); } if (loadComboBox.Items.Count > 0) // This will implicitly call SetFormulaToSave() on the selected item, // possibly overwriting the value we set from the create new combo box. // I.e. If only one combo box has items, use the selected item in that // combo box. If both combo boxes have items, show this one. // // This is required for consistency. Each time the user explicitly // displays an existing strategy, we set that strategy as the location // to save back to. If we display an existing strategy automatically, // when we first open the window, we should treat that the same as // if the user requested to display this strategy. loadComboBox.SelectedIndex = 0; } private void sourceTextBox_TextChanged(object sender, EventArgs e) { if (!internalUpdateInProgress) { CheckSyntaxSoon(); UpdateHelp(); } } private void OnSourceTextBoxSelectionChanged() { // TODO call this any time the cursor moves! There is no standard event handler for that. // https://stackoverflow.com/questions/36308850/is-there-selection-text-event-in-text-box // Suggestions include using a RichTextBox instead of a TexboxBox, catching any keyboard // or mouse event that might change the text box, or add an idle handler that checks for // changes. if (!internalUpdateInProgress) { UpdateHelp(); } } private void CheckSyntaxSoon() { CheckSyntaxNow(); } private async void CheckSyntaxNow() { string source = GetSource(); statusLabel.ForeColor = Color.Gray; statusLabel.Text = "Checking..."; // Show edit image when user changed the formula. if (_suspendSyntaxChecking && isFormulaChanged()) { statePictureBox.Image = _editImage; statePictureBox.Visible = true; } CompilerResult compilerResult; try { // The following line caused the following exception which I caught because I was // running in the debugger. compilerResult = await TempData.SyntaxCheck(source); } catch (Exception ex) { // Some type of problem. Let the GUI hang in the unknown state. The next time the // user types or similar, we will try again. string debugView = ex.ToString(); return; } if (source != GetSource()) { // This request is obsolete. Something's already changed. // Ignore this result. return; } if (compilerResult.Valid) { statusLabel.ForeColor = Color.Green; statusLabel.Text = "✔ Valid for Alerts " + (compilerResult.TopList?"and Top Lists": "only"); UpdateErrorLocation(); } else { statusLabel.ForeColor = Color.Red; statusLabel.Text = "✘ " + compilerResult.ErrorMessage; UpdateErrorLocation(compilerResult.ErrorLocation); } if (!_suspendSyntaxChecking) _suspendSyntaxChecking = true; } private const char ERROR_MARKER = '▶'; private const string ERROR_MARKER_STRING = "▶"; private bool internalUpdateInProgress = false; /// /// When we get a syntax error, we use this function to show the location of that error. /// We display a "▶" right before the offending code. We keep the cursor and/or selection /// the same, so you can keep typing without realizing that we're adding and moving and /// removing that glyph all the time. /// /// /// 0 for the beginning of the source. /// 1 for the position between the first two characters. /// -1 meas not to display that glyph at all. /// private void UpdateErrorLocation(int location = -1) { internalUpdateInProgress = true; int selectionStart = sourceDelayTextBox.SelectionStart; int selectionEnd = selectionStart + sourceDelayTextBox.SelectionLength; string text = sourceDelayTextBox.Text; int oldLocation = text.IndexOf(ERROR_MARKER); if (oldLocation >= 0) { if (selectionEnd > oldLocation) { selectionEnd--; if (selectionStart > oldLocation) { selectionStart--; } } text = text.Replace(ERROR_MARKER_STRING, ""); } if (location >= 0) { text = text.Insert(location, ERROR_MARKER_STRING); if (selectionStart >= location) { selectionStart++; if (selectionEnd >= location) { selectionEnd++; } } } sourceDelayTextBox.Text = text; sourceDelayTextBox.SelectionStart = selectionStart; sourceDelayTextBox.SelectionLength = selectionEnd - selectionStart; internalUpdateInProgress = false; } /// /// /// /// What the user typed and what we want to send to the server. private string GetSource() { return sourceDelayTextBox.Text.Replace(ERROR_MARKER_STRING, ""); } /// /// Which formula the user is currently editing. /// This becomes important when the user hits save. /// private FormulaInfo FormulaToSave; /// /// Valid internal codes for user defined filters. /// private static readonly Regex INTERNAL_CODE = new Regex("^U[1-9]?[0-9]$", RegexOptions.Compiled); /// /// /// /// Something like U2 or U99 /// True if the code is valid. private static bool InternalCodeIsValid(string internalCode) { return INTERNAL_CODE.IsMatch(internalCode); } private void SetFormulaToSave(FormulaInfo formulaToSave) { if ((null == formulaToSave) || !InternalCodeIsValid(formulaToSave.InternalCode)) { // It seems like we should have some rules about where we check this. return; } FormulaToSave = formulaToSave; whichToSaveLabel.Text = formulaToSave.InternalCode; // This next trick is rather inconsistent. Everywhere else we get the images from our normal server connection. // In general we try to avoid HTTP to simplify troubleshooting. If all of our data comes from one server // connection we have less to troubleshoot. whichToSavePictureBox.ImageLocation = "https://static.trade-ideas.com/Filters/Min" + formulaToSave.InternalCode + ".gif"; saveButton.Enabled = true; deleteButton.Enabled = true; } /// /// Someone selected one of the free codes. Grab it from the GUI so we know where to save things when the user is done. /// private void SetInternalCodeFromFree() { FormulaInfo formula = createNewComboBox.SelectedItem as FormulaInfo; if (null != formula) { SetFormulaToSave(formula); } } private void createNewComboBox_SelectedValueChanged(object sender, EventArgs e) { SetInternalCodeFromFree(); } /// /// Select one of the items in the combo box. /// If we can't find a matching item, do nothing. /// /// The ComboBox to change /// See Selectable.Internal private static void SelectMatching(ComboBox control, string internalName) { foreach (object item in control.Items) { if ((item is Selectable selectable) && (selectable.Internal == internalName)) { control.SelectedItem = item; break; } } } /// /// Someone asked to load a saved setting. /// /// /// private void loadComboBox_SelectedValueChanged(object sender, EventArgs e) { if (!(loadComboBox.SelectedItem is FormulaInfo formula)) { return; } // Check if there are any unsaved changes. // If so, ask the user to confirm. if (_suspendSyntaxChecking && isFormulaChanged()) { using (new CenterWinDialog(this)) { var result = MessageBox.Show("Save Formula?", "Confirm", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result == DialogResult.Yes) { SaveButtonClick(); return; } } } descriptionTextBox.Text = formula.Description; sourceDelayTextBox.Text = formula.Source; //CheckSyntaxSoon(); Happens automatically. unitsTextBox.Text = formula.Units; SelectMatching(formatComboBox, formula.Format); SelectMatching(graphicsComboBox, formula.Graphics); SetFormulaToSave(formula); // Just loaded new custom formula. statePictureBox.Visible = false; } /// /// Return true if the user updated any part of the formula: Source, /// Description, Units, Format, or Graphics. /// /// private bool isFormulaChanged() { var formatChanged = false; if (formatComboBox.SelectedItem is Selectable selection) { formatChanged = FormulaToSave.Format != selection.Internal; } var graphicsChanged = false; selection = graphicsComboBox.SelectedItem as Selectable; if (null != selection) { graphicsChanged = FormulaToSave.Graphics != selection.Internal; }return (!GetSource().Equals(FormulaToSave.Source) || !descriptionTextBox.Text.Equals(FormulaToSave.Description) || !unitsTextBox.Text.Equals(FormulaToSave.Units) || formatChanged || graphicsChanged); } private void saveButton_Click(object sender, EventArgs e) { SaveButtonClick(); } /// /// The user hit the save button. /// Save a copy of the screen to our local cache. /// Copy that cache entry to the server, so it can save the change to the database. /// After the server has confirmed the change, tell the local windows to refresh and load the new values. /// private async void SaveButtonClick() { // Copy the value to a local cache then to the server. TradeIdeas.TIProGUI.GuiEnvironment.RecordUseCase("FormulaEditor.SaveButton", ConnectionMaster.SendManager); FormulaToSave.Description = descriptionTextBox.Text; FormulaToSave.Source = GetSource(); FormulaToSave.Units = unitsTextBox.Text; if (formatComboBox.SelectedItem is Selectable selection) { FormulaToSave.Format = selection.Internal; } selection = graphicsComboBox.SelectedItem as Selectable; if (null != selection) { FormulaToSave.Graphics = selection.Internal; } createNewComboBox.Items.Remove(FormulaToSave); var itemIndex = loadComboBox.Items.IndexOf(FormulaToSave); if ( itemIndex == -1) { //A new element is being inserted. //Get the list of elements. var itemsList = loadComboBox.Items.Cast().ToList(); //The new item is added to the list. itemsList.Add(FormulaToSave); //The list is sorted after adding the element. itemsList.Sort(formulasComparer); //The index of the element is obtained, after sorting the list var index = itemsList.IndexOf(FormulaToSave); if (index > -1) { //The element is inserted in the position that corresponds to it and this index is selected as index. //The existing list of items in the combobox could be replaced, but if there are many elements, this is more efficient. loadComboBox.Items.Insert(index, FormulaToSave); loadComboBox.SelectedIndex = index; } } else loadComboBox.SelectedIndex = itemIndex; await TempData.Save(ConnectionMaster.LoginManager, FormulaToSave); // Show saved image. statePictureBox.Image = _saveImage; statePictureBox.Visible = true; foreach (var form in Application.OpenForms) { Form test = form as Form; if (test != null && !test.IsDisposed && form is TradeIdeas.TIProGUI.ICanRefreshNow refreshable) { refreshable.RefreshNow(); } } } private void deleteButton_Click(object sender, EventArgs e) { // Move the formula to the available/empty/unused list on the screen. // Clear the value in the local cache, to look more like the server. FormulaToSave.Clear(); createNewComboBox.Items.Remove(FormulaToSave); loadComboBox.Items.Remove(FormulaToSave); createNewComboBox.Items.Insert(0, FormulaToSave); createNewComboBox.SelectedIndex = 0; // Need to suspend syntax checking after formula deletion to avoid a save message // window when a user selects a formula. _suspendSyntaxChecking = false; // Ask.text the server to do the real work. TempData.Delete(ConnectionMaster.LoginManager, FormulaToSave); TradeIdeas.TIProGUI.GuiEnvironment.RecordUseCase("FormulaEditor.DeleteButton", ConnectionMaster.SendManager); } FormulaEditorHelp HelpWindow; private const string FORM_TYPE = "FORMULA_EDITOR_FORM"; public static readonly WindowIconCache WindowIconCache = new WindowIconCache(FORM_TYPE); WindowIconCache ISaveLayout.WindowIconCache => WindowIconCache; public bool Pinned { get; set; } private void helpButton_Click(object sender, EventArgs e) { if (null == HelpWindow) { HelpWindow = new FormulaEditorHelp(); GuiEnvironment.SetWindowOpeningPosition(HelpWindow, this); } UpdateHelp(); HelpWindow.Show(); HelpWindow.BringToFront(); HelpWindow.WindowState = FormWindowState.Normal; TradeIdeas.TIProGUI.GuiEnvironment.RecordUseCase("FormulaEditor.HelpButton", ConnectionMaster.SendManager); } private void UpdateHelp() { if (null != HelpWindow) { string before = sourceDelayTextBox.Text.Substring(0, sourceDelayTextBox.SelectionStart).Replace(ERROR_MARKER_STRING, ""); string selection = sourceDelayTextBox.SelectedText.Replace(ERROR_MARKER_STRING, ""); string after = sourceDelayTextBox.Text.Substring(sourceDelayTextBox.SelectionStart + sourceDelayTextBox.SelectionLength).Replace(ERROR_MARKER_STRING, ""); HelpWindow.HighlightNamedItems(before, selection, after); } } private void FormulaEditor_FormClosed(object sender, FormClosedEventArgs e) { if (null != HelpWindow) { // HelpWindow.Close() would not be enough. When the user closes that window, the window // sits patiently waiting for the user to hit the help button again, when we call // HelpWindow.Show(). // // This tells the window we are done with it for good, so it should release all of its // resources. // // How to verify what I just said: Make sure the help window is displaying DevTools. // That window will survive a call to HelpWindow.Close(). But calling HelpWindow.Dispose() // will make the DevTools window disappear. HelpWindow.Dispose(); } } public void selectTheFont() { Font = GuiEnvironment.FontSettings; } public void SetSnapToGrid(bool enabled) { formSnapper1.Enabled = enabled; if (GuiEnvironment.RunningWin10 && enabled) { formSnapper1.Win10HeightAdjustment = GuiEnvironment.HEIGHT_INCREASE; formSnapper1.Win10WidthAdjustment = GuiEnvironment.WIDTH_INCREASE; } } public void SaveLayout(XmlNode parent) { } private void sourceDelayTextBox_KeyDown(object sender, KeyEventArgs e) { // This seems to cover most keys, including the arrows, backspace, and delete. // Tab does not get here. Tab automatically moves you to the next control. //System.Diagnostics.Debug.WriteLine("KeyDown: " + e.KeyCode + ", " + e.KeyData + ", " + e.KeyValue); // KeyDown: Delete, Delete, 46 // KeyDown: Back, Back, 8 // Backspace and delete should not try to delete the ERROR_MARKER. // If you have something selected, we allow the normal processing, possibly deleting the marker and other characters. // If you have a normal cursor, and you hit backspace or delete, and you were about to delete the ERROR_MARKER, we // move the cursor one space to the left (for backspace) or right (for delete). // Then we let the system process the request. So you'll typically erase in the same direction, just skipping the // magic ERROR_MARKER. switch (e.KeyCode) { case Keys.Back: if ((sourceDelayTextBox.SelectionLength == 0) && (sourceDelayTextBox.SelectionStart > 0) && (sourceDelayTextBox.Text[sourceDelayTextBox.SelectionStart - 1] == ERROR_MARKER)) { // The cursor sometimes jumps around more than I'd like but it works. // If you keep hitting backspace, and the cursor is just to the right of // the ERROR_MARKER, you'll see the following: // o The cursor moves left past the ERROR_MARKER. // o The character to the left of the ERROR_MARKER disappears. // o The cursor moves back to the right of the ERROR_MARKER. // That repeats until you stop deleting things, or the syntax checker moves the ERROR_MARKER. // Maybe this could be fixed where we get the result of the next syntax check? TODO sourceDelayTextBox.SelectionStart--; } break; case Keys.Delete: if ((sourceDelayTextBox.SelectionLength == 0) && (sourceDelayTextBox.SelectionStart < sourceDelayTextBox.Text.Length) && (sourceDelayTextBox.Text[sourceDelayTextBox.SelectionStart] == ERROR_MARKER)) { // This works perfectly. If you hit the delete button multiple times, the cursor will jump over the // ERROR_MARKER the first time they meet, Then the cursor will stay on the right side of the ERROR_MARKER. sourceDelayTextBox.SelectionStart++; } break; } } private void sourceDelayTextBox_KeyPress(object sender, KeyPressEventArgs e) { // This gets called for "normal" characters like "A" and "0", but not for special // things like the arrow keys. // Backspace appears here ((char)8 == e.KeyChar) but delete does not! //System.Diagnostics.Debug.WriteLine("KeyPress: " + e.KeyChar + (int)e.KeyChar); } private void sourceDelayTextBox_KeyUp(object sender, KeyEventArgs e) { // This happens once if and when you release a key. // If you hold a key down, you'll get the same events as if you just quickly pressed and released. // KeyPress, KeyDown, and PreviewKeyDown will be called repeatedly until you release the key. // However, you only get one KeyUp each time you release the key. // System.Diagnostics.Debug.WriteLine("KeyUp: " + e.KeyCode + ", " + e.KeyData + ", " + e.KeyValue); // Need to call UpdateHelp after the key is released. switch (e.KeyCode) { case Keys.Left: case Keys.Right: if (!internalUpdateInProgress) { UpdateHelp(); } break; } } private void sourceDelayTextBox_PreviewKeyDown(object sender, PreviewKeyDownEventArgs e) { // This seems to get everything. This is where you decide which keys, like tab, // are special and should not be delivered to KeyDown. //System.Diagnostics.Debug.WriteLine("PreviewKeyDown"); } private void sourceDelayTextBox_TextChanged(object sender, EventArgs e) { if (!internalUpdateInProgress) { CheckSyntaxSoon(); UpdateHelp(); } } private void sourceDelayTextBox_MouseClick(object sender, MouseEventArgs e) { if (!internalUpdateInProgress) { UpdateHelp(); } } } }