From e2374102ee38ed53b5bd7f8519ddf0a07a5d1b90 Mon Sep 17 00:00:00 2001 From: biwa <6475593+biwa@users.noreply.github.com> Date: Mon, 3 Jan 2022 14:33:34 +0100 Subject: [PATCH] UDBScript asynchronous execution (#684) Script run by UDBScript are now executed asynchronously --- Source/Core/General/General.cs | 1 + Source/Core/Map/MapSet.cs | 23 +- Source/Core/Windows/IMainForm.cs | 1 + Source/Core/Windows/MainForm.cs | 37 +- .../Plugins/CommentsPanel/CommentsDocker.cs | 2 +- Source/Plugins/UDBScript/API/MapWrapper.cs | 18 +- Source/Plugins/UDBScript/BuilderPlug.cs | 8 +- Source/Plugins/UDBScript/ProgressInfo.cs | 37 ++ Source/Plugins/UDBScript/ScriptRunner.cs | 215 ++++++----- Source/Plugins/UDBScript/UDBScript.csproj | 10 + .../Windows/ScriptRunnerForm.Designer.cs | 110 ++++++ .../UDBScript/Windows/ScriptRunnerForm.cs | 334 ++++++++++++++++++ .../UDBScript/Windows/ScriptRunnerForm.resx | 120 +++++++ 13 files changed, 801 insertions(+), 115 deletions(-) create mode 100644 Source/Plugins/UDBScript/ProgressInfo.cs create mode 100644 Source/Plugins/UDBScript/Windows/ScriptRunnerForm.Designer.cs create mode 100644 Source/Plugins/UDBScript/Windows/ScriptRunnerForm.cs create mode 100644 Source/Plugins/UDBScript/Windows/ScriptRunnerForm.resx diff --git a/Source/Core/General/General.cs b/Source/Core/General/General.cs index b315e8d2..e9ba62c2 100755 --- a/Source/Core/General/General.cs +++ b/Source/Core/General/General.cs @@ -144,6 +144,7 @@ namespace CodeImp.DoomBuilder internal const int WM_UIACTION = WM_USER + 1; internal const int WM_SYSCOMMAND = 0x112; internal const int WM_MOUSEHWHEEL = 0x020E; // [ZZ] + internal const int WM_MOUSEWHEEL = 0x20A; internal const int SC_KEYMENU = 0xF100; internal const int CB_SETITEMHEIGHT = 0x153; //internal const int CB_SHOWDROPDOWN = 0x14F; diff --git a/Source/Core/Map/MapSet.cs b/Source/Core/Map/MapSet.cs index 0edd2b0f..0ff4ff1b 100755 --- a/Source/Core/Map/MapSet.cs +++ b/Source/Core/Map/MapSet.cs @@ -101,6 +101,9 @@ namespace CodeImp.DoomBuilder.Map // Statics private static long emptylongname; private static UniValue virtualsectorvalue; + + // Concurrency + private bool issafetoaccess; // Disposing private bool isdisposed; @@ -169,6 +172,13 @@ namespace CodeImp.DoomBuilder.Map internal List UnknownUDMFData { get { return unknownudmfdata; } set { unknownudmfdata = value; } } + /// + /// If it's safe to access (either reading or modifying) the map data. May only be read or set from the UI thread to + /// avoid racing conditions. Code that wants to access the map data on on a timer or in another thread must honor + /// this setting to avoid exceptions + /// + public bool IsSafeToAccess { get { return issafetoaccess; } set { issafetoaccess = value; } } + #endregion #region ================== Constructor / Disposer @@ -190,6 +200,8 @@ namespace CodeImp.DoomBuilder.Map lastsectorindex = 0; autoremove = true; unknownudmfdata = new List(); + + issafetoaccess = true; // We have no destructor GC.SuppressFinalize(this); @@ -213,6 +225,8 @@ namespace CodeImp.DoomBuilder.Map autoremove = true; unknownudmfdata = new List(); + issafetoaccess = true; + // Deserialize Deserialize(stream); @@ -1077,9 +1091,14 @@ namespace CodeImp.DoomBuilder.Map // Update all sectors if(dosectors) { - foreach(Sector s in sectors) s.Triangulate(); + foreach (Sector s in sectors) + { + s.Triangulate(); + s.UpdateBBox(); + } + General.Map.CRenderer2D.Surfaces.AllocateBuffers(); - foreach(Sector s in sectors) s.CreateSurfaces(); + foreach (Sector s in sectors) s.CreateSurfaces(); General.Map.CRenderer2D.Surfaces.UnlockBuffers(); } } diff --git a/Source/Core/Windows/IMainForm.cs b/Source/Core/Windows/IMainForm.cs index f8b6eede..be38bb58 100755 --- a/Source/Core/Windows/IMainForm.cs +++ b/Source/Core/Windows/IMainForm.cs @@ -42,6 +42,7 @@ namespace CodeImp.DoomBuilder.Windows bool IsActiveWindow { get; } string ActiveDockerTabName { get; } //mxd RenderTargetControl Display { get; } + int ProcessingCount { get; } //mxd. Events event EventHandler OnEditFormValuesChanged; diff --git a/Source/Core/Windows/MainForm.cs b/Source/Core/Windows/MainForm.cs index 9d224e56..47d47e60 100755 --- a/Source/Core/Windows/MainForm.cs +++ b/Source/Core/Windows/MainForm.cs @@ -180,6 +180,7 @@ namespace CodeImp.DoomBuilder.Windows public StatusInfo Status { get { return status; } } public static Size ScaledIconSize = new Size(16, 16); //mxd public static SizeF DPIScaler = new SizeF(1.0f, 1.0f); //mxd + public int ProcessingCount { get { return processingcount; } } #endregion @@ -1352,22 +1353,26 @@ namespace CodeImp.DoomBuilder.Windows if(alt) mod |= (int)Keys.Alt; if(shift) mod |= (int)Keys.Shift; if(ctrl) mod |= (int)Keys.Control; - - // Scrollwheel up? - if(e.Delta > 0) + + // Only send key events when the main window can be focused (i.e. no modal dialogs are open) + if (CanFocus) { - // Invoke actions for scrollwheel - //for(int i = 0; i < e.Delta; i += 120) - General.Actions.KeyPressed((int)SpecialKeys.MScrollUp | mod); - General.Actions.KeyReleased((int)SpecialKeys.MScrollUp | mod); - } - // Scrollwheel down? - else if(e.Delta < 0) - { - // Invoke actions for scrollwheel - //for(int i = 0; i > e.Delta; i -= 120) - General.Actions.KeyPressed((int)SpecialKeys.MScrollDown | mod); - General.Actions.KeyReleased((int)SpecialKeys.MScrollDown | mod); + // Scrollwheel up? + if (e.Delta > 0) + { + // Invoke actions for scrollwheel + //for(int i = 0; i < e.Delta; i += 120) + General.Actions.KeyPressed((int)SpecialKeys.MScrollUp | mod); + General.Actions.KeyReleased((int)SpecialKeys.MScrollUp | mod); + } + // Scrollwheel down? + else if (e.Delta < 0) + { + // Invoke actions for scrollwheel + //for(int i = 0; i > e.Delta; i -= 120) + General.Actions.KeyPressed((int)SpecialKeys.MScrollDown | mod); + General.Actions.KeyReleased((int)SpecialKeys.MScrollDown | mod); + } } // Let the base know @@ -4394,7 +4399,7 @@ namespace CodeImp.DoomBuilder.Windows OnMouseHWheel(delta); m.Result = new IntPtr(delta); break; - + default: // Let the base handle the message base.WndProc(ref m); diff --git a/Source/Plugins/CommentsPanel/CommentsDocker.cs b/Source/Plugins/CommentsPanel/CommentsDocker.cs index a9d4de13..a91922ce 100755 --- a/Source/Plugins/CommentsPanel/CommentsDocker.cs +++ b/Source/Plugins/CommentsPanel/CommentsDocker.cs @@ -159,7 +159,7 @@ namespace CodeImp.DoomBuilder.CommentsPanel // This finds all comments and updates the list public void UpdateList() { - if(!preventupdate) + if(!preventupdate && General.Map.Map.IsSafeToAccess) { // Update vertices Dictionary newcomments = new Dictionary(StringComparer.Ordinal); diff --git a/Source/Plugins/UDBScript/API/MapWrapper.cs b/Source/Plugins/UDBScript/API/MapWrapper.cs index e1e432dd..b4e094c1 100644 --- a/Source/Plugins/UDBScript/API/MapWrapper.cs +++ b/Source/Plugins/UDBScript/API/MapWrapper.cs @@ -23,6 +23,7 @@ #region ================== Namespaces +using System; using System.Collections.Generic; using System.Linq; using CodeImp.DoomBuilder.BuilderModes; @@ -41,6 +42,7 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper private MapSet map; private VisualCameraWrapper visualcamera; + private Vector2D mousemappos; #endregion @@ -86,10 +88,7 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper { get { - if (General.Editing.Mode is ClassicMode) - return ((ClassicMode)General.Editing.Mode).MouseMapPos; - else - return ((VisualMode)General.Editing.Mode).GetHitPosition(); + return mousemappos; } } @@ -112,6 +111,11 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper { map = General.Map.Map; visualcamera = new VisualCameraWrapper(); + + if (General.Editing.Mode is ClassicMode) + mousemappos = ((ClassicMode)General.Editing.Mode).MouseMapPos; + else + mousemappos = ((VisualMode)General.Editing.Mode).GetHitPosition(); } #endregion @@ -416,8 +420,8 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper // Snap to map format accuracy General.Map.Map.SnapAllToAccuracy(); - // Update map - General.Map.Map.Update(); + // Update map. This has to run on the UI thread + BuilderPlug.Me.ScriptRunnerForm.RunAction(() => General.Map.Map.Update()); // Update textures General.Map.Data.UpdateUsedTextures(); @@ -1178,7 +1182,7 @@ namespace CodeImp.DoomBuilder.UDBScript.Wrapper sectors[i].Join(first); // Update - General.Map.Map.Update(); + BuilderPlug.Me.ScriptRunnerForm.RunAction(() => General.Map.Map.Update()); } #endregion diff --git a/Source/Plugins/UDBScript/BuilderPlug.cs b/Source/Plugins/UDBScript/BuilderPlug.cs index 4315b06b..d2ecd9e1 100644 --- a/Source/Plugins/UDBScript/BuilderPlug.cs +++ b/Source/Plugins/UDBScript/BuilderPlug.cs @@ -97,6 +97,7 @@ namespace CodeImp.DoomBuilder.UDBScript private Dictionary scriptslots; private string editorexepath; private PreferencesForm preferencesform; + private ScriptRunnerForm scriptrunnerform; #endregion @@ -108,6 +109,7 @@ namespace CodeImp.DoomBuilder.UDBScript internal ScriptRunner ScriptRunner { get { return scriptrunner; } } internal ScriptDirectoryStructure ScriptDirectoryStructure { get { return scriptdirectorystructure; } } internal string EditorExePath { get { return editorexepath; } } + public ScriptRunnerForm ScriptRunnerForm { get { return scriptrunnerform; } } #endregion @@ -138,6 +140,8 @@ namespace CodeImp.DoomBuilder.UDBScript editorexepath = General.Settings.ReadPluginSetting("externaleditor", string.Empty); + scriptrunnerform = new ScriptRunnerForm(); + FindEditor(); } @@ -617,7 +621,7 @@ namespace CodeImp.DoomBuilder.UDBScript return; scriptrunner = new ScriptRunner(currentscript); - scriptrunner.Run(); + scriptrunnerform.ShowDialog(); } [BeginAction("udbscriptexecuteslot1")] @@ -665,7 +669,7 @@ namespace CodeImp.DoomBuilder.UDBScript if (scriptslots.ContainsKey(slot) && scriptslots[slot] != null) { scriptrunner = new ScriptRunner(scriptslots[slot]); - scriptrunner.Run(); + scriptrunnerform.ShowDialog(); } } } diff --git a/Source/Plugins/UDBScript/ProgressInfo.cs b/Source/Plugins/UDBScript/ProgressInfo.cs new file mode 100644 index 00000000..9f734395 --- /dev/null +++ b/Source/Plugins/UDBScript/ProgressInfo.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CodeImp.DoomBuilder.UDBScript +{ + class ProgressInfo + { + IProgress progress; + IProgress status; + IProgress _log; + + public ProgressInfo(IProgress progress, IProgress status, IProgress log) + { + this.progress = progress; + this.status = status; + _log = log; + } + + public void setProgress(int p) + { + progress.Report(p); + } + + public void setStatus(string s) + { + status.Report(s); + } + + public void log(string s) + { + _log.Report(s); + } + } +} diff --git a/Source/Plugins/UDBScript/ScriptRunner.cs b/Source/Plugins/UDBScript/ScriptRunner.cs index a2a4f348..65ac7839 100644 --- a/Source/Plugins/UDBScript/ScriptRunner.cs +++ b/Source/Plugins/UDBScript/ScriptRunner.cs @@ -26,6 +26,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Threading; using System.Windows.Forms; using CodeImp.DoomBuilder.Map; using CodeImp.DoomBuilder.Windows; @@ -47,6 +48,7 @@ namespace CodeImp.DoomBuilder.UDBScript private ScriptInfo scriptinfo; Engine engine; Stopwatch stopwatch; + int oldprocessingcount; #endregion @@ -84,16 +86,18 @@ namespace CodeImp.DoomBuilder.UDBScript /// Message to show public void ShowMessage(object message) { - if (message == null) - message = string.Empty; + BuilderPlug.Me.ScriptRunnerForm.InvokePaused(new Action(() => { + if (message == null) + message = string.Empty; - stopwatch.Stop(); - MessageForm mf = new MessageForm("OK", null, message.ToString()); - DialogResult result = mf.ShowDialog(); - stopwatch.Start(); + stopwatch.Stop(); + MessageForm mf = new MessageForm("OK", null, message.ToString()); + DialogResult result = mf.ShowDialog(); + stopwatch.Start(); - if (result == DialogResult.Abort) - throw new UserScriptAbortException(); + if (result == DialogResult.Abort) + throw new UserScriptAbortException(); + })); } /// @@ -103,18 +107,21 @@ namespace CodeImp.DoomBuilder.UDBScript /// true if "Yes" was clicked, false if "No" was clicked public bool ShowMessageYesNo(object message) { - if (message == null) - message = string.Empty; + return (bool)BuilderPlug.Me.ScriptRunnerForm.InvokePaused(new Func(() => + { + if (message == null) + message = string.Empty; - stopwatch.Stop(); - MessageForm mf = new MessageForm("Yes", "No", message.ToString()); - DialogResult result = mf.ShowDialog(); - stopwatch.Start(); + stopwatch.Stop(); + MessageForm mf = new MessageForm("Yes", "No", message.ToString()); + DialogResult result = mf.ShowDialog(); + stopwatch.Start(); - if (result == DialogResult.Abort) - throw new UserScriptAbortException(); + if (result == DialogResult.Abort) + throw new UserScriptAbortException(); - return result == DialogResult.OK ? true : false; + return result == DialogResult.OK ? true : false; + })); } /// @@ -166,17 +173,16 @@ namespace CodeImp.DoomBuilder.UDBScript ParserOptions po = new ParserOptions(file.Remove(0, General.AppPath.Length)); engine.Execute(File.ReadAllText(file), po); } - catch (Esprima.ParserException e) + catch (ParserException e) { MessageBox.Show("There was an error while loading the library " + file + ":\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error); return false; } - catch (Jint.Runtime.JavaScriptException e) + catch (JavaScriptException e) { if (e.Error.Type != Jint.Runtime.Types.String) { - //MessageBox.Show("There is an error in the script in line " + e.LineNumber + ":\n\n" + e.Message + "\n\n" + e.StackTrace, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error); UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace); sef.ShowDialog(); } @@ -191,16 +197,74 @@ namespace CodeImp.DoomBuilder.UDBScript } /// - /// Runs the script + /// Handles the different exceptions we're expecting, and withdraws the undo snapshot if necessary. /// - public void Run() + /// The exception to handle + public void HandleExceptions(Exception e) + { + bool abort = false; + + if(e is UserScriptAbortException) + { + General.Interface.DisplayStatus(StatusType.Warning, "Script aborted"); + abort = true; + } + else if(e is ParserException) + { + MessageBox.Show("There is an error while parsing the script:\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error); + abort = true; + } + else if(e is JavaScriptException) + { + if (((JavaScriptException)e).Error.Type != Jint.Runtime.Types.String) + { + UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace); + sef.ShowDialog(); + } + else + General.Interface.DisplayStatus(StatusType.Warning, e.Message); // We get here if "throw" is used in a script + + abort = true; + } + else if(e is ExitScriptException) + { + if (!string.IsNullOrEmpty(e.Message)) + General.Interface.DisplayStatus(StatusType.Ready, e.Message); + } + else if(e is DieScriptException) + { + if (!string.IsNullOrEmpty(e.Message)) + General.Interface.DisplayStatus(StatusType.Warning, e.Message); + + abort = true; + } + else if(e is ExecutionCanceledException) + { + abort = true; + } + else // Catch anything else we didn't think about + { + UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace); + sef.ShowDialog(); + + abort = true; + } + + if (abort) + General.Map.UndoRedo.WithdrawUndo(); + } + + /// + /// Sets everything up for running the script. This has to be done on the UI thread. + /// + /// Cancellation token to cancel the running script + public void PreRun(CancellationToken cancellationtoken) { string importlibraryerrors; - bool abort = false; // If the script requires a higher version of UDBScript than this version ask the user if they want // to execute it anyways. Remember the choice for this session if "yes" was selected. - if(scriptinfo.Version > BuilderPlug.UDB_SCRIPT_VERSION && !scriptinfo.IgnoreVersion) + if (scriptinfo.Version > BuilderPlug.UDB_SCRIPT_VERSION && !scriptinfo.IgnoreVersion) { if (MessageBox.Show("The script requires a higher version of the feature set than this version of UDBScript supports. Executing this script might fail\n\nRequired feature version: " + scriptinfo.Version + "\nUDBScript feature version: " + BuilderPlug.UDB_SCRIPT_VERSION + "\n\nExecute anyway?", "UDBScript feature version too low", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No) return; @@ -208,22 +272,19 @@ namespace CodeImp.DoomBuilder.UDBScript scriptinfo.IgnoreVersion = true; } - // Read the current script file - string script = File.ReadAllText(scriptinfo.ScriptFile); - // Make sure the option value gets saved if an option is currently being edited BuilderPlug.Me.EndOptionEdit(); General.Interface.Focus(); - // Set engine options Options options = new Options(); - options.Constraint(new RuntimeConstraint(stopwatch)); + options.CancellationToken(cancellationtoken); options.AllowOperatorOverloading(); - options.SetTypeResolver(new TypeResolver { + options.SetTypeResolver(new TypeResolver + { MemberFilter = member => member.Name != nameof(GetType) }); - + // Create the script engine engine = new Engine(options); engine.SetValue("showMessage", new Action(ShowMessage)); @@ -260,65 +321,41 @@ namespace CodeImp.DoomBuilder.UDBScript // Tell the mode that a script is about to be run General.Editing.Mode.OnScriptRunBegin(); + General.Map.UndoRedo.CreateUndo("Run script " + scriptinfo.Name); + General.Map.Map.ClearAllMarks(false); + + General.Map.Map.IsSafeToAccess = false; + + // Disable all processing. Has to be done as many times as it was enabled. + // Save old value since after running the script we need to enable it as many times + oldprocessingcount = General.Interface.ProcessingCount; + for (int i = 0; i < oldprocessingcount; i++) + General.Interface.DisableProcessing(); + } + + /// + /// Runs the script + /// + public void Run(IProgress progress, IProgress status, IProgress log) + { + engine.SetValue("ProgressInfo", new ProgressInfo(progress, status, log)); + // Read the current script file + string script = File.ReadAllText(scriptinfo.ScriptFile); + // Run the script file - try - { - General.Map.UndoRedo.CreateUndo("Run script " + scriptinfo.Name); - General.Map.Map.ClearAllMarks(false); + ParserOptions po = new ParserOptions(scriptinfo.ScriptFile.Remove(0, General.AppPath.Length)); - ParserOptions po = new ParserOptions(scriptinfo.ScriptFile.Remove(0, General.AppPath.Length)); + stopwatch.Start(); + engine.Execute(script, po); + stopwatch.Stop(); + } - stopwatch.Start(); - engine.Execute(script, po); - stopwatch.Stop(); - } - catch (UserScriptAbortException) - { - General.Interface.DisplayStatus(StatusType.Warning, "Script aborted"); - abort = true; - } - catch (ParserException e) - { - MessageBox.Show("There is an error while parsing the script:\n\n" + e.Message, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error); - abort = true; - } - catch (Jint.Runtime.JavaScriptException e) - { - if (e.Error.Type != Jint.Runtime.Types.String) - { - //MessageBox.Show("There is an error in the script in line " + e.LineNumber + ":\n\n" + e.Message + "\n\n" + e.StackTrace, "Script error", MessageBoxButtons.OK, MessageBoxIcon.Error); - UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace); - sef.ShowDialog(); - } - else - General.Interface.DisplayStatus(StatusType.Warning, e.Message); // We get here if "throw" is used in a script - - abort = true; - } - catch(ExitScriptException e) - { - if (!string.IsNullOrEmpty(e.Message)) - General.Interface.DisplayStatus(StatusType.Ready, e.Message); - } - catch(DieScriptException e) - { - if (!string.IsNullOrEmpty(e.Message)) - General.Interface.DisplayStatus(StatusType.Warning, e.Message); - - abort = true; - } - catch (Exception e) // Catch anything else we didn't think about - { - UDBScriptErrorForm sef = new UDBScriptErrorForm(e.Message, e.StackTrace); - sef.ShowDialog(); - - abort = true; - } - - if (abort) - { - General.Map.UndoRedo.WithdrawUndo(); - } + /// + /// Cleanups and updates after the script stopped running. Has to be called from the UI thread. + /// + public void PostRun() + { + General.Map.Map.IsSafeToAccess = true; // Do some updates General.Map.Map.Update(); @@ -327,6 +364,10 @@ namespace CodeImp.DoomBuilder.UDBScript // Tell the mode that running the script ended General.Editing.Mode.OnScriptRunEnd(); + + // Enable processing again, if required + for (int i = 0; i < oldprocessingcount; i++) + General.Interface.EnableProcessing(); } #endregion diff --git a/Source/Plugins/UDBScript/UDBScript.csproj b/Source/Plugins/UDBScript/UDBScript.csproj index 1e259e3e..956b9a4d 100644 --- a/Source/Plugins/UDBScript/UDBScript.csproj +++ b/Source/Plugins/UDBScript/UDBScript.csproj @@ -93,6 +93,7 @@ ScriptOptionsControl.cs + True @@ -123,6 +124,12 @@ QueryOptionsForm.cs + + Form + + + ScriptRunnerForm.cs + Form @@ -162,6 +169,9 @@ QueryOptionsForm.cs + + ScriptRunnerForm.cs + UDBScriptErrorForm.cs diff --git a/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.Designer.cs b/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.Designer.cs new file mode 100644 index 00000000..2f7420cc --- /dev/null +++ b/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.Designer.cs @@ -0,0 +1,110 @@ +namespace CodeImp.DoomBuilder.UDBScript +{ + partial class ScriptRunnerForm + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.progressbar = new System.Windows.Forms.ProgressBar(); + this.lbStatus = new System.Windows.Forms.Label(); + this.btnAction = new System.Windows.Forms.Button(); + this.tbLog = new System.Windows.Forms.TextBox(); + this.SuspendLayout(); + // + // progressbar + // + this.progressbar.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.progressbar.Location = new System.Drawing.Point(12, 25); + this.progressbar.Name = "progressbar"; + this.progressbar.Size = new System.Drawing.Size(419, 23); + this.progressbar.Step = 1; + this.progressbar.Style = System.Windows.Forms.ProgressBarStyle.Continuous; + this.progressbar.TabIndex = 0; + // + // lbStatus + // + this.lbStatus.AutoSize = true; + this.lbStatus.Location = new System.Drawing.Point(12, 9); + this.lbStatus.Name = "lbStatus"; + this.lbStatus.Size = new System.Drawing.Size(84, 13); + this.lbStatus.TabIndex = 1; + this.lbStatus.Text = "Running script..."; + // + // btnAction + // + this.btnAction.Anchor = ((System.Windows.Forms.AnchorStyles)((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Right))); + this.btnAction.Location = new System.Drawing.Point(437, 25); + this.btnAction.Name = "btnAction"; + this.btnAction.Size = new System.Drawing.Size(75, 23); + this.btnAction.TabIndex = 2; + this.btnAction.Text = "Cancel"; + this.btnAction.UseVisualStyleBackColor = true; + this.btnAction.Click += new System.EventHandler(this.btnAction_Click); + // + // tbLog + // + this.tbLog.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) + | System.Windows.Forms.AnchorStyles.Left) + | System.Windows.Forms.AnchorStyles.Right))); + this.tbLog.Location = new System.Drawing.Point(12, 54); + this.tbLog.Multiline = true; + this.tbLog.Name = "tbLog"; + this.tbLog.ReadOnly = true; + this.tbLog.ScrollBars = System.Windows.Forms.ScrollBars.Both; + this.tbLog.Size = new System.Drawing.Size(500, 118); + this.tbLog.TabIndex = 3; + // + // ScriptRunnerForm + // + this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; + this.ClientSize = new System.Drawing.Size(524, 184); + this.ControlBox = false; + this.Controls.Add(this.tbLog); + this.Controls.Add(this.btnAction); + this.Controls.Add(this.lbStatus); + this.Controls.Add(this.progressbar); + this.MinimumSize = new System.Drawing.Size(540, 200); + this.Name = "ScriptRunnerForm"; + this.ShowIcon = false; + this.Text = "Running script"; + this.WindowState = System.Windows.Forms.FormWindowState.Minimized; + this.FormClosed += new System.Windows.Forms.FormClosedEventHandler(this.ScriptRunnerForm_FormClosed); + this.Shown += new System.EventHandler(this.ScriptRunnerForm_Shown); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private System.Windows.Forms.ProgressBar progressbar; + private System.Windows.Forms.Label lbStatus; + private System.Windows.Forms.Button btnAction; + private System.Windows.Forms.TextBox tbLog; + } +} \ No newline at end of file diff --git a/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.cs b/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.cs new file mode 100644 index 00000000..20eba06b --- /dev/null +++ b/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.cs @@ -0,0 +1,334 @@ +#region ================== Copyright (c) 2022 Boris Iwanski + +/* + * This program is free software: you can redistribute it and/or modify + * + * it under the terms of the GNU General Public License as published by + * + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program.If not, see. + */ + +#endregion + +#region ================== Namespaces + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Forms; +using CodeImp.DoomBuilder.Windows; + +#endregion + +namespace CodeImp.DoomBuilder.UDBScript +{ + public partial class ScriptRunnerForm : DelayedForm + { + #region ================== Constants + + /// + /// How long a script is allowed to run until the form is made visible. + /// + const int RUNTIME_THRESHOLD = 1000; + + #endregion + + #region ================== Variables + + /// + /// Cancellation token for stopping the script. + /// + CancellationTokenSource cancellationtokensource; + + /// + /// If script is currently executed or not. + /// + bool running; + + /// + /// How many milliseconds the script has been running + /// + double runningseconds; + + /// + /// Determines if the form should be automatically closed when the script is finished. + /// + bool autoclose; + + /// + /// Stopwatch used to determine how long the script is already running. + /// + Stopwatch stopwatch; + + /// + /// Timer for making the form visible when the script is running for too long. + /// + System.Windows.Forms.Timer timer; + + + #endregion + + #region ================== Methods + + public ScriptRunnerForm() + { + InitializeComponent(); + } + + /// + /// Invokes a method, but stops the timer before running the code, and starting the timer after the code ran. + /// + /// Method to invoke + /// Return value of the method + public object InvokePaused(Delegate method) + { + if (InvokeRequired) + { + return Invoke(new Action(() => InvokePaused(method))); + } + else + { + stopwatch.Stop(); + object result = Invoke(method); + stopwatch.Start(); + return result; + } + } + + /// + /// Invokes a method. + /// + /// Method to invoke + public void RunAction(Action action) + { + if (InvokeRequired) + Invoke(action); + else + action(); + } + + /// + /// Sets the value of the progress bar (in the range from 0 to 100). + /// + /// Value of the progress bar (in the range from 0 to 100) + private void SetProgress(int value) + { + if (progressbar.Style != ProgressBarStyle.Continuous) + progressbar.Style = ProgressBarStyle.Continuous; + + // Do some trickery to remove the movement of the progress bar, since it can + // otherwise screw with how much the progress bar is filled + if (progressbar.Value != value) + { + if (value > progressbar.Maximum) + value = progressbar.Maximum; + else if (value < progressbar.Minimum) + value = progressbar.Minimum; + + if (progressbar.Maximum == value) + { + progressbar.Value = value; + progressbar.Value = value - 1; + } + else + { + progressbar.Value = value + 1; + } + progressbar.Value = value; + } + + // Make the form visible so that the user can actually see the progress bar + MakeVisible(); + } + + private void SetProgressStatus(string status) + { + lbStatus.Text = status; + } + + private void Log(string text) + { + // If there's something in the log we don't want to automatically + // close the form, otherwise the user could not read the contents + autoclose = false; + + // Since we don't want to have a useless line at the end of the textbox + // we add a new line before adding the new text (unless there's no text + // at all yet, then we don't add a new line + if (!string.IsNullOrEmpty(tbLog.Text)) + tbLog.AppendText(Environment.NewLine); + + // Add the new text + tbLog.AppendText(text); + + // Make the form visible so that the user can actually see the status bar + MakeVisible(); + } + + private async void RunScript(CancellationToken cancellationtoken) + { + // Callbacks for setting the progress bar, status text, and adding log lines from the script + Progress progress = new Progress(SetProgress); + Progress status = new Progress(SetProgressStatus); + Progress log = new Progress(Log); + + running = true; + + // Prepare running the script + BuilderPlug.Me.ScriptRunner.PreRun(cancellationtoken); + + try + { + await Task.Run(() => BuilderPlug.Me.ScriptRunner.Run(progress, status, log)); + stopwatch.Stop(); + } + catch (Exception ex) + { + stopwatch.Stop(); + BuilderPlug.Me.ScriptRunner.HandleExceptions(ex); + } + + // Clean up and update after running the script + BuilderPlug.Me.ScriptRunner.PostRun(); + + running = false; + + btnAction.Text = "Close"; + btnAction.Enabled = true; + + if (autoclose) + { + MakeInvisible(); + //Hide(); + Close(); + } + } + + /// + /// Makes the form visible. + /// + private void MakeVisible() + { + Opacity = 1.0; + btnAction.Enabled = true; + } + + /// + /// Makes the form invisible. + /// + private void MakeInvisible() + { + Opacity = 0.0; + } + + #endregion + + #region ================== Events + + /// + /// Cancels the currently running script, or closes the form if no script is running. + /// + /// The sender + /// Event arguments + private void btnAction_Click(object sender, EventArgs e) + { + if (running) + { + btnAction.Enabled = false; + cancellationtokensource.Cancel(); + } + else + { + MakeInvisible(); + //Hide(); + Close(); + } + } + + /// + /// Sets everything up for running the script, and then immediately runs the script. + /// + /// The sender + /// Event arguments + private void ScriptRunnerForm_Shown(object sender, EventArgs e) + { + cancellationtokensource = new CancellationTokenSource(); + autoclose = true; + runningseconds = 0; + + progressbar.Value = 0; + progressbar.Style = ProgressBarStyle.Marquee; + + Text = "Running script"; + lbStatus.Text = "Running script..."; + + btnAction.Text = "Cancel"; + // Disable the button because it could otherwise be pressed while the form is invisible. + // It'll be enabled as soon as the form is made visible + btnAction.Enabled = false; + + tbLog.Clear(); + + // The timer ticks ever 100ms. The method it runs checks how long the script is running + // and makes the form visible if the runtime threshold has been reached + timer = new System.Windows.Forms.Timer(); + timer.Interval = 100; + timer.Tick += timerShow_Tick; + timer.Start(); + + // This stopwatch is used to measure how long the script has been running + stopwatch = new Stopwatch(); + stopwatch.Start(); + + // Start running the script + RunScript(cancellationtokensource.Token); + } + + protected override void OnLoad(EventArgs e) + { + base.OnLoad(e); + + MakeInvisible(); + } + + /// + /// Makes the form visible if the runtime threshold has been reached. Shows the elapsed time the script is running. + /// + /// The sender + /// Event arguments + private void timerShow_Tick(object sender, EventArgs e) + { + if (Opacity == 0.0 && stopwatch.ElapsedMilliseconds > 1000) + { + MakeVisible(); + } + + double newrunningsecods = Math.Floor(stopwatch.Elapsed.TotalSeconds); + + if(newrunningsecods > runningseconds) + { + runningseconds = newrunningsecods; + Text = "Running script (" + string.Format("{0:D2}:{1:D2}:{2:D2}", stopwatch.Elapsed.Hours, stopwatch.Elapsed.Minutes, stopwatch.Elapsed.Seconds) + ")"; + } + } + + private void ScriptRunnerForm_FormClosed(object sender, FormClosedEventArgs e) + { + timer.Stop(); + } + + #endregion + } +} \ No newline at end of file diff --git a/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.resx b/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.resx new file mode 100644 index 00000000..1af7de15 --- /dev/null +++ b/Source/Plugins/UDBScript/Windows/ScriptRunnerForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file