DFLayout.cs 22 KB

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