GPAL - Generally Positive Automation Library v1.0
GPAL The Fluent Automation LIbrary
Loading...
Searching...
No Matches
GPALForm.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 OpenQA.Selenium;
18using System;
19using System.Collections.Generic;
20using System.Drawing;
21using System.Linq;
22using System.Text;
23using System.Threading.Tasks;
24using System.Windows.Forms;
25using System.Windows.Forms.DataVisualization.Charting;
27using static GenerallyPositive.Enums;
29
31{
64 {
91 public delegate CallIfStatus CallAfterFillInDelegate(GPALForm myForm, IGPALGrid<string> tokens, int tokenIdx);
92
93 private readonly System.Windows.Forms.ToolTip _toolTip = new System.Windows.Forms.ToolTip
94 {
95 AutoPopDelay = 5000,
96 InitialDelay = 500,
97 ShowAlways = true
98 };
99
116 public dynamic ToGPALObject()
117 {
118 return this;
119 }
120
124 public string FormName
125 {
126 get
127 {
128 return FormSettings.myForm.FormName;
129 }
130 internal set
131 {
132 FormSettings.myForm.FormName = value;
133 }
134 }
135
136 private FormSettings FormSettings { get; set; }
137
138 internal GPALForm()
139 {
140 FormSettings = new FormSettings();
141 FormSettings.myForm = new GPALFormForm()
142 {
143 ControlList = new List<GPALControl>(),
144 };
145
146 FormSettings.myForm.SuspendLayout();
147 FormSettings.myForm.Left = FormSettings.left;
148 FormSettings.myForm.Top = FormSettings.top;
149 FormSettings.myForm.Width = FormSettings.width;
150 FormSettings.myForm.Height = FormSettings.height;
151 FormSettings.myForm.InitialSize = new Size(FormSettings.width, FormSettings.height);
152
153 FormSettings.myForm.AutoSize = true;
154
155 Size _initialPreferredSize = FormSettings.myForm.Size;
156
157 FormSettings.TableLayoutPanel = new TableLayoutPanel()
158 {
159 //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
160 Dock = DockStyle.Fill,
161 AutoSize = false,
162 AutoScroll = true,
163 };
164 FormSettings.myForm.Controls.Add(FormSettings.TableLayoutPanel);
165
166 System.Windows.Forms.Application.EnableVisualStyles();
167 }
168
169 #region <Settings>
170 internal GPALFormForm Form
171 {
172 get
173 {
174 return FormSettings.myForm;
175 }
176 }
177 internal List<GPALControl> ControlList
178 {
179 get
180 {
181 return FormSettings.myForm.ControlList;
182 }
183 }
190 {
191 FormSettings.width = width;
192 FormSettings.myForm.Width = width;
193 // ensure the right edge is on-screen, but the left edge could be off-screen if the form is too wide
194 // todo - how to handle out of bounds settings?
195 if (FormSettings.left > (Screen.PrimaryScreen.Bounds.Width - width))
196 FormSettings.left = Screen.PrimaryScreen.Bounds.Width - width - 2;
197
198 FormSettings.myForm.InitialSize = new Size(FormSettings.myForm.Width, FormSettings.myForm.Height);
199
200 return this;
201 }
202
213 {
214 FormSettings.buttonWidth = width;
215
216 foreach (GPALControl gPALControl in FormSettings.myForm.ControlList)
217 if (gPALControl is GPALButton gPALButton
218 && 0 >= gPALButton.Width
219 && gPALButton.WindowsControl is Control windowsControl)
220 {
221 if (0 < width)
222 ApplyWidth(width, windowsControl);
223 else
224 // no width is full width again. a button is added anchored left and right, so putting
225 // those back is what undoes a width this form was given earlier
226 GPALControl.OnUi(windowsControl, () =>
227 windowsControl.Anchor = windowsControl.Anchor | AnchorStyles.Left | AnchorStyles.Right);
228 }
229
230 return this;
231 }
232
238 {
239 FormSettings.height = height;
240 FormSettings.myForm.Height = height;
241 // ensure the bottom is on-screen, but this could put the grab bar off-screen
242 if (FormSettings.top > (Screen.PrimaryScreen.Bounds.Height - height))
243 FormSettings.top = Screen.PrimaryScreen.Bounds.Height - height - 2;
244 else
245 FormSettings.top = (Screen.PrimaryScreen.Bounds.Height - height) / 2; // center unless overridden by subsequent Top
246
247 FormSettings.myForm.InitialSize = new Size(FormSettings.myForm.Width, FormSettings.myForm.Height);
248
249 return this;
250 }
251
257 {
258 FormSettings.top = top;
259 FormSettings.myForm.Top = top;
260 return this;
261 }
262
268 {
269 FormSettings.left = left;
270 FormSettings.myForm.Left = left;
271 return this;
272 }
273
276 public int Left => FormSettings.myForm.Left;
280 public int Top => FormSettings.myForm.Top;
284 public int Width => FormSettings.myForm.Width;
288 public int Height => FormSettings.myForm.Height;
296 public IAllowFormSettingsAndActions CallAfterFillIn(GPALForm.CallAfterFillInDelegate callAfterFillIn)
297 {
298 FormSettings.myForm.CallAfterFillIn = callAfterFillIn;
299 return this;
300 }
301 #endregion <Settings>
302 #region <Forms Actions>
318 {
319 return gPALControl switch
320 {
321 GPALButton btn => AddButton(btn),
322 GPALChart ch => AddChart(ch),
323 GPALComboBox cb => AddComboBox(cb),
324 GPALDataGridView dgv => AddDataGridView(dgv),
325 GPALDateTimePicker dtp => AddDateTimePicker(dtp),
326 GPALCheckbox chk => AddCheckbox(chk),
327 GPALFileSelector fs => AddFileSelector(fs),
328 GPALInput inp => AddInput(inp),
329 GPALLabel lbl => AddLabel(lbl),
330 GPALLinkLabel lnk => AddLinkLabel(lnk),
331 GPALSpacer space => AddSpacer(space),
332 GPALListView lv => AddListView(lv),
333 GPALMenuBar mb => AddMenuBar(mb),
334 GPALNumericUpDown nud => AddNumericUpDown(nud),
335 GPALPicture pic => AddPicture(pic),
336 GPALProgressBar pb => AddProgressBar(pb),
337 GPALSlider sl => AddSlider(sl),
338 GPALRadioButton rb => AddRadioButton(rb),
339 GPALRichTextBox rtb => AddRichTextBox(rtb),
340 GPALStatusStrip ss => AddStatusStrip(ss),
341 GPALToolbar tb => AddToolbar(tb),
342 GPALTab tab => AddTab(tab),
343 GPALSplitter sp => AddSplitter(sp),
344 GPALGroupBox gb => AddGroupBox(gb),
345 GPALTableLayoutPanel tlp => AddTableLayoutPanel(tlp),
346 GPALTextArea ta => AddTextArea(ta),
347 GPALTreeView tv => AddTreeView(tv),
348
349 _ => this // or throw new NotSupportedException(...)
350 };
351 }
352
358 {
359 FormSettings.myForm.FormTitle = title;
360 FormSettings.myForm.Text = title;
361 return this;
362 }
363
371 {
372 FormSettings.myForm.InputDatabase = inputDatabase;
373 DatabaseHelper.TokenizeDatabase(inputDatabase);
374 FormHelper.FillInWithTokens(this, new List<IGPALGrid<string>> { inputDatabase.Tokens }, WriteMode.Overwrite);
375 return this;
376 }
377
385 {
386 FormSettings.myForm.InputDatabase = inputDatabase;
387 DatabaseHelper.TokenizeDatabase(inputDatabase);
388 FormHelper.FillInWithTokens(this, new List<IGPALGrid<string>> { inputDatabase.Tokens }, WriteMode.Append);
389 return this;
390 }
391
399 {
400 FormSettings.myForm.InputDatabase = inputDatabase;
401 DatabaseHelper.TokenizeDatabase(inputDatabase);
402 FormHelper.FillInWithTokens(this, new List<IGPALGrid<string>> { inputDatabase.Tokens }, WriteMode.Insert);
403 return this;
404 }
405
413 {
414 FormSettings.myForm.InputFile = inputFile;
415 if (false == FileHelper.TokenizeFile(inputFile))
416 {
417 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unable to tokenize file [{inputFile.Filename}]. Continuing.", inputFile, GPALObjectType.GPALFile);
418 return this;
419 }
420 FormHelper.FillInWithTokens(this, ((IGPALFileInternal)inputFile).TokenList, WriteMode.Overwrite);
421 return this;
422 }
423
431 {
432 FormSettings.myForm.InputFile = inputFile;
433 if (false == FileHelper.TokenizeFile(inputFile))
434 {
435 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unable to tokenize file [{inputFile.Filename}]. Continuing.", inputFile, GPALObjectType.GPALFile);
436 return this;
437 }
438 FormHelper.FillInWithTokens(this, ((IGPALFileInternal)inputFile).TokenList, WriteMode.Append);
439 return this;
440 }
441
449 {
450 FormSettings.myForm.InputFile = inputFile;
451 if (false == FileHelper.TokenizeFile(inputFile))
452 {
453 GPAL.PublishSimpleEvent(GPALEventType.ERROR, $"Unable to tokenize file [{inputFile.Filename}]. Continuing.", inputFile, GPALObjectType.GPALFile);
454 return this;
455 }
456 FormHelper.FillInWithTokens(this, ((IGPALFileInternal)inputFile).TokenList, WriteMode.Insert);
457 return this;
458 }
459
466 public IAllowFormSettingsAndActions FillInFrom(IGPALGrid<string> inputGrid)
467 {
468 FormHelper.FillInWithTokens(this, new List<IGPALGrid<string>> { inputGrid }, WriteMode.Overwrite);
469 return this;
470 }
471
478 public IAllowFormSettingsAndActions AppendFrom(IGPALGrid<string> inputGrid)
479 {
480 FormHelper.FillInWithTokens(this, new List<IGPALGrid<string>> { inputGrid }, WriteMode.Append);
481 return this;
482 }
483
490 public IAllowFormSettingsAndActions InsertFrom(IGPALGrid<string> inputGrid)
491 {
492 FormHelper.FillInWithTokens(this, new List<IGPALGrid<string>> { inputGrid }, WriteMode.Insert);
493 return this;
494 }
495 // a workflow raised from a button is still the form's guest, and a guest that fails should not take the
496 // house down with it. windows forms funnels everything thrown on the ui thread through here, so this is
497 // the one place to say so: publish it, leave the form standing, let whoever pressed the button read the
498 // log tab and press it again
499 private static bool workflowFailureHandled = false;
500
501 private void KeepFormAliveOnWorkflowFailure()
502 {
503 if (true == workflowFailureHandled) return;
504
505 workflowFailureHandled = true;
506
507 // only the subscription. the mode cannot be set once a control exists on the thread, and by the time
508 // a form is shown they all do. windows forms routes to a handler when one is attached, which is this
509 System.Windows.Forms.Application.ThreadException += (sender, args) =>
510 GPAL.PublishSimpleEvent(GPALEventType.EXCEPTION, $"The form kept going after [{args.Exception?.GetType().Name}]", null, GPALObjectType.GPALForm, args.Exception);
511 }
512
519 {
520 KeepFormAliveOnWorkflowFailure();
521 SizeForm();
522 FormSettings.myForm.ResumeLayout();
523 if (true == FormSettings.myForm.Visible)
524 FormSettings.myForm.Hide();
525 FormSettings.myForm.ShowDialog();
526
527 return this;
528 }
529
550 {
551 KeepFormAliveOnWorkflowFailure();
552 SizeForm();
553 FormSettings.myForm.ResumeLayout();
554 if (true == FormSettings.myForm.Visible)
555 FormSettings.myForm.Hide();
556 FormSettings.myForm.Show();
557
558 return this;
559 }
560
591 {
592 FormSettings.myForm.Hide();
593 return this;
594 }
595 #endregion <Forms Actions>
596 #region Helpers
605 private static void ApplyWidth(int width, Control control)
606 {
607 if (0 >= width)
608 return;
609
610 // marshalled, because a width can be changed while the form is up and the caller is not always the
611 // thread that owns it
612 GPALControl.OnUi(control, () =>
613 {
614 control.Width = width;
615 control.Anchor = control.Anchor & ~(AnchorStyles.Left | AnchorStyles.Right);
616 });
617 }
618
624 private void ApplyToolTip(GPALControl gpalControl, Control winControl)
625 {
626 if (!string.IsNullOrWhiteSpace(gpalControl.ToolTipText))
627 {
628 FormSettings.ToolTip.SetToolTip(winControl, gpalControl.ToolTipText);
629 if (gpalControl.ToolTipAutoPopDelayMs != 5000)
630 FormSettings.ToolTip.AutoPopDelay = gpalControl.ToolTipAutoPopDelayMs;
631 }
632 }
633
643 // put on a realized control whose definition carried a height, so the answer does not depend on the
644 // control having been given a name as well
645 internal const string SetHeightMarker = "gpal.height.set";
646
651 private static bool HasASetHeight(Control control, HashSet<string> sized)
652 {
653 return true == sized.Contains(control.Name ?? "")
654 || SetHeightMarker.Equals(control.Tag as string);
655 }
656
657 private static bool GrowsWithTheForm(Control control, HashSet<string> sized)
658 {
659 bool retVal = false;
660
661 if (false == HasASetHeight(control, sized))
662 // a container holds the rest of the form and has to take the height, or it collapses to its own
663 // preferred size: a TabControl given AutoSize is a tab strip and nothing under it
664 // TableLayoutPanel is not here. A nested one is a composite built to a fixed shape, such as the
665 // input and browse button of a file selector, and stretching it leaves its contents at the top
666 // of an empty area
667 retVal = control is TabControl
668 || control is SplitContainer
669 // multiline only. a single line TextBox is also a TextBoxBase and shows no more text at
670 // any greater height
671 || (control is TextBoxBase textBox && true == textBox.Multiline)
672 || control is ListView
673 || control is TreeView
674 || control is DataGridView
675 || control is ListBox;
676
677 return retVal;
678 }
679
695 private static void SettleRows(TableLayoutPanel panel, HashSet<string> sized)
696 {
697 if (null == panel) return;
698
699 SettleInside(panel, sized);
700
701 // A panel with explicit RowStyles is left as it was written, and so is a real grid: this stacks one
702 // control per row in one column, which is not what a multi-column panel is for
703 if (0 < panel.RowStyles.Count || 1 < panel.ColumnCount) return;
704
705 // RowCount is not updated by Controls.Add, so it reads zero here
706 Control[] inOrder = panel.Controls.Cast<Control>().ToArray();
707
708 if (1 > inOrder.Length) return;
709
710 panel.SuspendLayout();
711
712 panel.ColumnCount = 1;
713 panel.ColumnStyles.Clear();
714 panel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100f));
715
716 panel.RowCount = inOrder.Length;
717
718 for (int row = 0; row < inOrder.Length; row++)
719 {
720 panel.SetCellPosition(inOrder[row], new TableLayoutPanelCellPosition(0, row));
721
722
723 // AutoSize sizes a row from the control PreferredSize, which for a SplitContainer, and for any
724 // control given a height with WithHeight, is not the height it is meant to be. Those rows are set
725 // to that height outright. Everything else keeps AutoSize, which is what a label or a button wants
726 bool ownHeight = 0 < inOrder[row].Height
727 && (inOrder[row] is SplitContainer
728 || true == HasASetHeight(inOrder[row], sized));
729
730 panel.RowStyles.Add(true == GrowsWithTheForm(inOrder[row], sized)
731 ? new RowStyle(SizeType.Percent, 100f)
732 : true == ownHeight
733 // the margins are part of the row, or the control is clipped by exactly
734 // that much and the layout compensates by leaving slack elsewhere
735 ? new RowStyle(SizeType.Absolute, inOrder[row].Height
736 + inOrder[row].Margin.Top
737 + inOrder[row].Margin.Bottom)
738 : new RowStyle(SizeType.AutoSize));
739 }
740
741
742 panel.ResumeLayout(true);
743
744 settled.Add(panel);
745
746 // AutoScroll decides from the preferred height, which comes out larger than the rows actually
747 // drawn. Re-evaluated whenever the panel could have changed size and switched off whenever
748 // everything fits, so a panel that is not overflowing cannot show a scrollbar.
749 // HandleCreated as well as SizeChanged: this runs before the form is shown, when no panel has a
750 // handle and nothing can be measured
751 panel.HandleCreated += (sender, args) => ScrollOnlyWhenNeeded(panel);
752 panel.SizeChanged += (sender, args) => ScrollOnlyWhenNeeded(panel);
753 panel.VisibleChanged += (sender, args) => ScrollOnlyWhenNeeded(panel);
754
755 ScrollOnlyWhenNeeded(panel);
756 }
757
758 // Walks every child of every kind. A tab panel sits under a TabPage, a splitter pane under a
759 // SplitterPanel and a group box panel under the box, so anything looking only for TableLayoutPanel
760 // children reaches none of them
761 private static void SettleInside(Control parent, HashSet<string> sized)
762 {
763 foreach (Control child in parent.Controls)
764 if (child is TableLayoutPanel nested)
765 SettleRows(nested, sized);
766 else
767 SettleInside(child, sized);
768 }
769 // the panels this settled, and the only ones it may scroll. a panel that was skipped was skipped because
770 // it is somebody own construction: the file selector is two columns holding one row, and giving that a
771 // scrollbar draws one across the middle of a form
772 private static readonly HashSet<TableLayoutPanel> settled = new HashSet<TableLayoutPanel>();
773
774 // walks every panel this settled and takes the scroll decision again, once sizes are real
775 private static void MeasureAgain(Control parent)
776 {
777 foreach (Control child in parent.Controls)
778 {
779 if (child is TableLayoutPanel panel && true == settled.Contains(panel))
780 ScrollOnlyWhenNeeded(panel);
781
782 MeasureAgain(child);
783 }
784 }
785
786 // guards the re-entry: turning AutoScroll on or off resizes the panel, which raises SizeChanged again
787 private static readonly HashSet<TableLayoutPanel> measuring = new HashSet<TableLayoutPanel>();
788
789 private static void ScrollOnlyWhenNeeded(TableLayoutPanel panel)
790 {
791 if (null == panel || false == panel.IsHandleCreated || true == measuring.Contains(panel)) return;
792
793 measuring.Add(panel);
794
795 try
796 {
797 // measured against the width that would be left once a scrollbar is showing, not the width now.
798 // a panel a pixel over turns one on, that takes the width away, anything anchored gets narrower,
799 // anything that wraps gets taller, and the overflow grows instead of staying at a pixel
800 int narrower = panel.ClientSize.Width - SystemInformation.VerticalScrollBarWidth;
801 int wanted = panel.GetPreferredSize(new Size(0 < narrower ? narrower : panel.ClientSize.Width, 0)).Height;
802 int have = panel.ClientSize.Height;
803 bool overflowing = wanted > have;
804
805 //GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"Panel [{panel.Parent?.Name}] wants [{wanted}] has [{have}] rows [{panel.RowCount}] scroll [{overflowing}]");
806
807 // only a panel that is a tab or the form itself scrolls. a splitter pane or a group box panel
808 // that scrolls puts its own pair of scrollbars inside a row, which is worse than the clipping it
809 // is trying to avoid. a nested panel that does not fit needs its container made taller instead
810 bool mayScroll = panel.Parent is TabPage || panel.Parent is Form;
811
812 if (panel.AutoScroll != (overflowing && mayScroll))
813 panel.AutoScroll = overflowing && mayScroll;
814
815 // the scrollable area is stated rather than left to be computed. AutoScroll works it out from the
816 // panel preferred size, which comes out well over the rows it laid out, and the difference is
817 // empty space you can scroll to and nothing can fill: rows do not stretch unless they are Percent
818 panel.AutoScrollMinSize = true == (overflowing && mayScroll) ? new Size(0, wanted) : Size.Empty;
819
820 // the rows were laid out before the panel had its final size, and nothing recomputes them on its
821 // own: resizing the form by a single pixel is what forces the pass, so it is forced here instead
822 panel.PerformLayout();
823 }
824 finally
825 {
826 measuring.Remove(panel);
827 }
828 }
829
830 // Controls whose height was set with WithHeight, which keep it rather than growing
831 private HashSet<string> ControlsWithASetHeight()
832 {
833 HashSet<string> retVal = new HashSet<string>();
834
835 foreach (GPALControl entry in FormSettings.myForm.ControlList)
836 if (0 < entry.Height && false == string.IsNullOrEmpty(entry.Name))
837 retVal.Add(entry.Name);
838
839 return retVal;
840 }
841
846 private void SizeForm()
847 {
848 // apply explicit positioning regardless of whether width/height were set,
849 // otherwise StartPosition defaults to WindowsDefaultLocation and Left/Top are ignored
850 if (0 < FormSettings.top)
851 {
852 FormSettings.myForm.StartPosition = FormStartPosition.Manual;
853 FormSettings.myForm.Top = FormSettings.top;
854 }
855 if (0 < FormSettings.left)
856 {
857 FormSettings.myForm.StartPosition = FormStartPosition.Manual;
858 FormSettings.myForm.Left = FormSettings.left;
859 }
860
861 HashSet<string> sized = ControlsWithASetHeight();
862
863 foreach (Control child in FormSettings.myForm.Controls)
864 if (child is TableLayoutPanel root)
865 SettleRows(root, sized);
866
867 if (0 == FormSettings.height)
868 FitHeightToContent();
869
870 // the panels are settled and measured before the form is shown, when neither it nor they have their
871 // final size. Shown is the first moment they do, and taking the measurement again there is what
872 // dragging the form by one pixel was doing by hand
873 FormSettings.myForm.Shown += (sender, args) => MeasureAgain(FormSettings.myForm);
874 }
875
876 // the height a form with none of its own is laid out at, so its labels have a width to wrap to, before it
877 // is measured and fitted
878 private const int HeightToLayOutAt = 300;
879
886 private void FitHeightToContent()
887 {
888 GPALFormForm form = FormSettings.myForm;
889 TableLayoutPanel panel = form.Controls.OfType<TableLayoutPanel>().FirstOrDefault();
890
891 if (null != panel)
892 {
893 if (HeightToLayOutAt > form.Height)
894 form.Height = HeightToLayOutAt;
895
896 // the caller suspended layout and resumes it once this returns, so it is resumed for one pass here
897 // and suspended again after
898 form.ResumeLayout(true);
899
900 int strips = form.Controls.Cast<Control>()
901 .Where(control => control != panel && (DockStyle.Top == control.Dock || DockStyle.Bottom == control.Dock))
902 .Sum(control => control.Height);
903 int content = panel.GetPreferredSize(new Size(panel.ClientSize.Width, 0)).Height;
904 int frame = form.Height - form.ClientSize.Height;
905 int height = Math.Min(content + strips + frame, Screen.PrimaryScreen.WorkingArea.Height);
906
907 form.InitialSize = new Size(form.Width, height);
908 form.Height = height;
909
910 form.SuspendLayout();
911 }
912 }
913 #endregion Helpers
914 #region Add Controls
920 private IAllowFormSettingsAndActions AddButton(GPALButton gPALButton)
921 {
922 FormSettings.myForm.ControlList.Add(gPALButton);
923 Button button = new Button
924 {
925 Name = gPALButton.Name,
926 Text = gPALButton.Text,
927 // AutoSize = true,
928 // No Bottom, so the button keeps its own height rather than stretching to fill its row. The
929 // four-sided anchor is for the scrolling controls - grid, list, tree, text boxes - which show
930 // more content at a greater height
931 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
932 Enabled = gPALButton.Enabled,
933 Height = 10 < gPALButton.Height ? gPALButton.Height : 20,
934 };
935
936 gPALButton.WindowsControl = button;
937
938 ApplyWidth(0 < gPALButton.Width ? gPALButton.Width : FormSettings.buttonWidth, button);
939
940 foreach (var (evt, handler) in gPALButton.Callbacks)
941 {
942 if (handler == null) continue; // skip invalid
943
944 switch (evt)
945 {
946 case ControlEventType.DoubleClick:
947 button.DoubleClick += (EventHandler)handler;
948 break;
949
950 case ControlEventType.Default:
951 case ControlEventType.Click:
952 button.Click += (EventHandler)handler;
953 break;
954 // ignore unsupported
955 }
956 }
957
958 FormSettings.TableLayoutPanel.Controls.Add(button);
959
960 if (true == gPALButton.IsDefault)
961 {
962 FormSettings.myForm.AcceptButton = button;
963 button.DialogResult = DialogResult.OK;
964 button.TabIndex = 0;
965 }
966
967 if (!string.IsNullOrWhiteSpace(gPALButton.ToolTipText))
968 ApplyToolTip(gPALButton, button);
969
970 return this;
971 }
977 private IAllowFormSettingsAndActions AddChart(GPALChart gPALChart)
978 {
979 FormSettings.myForm.ControlList.Add(gPALChart);
980 // a control with no name has no AutomationId, and a control with no AutomationId cannot be named
981 // by a workflow. The chart is built before it gets here, so this is where it gets its name
982 ((Chart)gPALChart.WindowsControl).Name = gPALChart.Name;
983 ((Chart)gPALChart.WindowsControl).Enabled = gPALChart.IsEnabled;
984
985 FormSettings.TableLayoutPanel.Controls.Add((Chart)gPALChart.WindowsControl);
986 ((Chart)gPALChart.WindowsControl).Height = 10 < gPALChart.Height ? gPALChart.Height : 150;
987
988 Chart chart = gPALChart.WindowsControl;
989
990 ApplyWidth(gPALChart.Width, chart);
991
992 foreach (var (evt, handler) in gPALChart.Callbacks)
993 {
994 if (handler == null) continue; // skip invalid
995
996 switch (evt)
997 {
998 case ControlEventType.DragDrop:
999 chart.DragDrop += (DragEventHandler)handler;
1000 break;
1001
1002 case ControlEventType.DragEnter:
1003 chart.DragEnter += (DragEventHandler)handler;
1004 break;
1005
1006 case ControlEventType.DragLeave:
1007 chart.DragLeave += (EventHandler)handler;
1008 break;
1009
1010 case ControlEventType.DragOver:
1011 chart.DragOver += (DragEventHandler)handler;
1012 break;
1013
1014 case ControlEventType.DoubleClick:
1015 chart.DoubleClick += (EventHandler)handler;
1016 break;
1017
1018 case ControlEventType.Default:
1019 case ControlEventType.Click:
1020 chart.Click += (EventHandler)handler;
1021 break;
1022 // ignore unsupported
1023 }
1024 }
1025
1026 if (!string.IsNullOrWhiteSpace(gPALChart.ToolTipText))
1027 ApplyToolTip(gPALChart, chart);
1028
1029 return this;
1030 }
1036 private IAllowFormSettingsAndActions AddCheckbox(GPALCheckbox gPALCheckbox)
1037 {
1038 FormSettings.myForm.ControlList.Add(gPALCheckbox);
1039 CheckBox checkBox = new CheckBox
1040 {
1041 Name = gPALCheckbox.Name,
1042 Text = gPALCheckbox.Text,
1043 AutoSize = true,
1044 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1045 Checked = gPALCheckbox.Checked,
1046 Enabled = gPALCheckbox.Enabled,
1047 Height = 10 < gPALCheckbox.Height ? gPALCheckbox.Height : 20,
1048 };
1049 gPALCheckbox.WindowsControl = checkBox;
1050
1051 ApplyWidth(gPALCheckbox.Width, checkBox);
1052
1053 // has to be in place for proper presentation, user can also hook into this for their purposes
1054 checkBox.CheckedChanged += new EventHandler(delegate (Object sender, EventArgs a) { gPALCheckbox.Checked = ((CheckBox)sender).Checked; }); // copy changed text up to control
1055
1056 foreach (var (evt, handler) in gPALCheckbox.Callbacks)
1057 {
1058 if (handler == null) continue; // skip invalid
1059
1060 switch (evt)
1061 {
1062 case ControlEventType.Default:
1063 case ControlEventType.CheckedChanged:
1064 checkBox.CheckedChanged += (EventHandler)handler;
1065 break;
1066
1067 case ControlEventType.DoubleClick:
1068 checkBox.DoubleClick += (EventHandler)handler;
1069 break;
1070
1071 case ControlEventType.Click:
1072 checkBox.Click += (EventHandler)handler;
1073 break;
1074 // ignore unsupported
1075 }
1076 }
1077
1078 FormSettings.TableLayoutPanel.Controls.Add(checkBox);
1079
1080 if (!string.IsNullOrWhiteSpace(gPALCheckbox.ToolTipText))
1081 ApplyToolTip(gPALCheckbox, checkBox);
1082
1083 return this;
1084 }
1090 // the first thing on the list that can actually be chosen, for when there is nothing to fall back to
1091 private static int FirstEnabledIndex(ComboBox combo, GPALComboBox gpalCombo)
1092 {
1093 int retVal = -1;
1094
1095 for (int index = 0; index < combo.Items.Count && -1 == retVal; index++)
1096 if (false == gpalCombo.disabledItems.Contains(combo.Items[index].ToString()))
1097 retVal = index;
1098
1099 return retVal;
1100 }
1101
1102 private IAllowFormSettingsAndActions AddComboBox(GPALComboBox gpalCombo)
1103 {
1104 FormSettings.myForm.ControlList.Add(gpalCombo);
1105
1106 var combo = new ComboBox
1107 {
1108 Name = gpalCombo.Name,
1109 Enabled = gpalCombo.IsEnabled,
1110 DropDownStyle = ComboBoxStyle.DropDownList,
1111 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1112 Height = 10 < gpalCombo.Height ? gpalCombo.Height : 20,
1113 };
1114
1115 // populate items
1116 foreach (string item in gpalCombo.items)
1117 {
1118 combo.Items.Add(item);
1119 }
1120
1121 // restore selection if set — by name takes priority, then by index
1122 if (!string.IsNullOrEmpty(gpalCombo.selectedItem))
1123 {
1124 int idx = combo.Items.IndexOf(gpalCombo.selectedItem);
1125 if (0 <= idx) combo.SelectedIndex = idx;
1126 }
1127 else if (0 <= gpalCombo.selectedIndex && gpalCombo.selectedIndex < combo.Items.Count)
1128 {
1129 combo.SelectedIndex = gpalCombo.selectedIndex;
1130 }
1131
1132 // a disabled item is drawn greyed and refuses to be chosen. owner-draw is the only way windows
1133 // forms lets one entry of a dropdown say no, and it draws the ordinary way for everything else
1134 combo.DrawMode = DrawMode.OwnerDrawFixed;
1135
1136 combo.DrawItem += (sender, args) =>
1137 {
1138 if (0 > args.Index) return;
1139
1140 string text = combo.Items[args.Index].ToString();
1141 bool disabled = true == gpalCombo.disabledItems.Contains(text);
1142
1143 // the highlight belongs to something choosable, so a disabled row keeps the plain background
1144 if (false == disabled)
1145 args.DrawBackground();
1146 else
1147 using (SolidBrush background = new SolidBrush(SystemColors.Window))
1148 args.Graphics.FillRectangle(background, args.Bounds);
1149
1150 using (SolidBrush foreground = new SolidBrush(disabled ? SystemColors.GrayText : args.ForeColor))
1151 args.Graphics.DrawString(text, args.Font, foreground, args.Bounds);
1152
1153 if (false == disabled) args.DrawFocusRectangle();
1154 };
1155
1156 // put it back on the last thing that was allowed. SelectedIndexChanged fires for every route in,
1157 // including the keyboard and code, so this is the one place that catches them all
1158 combo.SelectedIndexChanged += (sender, args) =>
1159 {
1160 if (0 > combo.SelectedIndex) return;
1161
1162 string text = combo.Items[combo.SelectedIndex].ToString();
1163
1164 if (false == gpalCombo.disabledItems.Contains(text))
1165 {
1166 gpalCombo.lastEnabledIndex = combo.SelectedIndex;
1167 return;
1168 }
1169
1170 GPAL.PublishSimpleEvent(Enums.GPALEventType.CAUTION, $"[{text}] does not apply to what is selected, so it cannot be chosen", null, Enums.GPALObjectType.GPALForm);
1171
1172 combo.SelectedIndex = 0 <= gpalCombo.lastEnabledIndex && gpalCombo.lastEnabledIndex < combo.Items.Count
1173 ? gpalCombo.lastEnabledIndex
1174 : FirstEnabledIndex(combo, gpalCombo);
1175 };
1176
1177 gpalCombo.lastEnabledIndex = combo.SelectedIndex;
1178
1179 gpalCombo.WindowsControl = combo;
1180
1181 ApplyWidth(gpalCombo.Width, combo);
1182
1183 foreach (var (evt, handler) in gpalCombo.Callbacks)
1184 {
1185 if (handler == null) continue; // skip invalid
1186
1187 switch (evt)
1188 {
1189 case ControlEventType.Default:
1190 case ControlEventType.SelectedIndexChanged:
1191 combo.SelectedIndexChanged += (EventHandler)handler;
1192 break;
1193
1194 case ControlEventType.ValueChanged:
1195 combo.SelectedValueChanged += (EventHandler)handler;
1196 break;
1197
1198 case ControlEventType.Click:
1199 combo.Click += (EventHandler)handler;
1200 break;
1201 // ignore unsupported
1202 }
1203 }
1204
1205 // Apply tooltip if set
1206 if (false == string.IsNullOrEmpty(gpalCombo.ToolTipText))
1207 ApplyToolTip(gpalCombo, combo);
1208
1209 FormSettings.TableLayoutPanel.Controls.Add(combo);
1210
1211 return this;
1212 }
1218 private IAllowFormSettingsAndActions AddDataGridView(GPALDataGridView gPALDataGridView)
1219 {
1220 FormSettings.myForm.ControlList.Add(gPALDataGridView);
1221
1222 DataGridView dgv = new DataGridView
1223 {
1224 Name = gPALDataGridView.Name,
1225 ReadOnly = gPALDataGridView.ReadOnly,
1226 AutoGenerateColumns = gPALDataGridView.AutoGenerateColumns,
1227 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
1228 Enabled = gPALDataGridView.IsEnabled,
1229 AllowUserToAddRows = false, // usually not wanted in dashboards
1230 RowHeadersVisible = false, // optional – cleaner look
1231 AutoSize = true,
1232 Height = 10 < gPALDataGridView.Height ? gPALDataGridView.Height : 20,
1233 };
1234
1235 // Add explicit columns if supplied
1236 foreach (string colName in gPALDataGridView.ColumnNames)
1237 {
1238 dgv.Columns.Add(colName, colName);
1239 }
1240
1241 gPALDataGridView.WindowsControl = dgv;
1242
1243 ApplyWidth(gPALDataGridView.Width, dgv);
1244
1245 // Wire callbacks (same pattern as before)
1246 foreach (var (evt, handler) in gPALDataGridView.Callbacks)
1247 {
1248 if (handler == null) continue;
1249
1250 switch (evt)
1251 {
1252 case ControlEventType.Default:
1253 case ControlEventType.CellValueChanged:
1254 case ControlEventType.Change:
1255 dgv.CellValueChanged += (DataGridViewCellEventHandler)handler;
1256 break;
1257
1258 case ControlEventType.DoubleClick:
1259 dgv.DoubleClick += (EventHandler)handler;
1260 break;
1261
1262 case ControlEventType.Click:
1263 dgv.Click += (EventHandler)handler;
1264 break;
1265 // ignore unsupported
1266 }
1267 }
1268
1269 // Populate from grid if already set during fluent chain
1270 gPALDataGridView.SyncGridToControl(); // ← important line
1271 gPALDataGridView.ApplyColumnWidths();
1272 gPALDataGridView.ApplyWrap();
1273
1274 FormSettings.TableLayoutPanel.Controls.Add(dgv);
1275
1276 // the last column takes the width the others leave, and a control with no handle yet reports no
1277 // width to take it from. asked again on resize so the row keeps ending at the edge of the control
1278 if (true == gPALDataGridView.LastColumnFills)
1279 {
1280 void fillLastColumn(object s, EventArgs e) => gPALDataGridView.FillLastColumn();
1281
1282 dgv.Resize += fillLastColumn;
1283 dgv.HandleCreated += fillLastColumn;
1284
1285 // rows arriving or leaving is what shows and hides the vertical scrollbar, and that changes
1286 // the width the last column has to take. neither of those raises a resize
1287 dgv.RowsAdded += (s, e) => gPALDataGridView.FillLastColumn();
1288 dgv.RowsRemoved += (s, e) => gPALDataGridView.FillLastColumn();
1289 }
1290
1291 if (!string.IsNullOrWhiteSpace(gPALDataGridView.ToolTipText))
1292 ApplyToolTip(gPALDataGridView, dgv);
1293
1294 return this;
1295 }
1301 private IAllowFormSettingsAndActions AddDateTimePicker(GPALDateTimePicker gPALDateTimePicker)
1302 {
1303 FormSettings.myForm.ControlList.Add(gPALDateTimePicker);
1304
1305 DateTimePicker dtp = new DateTimePicker
1306 {
1307 Name = gPALDateTimePicker.Name,
1308 Value = gPALDateTimePicker.Value,
1309 Format = gPALDateTimePicker.Format,
1310 CustomFormat = gPALDateTimePicker.CustomFormat,
1311 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1312 Enabled = gPALDateTimePicker.IsEnabled,
1313 Height = 10 < gPALDateTimePicker.Height ? gPALDateTimePicker.Height : 10,
1314 };
1315
1316 gPALDateTimePicker.WindowsControl = dtp;
1317
1318 ApplyWidth(gPALDateTimePicker.Width, dtp);
1319
1320 foreach (var (evt, handler) in gPALDateTimePicker.Callbacks)
1321 {
1322 if (handler == null) continue; // skip invalid
1323
1324 switch (evt)
1325 {
1326 case ControlEventType.Default:
1327 case ControlEventType.ValueChanged:
1328 case ControlEventType.Change:
1329 dtp.ValueChanged += (EventHandler)handler;
1330 break;
1331
1332 case ControlEventType.Click:
1333 dtp.Click += (EventHandler)handler;
1334 break;
1335 // ignore unsupported
1336 }
1337 }
1338
1339 FormSettings.TableLayoutPanel.Controls.Add(dtp);
1340
1341 if (!string.IsNullOrWhiteSpace(gPALDateTimePicker.ToolTipText))
1342 ApplyToolTip(gPALDateTimePicker, dtp);
1343
1344 return this;
1345 }
1351 private IAllowFormSettingsAndActions AddFileSelector(GPALFileSelector gPALFileSelector)
1352 {
1353 FormSettings.myForm.ControlList.Add(gPALFileSelector);
1354
1355 if (gPALFileSelector._compact)
1356 return AddFileSelectorCompact(gPALFileSelector);
1357
1358 Button browseButton = new Button
1359 {
1360 Name = gPALFileSelector.Name + "_Browse",
1361 Text = "Browse...",
1362 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1363 Width = 80,
1364 Enabled = gPALFileSelector.IsEnabled
1365 };
1366
1367 TextBox displayBox = new TextBox
1368 {
1369 Name = gPALFileSelector.Name + "_Path",
1370 ReadOnly = true,
1371 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1372 // the path WithSelectedPath started it on, which is the whole point of naming one: a form opens
1373 // on the folders it works with rather than on a box telling the user to go and find them again
1374 Text = true == string.IsNullOrWhiteSpace(gPALFileSelector.SelectedPath)
1375 ? "(click Browse to select)"
1376 : gPALFileSelector.SelectedPath,
1377 Enabled = gPALFileSelector.IsEnabled
1378 };
1379
1380 TableLayoutPanel container = new TableLayoutPanel
1381 {
1382 ColumnCount = 2,
1383 RowCount = 1,
1384 Dock = DockStyle.Top, // full width, hugs a single row of content (no tall default panel gap)
1385 // same reason as the compact selector below: auto-sizing would measure the percent column from
1386 // the path sitting in the box, and a long one puts a horizontal scrollbar on the whole form
1387 Height = Math.Max(displayBox.PreferredHeight, browseButton.Height) + 4,
1388 Margin = new Padding(0)
1389 };
1390 container.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
1391 container.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90));
1392 container.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
1393 container.Controls.Add(browseButton, 0, 0);
1394 container.Controls.Add(displayBox, 1, 0);
1395
1396 gPALFileSelector.WindowsControl = container;
1397
1398 // Default, Change and Click are delivered by NotifySelection when the dialog closes, with the
1399 // FileSelectorEventArgs saying what was picked and whether it was cancelled. Wiring them here as
1400 // well would run the handler a second time with a plain EventArgs, because choosing a path sets
1401 // the box's text. TextChanged is the one event the box itself owns
1402 foreach (var (evt, handler) in gPALFileSelector.Callbacks)
1403 {
1404 if (handler == null) continue; // skip invalid
1405
1406 if (ControlEventType.TextChanged == evt)
1407 displayBox.TextChanged += (EventHandler)handler;
1408 }
1409
1410
1411 browseButton.Click += (s, e) =>
1412 {
1413 string[] selected = OpenBrowseDialog(gPALFileSelector);
1414 bool cancelled = selected == null || selected.Length == 0;
1415 gPALFileSelector.NotifySelection(cancelled, selected);
1416 displayBox.Text = cancelled ? "(selection cancelled)" : string.Join("; ", selected);
1417 };
1418
1419 FormSettings.TableLayoutPanel.Controls.Add(container);
1420
1421 if (!string.IsNullOrWhiteSpace(gPALFileSelector.ToolTipText))
1422 ApplyToolTip(gPALFileSelector, container);
1423
1424 return this;
1425 }
1426
1427 private IAllowFormSettingsAndActions AddFileSelectorCompact(GPALFileSelector gPALFileSelector)
1428 {
1429 PlaceholderTextBox textBox = new PlaceholderTextBox
1430 {
1431 Name = gPALFileSelector.Name + "_Path",
1432 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1433 Enabled = gPALFileSelector.IsEnabled,
1434 PlaceholderText = gPALFileSelector._placeholder,
1435 // after the placeholder, so the path replaces it rather than being taken for one
1436 Text = gPALFileSelector.SelectedPath
1437 };
1438
1439 Button dotButton = new Button
1440 {
1441 Name = gPALFileSelector.Name + "_Browse",
1442 Text = "...",
1443 Width = 28,
1444 Anchor = AnchorStyles.Top | AnchorStyles.Right,
1445 Enabled = gPALFileSelector.IsEnabled
1446 };
1447
1448 TableLayoutPanel container = new TableLayoutPanel
1449 {
1450 Name = gPALFileSelector.Name,
1451 ColumnCount = 2,
1452 RowCount = 1,
1453 Dock = DockStyle.Top, // full width, hugs a single row of content (no tall default panel gap)
1454 // no AutoSize. docking top already takes the width from the form, while auto-sizing measures the
1455 // percent column from what is in it, so a long path in the box asks the form to be wider than it
1456 // is and leaves a horizontal scrollbar that resizing never clears
1457 Height = Math.Max(textBox.PreferredHeight, dotButton.Height) + 4,
1458 Margin = new Padding(0)
1459 };
1460 container.RowStyles.Add(new RowStyle(SizeType.Percent, 100));
1461 container.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
1462 container.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 32));
1463 container.Controls.Add(textBox, 0, 0);
1464 container.Controls.Add(dotButton, 1, 0);
1465
1466 // WindowsControl points to the text box so SyncDisplay() and .Text access work naturally
1467 gPALFileSelector.WindowsControl = textBox;
1468
1469 // Default, Change and Click are delivered by NotifySelection when the dialog closes, with the
1470 // FileSelectorEventArgs saying what was picked and whether it was cancelled. Wiring them here as
1471 // well would run the handler a second time with a plain EventArgs, because choosing a path sets
1472 // the box's text. TextChanged is the one event the box itself owns
1473 foreach (var (evt, handler) in gPALFileSelector.Callbacks)
1474 {
1475 if (handler == null) continue; // skip invalid
1476
1477 if (ControlEventType.TextChanged == evt)
1478 textBox.TextChanged += (EventHandler)handler;
1479 }
1480
1481
1482 textBox.TextChanged += (s, e) => gPALFileSelector._selectedPath = textBox.Text;
1483
1484 dotButton.Click += (s, e) =>
1485 {
1486 string[] selected = OpenBrowseDialog(gPALFileSelector);
1487 bool cancelled = selected == null || selected.Length == 0;
1488 if (!cancelled)
1489 textBox.Text = selected[0];
1490 gPALFileSelector.NotifySelection(cancelled, selected);
1491 };
1492
1493 FormSettings.TableLayoutPanel.Controls.Add(container);
1494
1495 if (!string.IsNullOrWhiteSpace(gPALFileSelector.ToolTipText))
1496 ApplyToolTip(gPALFileSelector, container);
1497
1498 return this;
1499 }
1500
1501 private static string[] OpenBrowseDialog(GPALFileSelector gPALFileSelector)
1502 {
1503 if (gPALFileSelector._selectFolder)
1504 {
1505 using (FolderBrowserDialog fbd = new FolderBrowserDialog())
1506 {
1507 fbd.Description = gPALFileSelector._title;
1508 fbd.SelectedPath = gPALFileSelector._initialDirectory;
1509 return fbd.ShowDialog() == DialogResult.OK ? new[] { fbd.SelectedPath } : null;
1510 }
1511 }
1512 else
1513 {
1514 using (OpenFileDialog ofd = new OpenFileDialog())
1515 {
1516 ofd.Title = gPALFileSelector._title;
1517 ofd.InitialDirectory = gPALFileSelector._initialDirectory;
1518 ofd.Filter = gPALFileSelector._filter;
1519 ofd.Multiselect = gPALFileSelector._multiSelect;
1520 return ofd.ShowDialog() == DialogResult.OK ? ofd.FileNames : null;
1521 }
1522 }
1523 }
1529 private IAllowFormSettingsAndActions AddInput(GPALInput gPALInput)
1530 {
1531 FormSettings.myForm.ControlList.Add(gPALInput);
1532 PlaceholderTextBox input = new PlaceholderTextBox
1533 {
1534 Name = gPALInput.Name,
1535 Text = gPALInput.Text,
1536 AutoSize = true,
1537 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1538 Enabled = gPALInput.Enabled,
1539 Height = 10 < gPALInput.Height ? gPALInput.Height : 20,
1540 };
1541
1542 if (!string.IsNullOrEmpty(gPALInput.PlaceHolder))
1543 input.PlaceholderText = gPALInput.PlaceHolder;
1544
1545 if (true == gPALInput.InputIsPassword)
1546 input.PasswordChar = '\u25CF'; // bullet
1547
1548 if (true == gPALInput.IsReadOnly)
1549 input.ReadOnly = true;
1550
1551 gPALInput.WindowsControl = input;
1552
1553 ApplyWidth(gPALInput.Width, input);
1554
1555 input.Text = gPALInput.Text; // somehow text is getting truncated to one char from above
1556
1557 input.TextChanged += new EventHandler(delegate (Object sender, EventArgs a) { gPALInput.Text = ((TextBox)sender).Text; }); // copy changed text up to control
1558
1559 foreach (var (evt, handler) in gPALInput.Callbacks)
1560 {
1561 if (handler == null) continue; // skip invalid
1562
1563 switch (evt)
1564 {
1565 case ControlEventType.Default:
1566 case ControlEventType.TextChanged:
1567 input.TextChanged += (EventHandler)handler;
1568 break;
1569
1570 case ControlEventType.DoubleClick:
1571 input.DoubleClick += (EventHandler)handler;
1572 break;
1573
1574 case ControlEventType.Click:
1575 input.Click += (EventHandler)handler;
1576 break;
1577
1578 case ControlEventType.KeyDown:
1579 input.KeyDown += (KeyEventHandler)handler;
1580 break;
1581 // ignore unsupported
1582 }
1583 }
1584
1585 FormSettings.TableLayoutPanel.Controls.Add(input);
1586
1587 if (!string.IsNullOrWhiteSpace(gPALInput.ToolTipText))
1588 ApplyToolTip(gPALInput, input);
1589
1590 return this;
1591 }
1610 // narrower than this and the label has not been laid out yet
1611 private const int MinimumWidthToMeasure = 60;
1612
1613 private void FitLabelHeight(Label label)
1614 {
1615 // a label being laid out has a width of a few pixels before it is given its real one, and measuring
1616 // against that wraps one line of text onto three. the height that produces is then what everything
1617 // measuring this label adds up, so nothing is fitted until there is a width worth fitting to
1618 if (MinimumWidthToMeasure < label.Width)
1619 {
1620 Size measured = TextRenderer.MeasureText(label.Text, label.Font, new Size(label.Width, int.MaxValue),
1621 TextFormatFlags.WordBreak | TextFormatFlags.TextBoxControl);
1622
1623 int wanted = 18 > measured.Height ? 18 : measured.Height;
1624
1625 if (label.Height != wanted)
1626 label.Height = wanted;
1627 }
1628 }
1629
1636 private IAllowFormSettingsAndActions AddSpacer(GPALSpacer gPALSpacer)
1637 {
1638 FormSettings.myForm.ControlList.Add(gPALSpacer);
1639 Panel spacer = new Panel
1640 {
1641 Name = gPALSpacer.Name,
1642 Height = gPALSpacer.Height,
1643 Margin = Padding.Empty,
1644 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1645 };
1646
1647 gPALSpacer.WindowsControl = spacer;
1648
1649 FormSettings.TableLayoutPanel.Controls.Add(spacer);
1650
1651 return this;
1652 }
1653
1660 private IAllowFormSettingsAndActions AddLinkLabel(GPALLinkLabel gPALLinkLabel)
1661 {
1662 FormSettings.myForm.ControlList.Add(gPALLinkLabel);
1663 LinkLabel link = new LinkLabel
1664 {
1665 Name = gPALLinkLabel.Name,
1666 Enabled = gPALLinkLabel.IsEnabled,
1667 Text = gPALLinkLabel.Text,
1668 TextAlign = gPALLinkLabel.TextAlignment,
1669 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1670 AutoSize = false,
1671 };
1672 // the size a label uses, and regular rather than bold, so it reads as a link beside the labels
1673 link.Font = new Font(gPALLinkLabel.FontType.FontFamily, 10f, FontStyle.Regular);
1674 link.Height = 18 < gPALLinkLabel.Height ? gPALLinkLabel.Height : 18;
1675
1676 // sized to its text as the width changes, the same as a label, unless a height was asked for
1677 if (18 >= gPALLinkLabel.Height)
1678 link.SizeChanged += (sender, args) => FitLabelHeight(link);
1679
1680 link.LinkClicked += (sender, args) => gPALLinkLabel.Open();
1681
1682 gPALLinkLabel.WindowsControl = link;
1683
1684 ApplyWidth(gPALLinkLabel.Width, link);
1685
1686 foreach (var (evt, handler) in gPALLinkLabel.Callbacks)
1687 {
1688 if (handler == null) continue; // skip invalid
1689
1690 switch (evt)
1691 {
1692 case ControlEventType.DoubleClick:
1693 link.DoubleClick += (EventHandler)handler;
1694 break;
1695
1696 case ControlEventType.Default:
1697 case ControlEventType.Click:
1698 link.Click += (EventHandler)handler;
1699 break;
1700 // ignore unsupported
1701 }
1702 }
1703
1704 FormSettings.TableLayoutPanel.Controls.Add(link);
1705
1706 if (!string.IsNullOrWhiteSpace(gPALLinkLabel.ToolTipText))
1707 ApplyToolTip(gPALLinkLabel, link);
1708
1709 return this;
1710 }
1711
1712 private IAllowFormSettingsAndActions AddLabel(GPALLabel gPALLabel)
1713 {
1714 FormSettings.myForm.ControlList.Add(gPALLabel);
1715 Label label = new Label
1716 {
1717 Name = gPALLabel.Name,
1718 Enabled = gPALLabel.IsEnabled,
1719 Text = gPALLabel.Text,
1720 Font = gPALLabel.FontType,
1721 TextAlign = gPALLabel.TextAlignment,
1722 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1723 };
1724 label.Font = new Font(label.Font.FontFamily, 10f, FontStyle.Bold);
1725
1726 label.AutoSize = false;
1727 label.Height = 18 < gPALLabel.Height ? gPALLabel.Height : 18;
1728
1729 // a Label wraps its text and then clips every line below the first, which is how the end of a long
1730 // caption goes missing. no height was asked for, so the label takes the height its text needs at
1731 // whatever width the layout gives it, and takes it again each time that width changes
1732 if (18 >= gPALLabel.Height)
1733 label.SizeChanged += (sender, args) => FitLabelHeight(label);
1734
1735 gPALLabel.WindowsControl = label;
1736
1737 ApplyWidth(gPALLabel.Width, label);
1738
1739 foreach (var (evt, handler) in gPALLabel.Callbacks)
1740 {
1741 if (handler == null) continue; // skip invalid
1742
1743 switch (evt)
1744 {
1745 case ControlEventType.DoubleClick:
1746 label.DoubleClick += (EventHandler)handler;
1747 break;
1748
1749 case ControlEventType.Default:
1750 case ControlEventType.Click:
1751 label.Click += (EventHandler)handler;
1752 break;
1753 // ignore unsupported
1754 }
1755 }
1756
1757 FormSettings.TableLayoutPanel.Controls.Add(label);
1758
1759 if (!string.IsNullOrWhiteSpace(gPALLabel.ToolTipText))
1760 ApplyToolTip(gPALLabel, label);
1761
1762 return this;
1763 }
1769 private IAllowFormSettingsAndActions AddListView(GPALListView gPALListView)
1770 {
1771 FormSettings.myForm.ControlList.Add(gPALListView);
1772
1773 ListView listView = new ListView
1774 {
1775 Name = gPALListView.Name,
1776 View = gPALListView.ViewMode,
1777 FullRowSelect = gPALListView.FullRowSelect,
1778 MultiSelect = gPALListView.MultiSelect,
1779 CheckBoxes = gPALListView.checkBoxes,
1780 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
1781 Enabled = gPALListView.IsEnabled,
1782 Height = 10 < gPALListView.Height ? gPALListView.Height: 150,
1783 };
1784
1785 // Add columns
1786 foreach (string header in gPALListView.Columns)
1787 {
1788 listView.Columns.Add(header, -2, HorizontalAlignment.Left); // auto-size
1789 }
1790
1791 gPALListView.WindowsControl = listView;
1792
1793 ApplyWidth(gPALListView.Width, listView);
1794 gPALListView.ApplyColumnWidths();
1795
1796 // a greyed row is not a selectable one. ListView has no cancellable selection event, so the
1797 // selection is taken back off as soon as it lands
1798 listView.ItemSelectionChanged += (sender, args) =>
1799 {
1800 if (true == args.IsSelected && SystemColors.GrayText == args.Item.ForeColor)
1801 args.Item.Selected = false;
1802 };
1803
1804 foreach (var (evt, handler) in gPALListView.Callbacks)
1805 {
1806 if (handler == null) continue; // skip invalid
1807
1808 switch (evt)
1809 {
1810 case ControlEventType.Default:
1811 case ControlEventType.SelectedIndexChanged:
1812 case ControlEventType.Change:
1813 listView.SelectedIndexChanged += (EventHandler)handler;
1814 break;
1815
1816 case ControlEventType.DoubleClick:
1817 listView.DoubleClick += (EventHandler)handler;
1818 break;
1819
1820 case ControlEventType.Click:
1821 listView.Click += (EventHandler)handler;
1822 break;
1823
1824 // checking a row is not selecting one, and a list with checkboxes is usually asking
1825 // about the checks rather than the highlight
1826 case ControlEventType.ItemChecked:
1827 listView.ItemChecked += (ItemCheckedEventHandler)handler;
1828 break;
1829 // ignore unsupported
1830 }
1831 }
1832
1833 FormSettings.TableLayoutPanel.Controls.Add(listView);
1834
1835 if (gPALListView.Sortable)
1836 {
1837 var comparer = new ListViewItemComparer();
1838 listView.ListViewItemSorter = comparer;
1839 listView.ColumnClick += (s, e) =>
1840 {
1841 if (comparer.Column == e.Column)
1842 comparer.Order = comparer.Order == SortOrder.Ascending
1843 ? SortOrder.Descending : SortOrder.Ascending;
1844 else
1845 {
1846 comparer.Column = e.Column;
1847 comparer.Order = SortOrder.Ascending;
1848 }
1849 listView.Sort();
1850 };
1851 }
1852
1853 if (gPALListView.ColumnWeights.Count > 0)
1854 {
1855 var weights = gPALListView.ColumnWeights;
1856 double total = 0;
1857 foreach (var w in weights) total += w;
1858 if (total <= 0) total = 1;
1859
1860 void applyWeights(object s, EventArgs e)
1861 {
1862 int available = listView.ClientSize.Width - SystemInformation.VerticalScrollBarWidth;
1863 if (available <= 0) return;
1864 for (int i = 0; i < weights.Count && i < listView.Columns.Count; i++)
1865 listView.Columns[i].Width = (int)(available * weights[i] / total);
1866 }
1867
1868 listView.Resize += applyWeights;
1869 listView.HandleCreated += applyWeights;
1870 }
1871
1872 // the columns are worked out whole, once the control can measure itself and again when it
1873 // changes size. A control with no handle yet reports no width to divide up, and a list on a tab
1874 // nobody has opened has no handle until that tab is first shown
1875 if (true == gPALListView.LastColumnFills)
1876 {
1877 void layOutColumns(object s, EventArgs e) => gPALListView.LayOutColumns();
1878
1879 listView.Resize += layOutColumns;
1880 listView.HandleCreated += layOutColumns;
1881 }
1882
1883 if (!string.IsNullOrWhiteSpace(gPALListView.ToolTipText))
1884 ApplyToolTip(gPALListView, listView);
1885
1886 return this;
1887 }
1893 private IAllowFormSettingsAndActions AddNumericUpDown(GPALNumericUpDown gPALNumericUpDown)
1894 {
1895 FormSettings.myForm.ControlList.Add(gPALNumericUpDown);
1896
1897 NumericUpDown nud = new NumericUpDown
1898 {
1899 Name = gPALNumericUpDown.Name,
1900 Value = gPALNumericUpDown.Value,
1901 Minimum = gPALNumericUpDown.Minimum,
1902 Maximum = gPALNumericUpDown.Maximum,
1903 Increment = gPALNumericUpDown.Increment,
1904 DecimalPlaces = gPALNumericUpDown.DecimalPlaces,
1905 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1906 Enabled = gPALNumericUpDown.IsEnabled,
1907 Height = 10 < gPALNumericUpDown.Height ? gPALNumericUpDown.Height : 20,
1908 };
1909
1910 gPALNumericUpDown.WindowsControl = nud;
1911
1912 ApplyWidth(gPALNumericUpDown.Width, nud);
1913
1914 foreach (var (evt, handler) in gPALNumericUpDown.Callbacks)
1915 {
1916 if (handler == null) continue; // skip invalid
1917
1918 switch (evt)
1919 {
1920 case ControlEventType.Default:
1921 case ControlEventType.ValueChanged:
1922 case ControlEventType.Change:
1923 nud.ValueChanged += (EventHandler)handler;
1924 break;
1925
1926 case ControlEventType.Click:
1927 nud.Click += (EventHandler)handler;
1928 break;
1929 // ignore unsupported
1930 }
1931 }
1932
1933 FormSettings.TableLayoutPanel.Controls.Add(nud);
1934
1935 if (!string.IsNullOrWhiteSpace(gPALNumericUpDown.ToolTipText))
1936 ApplyToolTip(gPALNumericUpDown, nud);
1937
1938 return this;
1939 }
1945 private IAllowFormSettingsAndActions AddPicture(GPALPicture gPALPicture)
1946 {
1947 FormSettings.myForm.ControlList.Add(gPALPicture);
1948
1949 PictureBox pictureBox = new PictureBox
1950 {
1951 Name = gPALPicture.Name,
1952 Enabled = gPALPicture.IsEnabled,
1953 Image = gPALPicture.Image,
1954 SizeMode = gPALPicture.SizeMode,
1955 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
1956 Height = 10 < gPALPicture.Height ? gPALPicture.Height : 150,
1957 };
1958
1959 gPALPicture.WindowsControl = pictureBox;
1960
1961 ApplyWidth(gPALPicture.Width, pictureBox);
1962
1963 foreach (var (evt, handler) in gPALPicture.Callbacks)
1964 {
1965 if (handler == null) continue; // skip invalid
1966
1967 switch (evt)
1968 {
1969 case ControlEventType.Default:
1970 case ControlEventType.Click:
1971 pictureBox.Click += (EventHandler)handler;
1972 break;
1973
1974 case ControlEventType.DoubleClick:
1975 pictureBox.DoubleClick += (EventHandler)handler;
1976 break;
1977
1978 // ignore unsupported
1979 }
1980 }
1981
1982 FormSettings.TableLayoutPanel.Controls.Add(pictureBox);
1983
1984 if (!string.IsNullOrWhiteSpace(gPALPicture.ToolTipText))
1985 ApplyToolTip(gPALPicture, pictureBox);
1986
1987 return this;
1988 }
1989
1996 private IAllowFormSettingsAndActions AddSlider(GPALSlider gPALSlider)
1997 {
1998 FormSettings.myForm.ControlList.Add(gPALSlider);
1999
2000 TrackBar slider = new TrackBar
2001 {
2002 Name = gPALSlider.Name,
2003 Minimum = gPALSlider.Minimum,
2004 Maximum = gPALSlider.Maximum,
2005 TickFrequency = 1 > gPALSlider.TickFrequency ? 1 : gPALSlider.TickFrequency,
2006 Orientation = gPALSlider.Orientation,
2007 Value = gPALSlider.Value,
2008 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
2009 Enabled = gPALSlider.IsEnabled,
2010 Height = 10 < gPALSlider.Height ? gPALSlider.Height : 45,
2011 };
2012
2013 gPALSlider.WindowsControl = slider;
2014
2015 ApplyWidth(gPALSlider.Width, slider);
2016
2017 foreach (var (evt, handler) in gPALSlider.Callbacks)
2018 {
2019 if (handler == null) continue; // skip invalid
2020
2021 switch (evt)
2022 {
2023 case ControlEventType.Default:
2024 case ControlEventType.ValueChanged:
2025 case ControlEventType.Change:
2026 slider.ValueChanged += (EventHandler)handler;
2027 break;
2028
2029 case ControlEventType.Scroll:
2030 slider.Scroll += (EventHandler)handler;
2031 break;
2032
2033 case ControlEventType.Click:
2034 slider.Click += (EventHandler)handler;
2035 break;
2036
2037 // ignore unsupported
2038 }
2039 }
2040
2041 FormSettings.TableLayoutPanel.Controls.Add(slider);
2042
2043 if (!string.IsNullOrWhiteSpace(gPALSlider.ToolTipText))
2044 ApplyToolTip(gPALSlider, slider);
2045
2046 return this;
2047 }
2048
2054 private IAllowFormSettingsAndActions AddProgressBar(GPALProgressBar gPALProgressBar)
2055 {
2056 FormSettings.myForm.ControlList.Add(gPALProgressBar);
2057
2058 ProgressBar progressBar = new ProgressBar
2059 {
2060 Name = gPALProgressBar.Name,
2061 Minimum = gPALProgressBar.Minimum,
2062 Maximum = gPALProgressBar.Maximum,
2063 Value = gPALProgressBar.Value,
2064 Style = gPALProgressBar.Style,
2065 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
2066 Enabled = gPALProgressBar.IsEnabled,
2067 Height = 10 < gPALProgressBar.Height ? gPALProgressBar.Height : 20,
2068 };
2069
2070 gPALProgressBar.WindowsControl = progressBar;
2071
2072 ApplyWidth(gPALProgressBar.Width, progressBar);
2073
2074 foreach (var (evt, handler) in gPALProgressBar.Callbacks)
2075 {
2076 if (handler == null) continue; // skip invalid
2077
2078 switch (evt)
2079 {
2080 case ControlEventType.Default:
2081 case ControlEventType.Click:
2082 progressBar.Click += (EventHandler)handler;
2083 break;
2084
2085 // ignore unsupported
2086 }
2087 }
2088
2089 FormSettings.TableLayoutPanel.Controls.Add(progressBar);
2090
2091 if (!string.IsNullOrWhiteSpace(gPALProgressBar.ToolTipText))
2092 ApplyToolTip(gPALProgressBar, progressBar);
2093
2094 return this;
2095 }
2096
2102 private IAllowFormSettingsAndActions AddRadioButton(GPALRadioButton gPALRadioButton)
2103 {
2104 FormSettings.myForm.ControlList.Add(gPALRadioButton);
2105 RadioButton radioButton = new RadioButton
2106 {
2107 Name = gPALRadioButton.Name,
2108 Text = gPALRadioButton.Text,
2109 AutoSize = true,
2110 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right,
2111 Checked = gPALRadioButton.Checked,
2112 Enabled = gPALRadioButton.Enabled,
2113 Height = 10 < gPALRadioButton.Height ? gPALRadioButton.Height : 20,
2114 };
2115 gPALRadioButton.WindowsControl = radioButton;
2116
2117 ApplyWidth(gPALRadioButton.Width, radioButton);
2118 radioButton.CheckedChanged += new EventHandler(delegate (Object sender, EventArgs a) { gPALRadioButton.Checked = ((RadioButton)sender).Checked; }); // copy changed text up to control
2119
2120 foreach (var (evt, handler) in gPALRadioButton.Callbacks)
2121 {
2122 if (handler == null) continue; // skip invalid
2123
2124 switch (evt)
2125 {
2126 case ControlEventType.Default:
2127 case ControlEventType.CheckedChanged:
2128 radioButton.CheckedChanged += (EventHandler)handler;
2129 break;
2130
2131 case ControlEventType.Click:
2132 radioButton.Click += (EventHandler)handler;
2133 break;
2134 // ignore unsupported
2135 }
2136 }
2137
2138 FormSettings.TableLayoutPanel.Controls.Add(radioButton);
2139
2140 if (!string.IsNullOrWhiteSpace(gPALRadioButton.ToolTipText))
2141 ApplyToolTip(gPALRadioButton, radioButton);
2142
2143 return this;
2144 }
2150 private IAllowFormSettingsAndActions AddStatusStrip(GPALStatusStrip gPALStatusStrip)
2151 {
2152 FormSettings.myForm.ControlList.Add(gPALStatusStrip);
2153
2154 StatusStrip statusStrip = new StatusStrip
2155 {
2156 Name = gPALStatusStrip.Name,
2157 Dock = DockStyle.Bottom,
2158 SizingGrip = false,
2159 Enabled = gPALStatusStrip.IsEnabled,
2160 Height = 10 < gPALStatusStrip.Height ? gPALStatusStrip.Height : 20,
2161 };
2162
2163 ToolStripStatusLabel statusLabel = new ToolStripStatusLabel
2164 {
2165 Text = gPALStatusStrip.Text,
2166 Spring = true,
2167 TextAlign = ContentAlignment.MiddleLeft,
2168 Height = 10 < gPALStatusStrip.Height ? gPALStatusStrip.Height : 20,
2169 };
2170
2171 statusStrip.Items.Add(statusLabel);
2172
2173 gPALStatusStrip.WindowsControl = statusStrip;
2174
2175 foreach (var (evt, handler) in gPALStatusStrip.Callbacks)
2176 {
2177 if (handler == null) continue; // skip invalid
2178
2179 switch (evt)
2180 {
2181 case ControlEventType.Click:
2182 case ControlEventType.Default:
2183 statusStrip.ItemClicked += (sender, e) => ((EventHandler)handler)(sender, e);
2184 break;
2185
2186 // ignore unsupported
2187 }
2188 }
2189
2190 // StatusStrip is added directly to the form (not TableLayoutPanel)
2191 FormSettings.myForm.Controls.Add(statusStrip);
2192
2193 if (!string.IsNullOrWhiteSpace(gPALStatusStrip.ToolTipText))
2194 ApplyToolTip(gPALStatusStrip, statusStrip);
2195
2196 return this;
2197 }
2203 private IAllowFormSettingsAndActions AddMenuBar(GPALMenuBar gPALMenuBar)
2204 {
2205 FormSettings.myForm.ControlList.Add(gPALMenuBar);
2206
2207 MenuStrip menuStrip = new MenuStrip
2208 {
2209 Name = gPALMenuBar.Name,
2210 Enabled = gPALMenuBar.IsEnabled,
2211 };
2212
2213 var topMenus = new System.Collections.Generic.Dictionary<string, ToolStripMenuItem>();
2214 foreach (var (topMenu, item, isSeparator) in gPALMenuBar.menuEntries)
2215 {
2216 string menuKey = string.IsNullOrWhiteSpace(topMenu) ? "" : topMenu;
2217 if (!topMenus.TryGetValue(menuKey, out ToolStripMenuItem topItem))
2218 {
2219 topItem = new ToolStripMenuItem(menuKey);
2220 topMenus[menuKey] = topItem;
2221 menuStrip.Items.Add(topItem);
2222 }
2223
2224 if (isSeparator)
2225 {
2226 topItem.DropDownItems.Add(new ToolStripSeparator());
2227 continue;
2228 }
2229
2230 ToolStripMenuItem menuItem = new ToolStripMenuItem(item.Text);
2231 if (item.Image != null)
2232 menuItem.Image = item.Image;
2233 if (item.ShortCutKey != System.Windows.Forms.Keys.None)
2234 {
2235 menuItem.ShortcutKeys = item.ShortCutKey;
2236 menuItem.ShowShortcutKeys = true;
2237 }
2238 // ForEvent was destructured and thrown away, so every handler became a Click whatever the
2239 // workflow asked for. Click is what a menu item does, so it stays the default
2240 foreach (var (evt, handler) in item.Callbacks)
2241 {
2242 if (null == handler) continue; // skip invalid
2243
2244 switch (evt)
2245 {
2246 case ControlEventType.Default:
2247 case ControlEventType.Click:
2248 menuItem.Click += (EventHandler)handler;
2249 break;
2250
2251 case ControlEventType.DoubleClick:
2252 menuItem.DoubleClick += (EventHandler)handler;
2253 break;
2254 // ignore unsupported
2255 }
2256 }
2257 if (!string.IsNullOrWhiteSpace(item.ToolTipText))
2258 menuItem.ToolTipText = item.ToolTipText;
2259
2260 // what WithEnabled said before the form existed, applied now that the item does
2261 menuItem.Enabled = item.IsEnabled;
2262 item.WindowsControl = menuItem;
2263 topItem.DropDownItems.Add(menuItem);
2264 }
2265
2266 gPALMenuBar.WindowsControl = menuStrip;
2267
2268 // MenuStrip docks to the top of the form (not the TableLayoutPanel)
2269 FormSettings.myForm.Controls.Add(menuStrip);
2270 FormSettings.myForm.MainMenuStrip = menuStrip;
2271
2272 // A top-docked MenuStrip must sit above any top-docked toolbar. Docked siblings fill from the
2273 // edge in reverse child-index order, so pushing the menu to the highest index keeps it outermost.
2274 FormSettings.myForm.Controls.SetChildIndex(menuStrip, FormSettings.myForm.Controls.Count - 1);
2275
2276 return this;
2277 }
2284 private IAllowFormSettingsAndActions AddToolbar(GPALToolbar gPALToolbar)
2285 {
2286 FormSettings.myForm.ControlList.Add(gPALToolbar);
2287
2288 ToolStrip toolStrip = new ToolStrip
2289 {
2290 Name = gPALToolbar.Name,
2291 Enabled = gPALToolbar.IsEnabled,
2292 };
2293
2294 foreach (var (item, isSeparator) in gPALToolbar.toolbarEntries)
2295 {
2296 if (isSeparator)
2297 {
2298 toolStrip.Items.Add(new ToolStripSeparator());
2299 continue;
2300 }
2301
2302 ToolStripButton button = new ToolStripButton(item.Text);
2303 if (item.Image != null)
2304 {
2305 button.Image = item.Image;
2306 button.DisplayStyle = string.IsNullOrWhiteSpace(item.Text)
2307 ? ToolStripItemDisplayStyle.Image
2308 : ToolStripItemDisplayStyle.ImageAndText;
2309 }
2310 else
2311 button.DisplayStyle = ToolStripItemDisplayStyle.Text;
2312
2313 // the same as the menu bar: the event asked for is honoured rather than assumed to be Click
2314 foreach (var (evt, handler) in item.Callbacks)
2315 {
2316 if (null == handler) continue; // skip invalid
2317
2318 switch (evt)
2319 {
2320 case ControlEventType.Default:
2321 case ControlEventType.Click:
2322 button.Click += (EventHandler)handler;
2323 break;
2324
2325 case ControlEventType.DoubleClick:
2326 button.DoubleClick += (EventHandler)handler;
2327 break;
2328 // ignore unsupported
2329 }
2330 }
2331 if (!string.IsNullOrWhiteSpace(item.ToolTipText))
2332 button.ToolTipText = item.ToolTipText;
2333
2334 button.Enabled = item.IsEnabled;
2335 item.WindowsControl = button;
2336 toolStrip.Items.Add(button);
2337 }
2338
2339 gPALToolbar.WindowsControl = toolStrip;
2340
2341 // ToolStrip docks to the top of the form (not the TableLayoutPanel), below any MenuStrip.
2342 FormSettings.myForm.Controls.Add(toolStrip);
2343 if (FormSettings.myForm.MainMenuStrip != null)
2344 FormSettings.myForm.Controls.SetChildIndex(FormSettings.myForm.MainMenuStrip, FormSettings.myForm.Controls.Count - 1);
2345
2346 return this;
2347 }
2353 private IAllowFormSettingsAndActions AddTab(GPALTab gPALTab)
2354 {
2355 if (null == FormSettings.TabControl)
2356 {
2357 FormSettings.TabControl = new TabControl
2358 {
2359 Name = "tabControl",
2360 AutoSize = false,
2361 //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
2362 Dock = DockStyle.Fill,
2363 Multiline = true,
2364
2365 };
2366
2367 // a disabled TabPage greys its contents and is still selectable, so the tab stays reachable
2368 // and lands on a page nothing can be done with. refusing the switch is what disables a tab
2369 FormSettings.TabControl.Selecting += (sender, args) =>
2370 {
2371 // nothing selected yet means the pages are still being added, and refusing that would
2372 // leave the control showing no tab at all
2373 if (null == FormSettings.TabControl.SelectedTab) return;
2374
2375 if (null != args.TabPage && false == args.TabPage.Enabled) args.Cancel = true;
2376 };
2377
2378 FormSettings.TableLayoutPanel.Controls.Add(FormSettings.TabControl);
2379 }
2380
2381 TabPage tabPage = new TabPage
2382 {
2383 Name = gPALTab.Name,
2384 Text = gPALTab.Text,
2385 AutoSize = false,
2386 //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
2387 Dock = DockStyle.Fill,
2388 Enabled = gPALTab.Enabled
2389 };
2390
2391 TableLayoutPanel tmpTableLayoutPanel = new TableLayoutPanel
2392 {
2393 //Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
2394 Dock = DockStyle.Fill,
2395 AutoSize = false,
2396
2397 // the same as the form's own panel. without it a tab holding more than fits simply hides the
2398 // rest, with no scrollbar to say so, and the controls at the bottom may as well not be there
2399 AutoScroll = true
2400 };
2401
2402 gPALTab.WindowsControl = tabPage;
2403
2404 FormSettings.myForm.ControlList.Add(gPALTab);
2405
2406 foreach (var (evt, handler) in gPALTab.Callbacks)
2407 {
2408 if (handler == null) continue; // skip invalid
2409
2410 switch (evt)
2411 {
2412 case ControlEventType.Scroll:
2413 tabPage.Scroll += (ScrollEventHandler)handler;
2414 break;
2415
2416 case ControlEventType.Click:
2417 // the page's own click, which is the blank area around the controls on it. rarely what
2418 // anyone means by clicking a tab, which is why it has to be asked for by name
2419 tabPage.Click += (EventHandler)handler;
2420 break;
2421
2422 case ControlEventType.Default:
2423 case ControlEventType.SelectedIndexChanged:
2424 case ControlEventType.Change:
2425 // being the tab on show is what a tab is for, so it is the one you get without saying.
2426 // a TabPage has no event for it: the control that owns the pages has one, and it names
2427 // the page it moved to, so each tab hears only about itself
2428 FormSettings.TabControl.Selected += (sender, args) =>
2429 {
2430 if (args.TabPage == tabPage)
2431 ((EventHandler)handler)(gPALTab, args);
2432 };
2433 break;
2434 // ignore unsupported
2435 }
2436 }
2437
2438 tabPage.Controls.Add(tmpTableLayoutPanel);
2439 FormSettings.TabControl.Controls.Add(tabPage);
2440 FormSettings.TableLayoutPanel = tmpTableLayoutPanel;
2441
2442 if (!string.IsNullOrWhiteSpace(gPALTab.ToolTipText))
2443 ApplyToolTip(gPALTab, tabPage);
2444
2445 return this;
2446 }
2454
2455 private IAllowFormSettingsAndActions AddSplitter(GPALSplitter gPALSplitter)
2456 {
2457 FormSettings.myForm.ControlList.Add(gPALSplitter);
2458
2459 SplitContainer sc = new SplitContainer
2460 {
2461 Name = gPALSplitter.Name,
2462 Enabled = gPALSplitter.IsEnabled,
2463 // a vertical bar gives left and right panes, a horizontal one top and bottom
2464 Orientation = true == gPALSplitter.IsHorizontalSplit ? Orientation.Vertical : Orientation.Horizontal,
2465 // anchored rather than docked, the same as every other control GPAL adds. A docked control in a
2466 // TableLayoutPanel cell does not keep its own height: the cell sizes it, and the cell is measured
2467 // from PreferredSize, which for a SplitContainer ignores the height set on it. Anchoring left and
2468 // right still gives full width. The height is measured once the panes are filled, below, unless
2469 // .WithHeight already said what it should be. With no height of its own the splitter's row takes
2470 // the spare height of the form, and anchoring the bottom as well is what lets it fill that row
2471 // down to whatever sits below it, the way a list view does
2472 Anchor = 10 < gPALSplitter.Height
2473 ? AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
2474 : AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom,
2475 Height = 10 < gPALSplitter.Height ? gPALSplitter.Height : 0,
2476 AutoScaleMode = AutoScaleMode.Inherit,
2477 // the height is what says this is a band of a stated size rather than a container that takes
2478 // what is left. Reading that off the control's name meant an unnamed splitter grew instead
2479 Tag = 10 < gPALSplitter.Height ? SetHeightMarker : null
2480 };
2481
2482 if (true == gPALSplitter.IsPercentage)
2483 {
2484 double pct = gPALSplitter.Percentage;
2485
2486 // a proportion has to be reapplied as the form resizes, where a pixel distance stays put
2487 sc.SizeChanged += (s, e) =>
2488 {
2489 int along = true == gPALSplitter.IsHorizontalSplit ? sc.Width : sc.Height;
2490
2491 if (along > sc.Panel1MinSize + sc.Panel2MinSize + sc.SplitterWidth)
2492 sc.SplitterDistance = (int)(along * pct);
2493 };
2494 }
2495 else
2496 sc.SplitterDistance = gPALSplitter.Dimension;
2497
2498 gPALSplitter.WindowsControl = sc;
2499
2500 // where the user dragged the divider to is the one thing a workflow wants from a splitter, so that
2501 // is what a handler with no event named answers
2502 foreach (var (evt, handler) in gPALSplitter.Callbacks)
2503 {
2504 if (null == handler) continue; // skip invalid
2505
2506 switch (evt)
2507 {
2508 case ControlEventType.Default:
2509 case ControlEventType.SplitterMoved:
2510 sc.SplitterMoved += (SplitterEventHandler)handler;
2511 break;
2512
2513 case ControlEventType.DoubleClick:
2514 sc.DoubleClick += (EventHandler)handler;
2515 break;
2516
2517 case ControlEventType.Click:
2518 sc.Click += (EventHandler)handler;
2519 break;
2520 // ignore unsupported
2521 }
2522 }
2523
2524 TableLayoutPanel firstPane = MakeSplitPaneTlp();
2525 TableLayoutPanel secondPane = MakeSplitPaneTlp();
2526
2527 sc.Panel1.Controls.Add(firstPane);
2528 sc.Panel2.Controls.Add(secondPane);
2529 FormSettings.TableLayoutPanel.Controls.Add(sc);
2530
2531 FillContainer(gPALSplitter.FormControls, firstPane, secondPane, gPALSplitter.Name);
2532
2533
2534 // a SplitContainer cannot size itself to what is inside it, so it is measured here instead of being
2535 // given a number someone guessed. Side by side panes are as tall as the taller one; stacked panes are
2536 // as tall as both plus the bar. .WithHeight overrides all of it
2537 if (10 >= gPALSplitter.Height)
2538 {
2539 // asking a pane for its PreferredSize hands back the height it already has, because it is docked to
2540 // fill the pane it is in, so it can only ever agree with the splitter it was measured from. what is
2541 // actually wanted is the height of the controls in it, which is what this adds up
2542 Func<TableLayoutPanel, int> contentHeight = pane =>
2543 {
2544 int total = pane.Padding.Top + pane.Padding.Bottom;
2545
2546 foreach (Control child in pane.Controls)
2547 total += child.Height + child.Margin.Bottom;
2548
2549 return total;
2550 };
2551
2552
2553 sc.Height = true == gPALSplitter.IsHorizontalSplit
2554 ? Math.Max(contentHeight(firstPane), contentHeight(secondPane))
2555 : contentHeight(firstPane) + contentHeight(secondPane) + sc.SplitterWidth;
2556 }
2557
2558 if (!string.IsNullOrWhiteSpace(gPALSplitter.ToolTipText))
2559 ApplyToolTip(gPALSplitter, sc);
2560
2561 return this;
2562 }
2563
2564 // what a GroupBox pads by on its own, kept here because the bottom is adjusted from it
2565 private const int GroupBoxPadding = 3;
2566
2567 // how much further the bottom is padded so the last control's border clears the box's border
2568 private const int GroupBoxBottomClearance = 2;
2569
2574 private IAllowFormSettingsAndActions AddGroupBox(GPALGroupBox gPALGroupBox)
2575 {
2576 FormSettings.myForm.ControlList.Add(gPALGroupBox);
2577
2578 GroupBox gb = new GroupBox
2579 {
2580 Name = gPALGroupBox.Name,
2581 Text = gPALGroupBox.Text,
2582 Enabled = gPALGroupBox.IsEnabled,
2583 // full width and only as tall as what is inside. Docking it to fill hands its size to the parent,
2584 // which is what stops a box growing to show its own controls.
2585 // NOTE: DockStyle is not a flags enum, so Top | Bottom is Left rather than both
2586 Dock = DockStyle.Top,
2587 AutoSize = true,
2588 AutoSizeMode = AutoSizeMode.GrowAndShrink,
2589 // the box is measured from the panel docked inside it, and that measurement lands short: the
2590 // bottom border of the last control ends up under the box's own border. the default padding
2591 // plus this is what puts that border back where it can be seen
2592 Padding = new Padding(GroupBoxPadding, GroupBoxPadding, GroupBoxPadding, GroupBoxPadding + GroupBoxBottomClearance),
2593 };
2594
2595 // the panel inside has to measure its contents too, or the box is asked to grow around something that
2596 // never reports a size. A splitter pane is the opposite case and fills, so this is not MakeSplitPaneTlp
2597 TableLayoutPanel inside = new TableLayoutPanel
2598 {
2599 Dock = DockStyle.Top,
2600 //AutoSize = true,
2601 AutoSizeMode = AutoSizeMode.GrowAndShrink,
2602 ColumnCount = 1,
2603 };
2604
2605 inside.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100f));
2606 gb.Controls.Add(inside);
2607 FormSettings.TableLayoutPanel.Controls.Add(gb);
2608
2609 gPALGroupBox.WindowsControl = gb;
2610
2611 FillContainer(gPALGroupBox.FormControls, inside, null, gPALGroupBox.Name);
2612
2613 if (!string.IsNullOrWhiteSpace(gPALGroupBox.ToolTipText))
2614 ApplyToolTip(gPALGroupBox, gb);
2615
2616 return this;
2617 }
2618
2630 private void FillContainer(List<object> formControls, TableLayoutPanel firstPane, TableLayoutPanel secondPane, string name)
2631 {
2632 TableLayoutPanel outerPanel = FormSettings.TableLayoutPanel;
2633 TabControl outerTabControl = FormSettings.TabControl;
2634 TableLayoutPanel current = firstPane;
2635 bool markerSeen = false;
2636
2637 FormSettings.TabControl = null;
2638
2639 foreach (object formControl in formControls)
2640 {
2641 if (formControl is GPALSpliter)
2642 {
2643 if (null == secondPane)
2644 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{name}] has no second pane, so the pane marker does nothing. Everything added stays in the one panel.");
2645 else if (true == markerSeen)
2646 GPAL.PublishSimpleEvent(GPALEventType.DEBUG, $"[{name}] has more than one pane marker and a splitter has only two panes, so everything after the first marker stays in the second.");
2647 else
2648 {
2649 current = secondPane;
2650 markerSeen = true;
2651 }
2652
2653 continue;
2654 }
2655
2656 FormSettings.TableLayoutPanel = current;
2657 FormSettings.TabControl = null;
2658 WithFormControl((GPALControl)formControl);
2659 }
2660
2661 FormSettings.TableLayoutPanel = outerPanel;
2662 FormSettings.TabControl = outerTabControl;
2663 }
2664
2665 private TableLayoutPanel MakeSplitPaneTlp()
2666 {
2667 var tlp = new TableLayoutPanel
2668 {
2669 Dock = DockStyle.Fill,
2670 AutoSize = false,
2671 ColumnCount = 1,
2672 };
2673 tlp.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100f));
2674 return tlp;
2675 }
2676
2682 private IAllowFormSettingsAndActions AddTableLayoutPanel(GPALTableLayoutPanel gPALTableLayoutPanel)
2683 {
2684 FormSettings.myForm.ControlList.Add(gPALTableLayoutPanel);
2685 TableLayoutPanel tableLayoutPanel = new TableLayoutPanel
2686 {
2687 Name = gPALTableLayoutPanel.Name,
2688 GrowStyle = gPALTableLayoutPanel.GrowStyle,
2689 AutoScroll = false,
2690 ColumnCount = gPALTableLayoutPanel.ColumnCount,
2691 RowCount = gPALTableLayoutPanel.RowCount,
2692 Enabled = gPALTableLayoutPanel.IsEnabled,
2693 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right
2694 // todo : more settings
2695 };
2696
2697 // the control every other Add sets and this one did not, so the panel was built and then dropped:
2698 // nothing on screen, and every later property read fell down the not-yet-realized path
2699 gPALTableLayoutPanel.WindowsControl = tableLayoutPanel;
2700
2701 FormSettings.TableLayoutPanel.Controls.Add(tableLayoutPanel);
2702
2703 if (!string.IsNullOrWhiteSpace(gPALTableLayoutPanel.ToolTipText))
2704 ApplyToolTip(gPALTableLayoutPanel, tableLayoutPanel);
2705
2706 return this;
2707 }
2708
2714 private IAllowFormSettingsAndActions AddTextArea(GPALTextArea gPALTextArea)
2715 {
2716 FormSettings.myForm.ControlList.Add(gPALTextArea);
2717 PlaceholderTextBox textBox = new PlaceholderTextBox
2718 {
2719 Name = gPALTextArea.Name,
2720 Text = gPALTextArea.Text,
2721 Multiline = true,
2722 WordWrap = true,
2723 ScrollBars = ScrollBars.Both,
2724 Height = 10 < gPALTextArea.Height ? gPALTextArea.Height : 150,
2725 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
2726 Enabled = gPALTextArea.Enabled
2727 };
2728 if (true == gPALTextArea.IsReadOnly)
2729 textBox.ReadOnly = true;
2730
2731 if (!string.IsNullOrEmpty(gPALTextArea.PlaceHolder))
2732 textBox.PlaceholderText = gPALTextArea.PlaceHolder;
2733
2734 gPALTextArea.WindowsControl = textBox;
2735
2736 ApplyWidth(gPALTextArea.Width, textBox);
2737 textBox.Text = gPALTextArea.Text; // somehow text is getting truncated to one char from above
2738
2739 textBox.TextChanged += new EventHandler(delegate (Object sender, EventArgs a) { gPALTextArea.Text = ((TextBox)sender).Text; }); // copy changed text up to control
2740
2741 foreach (var (evt, handler) in gPALTextArea.Callbacks)
2742 {
2743 if (handler == null) continue; // skip invalid
2744
2745 switch (evt)
2746 {
2747 case ControlEventType.Default:
2748 case ControlEventType.TextChanged:
2749 textBox.TextChanged += (EventHandler)handler;
2750 break;
2751
2752 case ControlEventType.Click:
2753 textBox.Click += (EventHandler)handler;
2754 break;
2755 // ignore unsupported
2756 }
2757 }
2758
2759 FormSettings.TableLayoutPanel.Controls.Add(textBox);
2760
2761 if (!string.IsNullOrWhiteSpace(gPALTextArea.ToolTipText))
2762 ApplyToolTip(gPALTextArea, textBox);
2763
2764 return this;
2765 }
2772 private IAllowFormSettingsAndActions AddRichTextBox(GPALRichTextBox gPALRichTextBox)
2773 {
2774 FormSettings.myForm.ControlList.Add(gPALRichTextBox);
2775 RichTextBox richTextBox = new RichTextBox
2776 {
2777 Name = gPALRichTextBox.Name,
2778 Multiline = true,
2779 WordWrap = true,
2780 ReadOnly = gPALRichTextBox.IsReadOnly,
2781 ScrollBars = RichTextBoxScrollBars.Both,
2782 Height = 10 < gPALRichTextBox.Height ? gPALRichTextBox.Height : 150,
2783 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
2784 Enabled = gPALRichTextBox.Enabled
2785 };
2786 gPALRichTextBox.WindowsControl = richTextBox;
2787
2788 ApplyWidth(gPALRichTextBox.Width, richTextBox);
2789 GPALRichTextBox.RenderMarkdown(richTextBox, gPALRichTextBox.Text);
2790
2791 // the control's text is the rendered Markdown, so nothing is copied back up here: Text holds the
2792 // source the workflow gave and RenderMarkdown is what puts it on screen
2793 foreach (var (evt, handler) in gPALRichTextBox.Callbacks)
2794 {
2795 if (handler == null) continue; // skip invalid
2796
2797 switch (evt)
2798 {
2799 case ControlEventType.Default:
2800 case ControlEventType.TextChanged:
2801 richTextBox.TextChanged += (EventHandler)handler;
2802 break;
2803
2804 case ControlEventType.DoubleClick:
2805 richTextBox.DoubleClick += (EventHandler)handler;
2806 break;
2807
2808 case ControlEventType.Click:
2809 richTextBox.Click += (EventHandler)handler;
2810 break;
2811
2812 case ControlEventType.KeyDown:
2813 richTextBox.KeyDown += (KeyEventHandler)handler;
2814 break;
2815 // ignore unsupported
2816 }
2817 }
2818
2819 FormSettings.TableLayoutPanel.Controls.Add(richTextBox);
2820
2821 if (!string.IsNullOrWhiteSpace(gPALRichTextBox.ToolTipText))
2822 ApplyToolTip(gPALRichTextBox, richTextBox);
2823
2824 return this;
2825 }
2831 private IAllowFormSettingsAndActions AddTreeView(GPALTreeView gPALTreeView)
2832 {
2833 FormSettings.myForm.ControlList.Add(gPALTreeView);
2834
2835 TreeView tv = new TreeView
2836 {
2837 Name = gPALTreeView.Name,
2838 CheckBoxes = gPALTreeView.CheckBoxes,
2839 ShowLines = gPALTreeView.ShowLines,
2840 ShowPlusMinus = gPALTreeView.ShowPlusMinus,
2841 Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Bottom | AnchorStyles.Right,
2842 Enabled = gPALTreeView.IsEnabled,
2843 Height = 10 < gPALTreeView.Height ? gPALTreeView.Height : 150,
2844 };
2845
2846 // BeforeSelect can refuse outright, so a greyed node never becomes the selected one
2847 tv.BeforeSelect += (sender, args) =>
2848 {
2849 if (null != args.Node && SystemColors.GrayText == args.Node.ForeColor) args.Cancel = true;
2850 };
2851
2852 gPALTreeView.WindowsControl = tv;
2853
2854 ApplyWidth(gPALTreeView.Width, tv);
2855
2856 foreach (var (evt, handler) in gPALTreeView.Callbacks)
2857 {
2858 if (handler == null) continue; // skip invalid
2859
2860 switch (evt)
2861 {
2862 case ControlEventType.Default:
2863 case ControlEventType.AfterSelect:
2864 case ControlEventType.Change:
2865 tv.AfterSelect += (TreeViewEventHandler)handler;
2866 break;
2867
2868 case ControlEventType.DoubleClick:
2869 tv.DoubleClick += (EventHandler)handler;
2870 break;
2871
2872 case ControlEventType.Click:
2873 tv.Click += (EventHandler)handler;
2874 break;
2875 // ignore unsupported
2876 }
2877 }
2878
2879 FormSettings.TableLayoutPanel.Controls.Add(tv);
2880
2881 if (!string.IsNullOrWhiteSpace(gPALTreeView.ToolTipText))
2882 ApplyToolTip(gPALTreeView, tv);
2883
2884 return this;
2885 }
2886 #endregion <private>
2887 }
2888
2889 internal class ListViewItemComparer : System.Collections.IComparer
2890 {
2891 public int Column { get; set; } = 0;
2892 public SortOrder Order { get; set; } = SortOrder.Ascending;
2893
2894 public int Compare(object x, object y)
2895 {
2896 var lx = (ListViewItem)x;
2897 var ly = (ListViewItem)y;
2898
2899 string sx = Column == 0 ? lx.Text
2900 : Column < lx.SubItems.Count ? lx.SubItems[Column].Text : string.Empty;
2901 string sy = Column == 0 ? ly.Text
2902 : Column < ly.SubItems.Count ? ly.SubItems[Column].Text : string.Empty;
2903
2904 int result;
2905
2906 // Numeric (strip currency/percent symbols and commas)
2907 string nx = sx.Replace(",", "").Replace("$", "").Replace("%", "").Trim();
2908 string ny = sy.Replace(",", "").Replace("$", "").Replace("%", "").Trim();
2909 if (double.TryParse(nx, out double dx) && double.TryParse(ny, out double dy))
2910 result = dx.CompareTo(dy);
2911 // Date
2912 else if (System.DateTime.TryParse(sx, out System.DateTime dtx) &&
2913 System.DateTime.TryParse(sy, out System.DateTime dty))
2914 result = dtx.CompareTo(dty);
2915 // String
2916 else
2917 result = string.Compare(sx, sy, StringComparison.OrdinalIgnoreCase);
2918
2919 return Order == SortOrder.Ascending ? result : -result;
2920 }
2921 }
2922}
2923
File-side plumbing behind the fluent chain: writing a unit of work's data out in a delimited format,...
Definition FileHelper.cs:55
Class to define database usage. Currently only used for input from a table, sql or stored procedure....
GPAL File object instantied with GPAL.File Used to load tokens into a GPALGrid [rows/columns].
Definition GPALFile.cs:36
A GPAL button instantiated with GPAL.Button for use on GPAL forms. Callback EventHandler is invoked ...
A GPAL chart instantiated with GPAL.Chart for use on GPAL forms. Callback EventHandler is invoked on...
A GPAL checkbox instantiateds with GPAL,Checkbox for use on GPAL forms. Callback EventHandler is inv...
bool Checked
Runtime access to un/check the checkbox. Setting this writes through to the live control (if realized...
A dropdown list (ComboBox) for selecting one option from a predefined set of choices....
Base class for all GPAL form controls.
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...
string Name
Control name, only used for Information and Exception channel messages. NOTE: Use ....
Font FontType
The font currently being used.
dynamic WindowsControl
Underlying Windows form control, Button, Checkbox, Textbox, etc. NOTE: Use GPAL to instantiate form ...
A grid control instantiated with GPAL.DataGridView for use on GPAL forms, displaying the contents of ...
IReadOnlyList< string > ColumnNames
The column header names set via WithColumns(string[]).
bool AutoGenerateColumns
Whether columns are auto-generated when no column names were provided, as set via WithAutoGenerateCol...
A date/time picker control instantiated with GPAL.DateTimePicker for use on GPAL forms.
string CustomFormat
The custom display format string, as set via WithCustomFormat(string).
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).
A file/folder selector control instantiated with GPAL.FileSelector for use on GPAL forms....
Form object that contains the fluent methods to create interactive forms. Instatiated using GPAL....
Definition GPALForm.cs:64
int Left
The current left coordinate of the form, in pixels.
Definition GPALForm.cs:276
IAllowFormSettingsAndActions FillInFrom(GPALDatabase inputDatabase)
Fill in (overwrite) the forms inputs using tokens derived from the database. Used after ....
Definition GPALForm.cs:370
string FormName
The name of your form Currently not used, but will be used for Information and Exceptional channel me...
Definition GPALForm.cs:125
IAllowFormSettingsAndActions AppendFrom(GPALFile inputFile)
Append to the end of the forms inputs using tokens derived from the file. Used after ....
Definition GPALForm.cs:430
IAllowFormSettingsAndActions WithTitle(string title)
The title to display at the top of your form.
Definition GPALForm.cs:357
IAllowFormSettingsAndActions CallAfterFillIn(GPALForm.CallAfterFillInDelegate callAfterFillIn)
Add a handler to call after a row of tokens is consumed and after all inputs are filled in with data....
Definition GPALForm.cs:296
delegate CallIfStatus CallAfterFillInDelegate(GPALForm myForm, IGPALGrid< string > tokens, int tokenIdx)
Delegate callback for the CallAfterFillIn EventHandler which will be invoked after each row of tokens...
int Height
The current height of the form, in pixels.
Definition GPALForm.cs:288
IAllowFormSettingsAndActions WithWidth(int width)
Define the initial width of your GPAL form.
Definition GPALForm.cs:189
IAllowFormActionsAndFillIn Show()
Show the GPAL form so it may be automated by tokens. Use ShowDialog when the form workflow ends....
Definition GPALForm.cs:549
IAllowFormSettingsAndActions InsertFrom(IGPALGrid< string > inputGrid)
Insert at the beginning of the forms inputs using tokens derived from the GPALGrid....
Definition GPALForm.cs:490
dynamic ToGPALObject()
Return a GPALForm so that GPAL.Form does not have to be cast.
Definition GPALForm.cs:116
int Width
The current width of the form, in pixels.
Definition GPALForm.cs:284
IAllowFormSettingsAndActions WithHeight(int height)
Define the initial height of your GPAL form. Without it the form takes the height its content needs.
Definition GPALForm.cs:237
IAllowFormSettingsAndActions InsertFrom(GPALDatabase inputDatabase)
Insert at the beginning of the forms inputs using tokens derived from the database....
Definition GPALForm.cs:398
IAllowFormActions ShowDialog()
Show the GPAL form as a modal. Use LAST in a form workflow. NOTE: This blocks workflows and FillIn m...
Definition GPALForm.cs:518
IAllowFormSettingsAndActions FillInFrom(IGPALGrid< string > inputGrid)
Fill in (overwrite) the forms inputs using tokens derived from the GPALGrid. Used after ....
Definition GPALForm.cs:466
IAllowFormSettingsAndActions AppendFrom(GPALDatabase inputDatabase)
Append to the end of the forms inputs using tokens derived from the database. Used after ....
Definition GPALForm.cs:384
IAllowFormSettingsAndActions WithLeft(int left)
Define the initial coordinate of the left side of your GPAL form.
Definition GPALForm.cs:267
IAllowFormSettingsAndActions FillInFrom(GPALFile inputFile)
Fill in (overwrite) the forms inputs using tokens derived from the file. Used after ....
Definition GPALForm.cs:412
IAllowFormActions Hide()
Hide the GPAL form. This will essentially terminate the program if .ShowDialog() is the last method i...
Definition GPALForm.cs:590
IAllowFormSettingsAndActions WithFormControl(GPALControl gPALControl)
Add a form control (GPALButton, GPALChart, etc.) to a GPALForm. Controls are displayed on one line i...
Definition GPALForm.cs:317
IAllowFormSettingsAndActions InsertFrom(GPALFile inputFile)
Insert at the beginning of the forms inputs using tokens derived from the file. Used after ....
Definition GPALForm.cs:448
IAllowFormSettingsAndActions AppendFrom(IGPALGrid< string > inputGrid)
Append to the end of the forms inputs using tokens derived from the GPALGrid. Used after ....
Definition GPALForm.cs:478
IAllowFormSettingsAndActions WithTop(int top)
Define the initial coordinate of the top side of your GPAL form.
Definition GPALForm.cs:256
IAllowFormSettingsAndActions WithButtonWidth(int width)
The width every button on the form takes, in pixels, so a form full of buttons does not need the widt...
Definition GPALForm.cs:212
int Top
The current top coordinate of the form, in pixels.
Definition GPALForm.cs:280
A titled box around a set of controls, instantiated with GPAL.GroupBox and laid out like any other co...
A GPAL single line input instantiated with GPAL.Input for use on GPAL forms. Callback EventHandler i...
A GPAL lable instantiated with GPAL.Label for use on GPAL forms. Callback EventHandler is invoked on...
A list view control capable of displaying data in Details, List, LargeIcon, SmallIcon,...
bool Sortable
Whether column-click sorting is enabled.
IReadOnlyList< string > Columns
The column header names set via WithColumns(string[]).
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.
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).
A menu bar docked to the top of a GPAL form. Open a top-level menu with WithMenu(string),...
A numeric up/down spinner control instantiated with GPAL.NumericUpDown for use on GPAL forms.
decimal Minimum
The minimum allowed value, as set via WithMinimum(decimal).
int DecimalPlaces
The number of decimal places displayed, as set via WithDecimalPlaces(int).
decimal Maximum
The maximum allowed value, as set via WithMaximum(decimal).
decimal Increment
The spinner increment, as set via WithIncrement(decimal).
decimal Value
Runtime access to the value. Reads the live control once the form is up, so a value the user spun to ...
A progress indicator control that shows completion percentage or indeterminate activity....
PictureBoxSizeMode SizeMode
How the image fits the control.
System.Drawing.Image Image
The image on display.
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).
A GPAL radiobutton instantiated with GPAL.RadioButton for use GPAL forms. Callback EventHandler is i...
bool Checked
Runtime access to un/check the radiobutton. Setting this writes through to the live control (if reali...
A read-only display control for rendering simple Markdown - headings (#, ##, ###),...
new string Text
The Markdown source text. Setting this re-renders headings, bold spans, and bullet/numbered lists (wi...
A GPAL slider instantiated with GPAL.Slider, for picking a number from a range by dragging....
int TickFrequency
How far apart the tick marks are.
int Minimum
The lowest value the slider can be dragged to.
Orientation Orientation
Horizontal or vertical.
int Value
Where the slider sits, clamped to the range it was given.
int Maximum
The highest value the slider can be dragged to.
Empty space of a set height between two controls on a GPAL form, instantiated with GPAL....
Two panes with a bar between them, instantiated with GPAL.Splitter and laid out like any other contro...
A status bar (usually docked at the bottom of the form) that displays real-time messages,...
new string Text
The status text. Blank or whitespace falls back to "Ready". Setting this writes through to the first ...
A GPAL tab instantiated with GPAL.Tab for use on GPAL forms. Callback EventHandler is invoked on the...
A table layout panel instantiated with GPAL.TableLayoutPanel for use on GPAL forms,...
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.
A GPAL multiline textare instantiated with GPAL.TextArea for use on GPAL forms. Callback EventHandle...
A toolbar docked below the menu bar, containing GPALBarItem buttons and optional separators.
A tree view control instantiated with GPAL.TreeView for use on GPAL forms.
bool ShowLines
Whether lines are drawn between sibling nodes and their parent, as set via WithShowLines(bool).
bool ShowPlusMinus
Whether plus/minus expand buttons are displayed, as set via WithShowPlusMinus(bool).
bool CheckBoxes
Whether checkboxes are displayed next to each tree node, as set via WithCheckBoxes(bool).
Everything starts here, all the GPAL controls and global settings using fluent syntax are here....
Definition GPAL.cs:49
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