GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALControls.cs
1// =============================================================================
2// GPAL - Generally Positive Automation Library
3// Copyright © 2026 Software Decisions, Inc. All rights reserved.
4//
5// This file is part of GPAL.
6// Licensed under the Business Source License 1.1
7//
8// Primary development, architecture, and vision by Michael B. Vederman,
9// CEO of Software Decisions, Inc., Texas.
10//
11// Internal development maintained privately.
12// Public releases appear on GitHub: https://github.com/SoftwareDecisionsInc/GPAL.
13//
14// See LICENSE for full terms, including Additional Use Grant.
15// =============================================================================
16
17using System;
18using System.Collections.Concurrent;
19using System.Collections.Generic;
20using System.Drawing;
21using System.Linq;
22using System.Text;
23using System.Text.RegularExpressions;
24using System.Threading.Tasks;
25using System.Data;
26using System.Windows.Forms;
27using System.Windows.Forms.DataVisualization.Charting;
28using static GenerallyPositive.Enums;
29
31{
36 {
37 }
38 #region <GPAL Controls>
43 {
48 public Font FontType { get; internal set; } = new Font("Georgia", 12);
53 public ContentAlignment TextAlignment { get; internal set; } = ContentAlignment.MiddleLeft;
58 public bool AutoSize { get; internal set; } = true;
64 //public ContentAlignment ControlAlignment { get; internal set; } = ContentAlignment.MiddleLeft;
71 {
72 FontType = font;
73 return this;
74 }
75
80 public IAllowControlSettingsAndFontSettings WithTextAlignment(ContentAlignment textAlignment)
81 {
82 TextAlignment = textAlignment;
83 return this;
84 }
85
92 {
93 AutoSize = autoSize;
94 return this;
95 }
96
103 //public IAllowControlSettings WithAlignment(ContentAlignment alignment)
104 //{
105 // throw new NotImplementedException();
106 //}
111 public int Height { get; internal set; } = 0;
116 public int Width { get; internal set; } = 0;
117 private string _text;
130 public string Text
131 {
132 get => _text;
133 set
134 {
135 _text = value;
136 if (WindowsControl is Control ctrl && true == Alive(ctrl))
137 {
138 PostToUi(ctrl, () => ctrl.Text = value);
139 }
140 }
141 }
142
146 public string Name { get; internal set; }
150 public object Tag { get; internal set; }
155 public dynamic WindowsControl
156 {
157 get => _windowsControl;
158 internal set
159 {
160 _windowsControl = value;
161
162 // anything a workflow did before the form was built was held rather than dropped, and this is
163 // the moment it can be done for real
164 if (null != value)
165 Realized();
166 }
167 }
168
169 private dynamic _windowsControl;
170
175 internal virtual void Realized()
176 {
177 // the text controls hold their text in a field of their own and the form writes that field into
178 // the control it builds, so a line appended before then is already on screen. Its queued copy is
179 // dropped rather than appended again
180 while (true == pendingLines.TryDequeue(out string _))
181 {
182 }
183 }
184
185 public bool IsReady => WindowsControl != null;
186
187 // Line appending, shared by the text controls. Held on the base because the marshalling and the
188 // not-yet-created-handle case are the same for all of them
189 private readonly ConcurrentQueue<string> pendingLines = new ConcurrentQueue<string>();
190 private readonly object appendGate = new object();
191 private bool waitingForHandle = false;
192
193 internal void AppendLineCore(string text, int maxLines)
194 {
195 // Queued here and written only on the UI thread. Called from whichever thread published, which
196 // during a workflow is a worker while the UI thread is blocked inside Run
197 pendingLines.Enqueue(text);
198
199 DrainLines(maxLines);
200 }
201
202 // TextBoxBase, the type GPALTextArea and GPALRichTextBox share
203 private void DrainLines(int maxLines)
204 {
205 // A closing form still publishes, and posting to a disposed control makes Windows Forms recreate
206 // its handle
207 if (!(WindowsControl is TextBoxBase tb) || false == Alive(tb)) return;
208
209 // No handle means created but not yet shown, since the disposed case is handled above. The lines
210 // stay queued and HandleCreated drains them
211 if (false == tb.IsHandleCreated)
212 {
213 lock (appendGate)
214 {
215 if (false == waitingForHandle)
216 {
217 waitingForHandle = true;
218 tb.HandleCreated += (sender, args) => DrainLines(maxLines);
219 }
220 }
221
222 return;
223 }
224
225 MethodInvoker drain = () =>
226 {
227 // Posted while the handle existed and run after it went
228 if (false == Alive(tb) || false == tb.IsHandleCreated) return;
229
230 // the whole queue in one pass. Asking the control for its Lines splits the entire buffer, and
231 // rewriting Text at the cap rebuilds every line, so doing either once per queued line made a
232 // busy workflow take minutes here with the form unable to answer a click
233 List<string> arrived = new List<string>();
234
235 while (true == pendingLines.TryDequeue(out string line))
236 arrived.Add(line);
237
238 if (0 == arrived.Count) return;
239
240 string[] held = 0 < maxLines ? tb.Lines : new string[0];
241
242 if (0 < maxLines && held.Length + arrived.Count > maxLines)
243 {
244 // over the cap, so the tail of what is held plus the batch is written once
245 List<string> keeping = new List<string>(maxLines);
246
247 int roomForHeld = maxLines - Math.Min(arrived.Count, maxLines);
248
249 for (int each = Math.Max(0, held.Length - roomForHeld); each < held.Length; each++)
250 keeping.Add(held[each]);
251
252 for (int each = Math.Max(0, arrived.Count - maxLines); each < arrived.Count; each++)
253 keeping.Add(arrived[each]);
254
255 tb.Text = string.Join("\r\n", keeping);
256 }
257 else
258 {
259 if (0 < tb.TextLength) tb.AppendText("\r\n");
260
261 tb.AppendText(string.Join("\r\n", arrived));
262 }
263
264 tb.SelectionStart = tb.TextLength;
265 tb.ScrollToCaret();
266 };
267 // Posted rather than invoked. A workflow started from a button holds the UI thread, so an invoke
268 // from a worker would block until it finished
269 PostToUi(tb, drain);
270 }
271
272 internal void ClearLinesCore()
273 {
274 // The queue is emptied too, or lines held during a run arrive after the clear
275 while (true == pendingLines.TryDequeue(out string dropped)) { }
276
277 if (!(WindowsControl is TextBoxBase tb) || false == Alive(tb)) return;
278
279 if (false == tb.IsHandleCreated)
280 {
281 tb.Text = string.Empty;
282 return;
283 }
284
285 MethodInvoker clear = () =>
286 {
287 if (false == Alive(tb) || false == tb.IsHandleCreated) return;
288
289 tb.Clear();
290 };
291
292 PostToUi(tb, clear);
293 }
294
295 internal void ScrollToEndCore()
296 {
297 if (!(WindowsControl is TextBoxBase tb) || false == Alive(tb)) return;
298
299 PostToUi(tb, () => tb.ScrollToCaret());
300 }
301
312 internal static bool Alive(Control control)
313 {
314 // whether the control is still there, and nothing about whether it can be marshalled to. a control
315 // on a tab nobody has opened has no handle yet and is very much still there: its value is set on it
316 // now and painted when the tab is first shown. deciding that here would throw those writes away,
317 // so the handle is asked about where the marshalling happens instead
318 return null != control
319 && false == control.IsDisposed
320 && false == control.Disposing;
321 }
322
323 public void Focus()
324 {
325 if (WindowsControl is Control ctrl && true == Alive(ctrl))
326 ctrl.Focus();
327 }
331 public FormControlType ControlType { get; internal set; }
332 internal bool ReadOnly { get; set; }
333 internal bool IsChecked { get; set; }
334 internal bool IsEnabled { get; set; } = true;
335
344 public virtual bool Enabled
345 {
346 get
347 {
348 bool retVal = IsEnabled;
349
350 if (WindowsControl is Control control && true == Alive(control))
351 retVal = OnUiRead(control, () => control.Enabled, IsEnabled);
352
353 return retVal;
354 }
355 set
356 {
357 if (WindowsControl is Control control && true == Alive(control))
358 OnUi(control, () => control.Enabled = value);
359
360 // kept either way, so a control disabled before the form is built comes up disabled and a
361 // control read back after being closed still answers with what it was last told
362 IsEnabled = value;
363 }
364 }
365
371 public virtual IAllowControlSettings WithEnabled(bool enabled = true)
372 {
373 Enabled = enabled;
374 return this;
375 }
376
383 public virtual IAllowControlSettings WithReadOnly(bool readOnly = true)
384 {
385 IsReadOnly = readOnly;
386 return this;
387 }
388
392 internal bool IsReadOnly { get; set; }
393 internal string ToolTipText { get; private set; }
394 internal int ToolTipAutoPopDelayMs { get; private set; } = 5000; // default 5 seconds
395 internal System.Windows.Forms.Keys ShortCutKey { get; set; }
396 internal List<(ControlEventType EventType, Delegate Handler)> Callbacks { get; } = new List<(ControlEventType EventType, Delegate Handler)>();
397
415 where TDelegate : Delegate
416 {
417 Callbacks.Add((ControlEventType.Default, WrapHandler(handler, RunOnUiThread) ?? (Delegate)handler));
418
419 return this;
420 }
421
427 private const double UiHeldWarningSeconds = 2.0;
428
433 private int _callbackDepth;
434
440 private int _callbackPending;
441
446 private const int CallbackBacklogWarning = 5;
447
452 private System.Threading.Tasks.Task _callbackChain = System.Threading.Tasks.Task.CompletedTask;
453
458 private readonly object _callbackChainGate = new object();
459
464 private string CallbackOwner => true == string.IsNullOrEmpty(Name) ? GetType().Name : Name;
465
471 private void EnterCallback()
472 {
473 if (1 < System.Threading.Interlocked.Increment(ref _callbackDepth))
474 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"The callback for [{CallbackOwner}] was entered while it was still running, which is what a handler writing to its own control does", this, GPALObjectType.Other);
475 }
476
482 private void RunOnUiThread(Action call)
483 {
484 System.Diagnostics.Stopwatch held = System.Diagnostics.Stopwatch.StartNew();
485
486 EnterCallback();
487
488 try
489 {
490 call();
491 }
492 finally
493 {
494 // the depth comes back down even when the handler threw, or the next press is reported as a loop
495 System.Threading.Interlocked.Decrement(ref _callbackDepth);
496 }
497
498 if (UiHeldWarningSeconds < held.Elapsed.TotalSeconds)
499 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"The handler for [{CallbackOwner}] held the window for [{held.Elapsed.TotalSeconds:0.0}] seconds. Attach it with WithCallbackOffUIThread and it runs on a thread of its own", this, GPALObjectType.Other);
500 }
501
536 where TDelegate : Delegate
537 {
538 // a drag handler sets args.Effect and a key handler sets args.Handled, and the event reads both the
539 // moment the handler returns. running one of those on a thread of its own means the answer is set
540 // after it was wanted, so the drop is refused or the key goes through anyway
541 bool answersAsItReturns = handler is DragEventHandler || handler is KeyEventHandler;
542
543 Delegate wrapped = true == answersAsItReturns ? null : WrapHandler(handler, QueueInBackground);
544
545 if (null == wrapped)
546 {
547 GPAL.PublishSimpleEvent(GPALEventType.WARNING, true == answersAsItReturns
548 ? $"The event reads its answer from [{typeof(TDelegate).Name}] as soon as the handler returns, so the handler for [{CallbackOwner}] stays on the UI thread"
549 : $"[{typeof(TDelegate).Name}] is not a handler type GPAL knows, so the handler for [{CallbackOwner}] stays on the UI thread", this, GPALObjectType.Other);
550
551 wrapped = WrapHandler(handler, RunOnUiThread) ?? (Delegate)handler;
552 }
553
554 Callbacks.Add((ControlEventType.Default, wrapped));
555
556 return this;
557 }
558
564 private void RunInBackground(Action call)
565 {
566 EnterCallback();
567
568 try
569 {
570 call();
571 }
572 catch (Exception ex)
573 {
574 // nothing is waiting on this thread to catch it, and a handler that died in silence looks
575 // like a button that did nothing
576 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"A background handler for [{CallbackOwner}] ended in an exception", this, GPALObjectType.Other, ex);
577 }
578 finally
579 {
580 System.Threading.Interlocked.Decrement(ref _callbackDepth);
581 System.Threading.Interlocked.Decrement(ref _callbackPending);
582 }
583 }
584
594 private Delegate WrapHandler<TDelegate>(TDelegate handler, Action<Action> runner)
595 where TDelegate : Delegate
596 {
597 Delegate retVal;
598
599 switch (handler)
600 {
601 case EventHandler typed:
602 retVal = (EventHandler)((sender, args) => runner(() => typed(sender, args)));
603 break;
604
605 case KeyEventHandler typed:
606 retVal = (KeyEventHandler)((sender, args) => runner(() => typed(sender, args)));
607 break;
608
609 case DragEventHandler typed:
610 retVal = (DragEventHandler)((sender, args) => runner(() => typed(sender, args)));
611 break;
612
613 case ScrollEventHandler typed:
614 retVal = (ScrollEventHandler)((sender, args) => runner(() => typed(sender, args)));
615 break;
616
617 case SplitterEventHandler typed:
618 retVal = (SplitterEventHandler)((sender, args) => runner(() => typed(sender, args)));
619 break;
620
621 case ItemCheckedEventHandler typed:
622 retVal = (ItemCheckedEventHandler)((sender, args) => runner(() => typed(sender, args)));
623 break;
624
625 case TreeViewEventHandler typed:
626 retVal = (TreeViewEventHandler)((sender, args) => runner(() => typed(sender, args)));
627 break;
628
629 case DataGridViewCellEventHandler typed:
630 retVal = (DataGridViewCellEventHandler)((sender, args) => runner(() => typed(sender, args)));
631 break;
632
633 default:
634 retVal = null;
635 break;
636 }
637
638 return retVal;
639 }
640
646 private void QueueInBackground(Action call)
647 {
648 if (CallbackBacklogWarning == System.Threading.Interlocked.Increment(ref _callbackPending))
649 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"[{CallbackBacklogWarning}] handlers for [{CallbackOwner}] are waiting their turn, so the events are arriving faster than the handler runs", this, GPALObjectType.Other);
650
651 lock (_callbackChainGate)
652 _callbackChain = _callbackChain.ContinueWith(previous => RunInBackground(call));
653 }
654
662 public IAllowControlSettings ForEvent(ControlEventType eventType)
663 {
664 // replace the last "Default" entry with the explicit event
665 // (this is the only "magic" — keeps chain order natural)
666 if (Callbacks.Count > 0)
667 {
668 var last = Callbacks[Callbacks.Count - 1];
669 if (last.EventType == ControlEventType.Default)
670 {
671 Callbacks[Callbacks.Count - 1] = (eventType, last.Handler);
672 return this;
673 }
674 }
675
676 // if no prior Default > just add as explicit (rare misuse case)
677 Callbacks.Add((eventType, null)); // null handler = ignore later
678 return this;
679 }
680
685 public IAllowToolTipDelay WithToolTip(string toolTipText)
686 {
687 ToolTipText = toolTipText ?? "";
688 return this;
689 }
690
697 {
698 ToolTipAutoPopDelayMs = Math.Max(1000, delayInMs); // enforce reasonable minimum
699 return this;
700 }
701
716 public IAllowControlSettings WithShortcutKey(System.Windows.Forms.Keys shortCutKey)
717 {
718 ShortCutKey = shortCutKey;
719 return this;
720 }
721
731 internal static void OnUi(Control control, MethodInvoker work)
732 {
733 if (false == Alive(control))
734 return;
735
736 try
737 {
738 // no handle means no window to marshal to and no thread that owns it, so the work is done here
739 // and windows forms keeps it until the control is created
740 if (true == control.IsHandleCreated && true == control.InvokeRequired)
741 control.Invoke(work);
742 else
743 work();
744 }
745 catch (ObjectDisposedException)
746 {
747 // the form went between the check and the work
748 }
749 catch (InvalidOperationException)
750 {
751 // the handle went the same way, which Invoke reports as this
752 }
753 }
754
763 internal static void OnUiOrHere(Control control, MethodInvoker work)
764 {
765 if (true == Alive(control))
766 OnUi(control, work);
767 else
768 work();
769 }
770
778 internal static void PostToUi(Control control, MethodInvoker work)
779 {
780 if (false == Alive(control))
781 return;
782
783 try
784 {
785 if (true == control.IsHandleCreated && true == control.InvokeRequired)
786 control.BeginInvoke(work);
787 else
788 work();
789 }
790 catch (ObjectDisposedException)
791 {
792 // the form went between the check and the work
793 }
794 catch (InvalidOperationException)
795 {
796 // the handle went the same way, which BeginInvoke reports as this
797 }
798 }
799
810 internal static T OnUiRead<T>(Control control, Func<T> read, T whenGone)
811 {
812 T retVal = whenGone;
813
814 if (true == Alive(control))
815 try
816 {
817 retVal = true == control.IsHandleCreated && true == control.InvokeRequired
818 ? (T)control.Invoke(read)
819 : read();
820 }
821 catch (ObjectDisposedException)
822 {
823 // the form went between the check and the read
824 }
825 catch (InvalidOperationException)
826 {
827 // the handle went the same way, which Invoke reports as this
828 }
829
830 return retVal;
831 }
832
839 public void OnUiThread(Action action)
840 {
841 OnUiOrHere(WindowsControl as Control, () => action());
842 }
843
858 public IAllowControlSettings WithName(string name)
859 {
860 this.Name = name;
861 return this;
862 }
863
879 {
880 // Tag is on Control, so every realized control carries one. casting to TextBox threw on a button,
881 // a grid or anything else the moment this was called after the form was built
882 if (WindowsControl is Control control && true == Alive(control))
883 OnUi(control, () => control.Tag = tag);
884
885 this.Tag = tag;
886
887 return this;
888 }
889
895 {
896 this.Height = height;
897 return this;
898 }
899
907 {
908 this.Width = width;
909 return this;
910 }
911
926 {
927 if (null != WindowsControl)
928 ((TextBox)WindowsControl).Text = text; // this.Text will be updated via the TextChanged event;
929 else
930 this.Text = text;
931 return this;
932 }
933
955 public dynamic ToGPALObject()
956 {
957 return this;
958 }
959 }
960
996 {
997 internal ChartColorPalette ChartColorPalette { get; set; }
998 internal Series mySeries { get; set; }
999 internal string Title { get; set; }
1000
1001 internal GPALChart()
1002 {
1003 ControlType = FormControlType.Chart;
1004 Chart chart = new Chart
1005 {
1006 Name = Name,
1007 AutoSize = false,
1008 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1009 Dock = DockStyle.Fill,
1010 };
1011 chart.ChartAreas.Add(new ChartArea(Name));
1012 WindowsControl = chart;
1013 }
1019 public IAllowControlSettingsAndChartSettings WithPalette(ChartColorPalette colorPalette)
1020 {
1021 ((Chart)WindowsControl).Palette = colorPalette;
1022 return this;
1023 }
1024
1030 {
1031 ((Chart)WindowsControl).Series.Add(series);
1032 return this;
1033 }
1034
1040 {
1041 ((Chart)WindowsControl).Titles.Add(title);
1042 return this;
1043 }
1044
1047 public SeriesCollection Series
1048 {
1049 get
1050 {
1051 return ((Chart)(WindowsControl)).Series;
1052 }
1053 }
1054
1057 public LegendCollection Legends
1058 {
1059 get
1060 {
1061 return ((Chart)(WindowsControl)).Legends;
1062 }
1063 }
1064
1067 public TitleCollection Titles
1068 {
1069 get
1070 {
1071 return ((Chart)(WindowsControl)).Titles;
1072 }
1073 }
1074
1078 internal ChartAreaCollection ChartAreas
1079 {
1080 get
1081 {
1082 return ((Chart)WindowsControl).ChartAreas;
1083 }
1084 }
1085 }
1086
1118 {
1124 internal bool IsDefault { get; set; }
1125 internal GPALButton()
1126 {
1127 ControlType = FormControlType.Button;
1128 IsDefault = false;
1129 }
1136 {
1137 get
1138 {
1139 IsDefault = true;
1140 return this;
1141 }
1142 }
1143 }
1144
1163 {
1164 internal GPALCheckbox()
1165 {
1166 ControlType = FormControlType.Checkbox;
1167 }
1174 {
1175 this.Checked = isChecked;
1176 return this;
1177 }
1178
1183 public bool Checked
1184 {
1185 get
1186 {
1187 bool retVal = IsChecked;
1188
1189 if (WindowsControl is CheckBox cb && true == Alive(cb))
1190 retVal = OnUiRead(cb, () => cb.Checked, retVal);
1191
1192 return retVal;
1193 }
1194 set
1195 {
1196 if (WindowsControl is CheckBox cb && true == Alive(cb))
1197 {
1198 OnUi(cb, () => cb.Checked = value);
1199 }
1200 else
1201 {
1202 IsChecked = value;
1203 }
1204 }
1205 }
1206 }
1207
1224 public class GPALDataGridView : GPALControl, IAllowDataGridViewControlSettings
1225 {
1226 private IGPALGrid<string> _grid;
1227 private readonly List<string> _columnNames = new List<string>();
1228 private bool _autoGenerateColumns = false; // default false – only generate if explicitly asked or no columns set
1229
1230 internal GPALDataGridView()
1231 {
1232 ControlType = FormControlType.DataGridView;
1233 }
1234
1241 public IAllowDataGridViewControlSettings WithColumns(params string[] columnNames)
1242 {
1243 if (columnNames != null)
1244 {
1245 _columnNames.AddRange(columnNames);
1246 }
1247 return this;
1248 }
1249
1255 public new IAllowDataGridViewControlSettings WithReadOnly(bool readOnly = true)
1256 {
1257 base.ReadOnly = readOnly;
1258 return this;
1259 }
1260
1268 {
1269 _autoGenerateColumns = autoGenerate;
1270 return this;
1271 }
1272
1279 public IAllowDataGridViewControlSettings WithInput(IGPALGrid<string> grid)
1280 {
1281 this.CurrentGrid = grid;
1282 return this;
1283 }
1284
1293
1294
1295 // how wide each column is, in column order. Empty leaves every column as the control makes it
1296 internal readonly List<int> _columnWidths = new List<int>();
1297
1312 {
1313 _columnWidths.Clear();
1314
1315 if (null != widths)
1316 _columnWidths.AddRange(widths);
1317
1318 ApplyColumnWidths();
1319
1320 return this;
1321 }
1322
1323
1324 // which columns wrap their text, in column order. Empty is every column on one line, which is what a
1325 // grid does without being asked
1326 internal readonly List<bool> _columnWrap = new List<bool>();
1327
1348 {
1349 _columnWrap.Clear();
1350 wrapAsked = true;
1351
1352 if (null != wrap)
1353 _columnWrap.AddRange(wrap);
1354
1355 ApplyWrap();
1356
1357 return this;
1358 }
1359
1365 internal void ApplyWrap()
1366 {
1367 if (!(WindowsControl is DataGridView dgv) || false == Alive(dgv))
1368 return;
1369
1370 if (true == dgv.InvokeRequired)
1371 {
1372 OnUi(dgv, ApplyWrap);
1373 return;
1374 }
1375
1376 // nothing said is every column on one line. Said with no entries at all is every column wrapping,
1377 // which is the difference between never calling it and calling it bare
1378 bool everyColumn = 0 == _columnWrap.Count && true == wrapAsked;
1379
1380 for (int column = 0; column < dgv.Columns.Count; column++)
1381 {
1382 bool wraps = true == everyColumn
1383 || (column < _columnWrap.Count && true == _columnWrap[column]);
1384
1385 dgv.Columns[column].DefaultCellStyle.WrapMode = true == wraps
1386 ? DataGridViewTriState.True
1387 : DataGridViewTriState.False;
1388 }
1389
1390 dgv.AutoResizeRows(DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders);
1391 }
1392
1393 // whether WithWrap was called at all, which is how a bare call means every column
1394 private bool wrapAsked = false;
1395
1396 // what share of the width each column takes. Empty leaves the columns as the control makes them
1397 internal readonly List<double> _columnShares = new List<double>();
1398
1416 {
1417 _columnShares.Clear();
1418
1419 if (null != shares)
1420 _columnShares.AddRange(shares);
1421
1422 ApplyColumnWidths();
1423
1424 return this;
1425 }
1426
1427 // the widths are given before the form is built and again after, because a control that does not exist
1428 // yet cannot be told anything and one that already does will not be rebuilt
1429 internal void ApplyColumnWidths()
1430 {
1431 if (!(WindowsControl is DataGridView dgv) || false == Alive(dgv))
1432 return;
1433
1434 // shares first: they set the mode the whole control sizes by, and a pixel width said afterwards
1435 // still lands because a filled column keeps the width it is given until the next resize
1436 if (0 < _columnShares.Count)
1437 {
1438 dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
1439
1440 for (int column = 0; column < _columnShares.Count && column < dgv.Columns.Count; column++)
1441 dgv.Columns[column].FillWeight = (float)(0 < _columnShares[column] ? _columnShares[column] : 1);
1442 }
1443
1444 if (0 == _columnWidths.Count)
1445 return;
1446
1447 if (true == dgv.InvokeRequired)
1448 {
1449 OnUi(dgv, ApplyColumnWidths);
1450 return;
1451 }
1452
1453 for (int column = 0; column < _columnWidths.Count && column < dgv.Columns.Count; column++)
1454 if (0 < _columnWidths[column])
1455 dgv.Columns[column].Width = _columnWidths[column];
1456
1457 FillLastColumn();
1458 }
1459
1460 // whether a workflow said anything about the pixel widths, which is what asks the last column to end
1461 // the row at the edge of the control. shares reach the edge on their own through Fill
1462 // a width for every column, which is what says the workflow described the whole row. Widths for only
1463 // the first few leave the rest as the control sizes them, which is what the remarks promise. Shares
1464 // reach the edge through Fill on their own, so they are not filled a second time here
1465 internal bool LastColumnFills => 0 < _columnWidths.Count && _columnWidths.Count == (WindowsControl as DataGridView)?.Columns.Count && 0 == _columnShares.Count;
1466
1467 // setting a column's width lays the control out again, which raises the resize this runs from
1468 private bool filling = false;
1469
1477 internal void FillLastColumn()
1478 {
1479 if (!(WindowsControl is DataGridView dgv) || false == Alive(dgv) || false == LastColumnFills)
1480 return;
1481
1482 if (true == dgv.InvokeRequired)
1483 {
1484 OnUi(dgv, FillLastColumn);
1485 return;
1486 }
1487
1488 int last = dgv.Columns.Count - 1;
1489
1490 if (0 > last || true == filling)
1491 return;
1492
1493 filling = true;
1494
1495 try
1496 {
1497 // a filled column takes its width from the mode rather than from Width, so the fill only means
1498 // anything while the grid is sizing by pixels
1499 dgv.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.None;
1500
1501 if (last < _columnWidths.Count && 0 < _columnWidths[last])
1502 dgv.Columns[last].Width = _columnWidths[last];
1503
1504 int used = 0;
1505
1506 for (int before = 0; before < last; before++)
1507 used += dgv.Columns[before].Width;
1508
1509 // the grid's scrollbars are child controls sitting inside the client area, unlike the list
1510 // view's, so ClientSize still counts the vertical one. Without taking it off, a grid with
1511 // enough rows to scroll made the last column a scrollbar too wide and grew a horizontal one
1512 int verticalBar = 0;
1513
1514 foreach (Control child in dgv.Controls)
1515 if (child is VScrollBar bar && true == bar.Visible)
1516 verticalBar = bar.Width;
1517
1518 // the row header is part of the width the columns have to share, and it is not a column
1519 int left = dgv.ClientSize.Width - used - (true == dgv.RowHeadersVisible ? dgv.RowHeadersWidth : 0) - verticalBar - GPALListView.FillMargin;
1520
1521 if (left > dgv.Columns[last].Width)
1522 dgv.Columns[last].Width = left;
1523 }
1524 finally
1525 {
1526 filling = false;
1527 }
1528 }
1529
1540 {
1541 IGPALGrid<string> rows = DatabaseHelper.DataTableToGrid(dataTable, out List<DatabaseSettings.DatabaseColumn> columns);
1542
1543 // the names first, because the grid holds rows and nothing else, and a column with no name is shown
1544 // as its number
1545 if (0 == _columnNames.Count)
1546 foreach (DatabaseSettings.DatabaseColumn column in columns)
1547 _columnNames.Add(column.ColumnName);
1548
1549 return WithInput(rows);
1550 }
1551
1577 {
1578 DataTable table = null;
1579
1580 if (null != database)
1581 database.SaveTo(out table);
1582
1583 return WithInput(table);
1584 }
1585
1596 {
1597 IGPALGrid<string> rows = GPAL.Grid.ToGPALObject();
1598
1599 if (null != file)
1600 {
1601 FileHelper.GetGrid(((IGPALFileInternal)file).FileSettings, out List<IGPALGrid<string>> tokens);
1602
1603 // a file can hold more than one of them, and a grid control shows one, so they follow each
1604 // other down the control the way they follow each other through the file
1605 if (null != tokens)
1606 foreach (IGPALGrid<string> token in tokens)
1607 foreach (List<string> row in token)
1608 rows.AddRow(new List<string>(row));
1609 }
1610
1611 return WithInput(rows);
1612 }
1613
1614 // the range a GetGrid reads, counting from one so a workflow says what a person sees. Unset at either
1615 // end means as far as the control goes in that direction
1616 private int _startRow = 0;
1617 private int _startColumn = 0;
1618 private int _endRow = 0;
1619 private int _endColumn = 0;
1620
1627 {
1628 _startRow = row;
1629 return this;
1630 }
1631
1638 {
1639 _startColumn = column;
1640 return this;
1641 }
1642
1649 {
1650 _endRow = row;
1651 return this;
1652 }
1653
1660 {
1661 _endColumn = column;
1662 return this;
1663 }
1664
1677 public IAllowDataGridViewControlSettings GetGrid(out IGPALGrid<string> grid)
1678 {
1679 IGPALGrid<string> reading = GPAL.Grid.ToGPALObject();
1680
1681 if (WindowsControl is DataGridView dgv && true == Alive(dgv))
1682 {
1683 if (true == dgv.InvokeRequired)
1684 OnUi(dgv, () => ReadInto(dgv, reading));
1685 else
1686 ReadInto(dgv, reading);
1687 }
1688 else if (null != _grid)
1689 foreach (List<string> row in _grid)
1690 reading.AddRow(new List<string>(row));
1691
1692 grid = reading;
1693
1694 return this;
1695 }
1696
1702 private void ReadInto(DataGridView dgv, IGPALGrid<string> reading)
1703 {
1704 int firstRow = 1 > _startRow ? 1 : _startRow;
1705 int lastRow = 1 > _endRow ? dgv.Rows.Count : (_endRow > dgv.Rows.Count ? dgv.Rows.Count : _endRow);
1706 int firstColumn = 1 > _startColumn ? 1 : _startColumn;
1707 int lastColumn = 1 > _endColumn ? dgv.Columns.Count : (_endColumn > dgv.Columns.Count ? dgv.Columns.Count : _endColumn);
1708
1709 // the column names first, so a saved file says what its columns are, and only for the columns the
1710 // range covers
1711 if (0 < dgv.Columns.Count)
1712 {
1713 List<string> headers = new List<string>();
1714
1715 for (int column = firstColumn; column <= lastColumn; column++)
1716 headers.Add(dgv.Columns[column - 1].HeaderText ?? string.Empty);
1717
1718 reading.AddRow(headers);
1719 }
1720
1721 for (int row = firstRow; row <= lastRow; row++)
1722 {
1723 if (true == dgv.Rows[row - 1].IsNewRow)
1724 continue;
1725
1726 List<string> cells = new List<string>();
1727
1728 for (int column = firstColumn; column <= lastColumn; column++)
1729 cells.Add(dgv.Rows[row - 1].Cells[column - 1].Value?.ToString() ?? string.Empty);
1730
1731 reading.AddRow(cells);
1732 }
1733 }
1734
1735 internal void SyncGridToControl()
1736 {
1737 if (!(WindowsControl is DataGridView dgv) || false == Alive(dgv) || _grid == null)
1738 {
1739 return;
1740 }
1741
1742 if (dgv.InvokeRequired)
1743 {
1744 OnUi(dgv, SyncGridToControl);
1745 return;
1746 }
1747
1748 dgv.Rows.Clear();
1749 dgv.Columns.Clear();
1750
1751 // Determine column count from first non-null row (defensive)
1752 int columnCount = 0;
1753 foreach (List<string> row in _grid)
1754 {
1755 if (row != null)
1756 {
1757 columnCount = row.Count;
1758 break;
1759 }
1760 }
1761
1762 // Columns
1763 if (_columnNames.Count > 0)
1764 {
1765 int colIndex = 0;
1766 foreach (string header in _columnNames)
1767 {
1768 if (colIndex >= columnCount) break;
1769 dgv.Columns.Add(header, header);
1770 colIndex++;
1771 }
1772 }
1773 else if (_autoGenerateColumns && columnCount > 0)
1774 {
1775 for (int i = 0; i < columnCount; i++)
1776 {
1777 string header = $"Column {i + 1}";
1778 dgv.Columns.Add(header, header);
1779 }
1780 }
1781
1782 // Rows
1783 foreach (List<string> rowData in _grid)
1784 {
1785 if (rowData == null) continue;
1786
1787 var dgvRow = new DataGridViewRow();
1788 var cells = new DataGridViewCell[rowData.Count];
1789
1790 int colIndex = 0;
1791 foreach (string cellValue in rowData)
1792 {
1793 cells[colIndex] = new DataGridViewTextBoxCell
1794 {
1795 Value = cellValue ?? ""
1796 };
1797 colIndex++;
1798 }
1799
1800 dgvRow.Cells.AddRange(cells);
1801 dgv.Rows.Add(dgvRow);
1802 }
1803
1804 dgv.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
1805 dgv.AutoResizeRows(DataGridViewAutoSizeRowsMode.AllCellsExceptHeaders);
1806
1807 // the rows just sized every column to its own content, so the widths the workflow asked for are said
1808 // again here. Without this they lasted only until the grid was refilled, since the widths are applied
1809 // once as the form is built and a refill happens long after. The list view says its own again the
1810 // same way, from RefitSizedColumns
1811 ApplyColumnWidths();
1812 ApplyWrap();
1813 }
1814
1819 public IGPALGrid<string> CurrentGrid
1820 {
1821 get => _grid;
1822 set
1823 {
1824 _grid = value;
1825 SyncGridToControl();
1826 }
1827 }
1828
1831 public IReadOnlyList<string> ColumnNames => _columnNames.AsReadOnly();
1836 public bool AutoGenerateColumns => _autoGenerateColumns;
1837 }
1838
1856 {
1857 internal bool InputIsPassword { get; set; } = false;
1858 internal string PlaceHolder { get; set; }
1859 internal GPALInput()
1860 {
1861 ControlType = FormControlType.Input;
1862 }
1873 public bool ShowPassword
1874 {
1875 get
1876 {
1877 return false == InputIsPassword;
1878 }
1879 set
1880 {
1881 InputIsPassword = false == value;
1882
1883 if (WindowsControl is PlaceholderTextBox ptbInput && true == Alive(ptbInput))
1884 {
1885 char passwordChar = true == value ? '\0' : '●'; // bullet
1886
1887 OnUi(ptbInput, () => ptbInput.PasswordChar = passwordChar);
1888 }
1889 }
1890 }
1891
1893 {
1894 get
1895 {
1896 InputIsPassword = true;
1897 if (true == String.IsNullOrEmpty(PlaceHolder))
1898 PlaceHolder = "Enter Password";
1899 return this;
1900 }
1901 }
1902 public IAllowControlSettingsPlaceholderAndPassword WithPlaceholder(string PlaceholderText)
1903 {
1904 PlaceHolder = PlaceholderText;
1905 if (WindowsControl is PlaceholderTextBox tb && true == Alive(tb))
1906 tb.PlaceholderText = PlaceholderText;
1907 return this;
1908 }
1909
1910 public override IAllowControlSettings WithReadOnly(bool readOnly = true)
1911 {
1912 IsReadOnly = readOnly;
1913 if (WindowsControl is PlaceholderTextBox tb && true == Alive(tb))
1914 tb.ReadOnly = readOnly;
1915 return this;
1916 }
1917
1918 }
1919
1939 // TOFO: font type, font size
1941 {
1942 internal GPALLabel()
1943 {
1944 ControlType = FormControlType.Label;
1945 }
1946 }
1947
1959 public class GPALSpacer : GPALControl
1960 {
1961 internal GPALSpacer()
1962 {
1963 ControlType = FormControlType.Spacer;
1964 }
1965 }
1966
1986 public class GPALLinkLabel : GPALControl, IAllowControlSettingsAndUrl
1987 {
1988 internal GPALLinkLabel()
1989 {
1990 ControlType = FormControlType.LinkLabel;
1991 }
1992
1994 public string Url { get; internal set; }
1995
2003 {
2004 Url = url;
2005 return this;
2006 }
2007
2012 internal void Open()
2013 {
2014 string target = false == string.IsNullOrWhiteSpace(Url) ? Url : Text;
2015
2016 if (true == target?.StartsWith("http://", StringComparison.OrdinalIgnoreCase)
2017 || true == target?.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
2018 GPAL.Launcher.WithExecutable(target).Launch();
2019 }
2020 }
2021
2046 // TODO: group box
2047 public class GPALRadioButton : GPALControl, IAllowControlSettingsAndChecked
2048 {
2049 internal GPALRadioButton()
2050 {
2051 ControlType = FormControlType.RadioButton;
2052 }
2059 {
2060 this.Checked = isChecked;
2061 return this;
2062 }
2063
2068 public bool Checked
2069 {
2070 get
2071 {
2072 bool retVal = IsChecked;
2073
2074 if (WindowsControl is RadioButton rb && true == Alive(rb))
2075 retVal = OnUiRead(rb, () => rb.Checked, retVal);
2076
2077 return retVal;
2078 }
2079 set
2080 {
2081 if (WindowsControl is RadioButton rb && true == Alive(rb))
2082 {
2083 OnUi(rb, () => rb.Checked = value);
2084 }
2085 else
2086 {
2087 IsChecked = value;
2088 }
2089 }
2090 }
2091 }
2092
2111 public class GPALTab : GPALControl, IAllowControlSettings
2112 {
2113 internal GPALTab()
2114 {
2115 ControlType = FormControlType.Tab;
2116 }
2117 public override bool Enabled
2118 {
2119 get
2120 {
2121 // a GPALTab is one page, and its control is the TabPage. asking the TabControl would answer
2122 // for every tab at once
2123 bool retVal = IsEnabled;
2124
2125 if (WindowsControl is TabPage page && true == Alive(page))
2126 retVal = OnUiRead(page, () => page.Enabled, retVal);
2127
2128 return retVal;
2129 }
2130 set
2131 {
2132 IsEnabled = value;
2133
2134 if (!(WindowsControl is TabPage page) || false == Alive(page)) return;
2135
2136 OnUi(page, () => page.Enabled = value);
2137 }
2138 }
2142 public void Activate()
2143 {
2144 // marshalled like every other control setter: a workflow thread is where this gets called from,
2145 // and switching tabs from one is a cross thread call into the form
2146 if (WindowsControl is TabPage tabPage && tabPage.Parent is TabControl tabControl)
2147 OnUi(tabControl, () => tabControl.SelectedTab = tabPage);
2148 }
2149 }
2150
2156 public class GPALSpliter
2157 {
2158 internal GPALSpliter() { }
2159 }
2160
2179 public class GPALSplitter : GPALControl, IAllowSplitterControlSettings
2180 {
2182 internal bool IsHorizontalSplit { get; private set; } = true;
2184 internal int Dimension { get; private set; }
2186 internal double Percentage { get; private set; }
2187 internal bool IsPercentage => 0 < Percentage;
2189 internal List<object> FormControls { get; } = new List<object>();
2190
2191 internal GPALSplitter() { }
2192
2204 {
2205 Height = height;
2206 return this;
2207 }
2208 public IAllowSplitterControlSettings HSplit(int leftWidth)
2209 {
2210 IsHorizontalSplit = true;
2211 Dimension = leftWidth;
2212 return this;
2213 }
2220 public IAllowSplitterControlSettings HSplit(double leftPercent)
2221 {
2222 IsHorizontalSplit = true;
2223 Percentage = leftPercent;
2224 return this;
2225 }
2226
2232 {
2233 IsHorizontalSplit = false;
2234 Dimension = topHeight;
2235 return this;
2236 }
2237
2243 public IAllowSplitterControlSettings VSplit(double topPercent)
2244 {
2245 IsHorizontalSplit = false;
2246 Percentage = topPercent;
2247 return this;
2248 }
2249
2255 public IAllowSplitterControlSettings WithFormControl(object gPALFormControl)
2256 {
2257 FormControls.Add(gPALFormControl);
2258 return this;
2259 }
2260
2264 public new GPALSplitter ToGPALObject()
2265 {
2266 return this;
2267 }
2268 }
2269
2280 public class GPALGroupBox : GPALControl, IAllowGroupBoxControlSettings
2281 {
2283 internal List<object> FormControls { get; } = new List<object>();
2284
2285 internal GPALGroupBox() { }
2286
2293 {
2294 Text = text;
2295 return this;
2296 }
2297
2302 public IAllowGroupBoxControlSettings WithFormControl(object gPALFormControl)
2303 {
2304 FormControls.Add(gPALFormControl);
2305 return this;
2306 }
2307
2311 public new GPALGroupBox ToGPALObject()
2312 {
2313 return this;
2314 }
2315 }
2316
2322 {
2323 internal GPALTableLayoutPanel()
2324 {
2325 ControlType = FormControlType.TableLayoutPanel;
2326 }
2327
2328 // what the panel is set to before the form is built, the way IsEnabled backs Enabled. Each of the three
2329 // properties below used to name itself in its own else branch, so reading or writing one on a control
2330 // that had no WindowsControl yet recursed until the stack was gone, which is a StackOverflowException
2331 // and cannot be caught. The defaults are the WinForms ones
2332 private TableLayoutPanelGrowStyle growStyle = TableLayoutPanelGrowStyle.AddRows;
2333 private int columnCount = 0;
2334 private int rowCount = 0;
2340 public TableLayoutPanelGrowStyle GrowStyle
2341 {
2342 get
2343 {
2344 TableLayoutPanelGrowStyle retVal = growStyle;
2345
2346 if (WindowsControl is TableLayoutPanel panel && true == Alive(panel))
2347 retVal = OnUiRead(panel, () => panel.GrowStyle, retVal);
2348
2349 return retVal;
2350 }
2351 set
2352 {
2353 if (WindowsControl is TableLayoutPanel panel && true == Alive(panel))
2354 OnUi(panel, () => panel.GrowStyle = value);
2355
2356 growStyle = value; // kept either way, so the value survives the control not being there yet
2357 }
2358 }
2359
2363 public int ColumnCount
2364 {
2365 get
2366 {
2367 int retVal = columnCount;
2368
2369 if (WindowsControl is TableLayoutPanel panel && true == Alive(panel))
2370 retVal = OnUiRead(panel, () => panel.ColumnCount, retVal);
2371
2372 return retVal;
2373 }
2374 set
2375 {
2376 if (WindowsControl is TableLayoutPanel panel && true == Alive(panel))
2377 OnUi(panel, () => panel.ColumnCount = value);
2378
2379 columnCount = value; // kept either way, so the value survives the control not being there yet
2380 }
2381 }
2382
2387 public int RowCount
2388 {
2389 get
2390 {
2391 int retVal = rowCount;
2392
2393 if (WindowsControl is TableLayoutPanel panel && true == Alive(panel))
2394 retVal = OnUiRead(panel, () => panel.RowCount, retVal);
2395
2396 return retVal;
2397 }
2398 set
2399 {
2400 if (WindowsControl is TableLayoutPanel panel && true == Alive(panel))
2401 OnUi(panel, () => panel.RowCount = value);
2402
2403 rowCount = value; // kept either way, so the value survives the control not being there yet
2404 }
2405 }
2406
2407
2414 {
2415 ColumnCount = columnCount;
2416 return this;
2417 }
2418
2425 {
2426 this.Enabled = enabled;
2427 return this;
2428 }
2429
2435 public IAllowControlSettingsAndTableLayoutPanelSettings WithGrowStyle(TableLayoutPanelGrowStyle tableLayoutPanelGrowStyle)
2436 {
2437 this.GrowStyle = tableLayoutPanelGrowStyle;
2438 return this;
2439 }
2440
2447 {
2448 RowCount = rowCount;
2449 return this;
2450 }
2451 }
2452
2470 {
2471 internal string PlaceHolder { get; set; }
2472
2473 // lines wait here until the thread that owns the control is free to take them. see AppendLine
2474
2475 internal GPALTextArea()
2476 {
2477 ControlType = FormControlType.TextArea;
2478 }
2479
2480 public IAllowControlSettingsAndPlaceholder WithPlaceholder(string PlaceholderText)
2481 {
2482 PlaceHolder = PlaceholderText;
2483 if (WindowsControl is PlaceholderTextBox tb && true == Alive(tb))
2484 tb.PlaceholderText = PlaceholderText;
2485 return this;
2486 }
2487
2488 public override IAllowControlSettings WithReadOnly(bool readOnly = true)
2489 {
2490 IsReadOnly = readOnly;
2491 if (WindowsControl is PlaceholderTextBox tb && true == Alive(tb))
2492 tb.ReadOnly = readOnly;
2493 return this;
2494 }
2495
2502 public void AppendLine(string text, int maxLines = 0) => AppendLineCore(text, maxLines);
2503
2507 public void Clear() => ClearLinesCore();
2508
2512 public void ScrollToEnd() => ScrollToEndCore();
2513 }
2514
2529 public class GPALRichTextBox : GPALControl, IAllowRichTextBoxControlSettings
2530 {
2531 private string _text = string.Empty;
2532
2533 internal GPALRichTextBox()
2534 {
2535 ControlType = FormControlType.RichTextBox;
2536 // a rich text box is a rendered view of markdown, so it is read only until told otherwise
2537 IsReadOnly = true;
2538 }
2539
2545 public new string Text
2546 {
2547 get => _text;
2548 set
2549 {
2550 _text = value ?? string.Empty;
2551 if (WindowsControl is RichTextBox rtb && true == Alive(rtb))
2552 {
2553 PostToUi(rtb, () => RenderMarkdown(rtb, _text));
2554 }
2555 }
2556 }
2557
2565 public void AppendLine(string text, int maxLines = 0)
2566 {
2567 _text = 0 == _text.Length ? text : _text + Environment.NewLine + text;
2568
2569 AppendLineCore(text, maxLines);
2570 }
2571
2575 public void Clear()
2576 {
2577 _text = string.Empty;
2578
2579 ClearLinesCore();
2580 }
2581
2585 public void ScrollToEnd() => ScrollToEndCore();
2586
2593 {
2594 this.Text = text;
2595 return this;
2596 }
2597
2598
2599 public override IAllowControlSettings WithReadOnly(bool readOnly = true)
2600 {
2601 IsReadOnly = readOnly;
2602 if (WindowsControl is RichTextBox rtb && true == Alive(rtb))
2603 rtb.ReadOnly = readOnly;
2604 return this;
2605 }
2606
2607 // matches **bold**, *italic*, __underline__, ~~strikethrough~~, `code`, {colorname}text{} — bold before italic to avoid ** ambiguity
2608 private static readonly Regex _inlinePattern = new Regex(
2609 @"\*\*(?<b>.+?)\*\*|\*(?<i>.+?)\*|__(?<u>.+?)__|~~(?<s>.+?)~~|`(?<code>.+?)`|{(?<c>[^}]+)}(?<ct>.+?)\{\}",
2610 RegexOptions.Compiled);
2611
2620 // the fonts a render asks for, kept rather than made again. A render writes a font per styled run and
2621 // setting SelectionFont copies what it needs out of the font, so one instance serves every run that
2622 // wants the same family, size and style. Made fresh each time they were never disposed, and a font
2623 // holds a GDI handle: a form that keeps appending to a rich text box re-renders the whole document
2624 // each time, so the handles ran out and the next thing the control was asked to do threw
2625 private static readonly Dictionary<string, Font> renderFonts = new Dictionary<string, Font>();
2626 private static readonly object renderFontGate = new object();
2627
2635 private static Font RenderFont(string family, float size, FontStyle style)
2636 {
2637 string key = $"{family}|{size}|{(int)style}";
2638 Font retVal;
2639
2640 lock (renderFontGate)
2641 {
2642 if (false == renderFonts.TryGetValue(key, out retVal))
2643 {
2644 retVal = new Font(family, size, style);
2645 renderFonts[key] = retVal;
2646 }
2647 }
2648
2649 return retVal;
2650 }
2651
2652 internal static void RenderMarkdown(RichTextBox rtb, string markdown)
2653 {
2654 rtb.Clear();
2655
2656 Font baseFont = rtb.Font;
2657 Color baseColor = rtb.ForeColor;
2658 string codeFamily = "Courier New";
2659 string[] lines = (markdown ?? string.Empty).Replace("\r\n", "\n").Split('\n');
2660
2661 foreach (string rawLine in lines)
2662 {
2663 string line = rawLine;
2664
2665 int headingLevel = 0;
2666 while (headingLevel < line.Length && line[headingLevel] == '#')
2667 headingLevel++;
2668 if (headingLevel > 0 && headingLevel <= 3 && headingLevel < line.Length && line[headingLevel] == ' ')
2669 line = line.Substring(headingLevel + 1);
2670 else
2671 headingLevel = 0;
2672
2673 int leadingSpaces = 0;
2674 while (leadingSpaces < line.Length && line[leadingSpaces] == ' ')
2675 leadingSpaces++;
2676 int indentLevel = leadingSpaces / 2;
2677 string trimmed = line.Substring(leadingSpaces);
2678
2679 // horizontal rule
2680 if (trimmed == "---")
2681 {
2682 rtb.SelectionStart = rtb.TextLength;
2683 rtb.SelectionLength = 0;
2684 rtb.SelectionFont = RenderFont(baseFont.FontFamily.Name, baseFont.Size, FontStyle.Regular);
2685 rtb.SelectionColor = Color.Gray;
2686 rtb.AppendText(new string('-', 48) + "\n");
2687 continue;
2688 }
2689
2690 bool isBulletItem = false;
2691 bool isListItem = false;
2692 if (trimmed.StartsWith("- ") || trimmed.StartsWith("* "))
2693 {
2694 isBulletItem = true;
2695 isListItem = true;
2696 trimmed = trimmed.Substring(2);
2697 }
2698 else
2699 {
2700 Match numberedMatch = Regex.Match(trimmed, @"^(\d+\.\s+)(.*)$");
2701 if (numberedMatch.Success)
2702 {
2703 isListItem = true;
2704 trimmed = numberedMatch.Groups[1].Value + numberedMatch.Groups[2].Value;
2705 }
2706 }
2707
2708 int lineStart = rtb.TextLength;
2709
2710 FontStyle headingBase = headingLevel > 0 ? FontStyle.Bold : FontStyle.Regular;
2711 float headingSize = headingLevel > 0 ? baseFont.Size + (4 - headingLevel) * 2 : baseFont.Size;
2712
2713 foreach ((string segmentText, FontStyle style, Color? color, bool isCode) in SplitInline(trimmed, headingBase))
2714 {
2715 rtb.SelectionStart = rtb.TextLength;
2716 rtb.SelectionLength = 0;
2717 rtb.SelectionFont = isCode
2718 ? RenderFont(codeFamily, baseFont.Size, FontStyle.Regular)
2719 : RenderFont(baseFont.FontFamily.Name, headingSize, style);
2720 rtb.SelectionColor = isCode ? Color.SteelBlue : (color ?? baseColor);
2721 rtb.AppendText(segmentText);
2722 }
2723 rtb.AppendText("\n");
2724
2725 if (isListItem)
2726 {
2727 rtb.SelectionStart = lineStart;
2728 rtb.SelectionLength = rtb.TextLength - lineStart;
2729 rtb.SelectionBullet = isBulletItem;
2730 rtb.SelectionIndent = 20 * (indentLevel + 1);
2731 }
2732 }
2733
2734 rtb.SelectionStart = 0;
2735 rtb.SelectionLength = 0;
2736 }
2737
2744 private static List<(string Text, FontStyle Style, Color? Color, bool IsCode)> SplitInline(string line, FontStyle inheritBase = FontStyle.Regular)
2745 {
2746 var result = new List<(string Text, FontStyle Style, Color? Color, bool IsCode)>();
2747 int pos = 0;
2748
2749 foreach (Match m in _inlinePattern.Matches(line))
2750 {
2751 if (m.Index > pos)
2752 result.Add((line.Substring(pos, m.Index - pos), inheritBase, null, false));
2753
2754 if (m.Groups["b"].Success)
2755 result.Add((m.Groups["b"].Value, inheritBase | FontStyle.Bold, null, false));
2756 else if (m.Groups["i"].Success)
2757 result.Add((m.Groups["i"].Value, inheritBase | FontStyle.Italic, null, false));
2758 else if (m.Groups["u"].Success)
2759 result.Add((m.Groups["u"].Value, inheritBase | FontStyle.Underline, null, false));
2760 else if (m.Groups["s"].Success)
2761 result.Add((m.Groups["s"].Value, inheritBase | FontStyle.Strikeout, null, false));
2762 else if (m.Groups["code"].Success)
2763 result.Add((m.Groups["code"].Value, FontStyle.Regular, null, true));
2764 else if (m.Groups["c"].Success)
2765 {
2766 Color named = Color.FromName(m.Groups["c"].Value);
2767 Color? resolved = named.IsKnownColor || named.A > 0 ? (Color?)named : null;
2768 // recurse so bold/italic/underline inside a color span still render correctly
2769 foreach (var inner in SplitInline(m.Groups["ct"].Value, inheritBase))
2770 result.Add((inner.Item1, inner.Item2, inner.Item3 ?? resolved, inner.Item4));
2771 }
2772
2773 pos = m.Index + m.Length;
2774 }
2775
2776 if (pos < line.Length)
2777 result.Add((line.Substring(pos), inheritBase, null, false));
2778 if (result.Count == 0)
2779 result.Add((string.Empty, inheritBase, null, false));
2780
2781 return result;
2782 }
2783 }
2784
2801 public class GPALComboBox : GPALControl, IAllowComboBoxControlSettings
2802 {
2803 internal readonly List<string> items = new List<string>();
2804 internal int selectedIndex = -1;
2805 internal string selectedItem = null;
2806
2807 // items that stay on the list but cannot be chosen. an option that does not apply is worth showing
2808 // greyed, because a list that silently loses an entry reads as a list that never had one
2809 internal readonly HashSet<string> disabledItems = new HashSet<string>();
2810
2811 // what to fall back to when a disabled item is picked, so the box never sits on something invalid
2812 internal int lastEnabledIndex = -1;
2813
2814 internal GPALComboBox()
2815 {
2816 ControlType = FormControlType.ComboBox;
2817 }
2818
2824 public IAllowComboBoxControlSettings WithItems(params string[] itemArray)
2825 {
2826 if (null != itemArray)
2827 {
2828 items.AddRange(itemArray);
2829 ShowItems(itemArray);
2830 }
2831 return this;
2832 }
2833
2834 // the list a form was built with is one thing and the list on screen is another. adding items before
2835 // the form is shown only has the first to add to; adding them after has both, and a dropdown that does
2836 // not change is the whole reason a list would look stale
2837 private void ShowItems(IEnumerable<string> itemsToShow)
2838 {
2839 if (!(WindowsControl is ComboBox cb) || false == Alive(cb)) return;
2840
2841 object[] adding = itemsToShow.Cast<object>().ToArray();
2842
2843 OnUi(cb, () => cb.Items.AddRange(adding));
2844 }
2845
2852 public void DisableItem(string item)
2853 {
2854 if (true == string.IsNullOrEmpty(item)) return;
2855
2856 disabledItems.Add(item);
2857 Redraw();
2858 }
2859
2864 public void EnableItem(string item)
2865 {
2866 if (true == string.IsNullOrEmpty(item)) return;
2867
2868 disabledItems.Remove(item);
2869 Redraw();
2870 }
2871
2877 public bool IsItemEnabled(string item)
2878 {
2879 return false == disabledItems.Contains(item);
2880 }
2881
2882 private void Redraw()
2883 {
2884 if (!(WindowsControl is ComboBox cb) || false == Alive(cb)) return;
2885
2886 OnUi(cb, () => cb.Invalidate());
2887 }
2888
2893 public void ClearItems()
2894 {
2895 items.Clear();
2896 disabledItems.Clear();
2897 selectedIndex = -1;
2898 selectedItem = null;
2899 lastEnabledIndex = -1;
2900
2901 if (!(WindowsControl is ComboBox cb) || false == Alive(cb)) return;
2902
2903 OnUi(cb, () => cb.Items.Clear());
2904 }
2905
2911 public IAllowComboBoxControlSettings WithItems(IEnumerable<string> itemCollection)
2912 {
2913 if (null != itemCollection)
2914 {
2915 items.AddRange(itemCollection);
2916 }
2917 return this;
2918 }
2919
2927 {
2928 if (null != itemArray)
2929 {
2930 foreach (object item in itemArray)
2931 items.Add(item?.ToString() ?? string.Empty);
2932 }
2933 return this;
2934 }
2935
2943 {
2944 selectedIndex = index;
2945 selectedItem = null;
2946 return this;
2947 }
2948
2956 {
2957 selectedItem = name;
2958 selectedIndex = -1;
2959 return this;
2960 }
2961
2966 public string SelectedValue
2967 {
2968 get
2969 {
2970 int idx = SelectedIndex;
2971 if ((0 > idx) || (idx >= items.Count))
2972 return null;
2973 return items[idx];
2974 }
2975 }
2976
2983
2984 public int SelectedIndex
2985 {
2986 // read on the ui thread when asked from another one, the way every setter here already writes on it.
2987 // a workflow running off the ui thread is the normal case, not an odd one
2988 get
2989 {
2990 int retVal = selectedIndex;
2991
2992 if (WindowsControl is ComboBox cb && true == Alive(cb))
2993 retVal = OnUiRead(cb, () => cb.SelectedIndex, retVal);
2994
2995 return retVal;
2996 }
2997 set
2998 {
2999 if ((0 > value) || (value >= items.Count)) return;
3000
3001 selectedIndex = value;
3002 if (WindowsControl is ComboBox cb && true == Alive(cb))
3003 {
3004 OnUi(cb, () => cb.SelectedIndex = value);
3005 }
3006 }
3007 }
3008 }
3009
3041 public class GPALPicture : GPALControl, IAllowPictureControlSettings
3042 {
3043 private System.Drawing.Image _image;
3044 private PictureBoxSizeMode _sizeMode = PictureBoxSizeMode.Zoom;
3045
3046 internal GPALPicture()
3047 {
3048 ControlType = FormControlType.Picture;
3049 }
3050
3055 public System.Drawing.Image Image => _image;
3056
3061 public PictureBoxSizeMode SizeMode => _sizeMode;
3062
3068 public IAllowPictureControlSettings WithImage(System.Drawing.Image image)
3069 {
3070 _image = image;
3071 SyncImageToControl();
3072 return this;
3073 }
3074
3085 {
3086 if (true == System.IO.File.Exists(file?.Filename))
3087 using (var stream = new System.IO.MemoryStream(System.IO.File.ReadAllBytes(file.Filename)))
3088 _image = new Bitmap(System.Drawing.Image.FromStream(stream));
3089 else
3090 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Image file not found for picture [{file?.Filename}]", this, GPALObjectType.Other);
3091
3092 SyncImageToControl();
3093 return this;
3094 }
3095
3101 public IAllowPictureControlSettings WithSizeMode(PictureBoxSizeMode sizeMode)
3102 {
3103 _sizeMode = sizeMode;
3104 SyncImageToControl();
3105 return this;
3106 }
3107
3111 private void SyncImageToControl()
3112 {
3113 if (WindowsControl is PictureBox box && true == Alive(box))
3114 OnUi(box, () =>
3115 {
3116 box.SizeMode = _sizeMode;
3117 box.Image = _image;
3118 });
3119 }
3120 }
3121
3140 public class GPALSlider : GPALControl, IAllowSliderControlSettings
3141 {
3142 private int _value = 0;
3143 private int _minimum = 0;
3144 private int _maximum = 100;
3145 private int _tickFrequency = 10;
3146 private Orientation _orientation = Orientation.Horizontal;
3147
3148 internal GPALSlider()
3149 {
3150 ControlType = FormControlType.Slider;
3151 }
3152
3157 public int Value
3158 {
3159 get => OnUiRead(WindowsControl as TrackBar, () => ((TrackBar)WindowsControl).Value, _value);
3160
3161 set
3162 {
3163 _value = value < _minimum ? _minimum : value > _maximum ? _maximum : value;
3164
3165 SyncToControl();
3166 }
3167 }
3168
3170 public int Minimum => _minimum;
3171
3173 public int Maximum => _maximum;
3174
3176 public int TickFrequency => _tickFrequency;
3177
3179 public Orientation Orientation => _orientation;
3180
3187 {
3188 _minimum = min;
3189
3190 if (_value < min)
3191 _value = min;
3192
3193 SyncToControl();
3194 return this;
3195 }
3196
3203 {
3204 _maximum = max;
3205
3206 if (_value > max)
3207 _value = max;
3208
3209 SyncToControl();
3210 return this;
3211 }
3212
3219 {
3220 Value = value;
3221 return this;
3222 }
3223
3230 {
3231 _tickFrequency = tickFrequency;
3232
3233 SyncToControl();
3234 return this;
3235 }
3236
3243 {
3244 _orientation = orientation;
3245
3246 SyncToControl();
3247 return this;
3248 }
3249
3254 private void SyncToControl()
3255 {
3256 if (WindowsControl is TrackBar bar && true == Alive(bar))
3257 OnUi(bar, () =>
3258 {
3259 bar.Minimum = _minimum;
3260 bar.Maximum = _maximum;
3261 bar.TickFrequency = 1 > _tickFrequency ? 1 : _tickFrequency;
3262 bar.Orientation = _orientation;
3263 bar.Value = _value;
3264 });
3265 }
3266 }
3267
3268 public class GPALProgressBar : GPALControl, IAllowProgressBarControlSettings
3269 {
3270 private int _value = 0;
3271 private int _minimum = 0;
3272 private int _maximum = 100;
3273 private ProgressBarStyle _style = ProgressBarStyle.Blocks;
3274
3275 internal GPALProgressBar()
3276 {
3277 ControlType = FormControlType.ProgressBar;
3278 }
3279
3287 {
3288 this.Value = value;
3289 return this;
3290 }
3291
3298 {
3299 _minimum = min;
3300 if (_value < min) _value = min;
3301 SyncValueToControl();
3302 return this;
3303 }
3304
3311 {
3312 _maximum = max;
3313 if (_value > max) _value = max;
3314 SyncValueToControl();
3315 return this;
3316 }
3317
3323 public IAllowProgressBarControlSettings WithStyle(ProgressBarStyle style)
3324 {
3325 this.Style = style;
3326 return this;
3327 }
3328
3334 public void Increment(int step = 1)
3335 {
3336 this.Value += step;
3337 }
3338
3344 private void SyncValueToControl()
3345 {
3346 if (WindowsControl is ProgressBar pb && true == Alive(pb))
3347 {
3348 OnUi(pb, () => Apply(pb));
3349 }
3350 }
3351
3359 private void Apply(ProgressBar pb)
3360 {
3361 // maximum first when it is going up, minimum first when it is coming down, because the control
3362 // refuses a minimum above its own maximum and a maximum below its own minimum
3363 if (pb.Maximum < _maximum)
3364 {
3365 pb.Maximum = _maximum;
3366 pb.Minimum = _minimum;
3367 }
3368 else
3369 {
3370 pb.Minimum = _minimum;
3371 pb.Maximum = _maximum;
3372 }
3373
3374 pb.Value = _value;
3375 }
3376
3382 private void SyncStyleToControl()
3383 {
3384 if (WindowsControl is ProgressBar pb && true == Alive(pb))
3385 {
3386 OnUi(pb, () => pb.Style = _style);
3387 }
3388 }
3389
3395 public int Value
3396 {
3397 get => _value;
3398 set
3399 {
3400 _value = Math.Max(_minimum, Math.Min(_maximum, value));
3401 SyncValueToControl();
3402 }
3403 }
3404
3407 public int Minimum => _minimum;
3411 public int Maximum => _maximum;
3416 public ProgressBarStyle Style
3417 {
3418 get => _style;
3419 set
3420 {
3421 _style = value;
3422 SyncStyleToControl();
3423 }
3424 }
3425 }
3426
3440 public class GPALStatusStrip : GPALControl, IAllowStatusStripControlSettings
3441 {
3442 private string _text = "";
3443
3444 internal GPALStatusStrip()
3445 {
3446 ControlType = FormControlType.StatusStrip;
3447 }
3448
3454 public new string Text
3455 {
3456 get => _text;
3457 set
3458 {
3459 _text = value ?? "";
3460 SyncTextToControl();
3461 }
3462 }
3463
3470 {
3471 this.Text = text;
3472 return this;
3473 }
3474
3481 private void SyncTextToControl()
3482 {
3483 if (WindowsControl is StatusStrip strip && true == Alive(strip) && strip.Items.Count > 0)
3484 {
3485 ToolStripStatusLabel label = strip.Items[0] as ToolStripStatusLabel;
3486 if (label != null)
3487 {
3488 PostToUi(strip, () => label.Text = _text);
3489 }
3490 }
3491 }
3492 }
3493
3511 public class GPALBarItem : GPALControl, IAllowBarItemSettings
3512 {
3513 internal System.Drawing.Image Image { get; set; }
3514
3515 internal GPALBarItem()
3516 {
3517 ControlType = FormControlType.BarItem;
3518 }
3519
3527 public new IAllowBarItemSettings WithEnabled(bool trueFalse = true)
3528 {
3529 Enabled = trueFalse;
3530 return this;
3531 }
3532
3537 public override bool Enabled
3538 {
3539 get
3540 {
3541 return WindowsControl is ToolStripItem strip ? strip.Enabled : IsEnabled;
3542 }
3543 set
3544 {
3545 IsEnabled = value;
3546
3547 if (!(WindowsControl is ToolStripItem strip)) return;
3548
3549 // a ToolStripItem is not a Control and has no InvokeRequired of its own. the strip that owns
3550 // it is one, and that is what has to be asked whether this is the right thread
3551 Control owner = strip.Owner;
3552
3553 OnUiOrHere(owner, () => strip.Enabled = value);
3554 }
3555 }
3556
3562 public new IAllowBarItemSettings WithText(string text)
3563 {
3564 this.Text = text;
3565 return this;
3566 }
3567
3573 public IAllowBarItemSettings WithImage(System.Drawing.Image image)
3574 {
3575 this.Image = image;
3576 return this;
3577 }
3578
3585 {
3586 try
3587 {
3588 string path = file?.Filenames?.FirstOrDefault() ?? (string)file;
3589 if (!string.IsNullOrWhiteSpace(path) && System.IO.File.Exists(path))
3590 this.Image = System.Drawing.Image.FromFile(path);
3591 else
3592 GPAL.PublishSimpleEvent(GPALEventType.WARNING, $"Image file not found for bar item [{path}]", this, GPALObjectType.Other);
3593 }
3594 catch (Exception ex)
3595 {
3596 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"Failed to load bar item image [{file?.Filename}]", this, GPALObjectType.Other, ex);
3597 }
3598 return this;
3599 }
3600 }
3601
3620 public class GPALMenuBar : GPALControl, IAllowMenuBarControlSettings
3621 {
3622 private string _currentMenu;
3623 internal readonly List<(string TopMenu, GPALBarItem Item, bool IsSeparator)> menuEntries
3624 = new List<(string, GPALBarItem, bool)>();
3625
3626 internal GPALMenuBar()
3627 {
3628 ControlType = FormControlType.MenuBar;
3629 }
3630
3638 {
3639 _currentMenu = topMenu;
3640 return this;
3641 }
3642
3649 {
3650 menuEntries.Add((_currentMenu, item, false));
3651 return this;
3652 }
3653
3659 {
3660 menuEntries.Add((_currentMenu, null, true));
3661 return this;
3662 }
3663 }
3664
3679 public class GPALToolbar : GPALControl, IAllowToolbarControlSettings
3680 {
3681 internal readonly List<(GPALBarItem Item, bool IsSeparator)> toolbarEntries
3682 = new List<(GPALBarItem, bool)>();
3683
3684 internal GPALToolbar()
3685 {
3686 ControlType = FormControlType.Toolbar;
3687 }
3688
3695 {
3696 toolbarEntries.Add((item, false));
3697 return this;
3698 }
3699
3705 {
3706 toolbarEntries.Add((null, true));
3707 return this;
3708 }
3709 }
3710
3725 public class GPALListView : GPALControl, IAllowListViewControlSettings
3726 {
3727 private readonly List<string> columnHeaders = new List<string>();
3728 private readonly List<double> columnWeights = new List<double>();
3729 private ListViewMode viewMode = ListViewMode.Details;
3730 internal bool checkBoxes = false;
3731 private bool fullRowSelect = true;
3732 private bool multiSelect = false;
3733 private bool sortable = false;
3734
3735 internal GPALListView()
3736 {
3737 ControlType = FormControlType.ListView;
3738 }
3739
3752 {
3753 CheckBoxes = enable;
3754 return this;
3755 }
3756
3760 public bool CheckBoxes
3761 {
3762 get
3763 {
3764 return WindowsControl is ListView lv ? lv.CheckBoxes : checkBoxes;
3765 }
3766 set
3767 {
3768 checkBoxes = value;
3769
3770 if (!(WindowsControl is ListView lv) || false == Alive(lv)) return;
3771
3772 OnUi(lv, () => lv.CheckBoxes = value);
3773 }
3774 }
3775
3776 public IAllowListViewControlSettings WithColumns(params string[] headers)
3777 {
3778 if (headers != null)
3779 {
3780 columnHeaders.AddRange(headers);
3781 }
3782 return this;
3783 }
3784
3790 public IAllowListViewControlSettings WithView(ListViewMode view)
3791 {
3792 viewMode = view;
3793 return this;
3794 }
3795
3802 {
3803 fullRowSelect = enable;
3804 return this;
3805 }
3806
3813 {
3814 multiSelect = enable;
3815 return this;
3816 }
3817
3821 public IAllowListViewControlSettings WithSortable(bool sortable = true)
3822 {
3823 this.sortable = sortable;
3824 return this;
3825 }
3826
3832 {
3833 columnWeights.Add(weight);
3834 return this;
3835 }
3836
3840 public IReadOnlyList<string> Columns => columnHeaders.AsReadOnly();
3841
3845 public IReadOnlyList<double> ColumnWeights => columnWeights.AsReadOnly();
3846
3848 public bool Sortable => sortable;
3852 public View ViewMode
3853 {
3854 get
3855 {
3856 switch (viewMode)
3857 {
3858 case ListViewMode.List: return View.List;
3859 case ListViewMode.LargeIcon: return View.LargeIcon;
3860 case ListViewMode.SmallIcon: return View.SmallIcon;
3861 case ListViewMode.Tile: return View.Tile;
3862 default: return View.Details;
3863 }
3864 }
3865 }
3866
3869 public bool FullRowSelect => fullRowSelect;
3873 public bool MultiSelect => multiSelect;
3874
3875 public int SelectedIndex
3876 {
3877 get
3878 {
3879 int retVal = -1;
3880
3881 if (WindowsControl is ListView lv && true == Alive(lv))
3882 retVal = OnUiRead(lv, () => 0 < lv.SelectedItems.Count ? lv.SelectedItems[0].Index : -1, retVal);
3883
3884 return retVal;
3885 }
3886 }
3887
3888 public void ClearItems()
3889 {
3890 if (!(WindowsControl is ListView lv) || false == Alive(lv)) return;
3891 OnUi(lv, () => lv.Items.Clear());
3892 }
3893
3899 public void DisableItem(int index)
3900 {
3901 SetItemEnabled(index, false);
3902 }
3903
3908 public void EnableItem(int index)
3909 {
3910 SetItemEnabled(index, true);
3911 }
3912
3918 public bool IsItemEnabled(int index)
3919 {
3920 bool retVal = true;
3921
3922 if (!(WindowsControl is ListView lv) || false == Alive(lv)) return retVal;
3923
3924 MethodInvoker read = () =>
3925 {
3926 if (0 <= index && index < lv.Items.Count)
3927 retVal = SystemColors.GrayText != lv.Items[index].ForeColor;
3928 };
3929
3930 OnUi(lv, read);
3931
3932 return retVal;
3933 }
3934
3935 // the colour is the state, so what a row looks like and what it does can never disagree
3936 private void SetItemEnabled(int index, bool enabled)
3937 {
3938 if (!(WindowsControl is ListView lv) || false == Alive(lv)) return;
3939
3940 MethodInvoker paint = () =>
3941 {
3942 if (0 > index || index >= lv.Items.Count) return;
3943
3944 lv.Items[index].ForeColor = true == enabled ? SystemColors.WindowText : SystemColors.GrayText;
3945
3946 if (false == enabled) lv.Items[index].Selected = false;
3947 };
3948
3949 OnUi(lv, paint);
3950 }
3951
3952 public void AddItem(string title, params string[] subItems)
3953 {
3954 // rows added before the form built the list are held until it exists rather than dropped. A
3955 // workflow that fills a list at startup is the ordinary case, and losing those rows silently is
3956 // the kind of thing that reads as an empty list with nothing to explain it
3957 if (!(WindowsControl is ListView lv) || false == Alive(lv))
3958 {
3959 pendingRows.Enqueue(new Tuple<string, string[]>(title, subItems ?? new string[0]));
3960 return;
3961 }
3962
3963 var item = new ListViewItem(title);
3964 foreach (var s in subItems) item.SubItems.Add(s);
3965 OnUi(lv, () => lv.Items.Add(item));
3966 }
3967
3968 // what was added before the list existed, in the order it was added
3969 private readonly System.Collections.Concurrent.ConcurrentQueue<Tuple<string, string[]>> pendingRows =
3970 new System.Collections.Concurrent.ConcurrentQueue<Tuple<string, string[]>>();
3971
3975 internal override void Realized()
3976 {
3977 while (true == pendingRows.TryDequeue(out Tuple<string, string[]> held))
3978 AddItem(held.Item1, held.Item2);
3979
3980 // a list on a tab nobody has opened has no handle, so every fit asked for before now was refused.
3981 // The handle arrives when the tab is first shown, and the rows are measurable from then on
3982 if (WindowsControl is ListView lv)
3983 {
3984 lv.HandleCreated += (sender, args) => LayOutColumns();
3985 lv.VisibleChanged += (sender, args) => LayOutColumns();
3986 lv.Resize += (sender, args) => LayOutColumns();
3987 }
3988 }
3989
3990
4008 public IAllowListViewControlSettings WithColumnWidth(params double[] shares)
4009 {
4010 columnWeights.Clear();
4011
4012 if (null != shares)
4013 columnWeights.AddRange(shares);
4014
4015 return this;
4016 }
4017
4018 // how wide each column is, in column order. Empty leaves every column as the control makes it
4019 internal readonly List<int> _columnWidths = new List<int>();
4020
4040 {
4041 _columnWidths.Clear();
4042
4043 if (null != widths)
4044 _columnWidths.AddRange(widths);
4045
4046 ApplyColumnWidths();
4047
4048 return this;
4049 }
4050
4051 // whether a workflow said anything about the widths at all. With shares, the form reflows them on
4052 // every resize and this stays out of the way: two handlers pulling the same column two ways is what
4053 // the old code did
4054 internal bool LastColumnFills => 0 == columnWeights.Count;
4055
4056 // how far short of the edge the last column stops, so a row never tips the control into a horizontal
4057 // scrollbar as the form is dragged narrower
4058 internal const int FillMargin = 2;
4059
4060 // narrow enough to be a column, wide enough to be clicked on
4061 private const int NarrowestColumn = 24;
4062
4063 // laying a column out lays the control out again, which raises the resize this runs from
4064 private bool layingOut = false;
4065
4078 internal void LayOutColumns()
4079 {
4080 if (!(WindowsControl is ListView lv) || false == Alive(lv) || 0 < columnWeights.Count)
4081 return;
4082
4083 if (true == lv.InvokeRequired)
4084 {
4085 OnUi(lv, LayOutColumns);
4086 return;
4087 }
4088
4089 if (false == lv.IsHandleCreated || 0 == lv.Columns.Count || true == layingOut)
4090 return;
4091
4092 layingOut = true;
4093
4094 try
4095 {
4096 int available = Math.Max(NarrowestColumn * lv.Columns.Count, lv.ClientSize.Width - FillMargin);
4097 int[] widths = new int[lv.Columns.Count];
4098 int named = 0;
4099 int unnamed = 0;
4100
4101 // a named width is a positive number. A sentinel, a zero, or a column nobody mentioned is one
4102 // of the columns that shares what is left
4103 for (int column = 0; column < widths.Length; column++)
4104 if (column < _columnWidths.Count && 0 < _columnWidths[column])
4105 {
4106 widths[column] = _columnWidths[column];
4107 named += widths[column];
4108 }
4109 else
4110 unnamed++;
4111
4112 if (0 < unnamed)
4113 {
4114 int each = Math.Max(NarrowestColumn, (available - named) / unnamed);
4115
4116 for (int column = 0; column < widths.Length; column++)
4117 if (0 == widths[column])
4118 widths[column] = each;
4119 }
4120
4121 // wider than the control, so everything comes down together rather than the last column being
4122 // squeezed on its own or a scrollbar appearing
4123 int total = 0;
4124
4125 foreach (int width in widths)
4126 total += width;
4127
4128 if (available < total && 0 < total)
4129 {
4130 for (int column = 0; column < widths.Length; column++)
4131 widths[column] = Math.Max(NarrowestColumn, widths[column] * available / total);
4132
4133 total = 0;
4134
4135 foreach (int width in widths)
4136 total += width;
4137 }
4138
4139 // the last column ends the row at the edge, which is what -2 was always standing in for
4140 widths[widths.Length - 1] += available - total;
4141
4142 if (NarrowestColumn > widths[widths.Length - 1])
4143 widths[widths.Length - 1] = NarrowestColumn;
4144
4145 for (int column = 0; column < widths.Length; column++)
4146 lv.Columns[column].Width = widths[column];
4147 }
4148 finally
4149 {
4150 layingOut = false;
4151 }
4152 }
4153
4154// the widths are given before the form is built and again after, because a control that does not exist
4155 // yet cannot be told anything and one that already does will not be rebuilt
4156 internal void ApplyColumnWidths()
4157 {
4158 LayOutColumns();
4159 }
4160
4164 public string SelectedText
4165 {
4166 get
4167 {
4168 string retVal = null;
4169
4170 // read on the ui thread and guarded, the way SelectedIndex above already is. Read raw it could
4171 // throw or answer with a half-updated selection when a workflow thread asked
4172 if (WindowsControl is ListView lv && true == Alive(lv))
4173 retVal = OnUiRead(lv, () => 0 < lv.SelectedItems.Count ? lv.SelectedItems[0].Text : null, retVal);
4174
4175 return retVal;
4176 }
4177 }
4178
4187 public IReadOnlyList<bool> CheckedItems
4188 {
4189 get
4190 {
4191 List<bool> retVal = new List<bool>();
4192
4193 if (WindowsControl is ListView lv && true == Alive(lv))
4194 retVal = OnUiRead(lv, () =>
4195 {
4196 List<bool> ticked = new List<bool>();
4197
4198 foreach (ListViewItem item in lv.Items)
4199 ticked.Add(item.Checked);
4200
4201 return ticked;
4202 }, retVal);
4203
4204 return retVal.AsReadOnly();
4205 }
4206 }
4207
4216 public IReadOnlyList<bool> SelectedItems
4217 {
4218 get
4219 {
4220 List<bool> retVal = new List<bool>();
4221
4222 if (WindowsControl is ListView lv && true == Alive(lv))
4223 retVal = OnUiRead(lv, () =>
4224 {
4225 List<bool> chosen = new List<bool>();
4226
4227 foreach (ListViewItem item in lv.Items)
4228 chosen.Add(item.Selected);
4229
4230 return chosen;
4231 }, retVal);
4232
4233 return retVal.AsReadOnly();
4234 }
4235 }
4236 }
4237
4250 public class GPALNumericUpDown : GPALControl, IAllowNumericUpDownControlSettings
4251 {
4252 private decimal _value = 0m;
4253 private decimal _minimum = 0m;
4254 private decimal _maximum = 100m;
4255 private decimal _increment = 1m;
4256 private int _decimalPlaces = 0;
4257
4258 internal GPALNumericUpDown()
4259 {
4260 ControlType = FormControlType.NumericUpDown;
4261 }
4262
4269 {
4270 _value = value;
4271 return this;
4272 }
4273
4280 {
4281 _minimum = min;
4282 return this;
4283 }
4284
4291 {
4292 _maximum = max;
4293 return this;
4294 }
4295
4302 {
4303 _increment = inc;
4304 return this;
4305 }
4306
4313 {
4314 _decimalPlaces = places;
4315 return this;
4316 }
4317
4323 public decimal Value
4324 {
4325 get
4326 {
4327 decimal retVal = _value;
4328
4329 if (WindowsControl is NumericUpDown nud && true == Alive(nud))
4330 retVal = OnUiRead(nud, () => nud.Value, retVal);
4331
4332 return retVal;
4333 }
4334 set
4335 {
4336 if (WindowsControl is NumericUpDown nud && true == Alive(nud))
4337 {
4338 OnUi(nud, () => nud.Value = value);
4339 }
4340 else
4341 {
4342 _value = value;
4343 }
4344 }
4345 }
4346
4349 public decimal Minimum => _minimum;
4353 public decimal Maximum => _maximum;
4357 public decimal Increment => _increment;
4361 public int DecimalPlaces => _decimalPlaces;
4362 }
4363
4374 public class GPALDateTimePicker : GPALControl, IAllowDateTimePickerControlSettings
4375 {
4376 private DateTime _value = DateTime.Now;
4377 private DateTimePickerFormat _format = DateTimePickerFormat.Long;
4378 private string _customFormat = "";
4379
4380 internal GPALDateTimePicker()
4381 {
4382 ControlType = FormControlType.DateTimePicker;
4383 }
4384
4391 {
4392 _value = value;
4393 return this;
4394 }
4395
4403 public IAllowDateTimePickerControlSettings WithFormat(DateTimePickerFormat format)
4404 {
4405 _format = format;
4406 return this;
4407 }
4408
4415 {
4416 _customFormat = formatString;
4417 return this;
4418 }
4419
4425 public DateTime Value
4426 {
4427 get
4428 {
4429 DateTime retVal = _value;
4430
4431 if (WindowsControl is DateTimePicker dtp && true == Alive(dtp))
4432 retVal = OnUiRead(dtp, () => dtp.Value, retVal);
4433
4434 return retVal;
4435 }
4436 set
4437 {
4438 if (WindowsControl is DateTimePicker dtp && true == Alive(dtp))
4439 {
4440 OnUi(dtp, () => dtp.Value = value);
4441 }
4442 else
4443 {
4444 _value = value;
4445 }
4446 }
4447 }
4448
4451 public DateTimePickerFormat Format => _format;
4455 public string CustomFormat => _customFormat;
4456 }
4457
4467 public class GPALTreeView : GPALControl, IAllowTreeViewControlSettings
4468 {
4469 private bool _checkBoxes = false;
4470 private bool _showLines = true;
4471 private bool _showPlusMinus = true;
4472
4473 internal GPALTreeView()
4474 {
4475 ControlType = FormControlType.TreeView;
4476 }
4477
4484 {
4485 _checkBoxes = enable;
4486 return this;
4487 }
4488
4495 {
4496 _showLines = show;
4497 return this;
4498 }
4499
4506 {
4507 _showPlusMinus = show;
4508 return this;
4509 }
4510
4514 public bool CheckBoxes => _checkBoxes;
4518 public bool ShowLines => _showLines;
4522 public bool ShowPlusMinus => _showPlusMinus;
4523
4527 public void ClearNodes()
4528 {
4529 if (!(WindowsControl is TreeView tv) || false == Alive(tv)) return;
4530 OnUi(tv, () => tv.Nodes.Clear());
4531 }
4532
4538 {
4539 get
4540 {
4541 if (!(WindowsControl is TreeView tv) || false == Alive(tv)) return null;
4542
4543 TreeNode node = null;
4544
4545 OnUi(tv, () => node = tv.SelectedNode);
4546
4547 return null != node ? new GPALTreeNode(node) : null;
4548 }
4549 set
4550 {
4551 if (!(WindowsControl is TreeView tv) || false == Alive(tv)) return;
4552
4553 MethodInvoker select = () => tv.SelectedNode = value?.Node;
4554
4555 OnUi(tv, select);
4556 }
4557 }
4558
4563 public void BeginUpdate()
4564 {
4565 if (!(WindowsControl is TreeView tv) || false == Alive(tv)) return;
4566
4567 OnUi(tv, () => tv.BeginUpdate());
4568 }
4569
4573 public void EndUpdate()
4574 {
4575 if (!(WindowsControl is TreeView tv) || false == Alive(tv)) return;
4576
4577 OnUi(tv, () => tv.EndUpdate());
4578 }
4579
4585 public GPALTreeNode AddNode(string text)
4586 {
4587 if (!(WindowsControl is TreeView tv) || false == Alive(tv)) return null;
4588 TreeNode node = null;
4589 OnUi(tv, () => node = tv.Nodes.Add(text));
4590 return node != null ? new GPALTreeNode(node) : null;
4591 }
4592 }
4593
4598 public class GPALTreeNode
4599 {
4600 private readonly TreeNode _node;
4601
4602 internal GPALTreeNode(TreeNode node) { _node = node; }
4603
4604 // the node this handle stands for, so the tree can hand one back and be given one
4605 internal TreeNode Node => _node;
4606
4610 public string Text
4611 {
4612 get
4613 {
4614 TreeView tv = _node.TreeView;
4615 string retVal = null;
4616
4617 GPALControl.OnUiOrHere(tv, () => retVal = _node.Text);
4618
4619 return retVal;
4620 }
4621 set
4622 {
4623 TreeView tv = _node.TreeView;
4624
4625 GPALControl.OnUiOrHere(tv, () => _node.Text = value);
4626 }
4627 }
4628
4633 public object Tag
4634 {
4635 get { return _node.Tag; }
4636 set { _node.Tag = value; }
4637 }
4638
4642 public void Select()
4643 {
4644 TreeView tv = _node.TreeView;
4645
4646 if (null == tv) return;
4647
4648 GPALControl.OnUiOrHere(tv, () => tv.SelectedNode = _node);
4649 }
4650
4654 public void Collapse()
4655 {
4656 TreeView tv = _node.TreeView;
4657
4658 GPALControl.OnUiOrHere(tv, () => _node.Collapse());
4659 }
4660
4666 public GPALTreeNode AddChild(string text)
4667 {
4668 TreeNode child = null;
4669 TreeView tv = _node.TreeView;
4670 GPALControl.OnUiOrHere(tv, () => child = _node.Nodes.Add(text));
4671 return child != null ? new GPALTreeNode(child) : null;
4672 }
4673
4674
4680 public bool Enabled
4681 {
4682 get
4683 {
4684 return SystemColors.GrayText != _node.ForeColor;
4685 }
4686 set
4687 {
4688 TreeView tv = _node.TreeView;
4689
4690 // the colour is the state. a TreeNode has no Enabled of its own, and keeping the answer on the
4691 // node rather than in a list beside it means it cannot drift from what is on screen
4692 MethodInvoker paint = () => _node.ForeColor = true == value ? SystemColors.WindowText : SystemColors.GrayText;
4693
4694 GPALControl.OnUiOrHere(tv, paint);
4695 }
4696 }
4697
4701 public void Expand()
4702 {
4703 TreeView tv = _node.TreeView;
4704 GPALControl.OnUiOrHere(tv, () => _node.Expand());
4705 }
4706 }
4707
4712 public class FileSelectorEventArgs : EventArgs
4713 {
4717 public bool Cancelled { get; }
4721 public string SelectedPath { get; }
4725 public IReadOnlyList<string> SelectedPaths { get; }
4726
4733 public FileSelectorEventArgs(bool cancelled, string selectedPath, string[] selectedPaths)
4734 {
4735 Cancelled = cancelled;
4736 SelectedPath = selectedPath ?? "";
4737 if (selectedPaths == null || selectedPaths.Length == 0)
4738 {
4739 SelectedPaths = Array.AsReadOnly(Array.Empty<string>());
4740 }
4741 else
4742 {
4743 SelectedPaths = Array.AsReadOnly(selectedPaths);
4744 }
4745 }
4746 }
4747
4762 public class GPALFileSelector : GPALControl, IAllowFileSelectorControlSettings
4763 {
4764 internal string _title = "Select File/Folder";
4765 internal string _initialDirectory = "";
4766 internal string _filter = "All files (*.*)|*.*";
4767 internal bool _selectFolder = false; // true = FolderBrowserDialog, false = OpenFileDialog
4768 internal bool _multiSelect = false;
4769 internal bool _compact = false;
4770 internal string _placeholder = "";
4771 internal string _selectedPath = "";
4772 internal readonly List<string> _selectedPaths = new List<string>();
4773
4774 internal GPALFileSelector()
4775 {
4776 ControlType = FormControlType.FileSelector;
4777 }
4778
4785 {
4786 _title = string.IsNullOrWhiteSpace(title) ? "Select File/Folder" : title;
4787 return this;
4788 }
4789
4796 {
4797 _initialDirectory = path;
4798 return this;
4799 }
4800
4808 {
4809 _filter = string.IsNullOrWhiteSpace(filter) ? "All files (*.*)|*.*" : filter;
4810 return this;
4811 }
4812
4820 {
4821 _selectFolder = folderMode;
4822 return this;
4823 }
4824
4832 {
4833 _multiSelect = allowMulti;
4834 return this;
4835 }
4836
4844 {
4845 _compact = compact;
4846 return this;
4847 }
4848
4852 {
4853 _placeholder = placeholder ?? "";
4854 return this;
4855 }
4856
4874 {
4875 _selectedPath = path ?? "";
4876
4877 _selectedPaths.Clear();
4878
4879 if (false == string.IsNullOrEmpty(_selectedPath))
4880 _selectedPaths.Add(_selectedPath);
4881
4882 return this;
4883 }
4884
4885 public string SelectedPath => _selectedPath;
4889 public IReadOnlyList<string> SelectedPaths => _selectedPaths.AsReadOnly();
4890
4896 internal void UpdateSelection(string[] paths)
4897 {
4898 _selectedPaths.Clear();
4899 if (paths != null && paths.Length > 0)
4900 {
4901 _selectedPaths.AddRange(paths);
4902 _selectedPath = paths[0];
4903 SyncDisplay();
4904 }
4905 else
4906 {
4907 _selectedPath = "";
4908 }
4909 }
4918 internal void NotifySelection(bool cancelled, string[] paths)
4919 {
4920 UpdateSelection(paths);
4921
4922 var args = new FileSelectorEventArgs(cancelled, SelectedPath, paths);
4923
4924 foreach (var (evt, handler) in Callbacks)
4925 {
4926 ControlEventType target = evt == ControlEventType.Default ? ControlEventType.Change : evt;
4927
4928 if (target == ControlEventType.Change ||
4929 target == ControlEventType.Click ||
4930 target == ControlEventType.Default)
4931 {
4932 ((EventHandler)handler)?.Invoke(this, args);
4933 }
4934 }
4935 }
4936 public void Clear()
4937 {
4938 _selectedPath = "";
4939 _selectedPaths.Clear();
4940 if (WindowsControl is Control ctrl && true == Alive(ctrl))
4941 {
4942 PostToUi(ctrl, () => ctrl.Text = "");
4943 }
4944 }
4945
4946 public void SetPath(string path)
4947 {
4948 _selectedPath = path ?? "";
4949 _selectedPaths.Clear();
4950 if (!string.IsNullOrEmpty(_selectedPath))
4951 _selectedPaths.Add(_selectedPath);
4952 if (WindowsControl is Control ctrl && true == Alive(ctrl))
4953 {
4954 PostToUi(ctrl, () => ctrl.Text = _selectedPath);
4955 }
4956 }
4957
4958 private void SyncDisplay()
4959 {
4960 if (WindowsControl is Control ctrl && true == Alive(ctrl))
4961 {
4962 string display = _selectedPaths.Count > 0
4963 ? string.Join("; ", _selectedPaths)
4964 : "";
4965
4966 PostToUi(ctrl, () => ctrl.Text = display);
4967 }
4968 }
4969 }
4970
4971 #endregion <GPAL Controls>
4972}
4973
File-side plumbing behind the fluent chain: writing a unit of work's data out in a delimited format,...
Definition FileHelper.cs:55
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
List< string > Filenames
Get the list of filenames.
Definition GPALFile.cs:540
string Filename
We have only one file, accessing it.
Definition GPALFile.cs:468
Event arguments passed to a GPALFileSelector callback when the file/folder selection dialog is closed...
bool Cancelled
true if the user dismissed the dialog without selecting a path.
FileSelectorEventArgs(bool cancelled, string selectedPath, string[] selectedPaths)
Creates the event arguments for a completed file selector dialog.
string SelectedPath
The first selected path, or an empty string if none was selected.
IReadOnlyList< string > SelectedPaths
All selected paths. Contains more than one entry only when the dialog allowed multi-select.
A single item shared by GPAL menu bars and toolbars. Because it derives from GPALControl it speaks th...
override bool Enabled
Whether the item can be clicked. Readable and settable while the form is up, so a menu can follow wha...
new IAllowBarItemSettings WithEnabled(bool trueFalse=true)
Greys the item and stops it being clicked. A menu item that does not apply is worth showing greyed ra...
IAllowBarItemSettings WithImage(GPALFile file)
Sets the item's icon by loading it from the file referenced by a GPALFile.
new IAllowBarItemSettings WithText(string text)
Sets the item's display text.
IAllowBarItemSettings WithImage(System.Drawing.Image image)
Sets the item's icon from an in-memory image.
IAllowControlSettingsAndDefault Default
Fluent interface to designate this button as the default button. NOTE: Do NOT define more than one d...
SeriesCollection Series
Underlying Windows chart Series collection.
TitleCollection Titles
Underlying Windows chart Titles collection.
IAllowControlSettingsAndChartSettings WithPalette(ChartColorPalette colorPalette)
Set the color palette of the chart bars.
IAllowControlSettingsAndChartSettings WithSeries(Series series)
Sets the series to plot in the chart.
IAllowControlSettingsAndChartSettings WithTitle(string title)
The title to display at the top of the chart.
LegendCollection Legends
Underlying Windows chart Legends collection.
IAllowControlSettingsAndChecked WithChecked(bool isChecked)
Sets whether the checkbox is checked.
bool Checked
Runtime access to un/check the checkbox. Setting this writes through to the live control (if realized...
IAllowComboBoxControlSettings WithSelectedIndex(int index)
Sets which item is initially selected, by index. Ignored if index is out of range of the items added...
IAllowComboBoxControlSettings WithItems(Array itemArray)
Appends the items from a non-generic array (e.g. the result of Enum.GetValues) to the dropdown list,...
void ClearItems()
Empties the dropdown, so its contents can be replaced with a list that suits what has been chosen els...
bool IsItemEnabled(string item)
Whether the item is currently choosable.
IAllowComboBoxControlSettings WithSelectedItem(string name)
Sets which item is initially selected, by its display text. If the item is not found in the list the ...
void EnableItem(string item)
Makes a disabled item choosable again.
void DisableItem(string item)
Greys an item and stops it being chosen, leaving it on the list. Use it for an option that does not a...
IAllowComboBoxControlSettings WithItems(params string[] itemArray)
Appends the given items to the dropdown list, in order.
string SelectedValue
The text of the currently selected item.
IAllowComboBoxControlSettings WithItems(IEnumerable< string > itemCollection)
Appends the given items to the dropdown list, in order.
int SelectedIndex
The 0-based index of the currently selected item, or -1 if none is selected. Setting this writes thro...
Base class for all GPAL form controls.
IAllowControlSettings WithToolTipDelay(int delayInMs)
Sets how long the tooltip set via WithToolTip(string) remains visible.
IAllowToolTipDelay WithToolTip(string toolTipText)
Sets the tooltip text shown when hovering over the control.
IAllowControlSettings WithWidth(int width)
Sets the width of the control, in pixels. A control with no width stretches across its container,...
int Width
The width of the control, in pixels. 0 [default] lets the control stretch to its container.
int Height
The height of the control, in pixels. 0 [default] lets the control size itself.
ContentAlignment TextAlignment
The alignment currently in use.
virtual bool Enabled
Whether the control accepts input, before the form is built and after. Every WinForms control has En...
string Text
For text based controls, get or set the text in the control. Setting this records the text and hands...
IAllowForEventControlSettings WithCallbackOffUIThread< TDelegate >(TDelegate handler)
Attaches a handler that runs off the UI thread, so the form keeps painting while it works....
string Name
Control name, only used for Information and Exception channel messages. NOTE: Use ....
Font FontType
The font currently being used.
bool AutoSize
Autosize setting currently in use.
dynamic ToGPALObject()
Return a typeof(GPALControl) so that controls do not have to be cast.
FormControlType ControlType
The control type of WindowsControl. Set when the control is instantiated with GPAL.
object Tag
for attaching value-added objects to carry along on form controls
virtual IAllowControlSettings WithReadOnly(bool readOnly=true)
Whether the user can change what the control holds. Every control takes it; the ones that hold nothin...
IAllowControlSettings WithHeight(int height)
Sets the height of the control, in pixels.
IAllowControlSettingsAndFontSettings WithFont(Font font)
The font style and size for the label display.
IAllowControlSettings WithName(string name)
Sets the name for the control. Used in Information and Exception channel messages.
IAllowForEventControlSettings WithCallback< TDelegate >(TDelegate handler)
Sets an eventhandler to attach to the Windows form control event. See the GPAL controls for which ev...
IAllowControlSettingsAndFontSettings WithText(string text)
Set the text of the control.
virtual IAllowControlSettings WithEnabled(bool enabled=true)
Fluent interface to set the initial state of the control.
dynamic WindowsControl
Underlying Windows form control, Button, Checkbox, Textbox, etc. NOTE: Use GPAL to instantiate form ...
IAllowControlSettings ForEvent(ControlEventType eventType)
Specifies which event the most recently added WithCallback<TDelegate>(TDelegate) handler should be at...
IAllowControlSettings WithShortcutKey(System.Windows.Forms.Keys shortCutKey)
Set the keyboard shortcut that will activate this controls eventhandler defined with ....
void OnUiThread(Action action)
Runs action on the UI thread. If the calling thread already owns the UI, the action runs inline; oth...
IAllowControlSettingsAndFontSettings WithTextAlignment(ContentAlignment textAlignment)
The alignment of the text inside the label. .
IAllowControlSettingsAndFontSettings WithAutoSize(bool autoSize)
Whether or not the label contents autosize to fit the label. .
IAllowControlSettings WithTag(object tag)
Set the text of the control.
IAllowDataGridViewControlSettings WithColumnWidth(params double[] shares)
What share of the width each column takes, in column order.
IAllowDataGridViewControlSettings WithInput(GPALFile file)
The rows to show, read from a file.
IAllowDataGridViewControlSettings WithColumns(params string[] columnNames)
Sets the column header names for the grid. If not specified and WithAutoGenerateColumns(bool) is true...
IAllowDataGridViewControlSettings WithEndColumn(int column)
The last column a range covers, counting from one.
new IAllowDataGridViewControlSettings WithReadOnly(bool readOnly=true)
Sets whether the grid's cells are read-only.
IAllowDataGridViewControlSettings WithEndRow(int row)
The last row a range covers, counting from one.
IAllowDataGridViewControlSettings WithInput(IGPALDatabase database)
The rows to show, read by running the database's query.
IReadOnlyList< string > ColumnNames
The column header names set via WithColumns(string[]).
IAllowDataGridViewControlSettings GetGrid(out IGPALGrid< string > grid)
Reads what the grid is showing into a GPAL grid, headers included.
IAllowDataGridViewControlSettings WithStartRow(int row)
The first row a range covers, counting from one.
IAllowDataGridViewControlSettings WithAutoGenerateColumns(bool autoGenerate)
Sets whether columns should be auto-generated (named "Column 1", "Column 2", etc.) when no column nam...
IAllowDataGridViewControlSettings WithStartColumn(int column)
The first column a range covers, counting from one.
IAllowDataGridViewControlSettings WithInput(IGPALGrid< string > grid)
Sets the data grid to display and immediately syncs it to the underlying Windows control via SyncGrid...
IGPALGrid< string > CurrentGrid
The grid data currently displayed, as set via WithInput(IGPALGrid<string>). Setting this refreshes th...
bool AutoGenerateColumns
Whether columns are auto-generated when no column names were provided, as set via WithAutoGenerateCol...
IAllowDataGridViewControlSettings WithInput(DataTable dataTable)
The rows to show, as a DataTable.
IAllowDataGridViewControlSettings WithColumnWidth(params int[] widths)
How wide each column is, in column order and in pixels.
IAllowDataGridViewControlSettings WithWrap(params bool[] wrap)
Which columns wrap their text rather than running off the end of the cell, in column order....
IAllowDateTimePickerControlSettings WithValue(DateTime value)
Sets the initial date/time value. Defaults to the current date/time.
string CustomFormat
The custom display format string, as set via WithCustomFormat(string).
IAllowDateTimePickerControlSettings WithCustomFormat(string formatString)
Sets a custom display format string, used when Format is DateTimePickerFormat.Custom.
IAllowDateTimePickerControlSettings WithFormat(DateTimePickerFormat format)
Sets the display format. Defaults to DateTimePickerFormat.Long. Use DateTimePickerFormat....
DateTime Value
Runtime access to the date/time. Reads the live control once the form is up, so the date the user pic...
DateTimePickerFormat Format
The display format, as set via WithFormat(DateTimePickerFormat).
IAllowFileSelectorControlSettings WithInitialDirectory(string path)
Sets the directory the selection dialog initially opens to.
IAllowFileSelectorControlSettings WithPlaceholder(string placeholder)
Sets placeholder hint text shown in the text field when it is empty. Only applies in compact mode.
IAllowFileSelectorControlSettings WithFilter(string filter)
Sets the file type filter used by the OpenFileDialog when WithSelectFolder(bool) is false.
IAllowFileSelectorControlSettings WithMultiSelect(bool allowMulti)
Sets whether the file dialog allows selecting multiple files. Has no effect when WithSelectFolder(boo...
IAllowFileSelectorControlSettings WithCompact(bool compact)
When true, renders as an editable text field with a small "..." browse button on the right,...
IAllowFileSelectorControlSettings WithSelectFolder(bool folderMode)
Sets whether the control opens a FolderBrowserDialog (folder selection) or an OpenFileDialog (file se...
IAllowFileSelectorControlSettings WithTitle(string title)
Sets the title bar text of the selection dialog.
IReadOnlyList< string > SelectedPaths
All paths selected in the most recent dialog.
IAllowFileSelectorControlSettings WithSelectedPath(string path)
Sets the path the selector starts on, as though it had already been chosen. SelectedPath reads it ba...
IAllowGroupBoxControlSettings WithFormControl(object gPALFormControl)
Add a control inside the box. A container can go in here too, so a group box can hold a splitter.
new GPALGroupBox ToGPALObject()
Returns this group box so it can be handed to a form or to another container.
new IAllowGroupBoxControlSettings WithText(string text)
The caption along the top of the box.
bool ShowPassword
The input a password or other sensitive information. The input will be masked with a bullet characte...
override IAllowControlSettings WithReadOnly(bool readOnly=true)
Whether the user can change what the control holds. Every control takes it; the ones that hold nothin...
IAllowListViewControlSettings WithSortable(bool sortable=true)
Enables click-to-sort on all columns. Clicking a column header sorts ascending; clicking again revers...
bool Sortable
Whether column-click sorting is enabled.
void EnableItem(int index)
Makes a disabled row selectable again.
IReadOnlyList< bool > CheckedItems
Which rows are ticked, one entry per row, in the order the rows are currently in. That is the order ...
IAllowListViewControlSettings WithFullRowSelect(bool enable)
Sets whether selecting an item highlights the entire row. Defaults to true.
IAllowListViewControlSettings WithColumnWeight(double weight)
IReadOnlyList< string > Columns
The column header names set via WithColumns(string[]).
IAllowListViewControlSettings WithColumnWidth(params int[] widths)
How wide each column is, in column order and in pixels.
string SelectedText
The text of the first selected item, or null if nothing is selected.
IAllowListViewControlSettings WithColumnWidth(params double[] shares)
What share of the width each column takes, in column order.
bool IsItemEnabled(int index)
Whether the row at this position can be selected.
void DisableItem(int index)
Greys a row and stops it being selected, leaving it on the list. Rows are addressed by position,...
IReadOnlyList< double > ColumnWeights
Proportional column weights set via WithColumnWeight(double).
View ViewMode
The view mode set via WithView(ListViewMode), mapped to the underlying WinForms View.
IAllowListViewControlSettings WithView(ListViewMode view)
Sets how items are displayed (Details, List, LargeIcon, SmallIcon, or Tile). Defaults to ListViewMode...
bool CheckBoxes
Whether rows carry a checkbox. Settable while the form is up.
bool MultiSelect
Whether multi-select is enabled, as set via WithMultiSelect(bool).
bool FullRowSelect
Whether full-row selection is enabled, as set via WithFullRowSelect(bool).
IReadOnlyList< bool > SelectedItems
Which rows are selected, one entry per row, in the order the rows are currently in,...
IAllowListViewControlSettings WithCheckBoxes(bool enable=true)
Sets the column headers for the list view (used in ListViewMode.Details mode).
IAllowListViewControlSettings WithMultiSelect(bool enable)
Sets whether multiple items can be selected at once. Defaults to false.
IAllowMenuBarControlSettings WithMenu(string topMenu)
Opens (or reselects) a top-level menu. Subsequent WithItem(GPALBarItem) and WithSeparator calls attac...
IAllowMenuBarControlSettings WithItem(GPALBarItem item)
Adds an item to the currently open top-level menu.
IAllowMenuBarControlSettings WithSeparator()
Adds a separator line to the currently open top-level menu.
decimal Minimum
The minimum allowed value, as set via WithMinimum(decimal).
int DecimalPlaces
The number of decimal places displayed, as set via WithDecimalPlaces(int).
IAllowNumericUpDownControlSettings WithIncrement(decimal inc)
Sets the amount the value changes when the up/down spinner is clicked. Defaults to 1.
IAllowNumericUpDownControlSettings WithMinimum(decimal min)
Sets the minimum allowed value. Defaults to 0.
decimal Maximum
The maximum allowed value, as set via WithMaximum(decimal).
IAllowNumericUpDownControlSettings WithDecimalPlaces(int places)
Sets how many decimal places are displayed. Defaults to 0.
decimal Increment
The spinner increment, as set via WithIncrement(decimal).
IAllowNumericUpDownControlSettings WithValue(decimal value)
Sets the initial value.
IAllowNumericUpDownControlSettings WithMaximum(decimal max)
Sets the maximum allowed value. Defaults to 100.
decimal Value
Runtime access to the value. Reads the live control once the form is up, so a value the user spun to ...
IAllowPictureControlSettings WithImage(GPALFile file)
The image to display, read from a file.
IAllowPictureControlSettings WithImage(System.Drawing.Image image)
The image to display.
PictureBoxSizeMode SizeMode
How the image fits the control.
IAllowPictureControlSettings WithSizeMode(PictureBoxSizeMode sizeMode)
How the image fits the control.
System.Drawing.Image Image
The image on display.
IAllowProgressBarControlSettings WithValue(int value)
Sets the initial progress value, clamped to Minimum/Maximum, and syncs it to the underlying control.
int Maximum
The maximum progress value, as set via WithMaximum(int).
ProgressBarStyle Style
The current visual style. Setting this writes through to the live control (if realized),...
int Value
The current progress value, clamped to Minimum/Maximum. Setting this writes through to the live contr...
int Minimum
The minimum progress value, as set via WithMinimum(int).
IAllowProgressBarControlSettings WithStyle(ProgressBarStyle style)
Sets the visual style of the progress bar (e.g. continuous, blocks, marquee).
IAllowProgressBarControlSettings WithMinimum(int min)
Sets the minimum progress value. If Value is below this, it is raised to match.
void Increment(int step=1)
Runtime access to advance the progress value by step , clamped to Minimum/Maximum.
IAllowProgressBarControlSettings WithMaximum(int max)
Sets the maximum progress value. If Value exceeds this, it is lowered to match.
IAllowControlSettingsAndChecked WithChecked(bool isChecked)
Sets whether the radiobutton is checked.
bool Checked
Runtime access to un/check the radiobutton. Setting this writes through to the live control (if reali...
override IAllowControlSettings WithReadOnly(bool readOnly=true)
Whether the user can change what the control holds. Every control takes it; the ones that hold nothin...
new string Text
The Markdown source text. Setting this re-renders headings, bold spans, and bullet/numbered lists (wi...
new IAllowRichTextBoxControlSettings WithText(string text)
Sets the initial Markdown source text.
void AppendLine(string text, int maxLines=0)
Appends text as a new line, thread-safe. The line is appended verbatim rather than rendered as Mark...
void Clear()
Empties the control and anything queued for it, thread-safe.
void ScrollToEnd()
Scrolls to the last line, thread-safe.
int TickFrequency
How far apart the tick marks are.
int Minimum
The lowest value the slider can be dragged to.
IAllowSliderControlSettings WithOrientation(Orientation orientation)
Horizontal or vertical.
IAllowSliderControlSettings WithValue(int value)
Where the slider starts.
IAllowSliderControlSettings WithMaximum(int max)
The highest value the slider can be dragged to.
Orientation Orientation
Horizontal or vertical.
IAllowSliderControlSettings WithTickFrequency(int tickFrequency)
How far apart the tick marks are.
int Value
Where the slider sits, clamped to the range it was given.
int Maximum
The highest value the slider can be dragged to.
IAllowSliderControlSettings WithMinimum(int min)
The lowest value the slider can be dragged to.
IAllowSplitterControlSettings VSplit(int topHeight)
A horizontal bar with a top and a bottom pane, the top one this many pixels tall.
IAllowSplitterControlSettings VSplit(double topPercent)
A horizontal bar with a top and a bottom pane, the top one this share of the height,...
new IAllowSplitterControlSettings WithHeight(int height)
A vertical bar with a left and a right pane, the left one this many pixels wide.
new GPALSplitter ToGPALObject()
Returns this splitter so it can be handed to a form or to another container.
IAllowSplitterControlSettings HSplit(double leftPercent)
A vertical bar with a left and a right pane, the left one this share of the width,...
IAllowSplitterControlSettings WithFormControl(object gPALFormControl)
Add a control to the splitter. Everything before GPAL.SplitRight or GPAL.SplitBottom belongs to the f...
new IAllowStatusStripControlSettings WithText(string text)
Sets the initial status text. Blank or whitespace falls back to "Ready".
new string Text
The status text. Blank or whitespace falls back to "Ready". Setting this writes through to the first ...
void Activate()
Activate (switch to) this tab at runtime, bringing its controls into view.
TableLayoutPanelGrowStyle GrowStyle
The direction in which the panel automatically adds new rows or columns when it runs out of space,...
int RowCount
The number of rows in the panel, mirroring the underlying TableLayoutPanel.RowCount.
int ColumnCount
The number of columns in the panel, mirroring the underlying TableLayoutPanel.ColumnCount.
IAllowControlSettingsAndTableLayoutPanelSettings WithColumnCount(int columnCount)
Sets the number of columns in the panel.
IAllowControlSettingsAndTableLayoutPanelSettings WithRowCount(int rowCount)
Sets the number of rows in the panel.
IAllowControlSettingsAndTableLayoutPanelSettings WithGrowStyle(TableLayoutPanelGrowStyle tableLayoutPanelGrowStyle)
Sets the direction in which the panel automatically adds new rows or columns when it runs out of spac...
new IAllowControlSettingsAndTableLayoutPanelSettings WithEnabled(bool enabled=true)
Fluent interface to set the initial state of the control.
override IAllowControlSettings WithReadOnly(bool readOnly=true)
Whether the user can change what the control holds. Every control takes it; the ones that hold nothin...
void Clear()
Empties the control and anything queued for it, thread-safe.
void AppendLine(string text, int maxLines=0)
Appends text as a new line, thread-safe. When maxLines is greater than zero and the control already...
void ScrollToEnd()
Scrolls the text area to the last line, thread-safe.
IAllowToolbarControlSettings WithItem(GPALBarItem item)
Adds a button to the toolbar.
IAllowToolbarControlSettings WithSeparator()
Adds a vertical separator between toolbar buttons.
A handle to a tree node returned by GPALTreeView.AddNode(string). Use it to add children and control ...
object Tag
Anything the workflow wants to carry on the node: the selector it stands for, the record it came from...
void Collapse()
Hides this node's children, thread-safe.
bool Enabled
Whether this node can be selected. A disabled node stays on the tree, greyed, because a branch that v...
void Select()
Makes this the selected node, thread-safe.
GPALTreeNode AddChild(string text)
Adds a child node under this node, thread-safe.
void Expand()
Expands this node to show its children, thread-safe.
bool ShowLines
Whether lines are drawn between sibling nodes and their parent, as set via WithShowLines(bool).
IAllowTreeViewControlSettings WithShowLines(bool show)
Sets whether lines are drawn between sibling nodes and their parent.
IAllowTreeViewControlSettings WithCheckBoxes(bool enable)
Sets whether checkboxes are displayed next to each tree node.
void BeginUpdate()
Stops the tree redrawing until EndUpdate is called. Building a large tree without it is what makes on...
GPALTreeNode SelectedNode
The node currently selected, or null when nothing is. Settable, so a workflow can put the user where ...
IAllowTreeViewControlSettings WithShowPlusMinus(bool show)
Sets whether plus/minus expand buttons are displayed next to nodes that have children.
void ClearNodes()
Removes all nodes from the tree, thread-safe.
void EndUpdate()
Lets the tree redraw again after BeginUpdate.
bool ShowPlusMinus
Whether plus/minus expand buttons are displayed, as set via WithShowPlusMinus(bool).
GPALTreeNode AddNode(string text)
Adds a top-level node and returns a GPALTreeNode for attaching children. Thread-safe.
bool CheckBoxes
Whether checkboxes are displayed next to each tree node, as set via WithCheckBoxes(bool).
https://stackoverflow.com/questions/11873378/adding-placeholder-text-to-textbox
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
static IAllowGridActions< string > Grid
New GPALGrid<string></string> (rows/columns).
Definition GPAL.cs:532
static void PublishSimpleEvent(GPALEventType gPALEventType, string msg, dynamic gPALObject=null, Enums.GPALObjectType gPALObjectType=GPALObjectType.None, Exception ex=null)
Publish a message to either the information channel or exception channel (if exception passed in) Pub...
Definition GPAL.cs:2568