Added support for on-the-fly textview contents updates Added iterative view removal to ConsoleController Added dumy layouts to Common Added support for account removal Fixed command management (now supports leading, padding and trailing spaces) Various smaller changes
334 lines
13 KiB
C#
334 lines
13 KiB
C#
using Client.ConsoleForms.Graphics;
|
|
using Client.ConsoleForms.Parameters;
|
|
using Client.Properties;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
|
|
namespace Client.ConsoleForms
|
|
{
|
|
// Handles graphics and rendering instrumentation
|
|
public sealed class ConsoleController
|
|
{
|
|
public class KeyEvent
|
|
{
|
|
public bool ValidEvent { get; set; }
|
|
public ConsoleKeyInfo Event { get; }
|
|
|
|
internal KeyEvent(ConsoleKeyInfo info) { Event = info; ValidEvent = true; }
|
|
}
|
|
|
|
public static readonly ConsoleController singleton = new ConsoleController();
|
|
|
|
private readonly List<Tuple<View, LayoutMeta>> renderQueue = new List<Tuple<View, LayoutMeta>>();
|
|
private int width = Console.WindowWidth, height = Console.WindowHeight;
|
|
private CancellationPipe cancel;
|
|
private Task resizeListener;
|
|
|
|
public bool Dirty
|
|
{
|
|
get
|
|
{
|
|
Region occlusion = new Region();
|
|
for (int i = renderQueue.Count - 1; i >= 0; --i)
|
|
{
|
|
Tuple<int, int> lParams = renderQueue[i].Item2.ComputeLayoutParams(width, height);
|
|
Region test = renderQueue[i].Item1.Occlusion;
|
|
if (renderQueue[i].Item1.Dirty && test.Subtract(occlusion).Area > 0)
|
|
return true;
|
|
else occlusion = occlusion.Add(test);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
public bool ShouldExit { get; set; }
|
|
|
|
private ConsoleController(bool resizeListener = true)
|
|
{
|
|
if (resizeListener) EnableResizeListener();
|
|
RegisterListener((w, h) =>
|
|
{
|
|
// Corrective resizing to prevent rendering issues
|
|
if (w < 20 || h < 20)
|
|
{
|
|
Console.SetWindowSize(Math.Max(w, 60), Math.Max(h, 40));
|
|
return;
|
|
}
|
|
width = w;
|
|
height = h;
|
|
Draw();
|
|
});
|
|
|
|
RegisterListener((w1, h1, w2, h2) =>
|
|
{
|
|
// Corrective resizing to prevent rendering issues
|
|
if (w2 < 20 || h2 < 20)
|
|
Console.SetWindowSize(Math.Max(w2, 60), Math.Max(h2, 40));
|
|
Console.BackgroundColor = ConsoleColor.Black;
|
|
Console.Clear();
|
|
});
|
|
}
|
|
|
|
public void AddView(View v, bool redraw = true) => AddView(v, LayoutMeta.Centering(v), redraw);
|
|
public void AddView(View v, LayoutMeta meta, bool redraw = true)
|
|
{
|
|
renderQueue.Add(new Tuple<View, LayoutMeta>(v, meta));
|
|
Draw(false);
|
|
}
|
|
|
|
public void CloseIf(Predicate<View> p)
|
|
{
|
|
for(int i = renderQueue.Count - 1; i>=0; --i)
|
|
if (p(renderQueue[i].Item1))
|
|
CloseView(i);
|
|
}
|
|
public void CloseTop() => CloseView(renderQueue[renderQueue.Count - 1].Item1);
|
|
public void CloseView(int idx) => CloseView(renderQueue[idx].Item1);
|
|
public void CloseView(View v, bool redraw = true, int maxCloses = -1)
|
|
{
|
|
if (maxCloses == 0) return;
|
|
|
|
Region r = new Region();
|
|
bool needsRedraw = false;
|
|
int closed = 0;
|
|
for (int i = renderQueue.Count - 1; i >= 0; --i)
|
|
if (renderQueue[i].Item1.Equals(v))
|
|
{
|
|
// Compute occlusion region
|
|
Region test = renderQueue[i].Item1.Occlusion;
|
|
test.Offset(renderQueue[i].Item2.ComputeLayoutParams(width, height));
|
|
Region removing = test.Subtract(r);
|
|
needsRedraw |= removing.Area > 0;
|
|
|
|
// Check whether or not view is completely occluded: if it is not, a redraw is required, else redraw isn't necessary
|
|
Region cmp;
|
|
for (int j = i - 1; !needsRedraw && j >= 0; --j)
|
|
needsRedraw |= (cmp = renderQueue[j].Item1.Occlusion).Subtract(removing).Area != cmp.Area;
|
|
|
|
// Trigger close event (immediately before closing)
|
|
v.OnClose?.Invoke(v);
|
|
|
|
// Remove view from renderqueue and clear it from the screen
|
|
renderQueue.RemoveAt(i);
|
|
ClearRegion(removing);
|
|
if (++closed == maxCloses) break;
|
|
}
|
|
|
|
// Redraw if necessary
|
|
if (redraw && needsRedraw) Draw(false);
|
|
}
|
|
|
|
public void Draw() => Draw(false);
|
|
|
|
// downTo allows for partial rendering updates
|
|
private void Draw(bool ignoreOcclusion, int downTo = 0)
|
|
{
|
|
if (downTo < 0) downTo = 0;
|
|
if (downTo >= renderQueue.Count) return;
|
|
Console.CursorVisible = false;
|
|
byte[] occlusionMap = new byte[(renderQueue.Count / 8) + (renderQueue.Count % 8 != 0 ? 1 : 0)];
|
|
Stack<Tuple<int, int>> layoutParams = new Stack<Tuple<int, int>>();
|
|
|
|
Region occlusion = new Region();
|
|
for (int i = renderQueue.Count - 1; i >= downTo; --i)
|
|
{
|
|
Tuple<int, int> lParams = renderQueue[i].Item2.ComputeLayoutParams(width, height);
|
|
if (!ignoreOcclusion)
|
|
{
|
|
Region test = renderQueue[i].Item1.Occlusion;
|
|
test.Offset(lParams);
|
|
if (test.Subtract(occlusion).Area == 0)
|
|
occlusionMap[i / 8] |= (byte)(1 << (i % 8));
|
|
else
|
|
{
|
|
occlusion = occlusion.Add(test);
|
|
layoutParams.Push(lParams);
|
|
}
|
|
}
|
|
else layoutParams.Push(lParams);
|
|
}
|
|
|
|
for (int i = downTo; i < renderQueue.Count; ++i)
|
|
if ((occlusionMap[i / 8] & (1 << (i % 8))) == 0)
|
|
renderQueue[i].Item1.Draw(layoutParams.Pop());
|
|
}
|
|
|
|
public KeyEvent ReadKey(bool redrawOnDirty = true)
|
|
{
|
|
KeyEvent keyInfo = new KeyEvent(Console.ReadKey(true));
|
|
int lowestDirty = renderQueue.Count;
|
|
int count = renderQueue.Count - 1;
|
|
for (int i = count; i >= 0; --i)
|
|
if (renderQueue[i].Item1.HandleKeyEvent(keyInfo, i == count, false))
|
|
lowestDirty = i;
|
|
if (redrawOnDirty) Draw(false, lowestDirty);
|
|
return keyInfo;
|
|
}
|
|
|
|
public void EnableResizeListener()
|
|
{
|
|
if (cancel != null) return;
|
|
// Set up console window resize listener
|
|
cancel = new CancellationPipe();
|
|
resizeListener = new Task(() => ConsoleResizeListener(cancel)); // Start resize listener asynchronously
|
|
resizeListener.Start();
|
|
}
|
|
|
|
public async void DisableResizeListener()
|
|
{
|
|
if (cancel == null) return;
|
|
cancel.Cancel();
|
|
await resizeListener;
|
|
}
|
|
|
|
|
|
public delegate void WindowChangeListener(int fromWidth, int fromHeight, int toWidth, int toHeight);
|
|
public delegate void WindowChangeCompleteListener(int width, int height);
|
|
|
|
private readonly List<WindowChangeListener> changeListeners = new List<WindowChangeListener>();
|
|
private readonly List<WindowChangeCompleteListener> completeListeners = new List<WindowChangeCompleteListener>();
|
|
|
|
public void RegisterListener(WindowChangeListener listener) => changeListeners.Add(listener);
|
|
public void RegisterListener(WindowChangeCompleteListener listener) => completeListeners.Add(listener);
|
|
public void UnRegisterListener(WindowChangeListener listener) => changeListeners.RemoveAll(p => p == listener);
|
|
public void UnRegisterListener(WindowChangeCompleteListener listener) => completeListeners.RemoveAll(p => p == listener);
|
|
|
|
private void ConsoleResizeListener(CancellationPipe cancel)
|
|
{
|
|
int consoleWidth = Console.WindowWidth;
|
|
int consoleHeight = Console.WindowHeight;
|
|
bool trigger = false;
|
|
int trigger_inc = 0;
|
|
|
|
while (!cancel.Cancelled)
|
|
{
|
|
int readWidth = Console.WindowWidth;
|
|
int readHeight = Console.WindowHeight;
|
|
if (readWidth != consoleWidth || readHeight != consoleHeight)
|
|
{
|
|
trigger = true;
|
|
foreach (var listener in changeListeners) listener(consoleWidth, consoleHeight, readWidth, readHeight);
|
|
consoleWidth = readWidth;
|
|
consoleHeight = readHeight;
|
|
}
|
|
else if (trigger && ++trigger_inc >= 5)
|
|
{
|
|
foreach (var listener in completeListeners) listener(consoleWidth, consoleHeight);
|
|
trigger = false;
|
|
}
|
|
System.Threading.Thread.Sleep(50);
|
|
}
|
|
}
|
|
|
|
public static void ClearRegion(Region r, ConsoleColor clearColor = ConsoleColor.Black)
|
|
{
|
|
foreach (var rect in r.SubRegions) ClearRegion(rect, clearColor);
|
|
}
|
|
|
|
public static void ClearRegion(Rectangle rect, ConsoleColor clearColor = ConsoleColor.Black)
|
|
{
|
|
Console.BackgroundColor = clearColor;
|
|
Console.ForegroundColor = ConsoleColor.White;
|
|
for (int i = rect.Top; i < rect.Bottom; ++i)
|
|
{
|
|
Console.SetCursorPosition(rect.Left, i);
|
|
for (int j = rect.Right - rect.Left; j > 0; --j)
|
|
Console.Write(' ');
|
|
}
|
|
}
|
|
|
|
private static ViewData DoElementParse(XmlNode el, LangManager lang)
|
|
{
|
|
ViewData data = new ViewData(el.LocalName, lang.MapIfExists(el.InnerText));
|
|
|
|
if (el.Attributes != null)
|
|
foreach (var attr in el.Attributes)
|
|
if (attr is XmlAttribute)
|
|
data.attributes[((XmlAttribute)attr).Name] = ((XmlAttribute)attr).Value;
|
|
|
|
if (el.ChildNodes != null)
|
|
foreach (var child in el.ChildNodes)
|
|
if (child is XmlNode) data.nestedData.Add(DoElementParse((XmlNode)child, lang));
|
|
|
|
return data;
|
|
}
|
|
|
|
private static Dictionary<string, List<Tuple<string, View>>> cache = new Dictionary<string, List<Tuple<string, View>>>();
|
|
|
|
public static List<Tuple<string, View>> LoadResourceViews(string name, LangManager lang, bool doCache = true)
|
|
{
|
|
if (cache.ContainsKey(name))
|
|
return cache[name];
|
|
|
|
PropertyInfo[] properties = typeof(Resources).GetProperties(BindingFlags.NonPublic | BindingFlags.Static);
|
|
foreach (var prop in properties)
|
|
if (prop.Name.Equals(name) && prop.PropertyType.Equals(typeof(string)))
|
|
return LoadViews((string)prop.GetValue(null), lang, doCache ? name : null);
|
|
throw new SystemException($"Resource { name } could not be located!");
|
|
}
|
|
public static List<Tuple<string, View>> LoadViews(string xml, LangManager lang, string cacheID = null)
|
|
{
|
|
if (cacheID != null && cache.ContainsKey(cacheID))
|
|
return cache[cacheID];
|
|
|
|
XmlDocument doc = new XmlDocument();
|
|
doc.LoadXml(xml);
|
|
|
|
string ns = doc.FirstChild.NextSibling.Attributes != null ? doc.FirstChild.NextSibling.Attributes.GetNamedItem("xmlns")?.Value ?? "" : "";
|
|
|
|
List<Tuple<string, View>> views = new List<Tuple<string, View>>();
|
|
|
|
foreach (var child in doc.FirstChild.NextSibling.ChildNodes)
|
|
if (!(child is XmlNode) || child is XmlComment) continue;
|
|
else views.Add(LoadView(ns, DoElementParse((XmlNode)child, lang), lang));
|
|
|
|
if (cacheID != null) cache[cacheID] = views;
|
|
|
|
return views;
|
|
}
|
|
|
|
public static Tuple<string, View> LoadView(string ns, ViewData data, LangManager lang)
|
|
{
|
|
Type type;
|
|
try { type = Type.GetType(ns + '.' + data.Name, true); }
|
|
catch { type = Type.GetType(data.Name, true); }
|
|
|
|
ConstructorInfo info = type.GetConstructor(new Type[] { typeof(ViewData), typeof(LangManager) });
|
|
|
|
string id = data.attributes.ContainsKey("id") ? data.attributes["id"] : "";
|
|
data.attributes["xmlns"] = ns;
|
|
|
|
return new Tuple<string, View>(id, (View)info.Invoke(new object[] { data, lang }));
|
|
}
|
|
|
|
public delegate void Runnable();
|
|
public void Popup(string message, long timeout, ConsoleColor borderColor = ConsoleColor.Blue, Runnable onExpire = null)
|
|
{
|
|
TextView popup = new TextView(
|
|
new ViewData("ConsoleForms.TextBox")
|
|
.SetAttribute("padding_left", 2)
|
|
.SetAttribute("padding_right", 2)
|
|
.SetAttribute("padding_top", 1)
|
|
.SetAttribute("padding_bottom", 1)
|
|
.AddNested(new ViewData("Text", message)), // Add message
|
|
LangManager.NO_LANG
|
|
)
|
|
{
|
|
BackgroundColor = ConsoleColor.White,
|
|
TextColor = ConsoleColor.Black,
|
|
BorderColor = borderColor,
|
|
};
|
|
|
|
AddView(popup, LayoutMeta.Centering(popup));
|
|
|
|
new Timer(() => {
|
|
CloseView(popup);
|
|
onExpire?.Invoke();
|
|
}, timeout).Start();
|
|
|
|
}
|
|
}
|
|
}
|