#region ================== Copyright (c) 2007 Pascal vd Heiden /* * Copyright (c) 2007 Pascal vd Heiden, www.codeimp.com * This program is released under GNU General Public License * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * */ #endregion #region ================== Namespaces using System; using System.Globalization; using System.IO; using System.Collections.Generic; using System.Drawing; using System.Windows.Forms; using CodeImp.DoomBuilder.Data; using CodeImp.DoomBuilder.Windows; using CodeImp.DoomBuilder.Config; using CodeImp.DoomBuilder.Compilers; using CodeImp.DoomBuilder.ZDoom.Scripting; using System.Text; using ScintillaNET; #endregion namespace CodeImp.DoomBuilder.Controls { public abstract class ScriptDocumentTab : TabPage { #region ================== Constants private const int EDITOR_BORDER_TOP = 4; private const int EDITOR_BORDER_BOTTOM = 4; private const int EDITOR_BORDER_LEFT = 4; private const int EDITOR_BORDER_RIGHT = 4; const int SC_FOLDLEVELBASE = 1024; #endregion #region ================== Variables // The script edit control protected readonly ScriptEditorControl editor; private string title; //mxd protected ScriptDocumentTabType tabtype; //mxd // Derived classes must set this! protected ScriptConfiguration config; // The panel we're on protected readonly ScriptEditorPanel panel; #endregion #region ================== Properties public virtual bool ExplicitSave { get { return true; } } public virtual bool IsSaveAsRequired { get { return true; } } public virtual bool IsClosable { get { return true; } } public virtual bool IsReconfigurable { get { return true; } } public virtual bool IsReadOnly { get { return false; } } //mxd public virtual string Filename { get { return null; } } public ScriptEditorPanel Panel { get { return panel; } } public ScriptEditorControl Editor { get { return editor; } } //mxd public string Title { get { return title; } } //mxd public bool IsChanged { get { return editor.IsChanged; } } public int SelectionStart { get { return editor.SelectionStart; } set { editor.SelectionStart = value; } } public int SelectionEnd { get { return editor.SelectionEnd; } set { editor.SelectionEnd = value; } } public bool ShowWhitespace { get { return editor.ShowWhitespace; } set { editor.ShowWhitespace = value; } } //mxd public bool WrapLongLines { get { return editor.WrapLongLines; } set { editor.WrapLongLines = value; } } //mxd public string SelectedText { get { return editor.SelectedText; } } //mxd public ScriptConfiguration Config { get { return config; } } #endregion #region ================== Events (mxd) public new event EventHandler OnTextChanged; //mxd #endregion #region ================== Constructor // Constructor protected ScriptDocumentTab(ScriptEditorPanel panel, ScriptConfiguration config) { // Keep panel and config this.panel = panel; this.config = config; //mxd this.BackColor = SystemColors.ControlLightLight; //mxd // Make the script control editor = new ScriptEditorControl(); editor.Location = new Point(EDITOR_BORDER_LEFT, EDITOR_BORDER_TOP); editor.Size = new Size(this.ClientSize.Width - EDITOR_BORDER_LEFT - EDITOR_BORDER_RIGHT, this.ClientSize.Height - EDITOR_BORDER_TOP - EDITOR_BORDER_BOTTOM); editor.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; editor.Name = "editor"; editor.TabStop = true; editor.TabIndex = 1; this.Controls.Add(editor); // Bind events editor.OnExplicitSaveTab += panel.ExplicitSaveCurrentTab; editor.OnOpenFindAndReplace += panel.OpenFindAndReplace; editor.OnFindNext += panel.FindNext; editor.OnFindPrevious += panel.FindPrevious; //mxd editor.OnFindNextWrapAround += panel.FindNextWrapAround; editor.OnFindPreviousWrapAround += panel.FindPreviousWrapAround; editor.OnTextChanged += editor_TextChanged; //mxd editor.OnGoToLine += panel.GoToLine; //mxd editor.OnCompileScript -= Compile; //mxd //mxd. Bind functionbar events editor.OnFunctionBarDropDown += functionbar_DropDown; //mxd. Setup styles editor.SetupStyles(config); //mxd. Apply panel settings editor.ShowWhitespace = panel.ShowWhitespace; editor.WrapLongLines = panel.WrapLongLines; //mxd. Set icon this.ImageIndex = panel.Icons.GetScriptIcon(config.ScriptType); } // Disposer protected override void Dispose(bool disposing) { // Remove events editor.OnExplicitSaveTab -= panel.ExplicitSaveCurrentTab; editor.OnOpenFindAndReplace -= panel.OpenFindAndReplace; editor.OnFindNext -= panel.FindNext; editor.OnFindPrevious -= panel.FindPrevious; //mxd editor.OnTextChanged -= editor_TextChanged; //mxd editor.OnGoToLine -= panel.GoToLine; //mxd editor.OnCompileScript -= Compile; //mxd base.Dispose(disposing); } #endregion #region ================== Methods // This launches keyword help website public bool LaunchKeywordHelp() { return editor.LaunchKeywordHelp(); } // This refreshes the style settings public virtual void RefreshSettings() { editor.RefreshStyle(); } // This moves the caret to the given line public virtual void MoveToLine(int linenumber) { editor.MoveToLine(linenumber); } // This clears all marks public virtual void ClearMarks() { editor.ClearMarks(); } // This creates error marks for errors that apply to this file public virtual void MarkScriptErrors(IEnumerable errors) { // Clear all marks ClearMarks(); // Go for all errors that apply to this script foreach(CompilerError e in errors) { if(VerifyErrorForScript(e)) { // Add a mark on the line where this error occurred editor.AddMark(e.linenumber); } } } // This verifies if the specified error applies to this script public virtual bool VerifyErrorForScript(CompilerError e) { return false; } // This compiles the script public virtual void Compile() { } // [ZZ] this removes trailing whitespace from every line protected void RemoveTrailingWhitespace() { // after changing the contents, selection should stay on the same line, and just move to the end of that line if it was on the trailing space. int vscroll = editor.Scintilla.FirstVisibleLine; int selectionStart = editor.SelectionStart; int selectionEnd = editor.SelectionEnd; // remember folding int[] folded = new int[editor.Scintilla.Lines.Count]; bool[] expanded = new bool[editor.Scintilla.Lines.Count]; for (int i = 0; i < folded.Length; i++) { Line l = editor.Scintilla.Lines[i]; folded[i] = l.FoldLevel; expanded[i] = l.Expanded; } int offset = 0; string text = editor.Text; string[] atext = text.Split(new char[] { '\n' }); for (int i = 0; i < atext.Length; i++) { string oldtext = atext[i]; string newtext = oldtext.TrimEnd(); int lendiff = oldtext.Length - newtext.Length; bool selectioninline1 = selectionStart >= offset && selectionStart < offset + oldtext.Length; bool selectioninline2 = selectionEnd >= offset && selectionEnd < offset + oldtext.Length; if (selectioninline1 && selectionStart > offset + newtext.Length) selectionStart = offset + newtext.Length; if (selectioninline2 && selectionEnd > offset + newtext.Length) selectionEnd = offset + newtext.Length; atext[i] = newtext; offset += newtext.Length + 1; // include \n if (selectionStart > offset) selectionStart -= lendiff; if (selectionEnd > offset) selectionEnd -= lendiff; } editor.Text = string.Join("\n", atext); editor.SelectionStart = selectionStart; editor.SelectionEnd = selectionEnd; editor.Scintilla.FirstVisibleLine = vscroll; // Restore folding // Looks like that has to be done from back to front, as the end result will otherwise have too much unfolded for // whatever reason. See https://github.com/jewalky/UltimateDoomBuilder/issues/392 for (int i = folded.Length - 1; i >= 0; i--) { Line l = editor.Scintilla.Lines[i]; l.FoldLevel = folded[i]; if (!expanded[i]) l.FoldLine(FoldAction.Contract); } } // This saves the document (used for both explicit and implicit) // Return true when successfully saved public virtual bool Save() { return false; } // This saves the document to a new file // Return true when successfully saved public virtual bool SaveAs(string filename) { return false; } // This changes the script configurations public virtual void ChangeScriptConfig(ScriptConfiguration newconfig) { config = newconfig; //mxd editor.SetupStyles(newconfig); //mxd List errors = UpdateNavigator(); //mxd if(panel.ActiveTab == this) panel.ShowErrors(errors, true); //mxd this.ImageIndex = panel.Icons.GetScriptIcon(config.ScriptType); //mxd. Set icon } // Call this to set the tab title protected void SetTitle(string title) { this.title = title; //mxd base.Text = (editor.IsChanged ? "\u25CF " + title : title); //mxd } //mxd protected void UpdateTitle() { SetTitle(title); } // Perform undo public void Undo() { editor.Undo(); } // Perform redo public void Redo() { editor.Redo(); } // Perform cut public void Cut() { editor.Cut(); } // Perform copy public void Copy() { editor.Copy(); } // Perform paste public void Paste() { editor.Paste(); } // Find next result (mxd) public bool FindNext(FindReplaceOptions options) { return FindNext(options, false); } // Find next result public bool FindNext(FindReplaceOptions options, bool useselectionstart) { return editor.FindNext(options, useselectionstart); } // Find previous result (mxd) public bool FindPrevious(FindReplaceOptions options) { return editor.FindPrevious(options); } // This replaces the selection with the given text public void ReplaceSelection(string replacement) { editor.ReplaceSelection(replacement); } // Save one undo checkpoint when making many changes in the editor. public void UndoTransaction(Action callback) { editor.UndoTransaction(callback); } //mxd internal List UpdateNavigator() { return editor.UpdateNavigator(this); } //mxd. TODO: remove this internal ScriptType VerifyScriptType() { ScriptTypeParserSE parser = new ScriptTypeParserSE(); TextResourceData data = new TextResourceData(new MemoryStream(editor.GetText()), new DataLocation(), config.Description); if(parser.Parse(data, false)) { if(parser.ScriptType != ScriptType.UNKNOWN && config.ScriptType != parser.ScriptType) return parser.ScriptType; } if(parser.HasError) panel.ShowErrors(new List { new CompilerError(parser.ErrorDescription, parser.ErrorSource, parser.ErrorLine) }, true); return ScriptType.UNKNOWN; } //mxd internal void InsertSnippet(string name) { string[] lines = config.GetSnippet(name); if(lines != null) editor.InsertSnippet(lines); } //mxd internal void IndentSelection(bool indent) { editor.IndentSelection(indent); } //mxd internal virtual void SetViewSettings(ScriptDocumentSettings settings) { if(settings.TabType != tabtype) throw new Exception("TabType mismatch!"); // Text must be exactly the same long hash = MurmurHash2.Hash(Text); bool applyfolding = General.Settings.ScriptShowFolding && (editor.Scintilla.Lexer == Lexer.Cpp || (int)editor.Scintilla.Lexer == 35); // 35 - custom CPP case insensitive style lexer if(hash == settings.Hash) { // Restore fold levels if(applyfolding) ApplyFolding(settings.FoldLevels ?? GetFoldLevels()); // Restore scroll editor.Scintilla.FirstVisibleLine = settings.FirstVisibleLine; // Restore caret position editor.Scintilla.SetEmptySelection(settings.CaretPosition); } // Do what Visual Studio does: fold all #regions else if(applyfolding) { ApplyFolding(GetFoldLevels()); } } internal void SetDefaultViewSettings() { if(General.Settings.ScriptShowFolding && (editor.Scintilla.Lexer == Lexer.Cpp || (int)editor.Scintilla.Lexer == 35)) // 35 - custom CPP case insensitive style lexer ApplyFolding(GetFoldLevels()); } private void ApplyFolding(Dictionary> foldlevelsarr) { // We'll want to fold deeper levels first... int[] fl = new int[foldlevelsarr.Keys.Count]; foldlevelsarr.Keys.CopyTo(fl, 0); List foldlevels = new List(fl); foldlevels.Sort((a, b) => -1 * a.CompareTo(b)); // Sort in descending order foreach(int level in foldlevels) { foreach(int line in foldlevelsarr[level]) editor.Scintilla.Lines[line].FoldLine(FoldAction.Contract); } } private Dictionary> GetFoldLevels() { Dictionary> foldlevels = new Dictionary>(); int foldlevel = SC_FOLDLEVELBASE; for(int i = 0; i < editor.Scintilla.Lines.Count; i++) { string line = editor.Scintilla.Lines[i].Text.TrimStart(); if(line.StartsWith("#region", true, CultureInfo.InvariantCulture)) { foldlevel++; if(!foldlevels.ContainsKey(foldlevel)) foldlevels.Add(foldlevel, new HashSet()); foldlevels[foldlevel].Add(i); } else if(line.StartsWith("#endregion", true, CultureInfo.InvariantCulture) && foldlevel > SC_FOLDLEVELBASE) { foldlevel--; } } return foldlevels; } //mxd internal virtual ScriptDocumentSettings GetViewSettings() { Dictionary> foldlevels = new Dictionary>(); for(int i = 0; i < editor.Scintilla.Lines.Count; i++) { if(!editor.Scintilla.Lines[i].Expanded) { if(!foldlevels.ContainsKey(editor.Scintilla.Lines[i].FoldLevel)) foldlevels.Add(editor.Scintilla.Lines[i].FoldLevel, new HashSet()); foldlevels[editor.Scintilla.Lines[i].FoldLevel].Add(i); } } return new ScriptDocumentSettings { Filename = this.Filename, FoldLevels = foldlevels, ScriptType = config.ScriptType, CaretPosition = editor.Scintilla.SelectionStart, TabType = tabtype, IsActiveTab = (this.Panel.ActiveTab == this), FirstVisibleLine = editor.Scintilla.FirstVisibleLine, Hash = MurmurHash2.Hash(Text), }; } #endregion #region ================== Events // Mouse released protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); // Focus to the editor! editor.Focus(); editor.GrabFocus(); } // Receiving focus? protected override void OnGotFocus(EventArgs e) { base.OnGotFocus(e); // Focus to the editor! editor.Focus(); editor.GrabFocus(); } //mxd private void functionbar_DropDown(object sender, EventArgs e) { if(editor.IsChanged) panel.ShowErrors(UpdateNavigator(), true); } //mxd private void editor_TextChanged(object sender, EventArgs eventArgs) { UpdateTitle(); if(OnTextChanged != null) OnTextChanged(this, EventArgs.Empty); } #endregion } }