BizHawk/src/BizHawk.Client.EmuHawk/tools/Lua/LuaConsole.cs

1545 lines
42 KiB
C#

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
using BizHawk.Client.Common;
using BizHawk.Client.EmuHawk.Properties;
using BizHawk.Client.EmuHawk.ToolExtensions;
using BizHawk.Common.CollectionExtensions;
using BizHawk.Common.PathExtensions;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.EmuHawk
{
public partial class LuaConsole : ToolFormBase, IToolFormAutoConfig
{
private const string IconColumnName = "Icon";
private const string ScriptColumnName = "Script";
private const string PathColumnName = "PathName";
private static readonly FilesystemFilterSet JustScriptsFSFilterSet = new(FilesystemFilter.LuaScripts);
private static readonly FilesystemFilterSet ScriptsAndTextFilesFSFilterSet = new(FilesystemFilter.LuaScripts, FilesystemFilter.TextFiles);
private static readonly FilesystemFilterSet SessionsFSFilterSet = new(new FilesystemFilter("Lua Session Files", new[] { "luases" }));
public static Icon ToolIcon
=> Resources.TextDocIcon;
private readonly LuaAutocompleteInstaller _luaAutoInstaller = new();
private readonly Dictionary<LuaFile, FileSystemWatcher> _watches = new();
private readonly int _defaultSplitDistance;
private LuaFile _lastScriptUsed = null;
[RequiredService]
private IEmulator Emulator { get; set; }
private bool _sortReverse;
private string _lastColumnSorted;
private readonly List<string> _consoleCommandHistory = new();
private int _consoleCommandHistoryIndex = -1;
public ToolDialogSettings.ColumnList Columns { get; set; }
public class LuaConsoleSettings
{
public LuaConsoleSettings()
{
Columns = new List<RollColumn>
{
new() { Name = IconColumnName, Text = " ", Visible = true, UnscaledWidth = 22, Type = ColumnType.Image },
new() { Name = ScriptColumnName, Text = "Script", Visible = true, UnscaledWidth = 92, Type = ColumnType.Text },
new() { Name = PathColumnName, Text = "Path", Visible = true, UnscaledWidth = 300, Type = ColumnType.Text }
};
}
public List<RollColumn> Columns { get; set; }
public bool ReloadOnScriptFileChange { get; set; }
public bool ToggleAllIfNoneSelected { get; set; } = true;
public int SplitDistance { get; set; }
public bool DisableLuaScriptsOnLoad { get; set; }
public bool WarnedOnceOnOverwrite { get; set; }
}
[ConfigPersist]
public LuaConsoleSettings Settings { get; set; }
protected override string WindowTitleStatic => "Lua Console";
public LuaConsole()
{
Settings = new LuaConsoleSettings();
_sortReverse = false;
_lastColumnSorted = "";
InitializeComponent();
ToggleScriptContextItem.Image = Resources.Refresh;
PauseScriptContextItem.Image = Resources.Pause;
EditScriptContextItem.Image = Resources.Cut;
RemoveScriptContextItem.Image = Resources.Close;
InsertSeperatorContextItem.Image = Resources.InsertSeparator;
StopAllScriptsContextItem.Image = Resources.Stop;
ClearRegisteredFunctionsContextItem.Image = Resources.Delete;
NewSessionMenuItem.Image = Resources.NewFile;
OpenSessionMenuItem.Image = Resources.OpenFile;
SaveSessionMenuItem.Image = Resources.SaveAs;
NewScriptMenuItem.Image = Resources.NewFile;
OpenScriptMenuItem.Image = Resources.OpenFile;
RefreshScriptMenuItem.Image = Resources.Refresh;
ToggleScriptMenuItem.Image = Resources.Checkbox;
PauseScriptMenuItem.Image = Resources.Pause;
EditScriptMenuItem.Image = Resources.Cut;
RemoveScriptMenuItem.Image = Resources.Delete;
InsertSeparatorMenuItem.Image = Resources.InsertSeparator;
MoveUpMenuItem.Image = Resources.MoveUp;
MoveDownMenuItem.Image = Resources.MoveDown;
StopAllScriptsMenuItem.Image = Resources.Stop;
RegisterSublimeText2MenuItem.Image = Resources.GreenCheck;
ClearRegisteredFunctionsLogContextItem.Image = Resources.Delete;
NewScriptToolbarItem.Image = Resources.NewFile;
OpenScriptToolbarItem.Image = Resources.OpenFile;
ToggleScriptToolbarItem.Image = Resources.Checkbox;
RefreshScriptToolbarItem.Image = Resources.Refresh;
PauseToolbarItem.Image = Resources.Pause;
EditToolbarItem.Image = Resources.Cut;
RemoveScriptToolbarItem.Image = Resources.Delete;
DuplicateToolbarButton.Image = Resources.Duplicate;
ClearConsoleToolbarButton.Image = Resources.ClearConsole;
MoveUpToolbarItem.Image = Resources.MoveUp;
toolStripButtonMoveDown.Image = Resources.MoveDown;
InsertSeparatorToolbarItem.Image = Resources.InsertSeparator;
EraseToolbarItem.Image = Resources.Erase;
RecentScriptsSubMenu.Image = Resources.Recent;
Icon = ToolIcon;
Closing += (o, e) =>
{
if (AskSaveChanges())
{
Settings.Columns = LuaListView.AllColumns;
DisplayManager.ClearApiHawkSurfaces();
ResetDrawSurfacePadding();
ClearFileWatches();
LuaImp?.Close();
DisplayManager.OSD.ClearGuiText();
}
else
{
e.Cancel = true;
}
};
LuaListView.QueryItemText += LuaListView_QueryItemText;
LuaListView.QueryItemBkColor += LuaListView_QueryItemBkColor;
LuaListView.QueryItemIcon += LuaListView_QueryItemImage;
// this is bad, in case we ever have more than one gui part running lua.. not sure how much other badness there is like that
LuaSandbox.DefaultLogger = WriteToOutputWindow;
_defaultSplitDistance = splitContainer1.SplitterDistance;
}
public ILuaLibraries LuaImp { get; private set; }
private IEnumerable<LuaFile> SelectedItems => LuaListView.SelectedRows.Select(index => LuaImp.ScriptList[index]);
private IEnumerable<LuaFile> SelectedFiles => SelectedItems.Where(x => !x.IsSeparator);
private void LuaConsole_Load(object sender, EventArgs e)
{
if (Settings.Columns.Exists(static c => c.Text is null)) Settings = new(); //HACK for previous config settings
if (Config.RecentLuaSession.AutoLoad && !Config.RecentLuaSession.Empty)
{
LoadSessionFromRecent(Config.RecentLuaSession.MostRecent);
}
else if (Config.RecentLua.AutoLoad)
{
if (!Config.RecentLua.Empty)
{
LoadLuaFile(Config.RecentLua.MostRecent);
}
}
LuaListView.AllColumns.Clear();
SetColumns();
splitContainer1.SetDistanceOrDefault(Settings.SplitDistance, _defaultSplitDistance);
}
private void BranchesMarkersSplit_SplitterMoved(object sender, SplitterEventArgs e)
{
Settings.SplitDistance = splitContainer1.SplitterDistance;
}
public override void Restart()
{
List<LuaFile> runningScripts = new();
// Things we need to do with the existing LuaImp before we can make a new one
if (LuaImp is not null)
{
if (LuaImp.IsRebootingCore)
{
// Even if the lua console is self-rebooting from client.reboot_core() we still want to re-inject dependencies
LuaImp.Restart(Emulator.ServiceProvider, Config, Emulator, Game);
return;
}
runningScripts = LuaImp.ScriptList.Where(lf => lf.Enabled).ToList();
// we don't use runningScripts here as the other scripts need to be stopped too
foreach (var file in LuaImp.ScriptList)
{
DisableLuaScript(file);
}
}
LuaFileList newScripts = new(LuaImp?.ScriptList, onChanged: SessionChangedCallback);
LuaFunctionList registeredFuncList = new(onChanged: UpdateRegisteredFunctionsDialog);
LuaImp?.Close();
LuaImp = new LuaLibraries(
newScripts,
registeredFuncList,
Emulator.ServiceProvider,
(MainForm) MainForm, //HACK
DisplayManager,
InputManager,
Config,
Emulator,
Game);
InputBox.AutoCompleteCustomSource.AddRange(LuaImp.Docs.Select(a => $"{a.Library}.{a.Name}").ToArray());
foreach (var file in runningScripts)
{
try
{
LuaSandbox.Sandbox(file.Thread, () =>
{
LuaImp.SpawnAndSetFileThread(file.Path, file);
LuaSandbox.CreateSandbox(file.Thread, Path.GetDirectoryName(file.Path));
file.State = LuaFile.RunState.Running;
}, () =>
{
file.State = LuaFile.RunState.Disabled;
});
}
catch (Exception ex)
{
DialogController.ShowMessageBox(ex.ToString());
}
}
UpdateDialog();
}
public void ToggleLastLuaScript()
{
if (_lastScriptUsed is not null)
{
ToggleLuaScript(_lastScriptUsed);
}
}
private void SetColumns()
{
LuaListView.AllColumns.AddRange(Settings.Columns);
LuaListView.Refresh();
}
private void AddFileWatches()
{
if (Settings.ReloadOnScriptFileChange)
{
ClearFileWatches();
foreach (var item in LuaImp.ScriptList.Where(s => !s.IsSeparator))
{
CreateFileWatcher(item);
}
}
}
private void ClearFileWatches()
{
foreach (var watch in _watches.Values)
watch.Dispose();
_watches.Clear();
}
private void CreateFileWatcher(LuaFile item)
{
if (_watches.ContainsKey(item))
return;
var (dir, file) = item.Path.SplitPathToDirAndFile();
// prevent error when (auto)loading session referencing scripts in deleted/renamed directories
if (!Directory.Exists(dir))
return;
var watcher = new FileSystemWatcher
{
Path = dir,
Filter = file,
NotifyFilter = NotifyFilters.LastAccess | NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.DirectoryName,
EnableRaisingEvents = true,
SynchronizingObject = this, // invoke event handlers on GUI thread
};
// TODO, Deleted and Renamed events
watcher.Changed += (_, _) => OnLuaFileChanged(item);
_watches.Add(item, watcher);
}
private void RemoveFileWatcher(LuaFile item)
{
if (_watches.TryGetValue(item, out var watcher))
{
_watches.Remove(item);
watcher.Dispose();
}
}
private void OnLuaFileChanged(LuaFile item)
{
if (item.Enabled && LuaImp.ScriptList.Contains(item))
{
RefreshLuaScript(item);
}
}
public void LoadLuaFile(string path)
{
var absolutePath = Path.GetFullPath(path);
var alreadyLoadedFile = LuaImp.ScriptList.FirstOrDefault(t => absolutePath == t.Path);
if (alreadyLoadedFile is not null)
{
if (!alreadyLoadedFile.Enabled && !Settings.DisableLuaScriptsOnLoad)
{
ToggleLuaScript(alreadyLoadedFile);
}
}
else
{
var luaFile = new LuaFile("", absolutePath);
LuaImp.ScriptList.Add(luaFile);
LuaListView.RowCount = LuaImp.ScriptList.Count;
Config.RecentLua.Add(absolutePath);
if (!Settings.DisableLuaScriptsOnLoad)
{
luaFile.State = LuaFile.RunState.Running;
EnableLuaFile(luaFile);
}
else
{
luaFile.State = LuaFile.RunState.Disabled;
}
if (Settings.ReloadOnScriptFileChange)
{
CreateFileWatcher(luaFile);
}
}
UpdateDialog();
}
public void RemoveLuaFile(string path)
{
var absolutePath = Path.GetFullPath(path);
var luaFile = LuaImp.ScriptList.FirstOrDefault(t => absolutePath == t.Path);
if (luaFile is not null)
{
RemoveLuaFile(luaFile);
UpdateDialog();
}
}
private void RemoveLuaFile(LuaFile item)
{
if (!item.IsSeparator)
{
DisableLuaScript(item);
RemoveFileWatcher(item);
}
LuaImp.ScriptList.Remove(item);
}
private void RemoveAllLuaFiles()
{
while (LuaImp.ScriptList.Count > 0)
{
RemoveLuaFile(LuaImp.ScriptList[^1]);
}
}
private void UpdateDialog()
{
LuaListView.RowCount = LuaImp.ScriptList.Count;
UpdateNumberOfScripts();
UpdateRegisteredFunctionsDialog();
}
private void SessionChangedCallback()
{
OutputMessages.Text =
(LuaImp.ScriptList.Changes ? "* " : "") +
Path.GetFileName(LuaImp.ScriptList.Filename);
}
private void LuaListView_QueryItemImage(int index, RollColumn column, ref Bitmap bitmap, ref int offsetX, ref int offsetY)
{
if (column.Name != IconColumnName)
{
return;
}
if (LuaImp.ScriptList[index].IsSeparator)
{
return;
}
bitmap = LuaImp.ScriptList[index].State switch
{
LuaFile.RunState.Running => Resources.ts_h_arrow_green,
LuaFile.RunState.Paused => Resources.Pause,
_ => Resources.Stop
};
}
private void LuaListView_QueryItemBkColor(int index, RollColumn column, ref Color color)
{
var lf = LuaImp.ScriptList[index];
if (lf.IsSeparator) color = BackColor;
else if (lf.Paused) color = Color.LightPink;
else if (lf.Enabled) color = Color.LightCyan;
}
private void LuaListView_QueryItemText(int index, RollColumn column, out string text, ref int offsetX, ref int offsetY)
{
text = "";
if (LuaImp.ScriptList[index].IsSeparator)
{
return;
}
if (column.Name == ScriptColumnName)
{
text = Path.GetFileNameWithoutExtension(LuaImp.ScriptList[index].Path); // TODO: how about allow the user to name scripts?
}
else if (column.Name == PathColumnName)
{
text = DressUpRelative(LuaImp.ScriptList[index].Path);
}
}
private string DressUpRelative(string path)
{
return path.StartsWithOrdinal(".\\") ? path.Replace(".\\", "") : path;
}
private void UpdateNumberOfScripts()
{
var message = "";
var total = LuaImp.ScriptList.Count(file => !file.IsSeparator);
var active = LuaImp.ScriptList.Count(file => !file.IsSeparator && file.Enabled);
var paused = LuaImp.ScriptList.Count(static lf => !lf.IsSeparator && lf.Paused);
if (total == 1)
{
message += $"{total} script ({active} active, {paused} paused)";
}
else if (total == 0)
{
message += $"{total} scripts";
}
else
{
message += $"{total} scripts ({active} active, {paused} paused)";
}
NumberOfScripts.Text = message;
}
private void WriteLine(string message) => WriteToOutputWindow(message + "\n");
private int _messageCount;
private const int MaxCount = 100;
public void WriteToOutputWindow(string message)
{
if (!OutputBox.IsHandleCreated || OutputBox.IsDisposed)
{
return;
}
_messageCount++;
if (_messageCount > MaxCount) return;
if (_messageCount == MaxCount) message += "\nFlood warning! Message cap reached, suppressing output.\n";
OutputBox.Invoke(() =>
{
OutputBox.Text += message;
OutputBox.SelectionStart = OutputBox.Text.Length;
OutputBox.ScrollToCaret();
});
}
public void ClearOutputWindow()
{
if (!OutputBox.IsHandleCreated || OutputBox.IsDisposed)
{
return;
}
OutputBox.Invoke(() =>
{
OutputBox.Text = "";
OutputBox.Refresh();
});
}
public bool LoadLuaSession(string path)
{
RemoveAllLuaFiles();
var result = LuaImp.ScriptList.Load(path, Settings.DisableLuaScriptsOnLoad);
foreach (var script in LuaImp.ScriptList)
{
if (!script.IsSeparator)
{
if (script.Enabled)
{
EnableLuaFile(script);
}
Config.RecentLua.Add(script.Path);
}
}
LuaImp.ScriptList.Changes = false;
Config.RecentLuaSession.Add(path);
UpdateDialog();
AddFileWatches();
ClearOutputWindow();
return result;
}
protected override void UpdateBefore()
{
if (LuaImp.IsUpdateSupressed)
{
return;
}
LuaImp.CallFrameBeforeEvent();
}
protected override void UpdateAfter()
{
if (LuaImp.IsUpdateSupressed)
{
return;
}
LuaImp.CallFrameAfterEvent();
ResumeScripts(true);
}
protected override void FastUpdateBefore()
{
if (Config.RunLuaDuringTurbo)
{
UpdateBefore();
}
}
protected override void FastUpdateAfter()
{
if (Config.RunLuaDuringTurbo)
{
UpdateAfter();
}
}
private void ResetDrawSurfacePadding()
{
var resized = false;
if (DisplayManager.ClientExtraPadding != (0, 0, 0, 0))
{
DisplayManager.ClientExtraPadding = (0, 0, 0, 0);
resized = true;
}
if (DisplayManager.GameExtraPadding != (0, 0, 0, 0))
{
DisplayManager.GameExtraPadding = (0, 0, 0, 0);
resized = true;
}
if (resized) MainForm.FrameBufferResized();
}
/// <summary>
/// resumes suspended Co-routines
/// </summary>
/// <param name="includeFrameWaiters">should frame waiters be waken up? only use this immediately before a frame of emulation</param>
public void ResumeScripts(bool includeFrameWaiters)
{
if (!LuaImp.ScriptList.Any()
|| LuaImp.IsUpdateSupressed
|| (MainForm.IsTurboing && !Config.RunLuaDuringTurbo))
{
return;
}
foreach (var lf in LuaImp.ScriptList.Where(static lf => lf.State is LuaFile.RunState.Running && lf.Thread is not null))
{
try
{
LuaSandbox.Sandbox(lf.Thread, () =>
{
var prohibit = lf.FrameWaiting && !includeFrameWaiters;
if (!prohibit)
{
var (waitForFrame, terminated) = LuaImp.ResumeScript(lf);
if (terminated)
{
LuaImp.CallExitEvent(lf);
lf.Stop();
DetachRegisteredFunctions(lf);
UpdateDialog();
}
lf.FrameWaiting = waitForFrame;
}
}, () =>
{
lf.Stop();
DetachRegisteredFunctions(lf);
LuaListView.Refresh();
});
}
catch (Exception ex)
{
DialogController.ShowMessageBox(ex.ToString());
}
}
_messageCount = 0;
}
private void DetachRegisteredFunctions(LuaFile lf)
{
foreach (var nlf in LuaImp.RegisteredFunctions
.Where(f => f.LuaFile == lf))
{
nlf.DetachFromScript();
}
}
private FileInfo GetSaveFileFromUser()
{
string initDir;
string initFileName;
if (!string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename))
{
(initDir, initFileName, _) = LuaImp.ScriptList.Filename.SplitPathToDirFileAndExt();
}
else
{
initDir = Config!.PathEntries.LuaAbsolutePath();
initFileName = Game.IsNullInstance() ? "NULL" : Game.FilesystemSafeName();
}
var result = this.ShowFileSaveDialog(
discardCWDChange: true,
filter: SessionsFSFilterSet,
initDir: initDir,
initFileName: initFileName);
return result is not null ? new FileInfo(result) : null;
}
private void SaveSessionAs()
{
var file = GetSaveFileFromUser();
if (file != null)
{
LuaImp.ScriptList.Save(file.FullName);
Config.RecentLuaSession.Add(file.FullName);
OutputMessages.Text = $"{file.Name} saved.";
}
}
private void LoadSessionFromRecent(string path)
{
var load = true;
if (LuaImp.ScriptList.Changes)
{
load = AskSaveChanges();
}
if (load)
{
if (!LoadLuaSession(path))
{
Config.RecentLuaSession.HandleLoadError(MainForm, path);
}
}
}
public override bool AskSaveChanges()
{
if (!LuaImp.ScriptList.Changes || string.IsNullOrEmpty(LuaImp.ScriptList.Filename)) return true;
var result = DialogController.DoWithTempMute(() => this.ModalMessageBox3(
caption: "Closing with Unsaved Changes",
icon: EMsgBoxIcon.Question,
text: $"Save {WindowTitleStatic} session?"));
if (result is null) return false;
if (result.Value) SaveOrSaveAs();
else LuaImp.ScriptList.Changes = false;
return true;
}
private void UpdateRegisteredFunctionsDialog()
{
if (LuaImp is null) return;
foreach (var form in Application.OpenForms.OfType<LuaRegisteredFunctionsList>().ToList())
{
form.UpdateValues(LuaImp.RegisteredFunctions);
}
}
private void SaveOrSaveAs()
{
if (!string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename))
{
LuaImp.ScriptList.Save(LuaImp.ScriptList.Filename);
Config.RecentLuaSession.Add(LuaImp.ScriptList.Filename);
}
else
{
SaveSessionAs();
}
}
private void FileSubMenu_DropDownOpened(object sender, EventArgs e)
{
SaveSessionMenuItem.Enabled = LuaImp.ScriptList.Changes;
}
private void RecentSessionsSubMenu_DropDownOpened(object sender, EventArgs e)
=> RecentSessionsSubMenu.ReplaceDropDownItems(Config!.RecentLuaSession.RecentMenu(this, LoadSessionFromRecent, "Session"));
private void RecentScriptsSubMenu_DropDownOpened(object sender, EventArgs e)
=> RecentScriptsSubMenu.ReplaceDropDownItems(Config!.RecentLua.RecentMenu(this, LoadLuaFile, "Script"));
private void NewSessionMenuItem_Click(object sender, EventArgs e)
{
var result = !LuaImp.ScriptList.Changes || AskSaveChanges();
if (result)
{
RemoveAllLuaFiles();
LuaImp.ScriptList.Clear();
ClearOutputWindow();
UpdateDialog();
}
}
private void OpenSessionMenuItem_Click(object sender, EventArgs e)
{
var initDir = Config!.PathEntries.LuaAbsolutePath();
Directory.CreateDirectory(initDir);
var result = this.ShowFileOpenDialog(
discardCWDChange: true,
filter: SessionsFSFilterSet,
initDir: initDir);
if (result is not null) LoadLuaSession(result);
}
private void SaveSessionMenuItem_Click(object sender, EventArgs e)
{
if (LuaImp.ScriptList.Changes)
{
SaveOrSaveAs();
OutputMessages.Text = $"{Path.GetFileName(LuaImp.ScriptList.Filename)} saved.";
}
}
private void SaveSessionAsMenuItem_Click(object sender, EventArgs e)
{
SaveSessionAs();
}
private void ScriptSubMenu_DropDownOpened(object sender, EventArgs e)
{
ToggleScriptMenuItem.Enabled =
PauseScriptMenuItem.Enabled =
EditScriptMenuItem.Enabled =
SelectedFiles.Any();
RemoveScriptMenuItem.Enabled =
DuplicateScriptMenuItem.Enabled =
MoveUpMenuItem.Enabled =
MoveDownMenuItem.Enabled =
LuaListView.AnyRowsSelected;
SelectAllMenuItem.Enabled = LuaImp.ScriptList.Any();
StopAllScriptsMenuItem.Enabled = LuaImp.ScriptList.Any(script => script.Enabled);
RegisteredFunctionsMenuItem.Enabled = LuaImp.RegisteredFunctions.Any();
}
private void NewScriptMenuItem_Click(object sender, EventArgs e)
{
var luaDir = Config!.PathEntries.LuaAbsolutePath();
string initDir;
string ext;
if (!string.IsNullOrWhiteSpace(LuaImp.ScriptList.Filename))
{
(initDir, ext, _) = LuaImp.ScriptList.Filename.SplitPathToDirFileAndExt();
}
else
{
initDir = luaDir;
ext = Path.GetFileNameWithoutExtension(Game.Name);
}
var result = this.ShowFileSaveDialog(
fileExt: ".lua",
filter: JustScriptsFSFilterSet,
initDir: initDir,
initFileName: ext);
if (string.IsNullOrWhiteSpace(result)) return;
const string TEMPLATE_FILENAME = ".template.lua";
var templatePath = Path.Combine(luaDir, TEMPLATE_FILENAME);
const string DEF_TEMPLATE_CONTENTS = "-- This template lives at `.../Lua/.template.lua`.\nwhile true do\n\t-- Code here will run once when the script is loaded, then after each emulated frame.\n\temu.frameadvance();\nend\n";
if (!File.Exists(templatePath)) File.WriteAllText(path: templatePath, contents: DEF_TEMPLATE_CONTENTS);
if (!Settings.WarnedOnceOnOverwrite && File.Exists(result))
{
// the user normally gets an "are you sure you want to overwrite" message from the OS
// but some newcomer users seem to think the New Script button is for opening up scripts
// mostly due to weird behavior in other emulators with their lua implementations
// we'll warn again the first time, clarifying usage then let the OS handle warning the user
Settings.WarnedOnceOnOverwrite = true;
if (!this.ModalMessageBox2("You are about to overwrite an existing Lua script.\n" +
"Keep in mind the \"New Lua Script\" option is for creating a brand new Lua script, not for opening Lua scripts.\n" +
"This warning will not appear again! (the file manager would be warning you about an overwrite anyways)\n" +
"Proceed with overwrite?", "Overwrite", EMsgBoxIcon.Warning, useOKCancel: true))
{
return;
}
}
File.Copy(sourceFileName: templatePath, destFileName: result, overwrite: true);
LuaImp.ScriptList.Add(new LuaFile(Path.GetFileNameWithoutExtension(result), result));
Config!.RecentLua.Add(result);
UpdateDialog();
Process.Start(new ProcessStartInfo
{
Verb = "Open",
FileName = result,
});
AddFileWatches();
}
private void OpenScriptMenuItem_Click(object sender, EventArgs e)
{
var initDir = Config!.PathEntries.LuaAbsolutePath();
Directory.CreateDirectory(initDir);
var result = this.ShowFileMultiOpenDialog(
discardCWDChange: true,
filter: ScriptsAndTextFilesFSFilterSet,
initDir: initDir);
if (result is null) return;
foreach (var file in result)
{
LoadLuaFile(file);
Config.RecentLua.Add(file);
}
UpdateDialog();
}
private void ToggleScriptMenuItem_Click(object sender, EventArgs e)
{
var files = !SelectedFiles.Any() && Settings.ToggleAllIfNoneSelected
? LuaImp.ScriptList
: SelectedFiles;
foreach (var file in files)
{
ToggleLuaScript(file);
}
UpdateDialog();
}
private void EnableLuaFile(LuaFile item)
{
try
{
LuaSandbox.Sandbox(null, () =>
{
LuaImp.SpawnAndSetFileThread(item.Path, item);
LuaSandbox.CreateSandbox(item.Thread, Path.GetDirectoryName(item.Path));
}, () =>
{
item.State = LuaFile.RunState.Disabled;
});
// there used to be a call here which did a redraw of the Gui/OSD, which included a call to `Tools.UpdateToolsAfter` --yoshi
}
catch (IOException)
{
item.State = LuaFile.RunState.Disabled;
WriteLine($"Unable to access file {item.Path}");
}
catch (Exception ex)
{
DialogController.ShowMessageBox(ex.ToString());
}
}
private void PauseScriptMenuItem_Click(object sender, EventArgs e)
{
foreach (var s in SelectedFiles)
{
s.TogglePause();
}
UpdateDialog();
}
private void EditScriptMenuItem_Click(object sender, EventArgs e)
{
foreach (var file in SelectedFiles)
{
Process.Start(new ProcessStartInfo
{
Verb = "Open",
FileName = file.Path
});
}
}
private void RemoveScriptMenuItem_Click(object sender, EventArgs e)
{
var items = SelectedItems.ToList();
if (items.Any())
{
foreach (var item in items)
{
RemoveLuaFile(item);
}
UpdateDialog();
DisplayManager.ClearApiHawkSurfaces();
DisplayManager.OSD.ClearGuiText();
if (!LuaImp.ScriptList.Any(static lf => !lf.IsSeparator)) ResetDrawSurfacePadding(); // just removed last script, reset padding
}
}
private void DuplicateScriptMenuItem_Click(object sender, EventArgs e)
{
if (LuaListView.AnyRowsSelected)
{
var script = SelectedItems.First();
if (script.IsSeparator)
{
LuaImp.ScriptList.Add(LuaFile.SeparatorInstance);
UpdateDialog();
return;
}
var (dir, fileNoExt, _) = script.Path.SplitPathToDirFileAndExt();
var result = this.ShowFileSaveDialog(
fileExt: ".lua",
filter: JustScriptsFSFilterSet,
initDir: dir,
initFileName: $"{fileNoExt} (1)");
if (result is null) return;
string text = File.ReadAllText(script.Path);
File.WriteAllText(result, text);
LuaImp.ScriptList.Add(new LuaFile(Path.GetFileNameWithoutExtension(result), result));
Config!.RecentLua.Add(result);
UpdateDialog();
Process.Start(new ProcessStartInfo
{
Verb = "Open",
FileName = result,
});
}
}
private void ClearConsoleMenuItem_Click(object sender, EventArgs e)
{
ClearOutputWindow();
}
private void InsertSeparatorMenuItem_Click(object sender, EventArgs e)
{
LuaImp.ScriptList.Insert(LuaListView.SelectionStartIndex ?? LuaImp.ScriptList.Count, LuaFile.SeparatorInstance);
UpdateDialog();
}
private void MoveUpMenuItem_Click(object sender, EventArgs e)
{
var indices = LuaListView.SelectedRows.ToList();
if (indices.Count == 0 || indices[0] == 0)
{
return;
}
foreach (var index in indices)
{
var file = LuaImp.ScriptList[index];
LuaImp.ScriptList.Remove(file);
LuaImp.ScriptList.Insert(index - 1, file);
}
var newIndices = indices.Select(t => t - 1);
LuaListView.DeselectAll();
foreach (var i in newIndices)
{
LuaListView.SelectRow(i, true);
}
UpdateDialog();
}
private void MoveDownMenuItem_Click(object sender, EventArgs e)
{
var indices = LuaListView.SelectedRows.ToList();
if (indices.Count == 0
|| indices[^1] == LuaImp.ScriptList.Count - 1) // at end already
{
return;
}
for (var i = indices.Count - 1; i >= 0; i--)
{
var file = LuaImp.ScriptList[indices[i]];
LuaImp.ScriptList.Remove(file);
LuaImp.ScriptList.Insert(indices[i] + 1, file);
}
var newIndices = indices.Select(t => t + 1);
LuaListView.DeselectAll();
foreach (var i in newIndices)
{
LuaListView.SelectRow(i, true);
}
UpdateDialog();
}
private void SelectAllMenuItem_Click(object sender, EventArgs e)
=> LuaListView.ToggleSelectAll();
private void StopAllScriptsMenuItem_Click(object sender, EventArgs e)
{
foreach (var file in LuaImp.ScriptList)
{
DisableLuaScript(file);
}
UpdateDialog();
}
private void RegisteredFunctionsMenuItem_Click(object sender, EventArgs e)
{
if (LuaImp.RegisteredFunctions.Any())
{
var alreadyOpen = false;
foreach (Form form in Application.OpenForms)
{
if (form is LuaRegisteredFunctionsList)
{
alreadyOpen = true;
form.Focus();
}
}
if (!alreadyOpen)
{
new LuaRegisteredFunctionsList((MainForm) MainForm, LuaImp.RegisteredFunctions)
{
StartLocation = this.ChildPointToScreen(LuaListView)
}.Show();
}
}
}
private void OptionsSubMenu_DropDownOpened(object sender, EventArgs e)
{
DisableScriptsOnLoadMenuItem.Checked = Settings.DisableLuaScriptsOnLoad;
ReturnAllIfNoneSelectedMenuItem.Checked = Settings.ToggleAllIfNoneSelected;
ReloadWhenScriptFileChangesMenuItem.Checked = Settings.ReloadOnScriptFileChange;
}
private void DisableScriptsOnLoadMenuItem_Click(object sender, EventArgs e)
{
Settings.DisableLuaScriptsOnLoad ^= true;
}
private void ToggleAllIfNoneSelectedMenuItem_Click(object sender, EventArgs e)
{
Settings.ToggleAllIfNoneSelected ^= true;
}
private void ReloadWhenScriptFileChangesMenuItem_Click(object sender, EventArgs e)
{
Settings.ReloadOnScriptFileChange ^= true;
if (Settings.ReloadOnScriptFileChange)
{
AddFileWatches();
}
else
{
ClearFileWatches();
}
}
private void RegisterToTextEditorsSubMenu_DropDownOpened(object sender, EventArgs e)
{
// Hide until this one is implemented
RegisterNotePadMenuItem.Visible = false;
if (_luaAutoInstaller.IsInstalled(LuaAutocompleteInstaller.TextEditors.Sublime2))
{
if (_luaAutoInstaller.IsBizLuaRegistered(LuaAutocompleteInstaller.TextEditors.Sublime2))
{
RegisterSublimeText2MenuItem.Text = "Sublime Text 2 (installed)";
RegisterSublimeText2MenuItem.SetStyle(FontStyle.Regular);
RegisterSublimeText2MenuItem.Image = Resources.GreenCheck;
}
else
{
RegisterSublimeText2MenuItem.Text = "Sublime Text 2 (detected)";
RegisterSublimeText2MenuItem.SetStyle(FontStyle.Italic);
RegisterSublimeText2MenuItem.Image = null;
}
}
else
{
RegisterSublimeText2MenuItem.Text = "Sublime Text 2";
RegisterSublimeText2MenuItem.SetStyle(FontStyle.Regular);
RegisterSublimeText2MenuItem.Image = null;
}
if (_luaAutoInstaller.IsInstalled(LuaAutocompleteInstaller.TextEditors.NotePad))
{
if (_luaAutoInstaller.IsBizLuaRegistered(LuaAutocompleteInstaller.TextEditors.NotePad))
{
RegisterNotePadMenuItem.Text = "Notepad++ (installed)";
RegisterNotePadMenuItem.SetStyle(FontStyle.Regular);
RegisterNotePadMenuItem.Image = Resources.GreenCheck;
}
else
{
RegisterNotePadMenuItem.Text = "Notepad++ (detected)";
RegisterNotePadMenuItem.SetStyle(FontStyle.Italic);
RegisterNotePadMenuItem.Image = null;
}
}
else
{
RegisterNotePadMenuItem.Text = "Notepad++";
RegisterNotePadMenuItem.SetStyle(FontStyle.Regular);
RegisterNotePadMenuItem.Image = null;
}
}
private void RegisterSublimeText2MenuItem_Click(object sender, EventArgs e)
{
_luaAutoInstaller.InstallBizLua(LuaAutocompleteInstaller.TextEditors.Sublime2, LuaImp.Docs);
}
private void RegisterNotePadMenuItem_Click(object sender, EventArgs e)
{
_luaAutoInstaller.InstallBizLua(LuaAutocompleteInstaller.TextEditors.NotePad, LuaImp.Docs);
}
private void FunctionsListMenuItem_Click(object sender, EventArgs e)
{
new LuaFunctionsForm(LuaImp.Docs).Show();
}
private void OnlineDocsMenuItem_Click(object sender, EventArgs e)
{
Process.Start("https://tasvideos.org/BizHawk/LuaFunctions");
}
private void ScriptListContextMenu_Opening(object sender, CancelEventArgs e)
{
ToggleScriptContextItem.Enabled =
PauseScriptContextItem.Enabled =
EditScriptContextItem.Enabled =
SelectedFiles.Any();
StopAllScriptsContextItem.Visible =
ScriptContextSeparator.Visible =
LuaImp.ScriptList.Any(file => file.Enabled);
ClearRegisteredFunctionsContextItem.Enabled =
LuaImp.RegisteredFunctions.Any();
}
private void ConsoleContextMenu_Opening(object sender, CancelEventArgs e)
{
RegisteredFunctionsContextItem.Enabled = LuaImp.RegisteredFunctions.Any();
CopyContextItem.Enabled = OutputBox.SelectedText.Any();
ClearConsoleContextItem.Enabled =
SelectAllContextItem.Enabled =
OutputBox.Text.Any();
ClearRegisteredFunctionsLogContextItem.Enabled =
LuaImp.RegisteredFunctions.Any();
}
private void ClearConsoleContextItem_Click(object sender, EventArgs e)
{
ClearOutputWindow();
}
private void SelectAllContextItem_Click(object sender, EventArgs e)
{
if (!OutputBox.IsHandleCreated || OutputBox.IsDisposed)
{
return;
}
OutputBox.Invoke(() =>
{
OutputBox.SelectAll();
OutputBox.Refresh();
});
}
private void CopyContextItem_Click(object sender, EventArgs e)
{
if (!OutputBox.IsHandleCreated || OutputBox.IsDisposed)
{
return;
}
OutputBox.Invoke(() =>
{
OutputBox.Copy();
OutputBox.Refresh();
});
}
private void ClearRegisteredFunctionsContextMenuItem_Click(object sender, EventArgs e)
{
LuaImp.RegisteredFunctions.Clear(Emulator);
}
public bool LoadByFileExtension(string path, out bool abort)
{
var ext = Path.GetExtension(path)?.ToLowerInvariant();
if (ext is ".luases")
{
LoadLuaSession(path);
abort = true;
return true;
}
abort = false;
if (ext is ".lua" or ".txt")
{
LoadLuaFile(path);
UpdateDialog();
return true;
}
return false;
}
private void LuaConsole_DragDrop(object sender, DragEventArgs e)
{
var filePaths = (string[])e.Data.GetData(DataFormats.FileDrop);
try
{
foreach (var path in filePaths)
{
_ = LoadByFileExtension(path, out var abort);
if (abort) return;
}
}
catch (Exception ex)
{
DialogController.ShowMessageBox(ex.ToString());
}
}
private void LuaListView_KeyDown(object sender, KeyEventArgs e)
{
if (e.IsPressed(Keys.Delete))
{
RemoveScriptMenuItem_Click(null, null);
}
else if (e.IsCtrl(Keys.A))
{
SelectAllMenuItem_Click(null, null);
}
else if (e.IsPressed(Keys.F12))
{
RegisteredFunctionsMenuItem_Click(null, null);
}
}
private void OutputBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.IsPressed(Keys.F12))
{
RegisteredFunctionsMenuItem_Click(null, null);
}
}
/// <summary>
/// Sorts the column Ascending on the first click and Descending on the second click.
/// </summary>
private void LuaListView_ColumnClick(object sender, InputRoll.ColumnClickEventArgs e)
{
var columnToSort = e.Column.Name;
var luaListTemp = new List<LuaFile>();
if (columnToSort != _lastColumnSorted)
{
_sortReverse = false;
}
// For getting the name of the .lua file, for some reason this field is kept blank in LuaFile.cs?
// The Name variable gets emptied again near the end just in case it would break something.
for (var i = 0; i < LuaImp.ScriptList.Count; i++)
{
var words = Regex.Split(LuaImp.ScriptList[i].Path, ".lua");
var split = words[0].Split(Path.DirectorySeparatorChar);
luaListTemp.Add(LuaImp.ScriptList[i]);
luaListTemp[i].Name = split[^1];
}
// Script, Path
switch (columnToSort)
{
case "Script":
luaListTemp = luaListTemp
.OrderBy(lf => lf.Name, _sortReverse)
.ThenBy(lf => lf.Path)
.ToList();
break;
case "PathName":
luaListTemp = luaListTemp
.OrderBy(lf => lf.Path, _sortReverse)
.ThenBy(lf => lf.Name)
.ToList();
break;
}
for (var i = 0; i < LuaImp.ScriptList.Count; i++)
{
LuaImp.ScriptList[i] = luaListTemp[i];
LuaImp.ScriptList[i].Name = "";
}
UpdateDialog();
_lastColumnSorted = columnToSort;
_sortReverse = !_sortReverse;
}
private void RefreshScriptMenuItem_Click(object sender, EventArgs e)
{
var files = !SelectedFiles.Any() && Settings.ToggleAllIfNoneSelected
? LuaImp.ScriptList
: SelectedFiles;
foreach (var file in files) RefreshLuaScript(file);
UpdateDialog();
}
private void InputBox_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Enter)
{
string consoleBeforeCall = OutputBox.Text;
// TODO: Maybe make these try-catches more general
if (!string.IsNullOrWhiteSpace(InputBox.Text))
{
if (InputBox.Text.Contains("emu.frameadvance("))
{
WriteLine("emu.frameadvance() can not be called from the console");
return;
}
LuaSandbox.Sandbox(null, () =>
{
LuaImp.ExecuteString($"console.log({InputBox.Text})");
}, () =>
{
LuaSandbox.Sandbox(null, () =>
{
LuaImp.ExecuteString(InputBox.Text);
if (OutputBox.Text == consoleBeforeCall)
{
WriteLine("Command successfully executed");
}
});
});
_messageCount = 0;
_consoleCommandHistory.Insert(0, InputBox.Text);
_consoleCommandHistoryIndex = -1;
InputBox.Clear();
}
}
else if (e.KeyCode == Keys.Up)
{
if (_consoleCommandHistoryIndex < _consoleCommandHistory.Count - 1)
{
_consoleCommandHistoryIndex++;
InputBox.Text = _consoleCommandHistory[_consoleCommandHistoryIndex];
InputBox.Select(InputBox.Text.Length, 0);
}
e.Handled = true;
}
else if (e.KeyCode == Keys.Down)
{
if (_consoleCommandHistoryIndex == 0)
{
_consoleCommandHistoryIndex--;
InputBox.Text = "";
}
else if (_consoleCommandHistoryIndex > 0)
{
_consoleCommandHistoryIndex--;
InputBox.Text = _consoleCommandHistory[_consoleCommandHistoryIndex];
InputBox.Select(InputBox.Text.Length, 0);
}
e.Handled = true;
}
else if (e.KeyCode == Keys.Tab)
{
ProcessTabKey(false);
e.Handled = true;
}
}
// For whatever reason an auto-complete TextBox doesn't respond to delete
// Which is annoying but worse is that it let's the key propagate
// If a script is highlighted in the ListView, and the user presses
// delete, it will remove the script without this hack
protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
{
if (keyData == Keys.Delete && InputBox.Focused)
{
return true;
}
return base.ProcessCmdKey(ref msg, keyData);
}
protected override bool ProcessTabKey(bool forward)
{
// TODO: Make me less dirty (please)
return false;
}
private void EraseToolbarItem_Click(object sender, EventArgs e)
{
DisplayManager.ClearApiHawkSurfaces();
}
// Stupid designer
protected void DragEnterWrapper(object sender, DragEventArgs e)
{
GenericDragEnter(sender, e);
}
private void LuaListView_DoubleClick(object sender, EventArgs e)
{
var index = LuaListView.CurrentCell?.RowIndex;
if (index < LuaImp.ScriptList.Count)
{
var file = LuaImp.ScriptList[index.Value];
ToggleLuaScript(file);
UpdateDialog();
}
}
private void ToggleLuaScript(LuaFile file)
{
if (file.IsSeparator)
{
return;
}
file.Toggle();
_lastScriptUsed = file;
if (file.Enabled && file.Thread is null)
{
LuaImp.RegisteredFunctions.RemoveForFile(file, Emulator); // First remove any existing registered functions for this file
EnableLuaFile(file);
}
else if (!file.Enabled && file.Thread is not null)
{
DisableLuaScript(file);
// there used to be a call here which did a redraw of the Gui/OSD, which included a call to `Tools.UpdateToolsAfter` --yoshi
}
LuaListView.Refresh();
}
private void DisableLuaScript(LuaFile file)
{
if (file.IsSeparator) return;
file.State = LuaFile.RunState.Disabled;
if (file.Thread is not null)
{
LuaImp.CallExitEvent(file);
LuaImp.RegisteredFunctions.RemoveForFile(file, Emulator);
file.Stop();
}
}
private void RefreshLuaScript(LuaFile file)
{
ToggleLuaScript(file);
ToggleLuaScript(file);
}
[RestoreDefaults]
private void RestoreDefaults()
{
Settings = new LuaConsoleSettings();
LuaListView.AllColumns.Clear();
SetColumns();
splitContainer1.SplitterDistance = _defaultSplitDistance;
UpdateDialog();
}
}
}