Exemple #1
0
        private void dgvServers_CellContentDoubleClick(object sender, DataGridViewCellEventArgs e)
        {
            if (e.ColumnIndex < 0 || e.RowIndex < 0)
            {
                return;
            }
            var sd = (dgvServers[e.ColumnIndex, e.RowIndex].OwningRow.Tag as ServerDescription);

            if (sd != null)
            {
                if (sd.RequiresPassword)
                {
                    if (passwordForm == null)
                    {
                        passwordForm = new FlyoutForm(panServerPassword);
                    }
                    passwordForm.Tag = sd;
                    passwordForm.Flyout();
                }
                else
                {
                    lastConnectionReturnToServerBrowser = true;
                    StartClientMode(sd.EndPoint, "");
                }
            }
        }
Exemple #2
0
        /////////////////////////////////////////////////////////////////////
        // CONSTRUCTOR
        /////////////////////////////////////////////////////////////////////

        public EditorForm()
        {
            InitializeComponent();
            if (IsDesignMode)
            {
                return;
            }

            //title
            lblTitle.Font = App.Theme.Titles.Large.Regular;

            //splash panel
            panMenuPage.Dock = DockStyle.Fill;

            //colours
            btnServerNew.Accent       = btnServerExisting.Accent = 2;
            btnServerTemporary.Accent = 3;
            lblAbout.ForeColor        = lblVersion.ForeColor = App.Theme.Background.Light.Colour;

            //about link
            lblAbout.Cursor      = Cursors.Hand;
            lblAbout.Font        = App.Theme.Controls.Normal.Underline;
            lblAbout.MouseEnter += (s, e) => { lblAbout.ForeColor = App.Theme.Foreground.Mid.Colour; };
            lblAbout.MouseLeave += (s, e) => { lblAbout.ForeColor = App.Theme.Background.Light.Colour; };
            lblAbout.Click      += (s, e) => { App.Website.LaunchWebsite(); };

            //version label
            lblVersion.Text = "v" + Marzersoft.Text.REGEX_VERSION_REPEATING_ZEROES.Replace(App.AssemblyVersion.ToString(), "");
            if (lblVersion.Text.IndexOf('.') == -1)
            {
                lblVersion.Text += ".0";
            }

            //client connection/server browser panel
            btnClient.TextAlign = btnServerNew.TextAlign = btnServerExisting.TextAlign
                                                               = btnServerTemporary.TextAlign = ContentAlignment.MiddleCenter;
            btnClientConnect.Image      = App.Images.Resource("next");
            btnClientCancel.Image       = App.Images.Resource("previous");
            btnClientConnect.ImageAlign = btnClientCancel.ImageAlign = ContentAlignment.MiddleCenter;
            tbClientAddress.Font        = tbClientPassword.Font = tbServerPassword.Font
                                                                      = nudClientUpdateInterval.Font = App.Theme.Monospaced.Normal.Regular;
            tbClientAddress.BackColor = tbClientPassword.BackColor = tbServerPassword.BackColor
                                                                         = nudClientUpdateInterval.BackColor = App.Theme.Background.Light.Colour;
            tbClientAddress.ForeColor = tbClientPassword.ForeColor = tbServerPassword.ForeColor
                                                                         = nudClientUpdateInterval.ForeColor = App.Theme.Foreground.BaseColour;
            lblManualEntry.Font       = lblServerBrowser.Font = App.Theme.Controls.Large.Regular;
            panServerBrowserPage.Dock = DockStyle.Fill;
            dgvServers.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
            dgvServers.Columns[1].DefaultCellStyle.Alignment   = DataGridViewContentAlignment.MiddleLeft;
            dgvServers.ShowCellToolTips = true;

            //'connecting' page
            panConnectingPage.Dock       = DockStyle.Fill;
            lblConnectingStatus.Width    = panConnectingPage.ClientSize.Width - 10;
            btnConnectingReconnect.Image = App.Images.Resource("refresh");
            btnConnectingBack.Image      = App.Images.Resource("previous");

            //file dialog filters
            FileFilterFactory filterFactory = new FileFilterFactory();

            filterFactory.Add("Text files", "txt");
            filterFactory.Add("C# files", "cs");
            filterFactory.Add("C/C++ files", "cpp", "h", "hpp", "cxx", "cc", "c", "inl", "inc", "rc", "hxx");
            filterFactory.Add("Log files", "log");
            filterFactory.Add("Javacode files", "java");
            filterFactory.Add("Javascript files", "js");
            filterFactory.Add("Visual Basic files", "vb", "vbs");
            filterFactory.Add("Web files", "htm", "html", "xml", "css", "htaccess", "php");
            filterFactory.Add("XML files", "xml", "xsl", "xslt", "xsd", "dtd");
            filterFactory.Add("PHP scripts", "php");
            filterFactory.Add("SQL scripts", "sql");
            filterFactory.Add("Luascript files", "lua");
            filterFactory.Add("Shell scripts", "bat", "sh", "ps");
            filterFactory.Add("Settings files", "ini", "config", "cfg", "conf", "reg");
            filterFactory.Add("Shader files", "hlsl", "glsl", "fx", "csh", "cshader", "dsh", "dshader",
                              "gsh", "gshader", "hlsli", "hsh", "hshader", "psh", "pshader", "vsh", "vshader");
            filterFactory.Apply(dlgServerCreateNew);
            filterFactory.Apply(dlgServerOpenExisting);

            //form styles
            FormBorderStyle      = FormBorderStyle.None;
            TextFlourishes       = false;
            Text                 = App.Name;
            CustomTitleBar       = true;
            ResizeHandleOverride = true;
            IconOverride         = (b) => { return(null); };
            flatIconImage        = App.Images.Resource("otex_icon_flat", App.Assembly, "OTEX");
            ImageOverride        = (b) => { return(flatIconImage); };

            //settings menu
            settingsForm            = new FlyoutForm(panSettings);
            settingsForm.Accent     = 3;
            settingsButton          = AddCustomTitleBarButton();
            settingsButton.Colour   = App.Theme.GetAccent(3).DarkDark.Colour;
            settingsButton.Image    = App.Images.Resource("cog", App.Assembly, "OTEX");
            settingsButton.OnClick += (b) => { settingsForm.Flyout(PointToScreen(b.Bounds.BottomMiddle())); };

            //logout button
            logoutButton          = AddCustomTitleBarButton();
            logoutButton.Colour   = Color.Red;
            logoutButton.Image    = App.Images.Resource("logout", App.Assembly, "OTEX");
            logoutButton.Visible  = false;
            logoutButton.OnClick += (b) =>
            {
                if (otexServer.Running && otexServer.ClientCount > 1 &&
                    !Logger.WarningQuestion("You are currently running in server mode. "
                                            + "Leaving the session will disconnect the other {0} connected users.\n\nLeave session?", otexServer.ClientCount - 1))
                {
                    return;
                }

                otexClient.Disconnect();
                otexServer.Stop();
            };

            // CREATE OTEX SERVER ///////////////////////////////////////////////

            /*
             * COMP7722: The OTEX Server is a self-contained class. "Host-mode" (i.e.
             * allowing a user to edit a load and edit a document using OTEX Editor without
             * first launching a dedicated server) simply launches a server and a client and
             * directly connects them together internally.
             */
            otexServer = new Server();
            otexServer.OnThreadException += (s, e) =>
            {
                Logger.W("Server: {0}: {1}", e.InnerException.GetType().FullName, e.InnerException.Message);
            };
            otexServer.OnStarted += (s) =>
            {
                Logger.I("Server: started for {0} on port {1}", s.FilePath, s.Port);
            };
            otexServer.OnClientConnected += (s, id) =>
            {
                Logger.I("Server: Client {0} connected.", id);
            };
            otexServer.OnClientDisconnected += (s, id) =>
            {
                Logger.I("Server: Client {0} disconnected.", id);
            };
            otexServer.OnStopped += (s) =>
            {
                Logger.I("Server: stopped.");
            };
            otexServer.OnFileSynchronized += (s) =>
            {
                Logger.I("Server: File synchronized.");
            };

            // CREATE OTEX CLIENT ///////////////////////////////////////////////

            /*
             * COMP7722: Like the server, the OTEX Client is a self-contained class.
             * All of the editor and OT functionality is handled via callbacks.
             */
            otexClient = new Client();
            otexClient.UpdateInterval     = (float)nudClientUpdateInterval.Value;
            otexClient.OnThreadException += (c, e) =>
            {
                Logger.W("Client: {0}: {1}", e.InnerException.GetType().Name, e.InnerException.Message);
            };
            otexClient.OnConnected += (c) =>
            {
                Logger.I("Client: connected to {0}:{1}.", c.ServerAddress, c.ServerPort);
                this.Execute(() =>
                {
                    /*
                     * COMP7722: when the client first connects, initial textbox contents is set to "",
                     * but must be done so while operation generation is disabled so
                     * TextChanging/TextChanged don't do diffs and push operations.
                     */
                    disableOperationGeneration = true;
                    tbEditor.Text = "";
                    disableOperationGeneration = false;

                    string ext = Path.GetExtension(c.ServerFilePath).ToLower();
                    if (ext.Length > 0 && (ext = ext.Substring(1)).Length > 0)
                    {
                        switch (ext)
                        {
                        case "cs": tbEditor.Language = Language.CSharp; break;

                        case "htm": tbEditor.Language = Language.HTML; break;

                        case "html": tbEditor.Language = Language.HTML; break;

                        case "js": tbEditor.Language = Language.JS; break;

                        case "lua": tbEditor.Language = Language.Lua; break;

                        case "php": tbEditor.Language = Language.PHP; break;

                        case "sql": tbEditor.Language = Language.SQL; break;

                        case "vb": tbEditor.Language = Language.VB; break;

                        case "vbs": tbEditor.Language = Language.VB; break;

                        case "xml": tbEditor.Language = Language.XML; break;

                        default: tbEditor.Language = Language.Custom; break;
                        }
                    }
                    else
                    {
                        tbEditor.Language = Language.Custom;
                    }

                    logoutButton.Visible = true;
                    EditorPage           = true;
                    tbEditor.Focus();
                }, false);
            };
            otexClient.OnRemoteOperations += (c, operations) =>
            {
                /*
                 * COMP7722: this event handler is fired when an OTEX Client receives remote
                 * operations from the server (they're already transformed internally, and just need
                 * to be applied).
                 *
                 * The "Execute" function is an extension method that ensures whatever delegate function
                 * is passed in will always be run on the main UI thread of a windows forms application,
                 * so this ensures the user is prevented from typing while the remote operations are
                 * being applied (virtually instantaneous).
                 */
                this.Execute(() =>
                {
                    disableOperationGeneration = true;

                    foreach (var operation in operations)
                    {
                        if (operation.IsInsertion)
                        {
                            tbEditor.InsertTextAndRestoreSelection(
                                new Range(tbEditor, tbEditor.PositionToPlace(operation.Offset),
                                          tbEditor.PositionToPlace(operation.Offset)),
                                operation.Text, null);
                        }
                        else if (operation.IsDeletion)
                        {
                            tbEditor.InsertTextAndRestoreSelection(
                                new Range(tbEditor, tbEditor.PositionToPlace(operation.Offset),
                                          tbEditor.PositionToPlace(operation.Offset + operation.Length)),
                                "", null);
                        }
                    }

                    disableOperationGeneration = false;
                }, false);
            };
            otexClient.OnMetadataUpdated += (c, id, md) =>
            {
                lock (remoteClients)
                {
                    remoteClients[id] = md.Deserialize <EditorClient>();
                }
                this.Execute(() => { tbEditor.Refresh(); });
            };
            otexClient.OnDisconnected += (c, serverSide) =>
            {
                Logger.I("Client: disconnected{0}.", serverSide ? " (connection closed by server)" : "");
                lock (remoteClients)
                {
                    remoteClients.Clear();
                }
                localClient.SelectionStart = 0;
                localClient.SelectionEnd   = 0;
                if (!closing)
                {
                    this.Execute(() =>
                    {
                        //non-host
                        if (serverSide)
                        {
                            if (lastConnectionEndpoint != null)
                            {
                                ConnectionLostPage = true;
                            }
                            else
                            {
                                MainMenuPage = true;
                            }
                        }
                        else
                        {
                            MainMenuPage = true;
                        }

                        Text = App.Name;
                        logoutButton.Visible = false;
                    });
                }
            };

            // CREATE OTEX SERVER LISTENER //////////////////////////////////////

            /*
             * COMP7722: I've given OTEX Servers the ability to advertise their existence to the
             * local network over UDP, so the ServerListener is a simple UDP listener. When new servers
             * are identified, or known servers change in some way, events are fired.
             */
            try
            {
                otexServerListener = new ServerListener();
                otexServerListener.OnThreadException += (sl, e) =>
                {
                    Logger.W("ServerListener: {0}: {1}", e.InnerException.GetType().FullName, e.InnerException.Message);
                };
                otexServerListener.OnServerAdded += (sl, s) =>
                {
                    Logger.I("ServerListener: new server {0}: {1}", s.ID, s.EndPoint);
                    this.Execute(() =>
                    {
                        var row = dgvServers.AddRow(s.Name.Length > 0 ? s.Name : "OTEX Server", s.TemporaryDocument ? "Yes" : "", s.EndPoint.Address,
                                                    s.EndPoint.Port, s.RequiresPassword ? "Yes" : "", string.Format("{0} / {1}", s.ClientCount, s.MaxClients), 0);
                        row.Tag = s;
                        s.Tag   = row;
                    });

                    s.OnUpdated += (sd) =>
                    {
                        Logger.I("ServerDescription: {0} updated.", sd.ID);
                        this.Execute(() =>
                        {
                            (s.Tag as DataGridViewRow).Update(s.Name.Length > 0 ? s.Name : "OTEX Server", s.TemporaryDocument ? "Yes" : "", s.EndPoint.Address,
                                                              s.EndPoint.Port, s.RequiresPassword ? "Yes" : "", string.Format("{0} / {1}", s.ClientCount, s.MaxClients), 0);
                        });
                    };

                    s.OnInactive += (sd) =>
                    {
                        Logger.I("ServerDescription: {0} inactive.", sd.ID);
                        this.Execute(() =>
                        {
                            var row = (s.Tag as DataGridViewRow);
                            row.DataGridView.Rows.Remove(row);
                            row.Tag = null;
                            s.Tag   = null;
                        });
                    };
                };
            }
            catch (Exception exc)
            {
                Logger.ErrorMessage("An error occurred while creating the server listener:\n\n{0}"
                                    + "\n\nYou can still use OTEX Editor, but the \"Public Documents\" list will be empty.",
                                    exc.Message);
                return;
            }

            // CREATE TEXT EDITOR ////////////////////////////////////////////////
            tbEditor                    = new FastColoredTextBox();
            tbEditor.Parent             = this;
            tbEditor.Dock               = DockStyle.Fill;
            tbEditor.BackBrush          = App.Theme.Background.Mid.Brush;
            tbEditor.IndentBackColor    = App.Theme.Background.Dark.Colour;
            tbEditor.ServiceLinesColor  = App.Theme.Background.Light.Colour;
            tbEditor.Font               = new Font(App.Theme.Monospaced.Normal.Regular.FontFamily, 11.0f);
            tbEditor.WordWrap           = true;
            tbEditor.WordWrapAutoIndent = true;
            tbEditor.WordWrapMode       = WordWrapMode.WordWrapControlWidth;
            tbEditor.TabLength          = 4;
            tbEditor.LineInterval       = 2;
            tbEditor.HotkeysMapping.Remove(Keys.Control | Keys.H);                     //remove default "replace" (CTRL + H, wtf?)
            tbEditor.HotkeysMapping[Keys.Control | Keys.R] = FCTBAction.ReplaceDialog; // CTRL + R for replace
            tbEditor.HotkeysMapping[Keys.Control | Keys.Y] = FCTBAction.Undo;          // CTRL + Y for undo

            /*
             * COMP7722: In this editor example, Operations are not generated by directly
             * intercepting key press events and the like, since they do not take special
             * circumstances like Undo, Redo, and text dragging with the mouse into account. Instead,
             * I've used a Least-Common-Substring-based diff generator (in this case, a package
             * called DiffPlex: https://www.nuget.org/packages/DiffPlex/) to compare text pre-
             * and post-change, and create the operations based on the calculated diffs.
             *
             * This does of course cause a slight overhead; in testing with large documents
             * (3.5mb of plain text, which is a lot!), diff calculation took ~100ms. Documents that
             * were more realistically-sized took ~3ms (on the same machine), which is imperceptible.
             *
             * I've also implemented some basic awareness painting, so the current position, line
             * and selection of other editors will be rendered (see PaintLine).
             */
            tbEditor.TextChanging += (sender, args) =>
            {
                if (disableOperationGeneration || !otexClient.Connected)
                {
                    return;
                }

                //cache previous version of the text
                previousText = tbEditor.Text;
            };
            tbEditor.TextChanged += (sender, args) =>
            {
                if (disableOperationGeneration || !otexClient.Connected)
                {
                    return;
                }

                //do diff on two versions of text
                var currentText = tbEditor.Text;
                var diffs       = differ.CreateCharacterDiffs(previousText, currentText, false, false);

                //convert changes into operations
                int position = 0;
                foreach (var diff in diffs.DiffBlocks)
                {
                    //skip unchanged characters
                    position = Math.Min(diff.InsertStartB, currentText.Length);

                    //process a deletion
                    if (diff.DeleteCountA > 0)
                    {
                        otexClient.Delete((uint)position, (uint)diff.DeleteCountA);
                    }

                    //process an insertion
                    if (position < (diff.InsertStartB + diff.InsertCountB))
                    {
                        otexClient.Insert((uint)position, currentText.Substring(position, diff.InsertCountB));
                    }
                }
            };
            tbEditor.SelectionChanged += (s, e) =>
            {
                if (!otexClient.Connected)
                {
                    return;
                }
                var sel = tbEditor.Selection;
                localClient.SelectionStart = tbEditor.PlaceToPosition(sel.Start);
                localClient.SelectionEnd   = tbEditor.PlaceToPosition(sel.End);
                PushUpdatedMetadata();
            };
            tbEditor.PaintLine += (s, e) =>
            {
                if (!otexClient.Connected || remoteClients.Count == 0)
                {
                    return;
                }

                Range lineRange = new Range(tbEditor, e.LineIndex);
                lock (remoteClients)
                {
                    var len = tbEditor.TextLength;
                    foreach (var kvp in remoteClients)
                    {
                        //check range
                        var selStart = tbEditor.PositionToPlace(kvp.Value.SelectionStart.Clamp(0, len));
                        var selEnd   = tbEditor.PositionToPlace(kvp.Value.SelectionEnd.Clamp(0, len));
                        var selRange = new Range(tbEditor, selStart, selEnd);
                        var range    = lineRange.GetIntersectionWith(selRange);
                        if (range.Length == 0 && !lineRange.Contains(selStart))
                        {
                            continue;
                        }

                        var ptStart = tbEditor.PlaceToPoint(range.Start);
                        var ptEnd   = tbEditor.PlaceToPoint(range.End);
                        var caret   = lineRange.Contains(selStart);
                        int colour  = kvp.Value.Colour & 0x00FFFFFF;

                        //draw "current line" fill
                        if (caret && selRange.Length == 0)
                        {
                            using (SolidBrush b = new SolidBrush((colour | 0x09000000).ToColour()))
                                e.Graphics.FillRectangle(b, e.LineRect);
                        }
                        //draw highlight
                        if (range.Length > 0)
                        {
                            using (SolidBrush b = new SolidBrush((colour | 0x20000000).ToColour()))
                                e.Graphics.FillRectangle(b, new Rectangle(ptStart.X, e.LineRect.Y,
                                                                          ptEnd.X - ptStart.X, e.LineRect.Height));
                        }
                        //draw caret
                        if (caret)
                        {
                            ptStart = tbEditor.PlaceToPoint(selStart);
                            using (Pen p = new Pen((colour | 0xBB000000).ToColour()))
                            {
                                p.Width = 2;
                                e.Graphics.DrawLine(p, ptEnd.X, e.LineRect.Top,
                                                    ptEnd.X, e.LineRect.Bottom);
                            }
                        }
                    }
                }
            };

            // CLIENT COLOURS ////////////////////////////////////////////////////
            cbClientColour.RegenerateItems(
                false, //darks
                true,  //mids
                false, //lights
                false, //transparents
                false, //monochromatics
                0.15f, //similarity threshold
                new Color[] { Color.Blue, Color.MediumBlue, Color.Red, Color.Fuchsia, Color.Magenta } //exlude (colours that contrast poorly with the app theme)
                );
            var cols = cbClientColour.Items.OfType <Color>().ToList();
            var col  = otexClient.ID.ToColour(cols.ToArray());

            ClientColour = col;
            cbClientColour.SelectedIndex = cols.IndexOf(col);
        }