DFLayout.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Drawing;
  4. using System.Globalization;
  5. using System.Linq;
  6. using System.Text;
  7. using System.Xml.Linq;
  8. using Expressive;
  9. using InABox.Clients;
  10. namespace InABox.Core
  11. {
  12. public interface IDFRenderer
  13. {
  14. object? GetFieldValue(string field);
  15. /// <summary>
  16. /// Retrieve a piece of additional data for a field.
  17. /// </summary>
  18. /// <param name="fieldName">The field name, which is the variable code.</param>
  19. /// <param name="dataField">The specific field to be retrieved from the variable.</param>
  20. /// <returns>A value, which is specific to the type of <paramref name="fieldName"/> and the specific <paramref name="dataField"/> being
  21. /// retrieved.</returns>
  22. object? GetFieldData(string fieldName, string dataField);
  23. void SetFieldValue(string field, object? value);
  24. /// <summary>
  25. /// Set the background colour for a field.
  26. /// </summary>
  27. void SetFieldColour(string field, Color? colour = null);
  28. }
  29. public class DFLayout
  30. {
  31. public DFLayout()
  32. {
  33. ColumnWidths = new List<string>();
  34. RowHeights = new List<string>();
  35. Elements = new List<DFLayoutControl>();
  36. HiddenElements = new List<DFLayoutControl>();
  37. Expressions = new Dictionary<string, CoreExpression>();
  38. ColourExpressions = new Dictionary<string, CoreExpression>();
  39. VariableReferences = new Dictionary<string, List<Tuple<ReferenceType, string>>>();
  40. }
  41. public List<string> ColumnWidths { get; }
  42. public List<string> RowHeights { get; }
  43. public List<DFLayoutControl> Elements { get; }
  44. public List<DFLayoutControl> HiddenElements { get; }
  45. private enum ReferenceType
  46. {
  47. Value,
  48. Colour
  49. }
  50. private Dictionary<string, CoreExpression> Expressions;
  51. private Dictionary<string, CoreExpression> ColourExpressions;
  52. private Dictionary<string, List<Tuple<ReferenceType, string>>> VariableReferences;
  53. public IDFRenderer? Renderer;
  54. public IEnumerable<DFLayoutControl> GetElements(bool includeHidden = false)
  55. {
  56. foreach (var element in Elements)
  57. {
  58. yield return element;
  59. }
  60. if (includeHidden)
  61. {
  62. foreach (var element in HiddenElements)
  63. {
  64. yield return element;
  65. }
  66. }
  67. }
  68. public string SaveLayout()
  69. {
  70. var sb = new StringBuilder();
  71. foreach (var column in ColumnWidths)
  72. sb.AppendFormat("C {0}\n", column);
  73. foreach (var row in RowHeights)
  74. sb.AppendFormat("R {0}\n", row);
  75. foreach (var element in Elements)
  76. sb.AppendFormat("E {0} {1}\n", element.GetType().EntityName(), element.SaveToString());
  77. var result = sb.ToString();
  78. return result;
  79. }
  80. private static Dictionary<string, Type>? _controls;
  81. /// <returns>A type which is a <see cref="DFLayoutControl"/></returns>
  82. private Type? GetElementType(string typeName)
  83. {
  84. _controls ??= CoreUtils.TypeList(
  85. AppDomain.CurrentDomain.GetAssemblies(),
  86. x => x.IsClass
  87. && !x.IsAbstract
  88. && !x.IsGenericType
  89. && typeof(DFLayoutControl).IsAssignableFrom(x)
  90. ).ToDictionary(
  91. x => x.EntityName(),
  92. x => x);
  93. return _controls.GetValueOrDefault(typeName);
  94. }
  95. private static bool IsHidden(DFLayoutControl element)
  96. => element is DFLayoutField field && field.GetPropertyValue<bool>("Hidden");
  97. public void LoadLayout(string layout)
  98. {
  99. ColumnWidths.Clear();
  100. RowHeights.Clear();
  101. Elements.Clear();
  102. var lines = layout.Split('\n');
  103. foreach (var line in lines)
  104. if (line.StartsWith("C "))
  105. {
  106. ColumnWidths.Add(line.Substring(2));
  107. }
  108. else if (line.StartsWith("R "))
  109. {
  110. RowHeights.Add(line.Substring(2));
  111. }
  112. else if (line.StartsWith("E ") || line.StartsWith("O "))
  113. {
  114. var typename = line.Split(' ').Skip(1).FirstOrDefault()
  115. ?.Replace("InABox.Core.Design", "InABox.Core.DFLayout")
  116. ?.Replace("DFLayoutChoiceField", "DFLayoutOptionField");
  117. if (!string.IsNullOrWhiteSpace(typename))
  118. {
  119. var type = GetElementType(typename);
  120. if(type != null)
  121. {
  122. var element = (Activator.CreateInstance(type) as DFLayoutControl)!;
  123. var json = string.Join(" ", line.Split(' ').Skip(2));
  124. element.LoadFromString(json);
  125. //Serialization.DeserializeInto(json, element);
  126. if(IsHidden(element))
  127. {
  128. HiddenElements.Add(element);
  129. }
  130. else
  131. {
  132. Elements.Add(element);
  133. }
  134. }
  135. else
  136. {
  137. Logger.Send(LogType.Error, ClientFactory.UserID, $"{typename} is not the name of any concrete DFLayoutControls!");
  138. }
  139. }
  140. }
  141. //else if (line.StartsWith("O "))
  142. //{
  143. // String typename = line.Split(' ').Skip(1).FirstOrDefault()?.Replace("PRSDesktop", "InABox.Core");
  144. // if (!String.IsNullOrWhiteSpace(typename))
  145. // {
  146. // Type type = Type.GetType(typename);
  147. // DesignControl element = Activator.CreateInstance(type) as DesignControl;
  148. // if (element != null)
  149. // {
  150. // String json = String.Join(" ", line.Split(' ').Skip(2));
  151. // element.LoadFromString(json);
  152. // //CoreUtils.DeserializeInto(json, element);
  153. // }
  154. // Elements.Add(element);
  155. // }
  156. //}
  157. // Invalid Line Hmmm..
  158. if (!ColumnWidths.Any())
  159. ColumnWidths.AddRange(new[] { "*", "Auto" });
  160. if (!RowHeights.Any())
  161. RowHeights.AddRange(new[] { "Auto" });
  162. }
  163. private void AddVariableReference(string reference, string fieldName, ReferenceType referenceType)
  164. {
  165. if (reference.Contains('.'))
  166. reference = reference.Split('.')[0];
  167. if(!VariableReferences.TryGetValue(reference, out var refs))
  168. {
  169. refs = new List<Tuple<ReferenceType, string>>();
  170. VariableReferences[reference] = refs;
  171. }
  172. refs.Add(new Tuple<ReferenceType, string>(referenceType, fieldName));
  173. }
  174. private object? GetFieldValue(string field)
  175. {
  176. if (field.Contains('.'))
  177. {
  178. var parts = field.Split('.');
  179. return Renderer?.GetFieldData(parts[0], string.Join('.', parts.Skip(1)));
  180. }
  181. else
  182. {
  183. return Renderer?.GetFieldValue(field);
  184. }
  185. }
  186. private void EvaluateValueExpression(string name)
  187. {
  188. var expression = Expressions[name];
  189. var values = new Dictionary<string, object?>();
  190. foreach (var field in expression.ReferencedVariables)
  191. {
  192. values[field] = GetFieldValue(field);
  193. }
  194. var oldValue = Renderer?.GetFieldValue(name);
  195. try
  196. {
  197. var value = expression?.Evaluate(values);
  198. if(value != oldValue)
  199. {
  200. Renderer?.SetFieldValue(name, value);
  201. }
  202. }
  203. catch (Exception e)
  204. {
  205. Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in Expression field '{name}': {CoreUtils.FormatException(e)}");
  206. }
  207. }
  208. private Color? ConvertObjectToColour(object? colour)
  209. {
  210. if(colour is string str)
  211. {
  212. if (str.StartsWith('#'))
  213. {
  214. var trimmed = str.TrimStart('#');
  215. try
  216. {
  217. if (trimmed.Length == 6)
  218. {
  219. return Color.FromArgb(
  220. Int32.Parse(trimmed[..2], NumberStyles.HexNumber),
  221. Int32.Parse(trimmed.Substring(2, 2), NumberStyles.HexNumber),
  222. Int32.Parse(trimmed.Substring(4, 2), NumberStyles.HexNumber));
  223. }
  224. else if (trimmed.Length == 8)
  225. {
  226. return Color.FromArgb(Int32.Parse(trimmed, NumberStyles.HexNumber));
  227. }
  228. else
  229. {
  230. return null;
  231. }
  232. }
  233. catch (Exception e)
  234. {
  235. Logger.Send(LogType.Error, "", $"Error parsing Colour Expression colour '{str}': {e.Message}");
  236. return null;
  237. }
  238. }
  239. else if(Enum.TryParse<KnownColor>(str, out var result))
  240. {
  241. return Color.FromKnownColor(result);
  242. }
  243. return null;
  244. }
  245. return null;
  246. }
  247. private void EvaluateColourExpression(string name)
  248. {
  249. var expression = ColourExpressions[name];
  250. var values = new Dictionary<string, object?>();
  251. foreach (var field in expression.ReferencedVariables)
  252. {
  253. values[field] = GetFieldValue(field);
  254. }
  255. try
  256. {
  257. var colour = expression?.Evaluate(values);
  258. Renderer?.SetFieldColour(name, ConvertObjectToColour(colour));
  259. }
  260. catch (Exception e)
  261. {
  262. Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in Expression field '{name}': {CoreUtils.FormatException(e)}");
  263. }
  264. }
  265. private void LoadExpression(string fieldName, string? expressionStr, ReferenceType referenceType)
  266. {
  267. if (string.IsNullOrWhiteSpace(expressionStr))
  268. return;
  269. var expression = new CoreExpression(expressionStr);
  270. foreach (var reference in expression.ReferencedVariables)
  271. {
  272. AddVariableReference(reference, fieldName, referenceType);
  273. }
  274. switch (referenceType)
  275. {
  276. case ReferenceType.Value:
  277. Expressions[fieldName] = expression;
  278. break;
  279. case ReferenceType.Colour:
  280. ColourExpressions[fieldName] = expression;
  281. break;
  282. }
  283. }
  284. public void LoadVariable(DigitalFormVariable variable, DFLayoutField field)
  285. {
  286. var properties = variable.LoadProperties(field);
  287. LoadExpression(field.Name, properties?.Expression, ReferenceType.Value);
  288. LoadExpression(field.Name, properties?.ColourExpression, ReferenceType.Colour);
  289. }
  290. public void LoadVariables(IEnumerable<DigitalFormVariable> variables)
  291. {
  292. foreach (var field in Elements.Where(x => x is DFLayoutField).Cast<DFLayoutField>())
  293. {
  294. var variable = variables.FirstOrDefault(x => string.Equals(x.Code, field.Name));
  295. if (variable != null)
  296. {
  297. LoadVariable(variable, field);
  298. }
  299. }
  300. }
  301. public static DFLayout FromLayoutString(string layoutString)
  302. {
  303. var layout = new DFLayout();
  304. layout.LoadLayout(layoutString);
  305. return layout;
  306. }
  307. #region Expression Fields
  308. public void ChangeField(string fieldName)
  309. {
  310. if (!VariableReferences.TryGetValue(fieldName, out var refs)) return;
  311. foreach(var (refType, refName) in refs)
  312. {
  313. switch (refType)
  314. {
  315. case ReferenceType.Value:
  316. EvaluateValueExpression(refName);
  317. break;
  318. case ReferenceType.Colour:
  319. EvaluateColourExpression(refName);
  320. break;
  321. }
  322. }
  323. }
  324. public void EvaluateExpressions()
  325. {
  326. foreach(var name in Expressions.Keys)
  327. {
  328. EvaluateValueExpression(name);
  329. }
  330. foreach(var name in ColourExpressions.Keys)
  331. {
  332. EvaluateColourExpression(name);
  333. }
  334. }
  335. #endregion
  336. #region Auto-generated Layouts
  337. public static string GetLayoutFieldDefaultHeight(DFLayoutField field)
  338. {
  339. if (field is DFLayoutSignaturePad || field is DFLayoutMultiSignaturePad)
  340. return "200";
  341. return "Auto";
  342. }
  343. public static DFLayoutField? GenerateLayoutFieldFromVariable(DigitalFormVariable variable)
  344. {
  345. DFLayoutField? field = Activator.CreateInstance(variable.FieldType()) as DFLayoutField;
  346. if(field == null)
  347. {
  348. return null;
  349. }
  350. field.Name = variable.Code;
  351. return field;
  352. }
  353. public static DFLayout GenerateAutoDesktopLayout(
  354. IList<DigitalFormVariable> variables)
  355. {
  356. var layout = new DFLayout();
  357. layout.ColumnWidths.Add("Auto");
  358. layout.ColumnWidths.Add("Auto");
  359. layout.ColumnWidths.Add("*");
  360. int row = 1;
  361. foreach(var variable in variables)
  362. {
  363. var rowHeight = "Auto";
  364. var rowNum = new DFLayoutLabel { Caption = row.ToString(), Row = row, Column = 1 };
  365. var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 };
  366. layout.Elements.Add(rowNum);
  367. layout.Elements.Add(label);
  368. var field = GenerateLayoutFieldFromVariable(variable);
  369. if(field != null)
  370. {
  371. field.Row = row;
  372. field.Column = 3;
  373. layout.Elements.Add(field);
  374. rowHeight = GetLayoutFieldDefaultHeight(field);
  375. }
  376. layout.RowHeights.Add(rowHeight);
  377. ++row;
  378. }
  379. return layout;
  380. }
  381. public static DFLayout GenerateAutoMobileLayout(
  382. IList<DigitalFormVariable> variables)
  383. {
  384. var layout = new DFLayout();
  385. layout.ColumnWidths.Add("Auto");
  386. layout.ColumnWidths.Add("*");
  387. var row = 1;
  388. var i = 0;
  389. foreach(var variable in variables)
  390. {
  391. var rowHeight = "Auto";
  392. layout.RowHeights.Add("Auto");
  393. var rowNum = new DFLayoutLabel { Caption = i + 1 + ".", Row = row, Column = 1 };
  394. var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 };
  395. layout.Elements.Add(rowNum);
  396. layout.Elements.Add(label);
  397. var field = GenerateLayoutFieldFromVariable(variable);
  398. if(field != null)
  399. {
  400. field.Row = row + 1;
  401. field.Column = 1;
  402. field.ColumnSpan = 2;
  403. layout.Elements.Add(field);
  404. rowHeight = GetLayoutFieldDefaultHeight(field);
  405. }
  406. layout.RowHeights.Add(rowHeight);
  407. row += 2;
  408. ++i;
  409. }
  410. return layout;
  411. }
  412. public static DFLayout GenerateAutoLayout(DFLayoutType type, IList<DigitalFormVariable> variables)
  413. {
  414. return type switch
  415. {
  416. DFLayoutType.Mobile => GenerateAutoDesktopLayout(variables),
  417. _ => GenerateAutoDesktopLayout(variables),
  418. };
  419. }
  420. public static DFLayoutField? GenerateLayoutFieldFromEditor(BaseEditor editor)
  421. {
  422. // TODO: Finish
  423. switch (editor)
  424. {
  425. case CheckBoxEditor _:
  426. var newField = new DFLayoutBooleanField();
  427. newField.Properties.Type = DesignBooleanFieldType.Checkbox;
  428. return newField;
  429. case CheckListEditor _:
  430. // TODO: At this point, it seems CheckListEditor is unused.
  431. throw new NotImplementedException();
  432. case UniqueCodeEditor _:
  433. case CodeEditor _:
  434. return new DFLayoutCodeField();
  435. /* Not implemented because we don't like it.
  436. case PopupEditor v:*/
  437. case CodePopupEditor codePopupEditor:
  438. // TODO: Let's look at this later. For now, using a lookup.
  439. var newLookupFieldPopup = new DFLayoutLookupField();
  440. newLookupFieldPopup.Properties.LookupType = codePopupEditor.Type.EntityName();
  441. return newLookupFieldPopup;
  442. case ColorEditor _:
  443. return new DFLayoutColorField();
  444. case CurrencyEditor _:
  445. // TODO: Make this a specialised editor
  446. return new DFLayoutDoubleField();
  447. case DateEditor _:
  448. return new DFLayoutDateField();
  449. case DateTimeEditor _:
  450. return new DFLayoutDateTimeField();
  451. case DoubleEditor _:
  452. return new DFLayoutDoubleField();
  453. case DurationEditor _:
  454. return new DFLayoutTimeField();
  455. case EmbeddedImageEditor _:
  456. return new DFLayoutEmbeddedImage();
  457. case FileNameEditor _:
  458. case FolderEditor _:
  459. // Unimplemented because these editors only apply to properties for server engine configuration; it
  460. // doesn't make sense to store filenames in the database, and hence no entity will ever try to be saved
  461. // with a property with these editors.
  462. throw new NotImplementedException("This has intentionally been left unimplemented.");
  463. case IntegerEditor _:
  464. return new DFLayoutIntegerField();
  465. case ComboLookupEditor _:
  466. case EnumLookupEditor _:
  467. var newComboLookupField = new DFLayoutOptionField();
  468. var comboValuesTable = (editor as StaticLookupEditor)!.Values("Key");
  469. newComboLookupField.Properties.Options = string.Join(",", comboValuesTable.ExtractValues<string>("Key"));
  470. return newComboLookupField;
  471. case LookupEditor lookupEditor:
  472. var newLookupField = new DFLayoutLookupField();
  473. newLookupField.Properties.LookupType = lookupEditor.Type.EntityName();
  474. return newLookupField;
  475. case ImageDocumentEditor _:
  476. case MiscellaneousDocumentEditor _:
  477. case VectorDocumentEditor _:
  478. case PDFDocumentEditor _:
  479. var newDocField = new DFLayoutDocumentField();
  480. newDocField.Properties.FileMask = (editor as BaseDocumentEditor)!.FileMask;
  481. return newDocField;
  482. case NotesEditor _:
  483. return new DFLayoutNotesField();
  484. case NullEditor _:
  485. return null;
  486. case PasswordEditor _:
  487. return new DFLayoutPasswordField();
  488. case PINEditor _:
  489. var newPINField = new DFLayoutPINField();
  490. newPINField.Properties.Length = ClientFactory.PINLength;
  491. return newPINField;
  492. // TODO: Implement JSON editors and RichText editors.
  493. case JsonEditor _:
  494. case MemoEditor _:
  495. case RichTextEditor _:
  496. case ScriptEditor _:
  497. return new DFLayoutTextField();
  498. case TextBoxEditor _:
  499. return new DFLayoutStringField();
  500. case TimestampEditor _:
  501. return new DFLayoutTimeStampField();
  502. case TimeOfDayEditor _:
  503. return new DFLayoutTimeField();
  504. case URLEditor _:
  505. return new DFLayoutURLField();
  506. }
  507. return null;
  508. }
  509. public static DFLayout GenerateEntityLayout(Type entityType)
  510. {
  511. var layout = new DFLayout();
  512. layout.ColumnWidths.Add("Auto");
  513. layout.ColumnWidths.Add("*");
  514. var properties = DatabaseSchema.Properties(entityType);
  515. var Row = 1;
  516. foreach (var property in properties)
  517. {
  518. var editor = EditorUtils.GetPropertyEditor(entityType, property);
  519. if (editor != null && !(editor is NullEditor) && editor.Editable != Editable.Hidden)
  520. {
  521. var field = GenerateLayoutFieldFromEditor(editor);
  522. if (field != null)
  523. {
  524. var label = new DFLayoutLabel { Caption = editor.Caption };
  525. label.Row = Row;
  526. label.Column = 1;
  527. field.Row = Row;
  528. field.Column = 2;
  529. field.Name = property.Name;
  530. layout.Elements.Add(label);
  531. layout.Elements.Add(field);
  532. layout.RowHeights.Add("Auto");
  533. Row++;
  534. }
  535. }
  536. }
  537. return layout;
  538. }
  539. public static DFLayout GenerateEntityLayout<T>()
  540. {
  541. return GenerateEntityLayout(typeof(T));
  542. }
  543. #endregion
  544. }
  545. }