DFLayout.cs 22 KB

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