using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; using CefSharp.WinForms; using CefSharp; using System.Web.Script.Serialization; // TODO enable search. // If the user hits ctrl-f, or something like that, a normal chrome search window should appear. // https://trade-ideas.atlassian.net/browse/PRO-101 namespace TradeIdeas.TIProGUI.FormulaEditor { public partial class FormulaEditorHelp : Form, IFont, ISnapToGrid { public FormulaEditorHelp() { InitializeComponent(); Icon = GuiEnvironment.Icon; StartPosition = FormStartPosition.Manual; SetSnapToGrid(GuiEnvironment.SnapToGrid); InitializeChromium(); } private ChromiumWebBrowser WebBrowser; static private readonly string URL = "https://www.trade-ideas.com/FormulaEditorAPI/Help/FormulaEditorHelp.html"; //static private readonly string DEBUG_TEST_URL = "http://127.0.0.1:5500/FormulaEditorHelp.html"; // The source code for these files is in ti-git:user-defined/formula-editor-help // Install that locally and run it in VS code if you want to use the DEBUG_TEST_URL. private void InitializeChromium() { try { //Debug.WriteLine("InitializeChromium called opening site: " + _url); WebBrowser = BrowserManager.RequestChromiumWebBrowser(URL); WebBrowser.Dock = DockStyle.Fill; WebBrowser.Location = new System.Drawing.Point(40, 20); WebBrowser.Size = new System.Drawing.Size(400, 200); WebBrowser.IsBrowserInitializedChanged += OnIsBrowserInitializedChanged; WebBrowser.LoadingStateChanged += OnLoadingStateChanged; WebBrowser.RequestHandler = new BrowserRequestHandler(WebBrowser); WebBrowser.Load(URL); Controls.Add(WebBrowser); //_webBrowser.PreviewKeyDown += new PreviewKeyDownEventHandler(_webBrowser_PreviewKeyDown); //_webBrowser.KeyboardHandler = new MyKeyboardHandler(); //_webBrowser.LoadingStateChanged += OnLoadingStateChanged; // In certain scenarios the ChromiumWebBrowser fails to load the Url passed // into the ChromiumWebBrowser constructor (https://github.com/cefsharp/CefSharp/issues/2234#issuecomment-360332625) //_webBrowser.IsBrowserInitializedChanged += OnIsBrowserInitializedChanged; } catch (Exception e) { string debugView = e.ToString(); } } private string ToShowBefore = null; private string ToShowSelection = null; private string ToShowAfter = null; private bool PageHasLoaded = false; private void OnLoadingStateChanged(object sender, LoadingStateChangedEventArgs e) { // https://cefsharp.github.io/api/91.1.x/html/E_CefSharp_IWebBrowser_LoadingStateChanged.htm // LoadingStateChanged gets called twice. We only care when loading is complete. if (!e.IsLoading) { // LoadingStateChanged will be called in the wrong thread. BeginInvoke((Action)(() => { this.Text = "Formula Editor Help"; PageHasLoaded = true; selectTheFont(); UpdateHelpNow(); })); } } private static JavaScriptSerializer JSON = new JavaScriptSerializer(); private void UpdateHelpNow() { if (PageHasLoaded && (null != ToShowBefore)) { try { WebBrowser.ExecuteScriptAsync("highlightNamedItems(" + JSON.Serialize(ToShowBefore) + ", " + JSON.Serialize(ToShowSelection) + ", " + JSON.Serialize(ToShowAfter) + ")"); ToShowBefore = ToShowSelection = ToShowAfter = null; } catch (Exception e) { // We get here when the server is down. Not sure what to do. // Suggestion / TODO: build a simple client side 404 document with a retry button and an // explanation. string debugView = e.ToString(); } } } private void FormulaEditorHelp_FormClosing(object sender, FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing) { e.Cancel = true; Hide(); } BrowserManager.RequestBrowserDisposal(WebBrowser); } private void OnIsBrowserInitializedChanged(object sender, EventArgs e) { WebBrowser.Load(URL); // If you call ShowDevTools() in the constructor you will get an exception. // This is a good place to call it. // if (GuiEnvironment.ShowDevTools) WebBrowser.ShowDevTools(); } internal void HighlightNamedItems(string before, string selection, string after) { ToShowBefore = before; ToShowSelection = selection; ToShowAfter = after; UpdateHelpNow(); } public void selectTheFont() { try { WebBrowser.ExecuteScriptAsync("selectTheFont(" + JSON.Serialize(GuiEnvironment.FontSettings.Name) + ", " + JSON.Serialize(GuiEnvironment.FontSettings.Size + "pt") + ")"); } catch { } } public void SetSnapToGrid(bool enabled) { formSnapper1.Enabled = enabled; if (GuiEnvironment.RunningWin10 && enabled) { formSnapper1.Win10HeightAdjustment = GuiEnvironment.HEIGHT_INCREASE; formSnapper1.Win10WidthAdjustment = GuiEnvironment.WIDTH_INCREASE; } } /// /// This allows the C# code to take over when the user clicks a hyperlink in the embedded browser. /// /// This was cloned from WelcomeScreen.cs, which references the following: /// https://stackoverflow.com/questions/42519668/how-to-get-resources-loaded-by-webpage-with-cefsharp /// private class BrowserRequestHandler : IRequestHandler { public BrowserRequestHandler(ChromiumWebBrowser webBrowser) { WebBrowser = webBrowser; } public IFrame Frame { get; private set; } public IRequest Request { get; private set; } public IBrowser Browser { get; private set; } private readonly ChromiumWebBrowser WebBrowser; public bool CanGetCookies(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request) { return false; } public bool CanSetCookie(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, Cookie cookie) { return false; } public bool GetAuthCredentials(IWebBrowser chromiumWebBrowser, IBrowser browser, string originUrl, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) { return false; } public IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator, ref bool disableDefaultHandling) { return null; } public bool OnBeforeBrowse(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool userGesture, bool isRedirect) { Request = request; string url = Request.Url; if (Request.TransitionType != TransitionType.LinkClicked) { // We are only changing the behavior when someone clicks on a link. // Let the embedded browser handle this request itself. return false; } else { // The user clicked on a link. Something like a filter icon, which links to the help for that filter. // We open a new window for that request. This window cannot change. It is running a JavaScript // application that is talking with the C# main program. Uri uri = new Uri(url); try { switch (uri.Scheme) { case "http": case "https": { // Stack overflow says that this next line is *the* way to open a URL in the // default browser. I don't trust it. Seems like a potential security // flaw to read a string from the network then run it from the shell. This // way I'm at least verifying that it is an http request and will start a // browser. The Uri object will also verify and sanitize the URL. I'm // tempted to go one step further and only connect to trade-ideas.com. System.Diagnostics.Process.Start(uri.ToString()); break; } case "showdevtools": { WebBrowser.ShowDevTools(); break; } } } catch { } // Tell the browser to cancel the navigation. return true; } } public CefReturnValue OnBeforeResourceLoad(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback) { callback.Dispose(); return CefReturnValue.Continue; } public bool OnCertificateError(IWebBrowser browserControl, IBrowser browser, CefErrorCode errorCode, string requestUrl, ISslInfo sslInfo, IRequestCallback callback) { callback.Dispose(); return false; } public void OnDocumentAvailableInMainFrame(IWebBrowser chromiumWebBrowser, IBrowser browser) { } public bool OnOpenUrlFromTab(IWebBrowser browserControl, IBrowser browser, IFrame frame, string targetUrl, WindowOpenDisposition targetDisposition, bool userGesture) { return false; } public void OnPluginCrashed(IWebBrowser browserControl, IBrowser browser, string pluginPath) { } public bool OnProtocolExecution(IWebBrowser browserControl, IBrowser browser, string url) { return false; } public bool OnQuotaRequest(IWebBrowser browserControl, IBrowser browser, string originUrl, long newSize, IRequestCallback callback) { callback.Dispose(); return false; } public void OnRenderProcessTerminated(IWebBrowser chromiumWebBrowser, IBrowser browser, CefTerminationStatus status, int errorCode, string errorMessage) { } public void OnRenderViewReady(IWebBrowser browserControl, IBrowser browser) { } public void OnResourceLoadComplete(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response, UrlRequestStatus status, long receivedContentLength) { } public void OnResourceRedirect(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response, ref string newUrl) { } public bool OnResourceResponse(IWebBrowser browserControl, IBrowser browser, IFrame frame, IRequest request, IResponse response) { return false; } public bool OnSelectClientCertificate(IWebBrowser browserControl, IBrowser browser, bool isProxy, string host, int port, System.Security.Cryptography.X509Certificates.X509Certificate2Collection certificates, ISelectClientCertificateCallback callback) { callback.Dispose(); return false; } } /* Basically just one type of message always sent from C# to JavaScript. * We list out all of the keywords, and some data about where the cursor is, both * together. We use both of those to update the HTML. * * Possible improvement: * We return a list of suggestions for the word under the cursor. So we can * always display a short version of what you see in the Code Completion * section of the help page, but this will be on the main window. * * We display the suggested completions and the definite keywords in HTML. * We have just one API call because both types of data come from the same * event (the contents of the script field changed) and both types of data * will get displayed in a similar way in the HTML. * * If the cursor is at the end of a word, show all valid words that start with the given word. * * When we perfectly match a word make sure we display that word first. (There might * not be room for all the words.) And display a short definition of that word. * This suggestion was aimed at the short version that we might embed directly * into the FormulaEditor window. * * When the cursor is in the middle of a word, do two searches. First look for any * valid words that could be made by adding letters at the position of the cursor. * Then, after that list, show any words that could come from adding letters at the * cursor and at the end. In fact, this is the only rule. Cursor at the beginning * or end of the word uses this same exact rule. * * If there is a selection, and all of the the characters are valid for the middle of a word * (allow [ at the beginning and ] at the end?) then treat the entire selection just like * a cursor. That makes sense. When someone types it will delete the selection then * insert just like a normal cursor. * * Do we really care about the characters that are selected? Maybe skip that part of the * rule. All of that will be deleted, and someone might be replacing any random * mistake. */ } }