forked from KSP-CKAN/CKAN
/
ScreenContainer.cs
307 lines (274 loc) · 11 KB
/
ScreenContainer.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
using System;
using System.Collections.Generic;
namespace CKAN.ConsoleUI.Toolkit {
/// <summary>
/// Common base class of ConsoleScreen and ConsoleDialog.
/// Encapsulates the logic for managing a group of ScreenObjects.
/// </summary>
public abstract class ScreenContainer {
/// <summary>
/// Initialize the container
/// </summary>
protected ScreenContainer()
{
AddBinding(Keys.CtrlL, (object sender, ConsoleTheme theme) => {
// Just redraw everything and keep running
DrawBackground(theme);
return true;
});
}
/// <summary>
/// Draw the contained screen objects and manage their interaction
/// </summary>
/// <param name="theme">The visual theme to use to draw the dialog</param>
/// <param name="process">Logic to drive the screen, default is normal user interaction</param>
public virtual void Run(ConsoleTheme theme, Action<ConsoleTheme> process = null)
{
DrawBackground(theme);
if (process == null) {
// This should be a simple default parameter, but C# has trouble
// with that for instance delegates.
process = Interact;
} else {
// Other classes can't call Draw directly, so do it for them once.
// Would be nice to make this cleaner somehow.
Draw(theme);
}
// Run the actual logic for the container
process(theme);
ClearBackground();
}
/// <summary>
/// Add a ScreenObject for inclusion in this display
/// </summary>
/// <param name="so">ScreenObject to Add</param>
protected void AddObject(ScreenObject so)
{
objects.Add(so);
so.OnBlur += Blur;
}
/// <summary>
/// Delegate type for key bindings
/// </summary>
public delegate bool KeyAction(object sender, ConsoleTheme theme);
/// <summary>
/// Bind an action to a key
/// </summary>
/// <param name="k">Key to bind</param>
/// <param name="a">Action to bind to the key</param>
public void AddBinding(ConsoleKeyInfo k, KeyAction a)
{
if (bindings.ContainsKey(k)) {
bindings[k] = a;
} else {
bindings.Add(k, a);
}
}
/// <summary>
/// Add custom key bindings
/// </summary>
/// <param name="keys">Keys to bind</param>
/// <param name="a">Action to bind to key</param>
public void AddBinding(IEnumerable<ConsoleKeyInfo> keys, KeyAction a)
{
foreach (ConsoleKeyInfo k in keys) {
AddBinding(k, a);
}
}
/// <summary>
/// Add a screen tip to show in the FooterBg
/// </summary>
/// <param name="key">User readable description of the key</param>
/// <param name="descrip">Description of the action</param>
/// <param name="displayIf">Function returning true to show the tip or false to hide it</param>
public void AddTip(string key, string descrip, Func<bool> displayIf = null)
{
if (displayIf == null) {
displayIf = () => true;
}
tips.Add(new ScreenTip(key, descrip, displayIf));
}
/// <summary>
/// Draw the basic background elements of the display.
/// Called once at the beginning and then again later if we need to reset the display.
/// NOT called every tick, to reduce flickering.
/// </summary>
protected virtual void DrawBackground(ConsoleTheme theme) { }
/// <summary>
/// Reset the display, called when closing it.
/// </summary>
protected virtual void ClearBackground() { }
/// <summary>
/// Draw all the contained ScreenObjects.
/// Also places the cursor where it should be.
/// </summary>
protected void Draw(ConsoleTheme theme)
{
lock (screenLock) {
Console.CursorVisible = false;
DrawFooter(theme);
for (int i = 0; i < objects.Count; ++i) {
objects[i].Draw(theme, i == focusIndex);
}
if (objects.Count > 0
&& focusIndex >= 0
&& focusIndex < objects.Count
&& objects[focusIndex].Focusable()) {
objects[focusIndex].PlaceCursor();
Console.CursorVisible = true;
} else {
Console.CursorVisible = false;
}
}
}
/// <summary>
/// Standard driver function for normal screen interaction.
/// Draws the screen and reads keys till done.
/// Each key is checked against the local bindings,
/// then the bindings of the focused ScreenObject.
/// Stops when 'done' is true.
/// </summary>
protected void Interact(ConsoleTheme theme)
{
focusIndex = -1;
Blur(null, true);
do {
Draw(theme);
ConsoleKeyInfo k = Console.ReadKey(true);
if (bindings.ContainsKey(k)) {
done = !bindings[k](this, theme);
} else if (objects.Count > 0) {
if (objects[focusIndex].Bindings.ContainsKey(k)) {
done = !objects[focusIndex].Bindings[k](this, theme);
} else {
objects[focusIndex].OnKeyPress(k);
}
}
} while (!done);
}
/// <summary>
/// Set our private 'done' flag to indicate that Interact should stop.
/// </summary>
protected void Quit() { done = true; }
/// <returns>
/// Currently focused ScreenObject
/// </returns>
protected ScreenObject Focused()
{
if (focusIndex >= 0 && focusIndex < objects.Count) {
return objects[focusIndex];
} else {
return null;
}
}
/// <summary>
/// Set the focus to a given ScreenObject
/// </summary>
/// <param name="so">ScreenObject to focus</param>
protected void SetFocus(ScreenObject so)
{
int index = objects.IndexOf(so);
if (index >= 0) {
focusIndex = index;
}
}
private void DrawFooter(ConsoleTheme theme)
{
Console.SetCursorPosition(0, Console.WindowHeight - 1);
Console.BackgroundColor = theme.FooterBg;
Console.Write(" ");
var tipLists = new List<List<ScreenTip>>() { tips };
if (objects.Count > 0) {
tipLists.Add(objects[focusIndex].Tips);
}
bool first = true;
foreach (var tipList in tipLists) {
for (int i = 0; i < tipList.Count; ++i) {
if (tipList[i].DisplayIf()) {
if (Console.CursorLeft + tipSeparator.Length + tipList[i].Key.Length + 5 > Console.WindowWidth) {
// Stop drawing if not even enough room for the key
break;
}
if (first) {
first = false;
} else {
Console.ForegroundColor = theme.FooterSeparatorFg;
Console.Write(tipSeparator);
}
Console.ForegroundColor = theme.FooterKeyFg;
Console.Write(tipList[i].Key);
Console.ForegroundColor = theme.FooterDescriptionFg;
string remainder;
if (tipList[i].Key == tipList[i].Description.Substring(0, 1)) {
remainder = tipList[i].Description.Substring(1);
} else {
remainder = $" - {tipList[i].Description}";
}
int maxW = Console.WindowWidth - Console.CursorLeft - 1;
if (remainder.Length > maxW && maxW > 0) {
Console.Write(remainder.Substring(0, maxW));
} else {
Console.Write(remainder);
}
}
}
}
// Windows cmd.exe auto-scrolls the whole window if you draw a
// character in the bottom right corner :(
Console.Write("".PadLeft(Console.WindowWidth - Console.CursorLeft - 1));
}
private void Blur(ScreenObject source, bool forward)
{
if (objects.Count > 0) {
int loops = 0;
do {
if (++loops > objects.Count) {
focusIndex = 0;
break;
}
if (forward) {
focusIndex = (focusIndex + 1) % objects.Count;
} else {
focusIndex = (focusIndex + objects.Count - 1) % objects.Count;
}
} while (!objects[focusIndex].Focusable());
}
}
private bool done = false;
private List<ScreenObject> objects = new List<ScreenObject>();
private int focusIndex = 0;
private Dictionary<ConsoleKeyInfo, KeyAction> bindings = new Dictionary<ConsoleKeyInfo, KeyAction>();
private List<ScreenTip> tips = new List<ScreenTip>();
private object screenLock = new object();
private static readonly string tipSeparator = $" {Symbols.vertLine} ";
}
/// <summary>
/// Object representing a tip to be shown in the footer
/// </summary>
public class ScreenTip {
/// <summary>
/// Initialize the object
/// </summary>
/// <param name="key">Description of the keypress</param>
/// <param name="descrip">Description of the bound action</param>
/// <param name="dispIf">Function that returns true to display the tip or false to hide it</param>
public ScreenTip(string key, string descrip, Func<bool> dispIf = null)
{
Key = key;
Description = descrip;
DisplayIf = dispIf != null ? dispIf : () => true;
}
/// <summary>
/// Description of the keypress
/// </summary>
public readonly string Key;
/// <summary>
/// Description of the bound action
/// </summary>
public readonly string Description;
/// <summary>
/// Function that returns true to display the tip or false to hide it
/// </summary>
public readonly Func<bool> DisplayIf;
}
}