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();
}
}
}
}