using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.Linq; using System.Text; using System.Xml.Linq; using Expressive; using InABox.Clients; namespace InABox.Core { public interface IDFRenderer { /// /// Retrieve the value of a field, for use in expressions. /// /// /// object? GetFieldValue(string field); /// /// Retrieve a piece of additional data for a field. /// /// The field name, which is the variable code. /// The specific field to be retrieved from the variable. /// A value, which is specific to the type of and the specific being /// retrieved. object? GetFieldData(string fieldName, string dataField); void SetFieldValue(string field, object? value); /// /// Set the background colour for a field. /// void SetFieldColour(string field, Color? colour = null); } public class DFLayout { public DFLayout() { ColumnWidths = new List(); RowHeights = new List(); Elements = new List(); HiddenElements = new List(); Expressions = new Dictionary(); ColourExpressions = new Dictionary(); VariableReferences = new Dictionary>>(); } public List ColumnWidths { get; } public List RowHeights { get; } public List Elements { get; } public List HiddenElements { get; } private enum ReferenceType { Value, Colour } private Dictionary Expressions; private Dictionary ColourExpressions; private Dictionary>> VariableReferences; public IDFRenderer? Renderer; public IEnumerable GetElements(bool includeHidden = false) { foreach (var element in Elements) { yield return element; } if (includeHidden) { foreach (var element in HiddenElements) { yield return element; } } } public string SaveLayout() { var sb = new StringBuilder(); foreach (var column in ColumnWidths) sb.AppendFormat("C {0}\n", column); foreach (var row in RowHeights) sb.AppendFormat("R {0}\n", row); foreach (var element in Elements) sb.AppendFormat("E {0} {1}\n", element.GetType().EntityName(), element.SaveToString()); var result = sb.ToString(); return result; } private static Dictionary? _controls; /// A type which is a private Type? GetElementType(string typeName) { _controls ??= CoreUtils.TypeList( AppDomain.CurrentDomain.GetAssemblies(), x => x.IsClass && !x.IsAbstract && !x.IsGenericType && typeof(DFLayoutControl).IsAssignableFrom(x) ).ToDictionary( x => x.EntityName(), x => x); return _controls.GetValueOrDefault(typeName); } private static bool IsHidden(DFLayoutControl element) => element is DFLayoutField field && field.GetPropertyValue("Hidden"); public void LoadLayout(string layout) { ColumnWidths.Clear(); RowHeights.Clear(); Elements.Clear(); var lines = layout.Split('\n'); foreach (var line in lines) if (line.StartsWith("C ")) { ColumnWidths.Add(line.Substring(2)); } else if (line.StartsWith("R ")) { RowHeights.Add(line.Substring(2)); } else if (line.StartsWith("E ") || line.StartsWith("O ")) { var typename = line.Split(' ').Skip(1).FirstOrDefault() ?.Replace("InABox.Core.Design", "InABox.Core.DFLayout") ?.Replace("DFLayoutChoiceField", "DFLayoutOptionField"); if (!string.IsNullOrWhiteSpace(typename)) { var type = GetElementType(typename); if(type != null) { var element = (Activator.CreateInstance(type) as DFLayoutControl)!; var json = string.Join(" ", line.Split(' ').Skip(2)); element.LoadFromString(json); //Serialization.DeserializeInto(json, element); if(IsHidden(element)) { HiddenElements.Add(element); } else { Elements.Add(element); } } else { Logger.Send(LogType.Error, ClientFactory.UserID, $"{typename} is not the name of any concrete DFLayoutControls!"); } } } //else if (line.StartsWith("O ")) //{ // String typename = line.Split(' ').Skip(1).FirstOrDefault()?.Replace("PRSDesktop", "InABox.Core"); // if (!String.IsNullOrWhiteSpace(typename)) // { // Type type = Type.GetType(typename); // DesignControl element = Activator.CreateInstance(type) as DesignControl; // if (element != null) // { // String json = String.Join(" ", line.Split(' ').Skip(2)); // element.LoadFromString(json); // //CoreUtils.DeserializeInto(json, element); // } // Elements.Add(element); // } //} // Invalid Line Hmmm.. if (!ColumnWidths.Any()) ColumnWidths.AddRange(new[] { "*", "Auto" }); if (!RowHeights.Any()) RowHeights.AddRange(new[] { "Auto" }); } private void AddVariableReference(string reference, string fieldName, ReferenceType referenceType) { if (reference.Contains('.')) reference = reference.Split('.')[0]; if(!VariableReferences.TryGetValue(reference, out var refs)) { refs = new List>(); VariableReferences[reference] = refs; } refs.Add(new Tuple(referenceType, fieldName)); } private object? GetFieldValue(string field) { if (field.Contains('.')) { var parts = field.Split('.'); return Renderer?.GetFieldData(parts[0], string.Join('.', parts.Skip(1))); } else { return Renderer?.GetFieldValue(field); } } private void EvaluateValueExpression(string name) { var expression = Expressions[name]; var values = new Dictionary(); foreach (var field in expression.ReferencedVariables) { values[field] = GetFieldValue(field); } var oldValue = Renderer?.GetFieldValue(name); try { var value = expression?.Evaluate(values); if(value != oldValue) { Renderer?.SetFieldValue(name, value); } } catch (Exception e) { Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in Expression field '{name}': {CoreUtils.FormatException(e)}"); } } private void EvaluateColourExpression(string name) { var expression = ColourExpressions[name]; var values = new Dictionary(); foreach (var field in expression.ReferencedVariables) { values[field] = GetFieldValue(field); } try { var colour = expression?.Evaluate(values); Renderer?.SetFieldColour(name, DFLayoutUtils.ConvertObjectToColour(colour)); } catch (Exception e) { Logger.Send(LogType.Error, ClientFactory.UserID, $"Error in Expression field '{name}': {CoreUtils.FormatException(e)}"); } } private void LoadExpression(string fieldName, string? expressionStr, ReferenceType referenceType) { if (string.IsNullOrWhiteSpace(expressionStr)) return; var expression = new CoreExpression(expressionStr); foreach (var reference in expression.ReferencedVariables) { AddVariableReference(reference, fieldName, referenceType); } switch (referenceType) { case ReferenceType.Value: Expressions[fieldName] = expression; break; case ReferenceType.Colour: ColourExpressions[fieldName] = expression; break; } } public void LoadVariable(DigitalFormVariable variable, DFLayoutField field) { var properties = variable.LoadProperties(field); LoadExpression(field.Name, properties?.Expression, ReferenceType.Value); LoadExpression(field.Name, properties?.ColourExpression, ReferenceType.Colour); } public void LoadVariables(IEnumerable variables) { foreach (var field in Elements.Where(x => x is DFLayoutField).Cast()) { var variable = variables.FirstOrDefault(x => string.Equals(x.Code, field.Name)); if (variable != null) { LoadVariable(variable, field); } } } public static DFLayout FromLayoutString(string layoutString) { var layout = new DFLayout(); layout.LoadLayout(layoutString); return layout; } #region Expression Fields public void ChangeField(string fieldName) { if (!VariableReferences.TryGetValue(fieldName, out var refs)) return; foreach(var (refType, refName) in refs) { switch (refType) { case ReferenceType.Value: EvaluateValueExpression(refName); break; case ReferenceType.Colour: EvaluateColourExpression(refName); break; } } } public void EvaluateExpressions() { foreach(var name in Expressions.Keys) { EvaluateValueExpression(name); } foreach(var name in ColourExpressions.Keys) { EvaluateColourExpression(name); } } #endregion #region Auto-generated Layouts public static string GetLayoutFieldDefaultHeight(DFLayoutField field) { if (field is DFLayoutSignaturePad || field is DFLayoutMultiSignaturePad) return "200"; return "Auto"; } public static DFLayoutField? GenerateLayoutFieldFromVariable(DigitalFormVariable variable) { DFLayoutField? field = Activator.CreateInstance(variable.FieldType()) as DFLayoutField; if(field == null) { return null; } field.Name = variable.Code; return field; } public static DFLayout GenerateAutoDesktopLayout( IList variables) { var layout = new DFLayout(); layout.ColumnWidths.Add("Auto"); layout.ColumnWidths.Add("Auto"); layout.ColumnWidths.Add("*"); int row = 1; var group = ""; foreach(var variable in variables) { if (!String.IsNullOrWhiteSpace(variable.Group) && !String.Equals(variable.Group, group)) { layout.RowHeights.Add("Auto"); var header = new DFLayoutHeader() { Header = variable.Group, Row = row, Column = 1, ColumnSpan = 3, Collapsed = !String.IsNullOrWhiteSpace(group) }; layout.Elements.Add(header); group = variable.Group; row++; } var rowHeight = "Auto"; var rowNum = new DFLayoutLabel { Caption = row.ToString(), Row = row, Column = 1 }; var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 }; layout.Elements.Add(rowNum); layout.Elements.Add(label); var field = GenerateLayoutFieldFromVariable(variable); if(field != null) { field.Row = row; field.Column = 3; layout.Elements.Add(field); rowHeight = GetLayoutFieldDefaultHeight(field); } layout.RowHeights.Add(rowHeight); ++row; } return layout; } public static DFLayout GenerateAutoMobileLayout( IList variables) { var layout = new DFLayout(); layout.ColumnWidths.Add("Auto"); layout.ColumnWidths.Add("*"); var row = 1; var i = 0; var group = ""; foreach(var variable in variables) { if (!String.IsNullOrWhiteSpace(variable.Group) && !String.Equals(variable.Group, group)) { layout.RowHeights.Add("Auto"); var header = new DFLayoutHeader() { Header = variable.Group, Row = row, Column = 1, ColumnSpan = 2, Collapsed = !String.IsNullOrWhiteSpace(group) }; layout.Elements.Add(header); group = variable.Group; row++; } var rowHeight = "Auto"; layout.RowHeights.Add("Auto"); var rowNum = new DFLayoutLabel { Caption = i + 1 + ".", Row = row, Column = 1 }; var label = new DFLayoutLabel { Caption = variable.Code, Row = row, Column = 2 }; layout.Elements.Add(rowNum); layout.Elements.Add(label); var field = GenerateLayoutFieldFromVariable(variable); if(field != null) { field.Row = row + 1; field.Column = 1; field.ColumnSpan = 2; layout.Elements.Add(field); rowHeight = GetLayoutFieldDefaultHeight(field); } layout.RowHeights.Add(rowHeight); row += 2; ++i; } return layout; } public static DFLayout GenerateAutoLayout(DFLayoutType type, IList variables) { return type switch { DFLayoutType.Mobile => GenerateAutoDesktopLayout(variables), _ => GenerateAutoDesktopLayout(variables), }; } public static DFLayoutField? GenerateLayoutFieldFromEditor(BaseEditor editor) { // TODO: Finish switch (editor) { case CheckBoxEditor _: var newField = new DFLayoutBooleanField(); newField.Properties.Type = DesignBooleanFieldType.Checkbox; return newField; case CheckListEditor _: // TODO: At this point, it seems CheckListEditor is unused. throw new NotImplementedException(); case UniqueCodeEditor _: case CodeEditor _: return new DFLayoutCodeField(); /* Not implemented because we don't like it. case PopupEditor v:*/ case CodePopupEditor codePopupEditor: // TODO: Let's look at this later. For now, using a lookup. var newLookupFieldPopup = new DFLayoutLookupField(); newLookupFieldPopup.Properties.LookupType = codePopupEditor.Type.EntityName(); return newLookupFieldPopup; case ColorEditor _: return new DFLayoutColorField(); case CurrencyEditor _: // TODO: Make this a specialised editor return new DFLayoutDoubleField(); case DateEditor _: return new DFLayoutDateField(); case DateTimeEditor _: return new DFLayoutDateTimeField(); case DoubleEditor _: return new DFLayoutDoubleField(); case DurationEditor _: return new DFLayoutTimeField(); case EmbeddedImageEditor _: return new DFLayoutEmbeddedImage(); case FileNameEditor _: case FolderEditor _: // Unimplemented because these editors only apply to properties for server engine configuration; it // doesn't make sense to store filenames in the database, and hence no entity will ever try to be saved // with a property with these editors. throw new NotImplementedException("This has intentionally been left unimplemented."); case IntegerEditor _: return new DFLayoutIntegerField(); case ComboLookupEditor _: case EnumLookupEditor _: var newComboLookupField = new DFLayoutOptionField(); var comboValuesTable = (editor as StaticLookupEditor)!.Values(typeof(object), "Key"); newComboLookupField.Properties.Options = string.Join(",", comboValuesTable.ExtractValues("Key")); return newComboLookupField; case LookupEditor lookupEditor: var newLookupField = new DFLayoutLookupField(); newLookupField.Properties.LookupType = lookupEditor.Type.EntityName(); return newLookupField; case ImageDocumentEditor _: case MiscellaneousDocumentEditor _: case VectorDocumentEditor _: case PDFDocumentEditor _: var newDocField = new DFLayoutDocumentField(); newDocField.Properties.FileMask = (editor as BaseDocumentEditor)!.FileMask; return newDocField; case NotesEditor _: return new DFLayoutNotesField(); case NullEditor _: return null; case PasswordEditor _: return new DFLayoutPasswordField(); case PINEditor _: var newPINField = new DFLayoutPINField(); newPINField.Properties.Length = ClientFactory.PINLength; return newPINField; // TODO: Implement JSON editors and RichText editors. case JsonEditor _: case MemoEditor _: case RichTextEditor _: case ButtonEditor _: case ScriptEditor _: return new DFLayoutTextField(); case TextBoxEditor _: return new DFLayoutStringField(); case TimestampEditor _: return new DFLayoutTimeStampField(); case TimeOfDayEditor _: return new DFLayoutTimeField(); case URLEditor _: return new DFLayoutURLField(); } return null; } public static DFLayout GenerateEntityLayout(Type entityType) { var layout = new DFLayout(); layout.ColumnWidths.Add("Auto"); layout.ColumnWidths.Add("*"); var properties = DatabaseSchema.Properties(entityType); var Row = 1; foreach (var property in properties) { var editor = EditorUtils.GetPropertyEditor(entityType, property); if (editor != null && !(editor is NullEditor) && editor.Editable.EditorVisible()) { var field = GenerateLayoutFieldFromEditor(editor); if (field != null) { var label = new DFLayoutLabel { Caption = editor.Caption }; label.Row = Row; label.Column = 1; field.Row = Row; field.Column = 2; field.Name = property.Name; layout.Elements.Add(label); layout.Elements.Add(field); layout.RowHeights.Add("Auto"); Row++; } } } return layout; } public static DFLayout GenerateEntityLayout() { return GenerateEntityLayout(typeof(T)); } #endregion } }