# Conflicts: # LehrerApp.TemplateDesigner.Tests/ProjectLifecycleTests.cs # LehrerApp.TemplateDesigner/DesignerViewModel.cs # LehrerApp.TemplateDesigner/MainWindow.axaml # LehrerApp.Templating/LayoutParser.cs # LehrerApp.Templating/QuestTemplateRenderer.cs
This commit is contained in:
@@ -16,6 +16,8 @@ public partial class DesignerViewModel : ObservableObject
|
||||
[ObservableProperty] private decimal _pageHeight = 297;
|
||||
[ObservableProperty] private string _unit = "mm";
|
||||
[ObservableProperty] private string _layoutSource = DefaultLayout;
|
||||
[ObservableProperty] private bool _useContinuationLayout;
|
||||
[ObservableProperty] private string _continuationLayoutSource = "PAGE 210 297 mm\nFLOWBOX 20 20 170 257 $Brieftext size=11\n";
|
||||
[ObservableProperty] private string _newElementType = "TEXT";
|
||||
[ObservableProperty] private string _newX = "20";
|
||||
[ObservableProperty] private string _newY = "50";
|
||||
@@ -49,7 +51,8 @@ public partial class DesignerViewModel : ObservableObject
|
||||
|
||||
public IReadOnlyList<string> Units { get; } = ["mm", "cm", "pt", "in"];
|
||||
public IReadOnlyList<PlaceholderType> PlaceholderTypes { get; } = Enum.GetValues<PlaceholderType>();
|
||||
public IReadOnlyList<string> ElementTypes { get; } = ["TEXT", "TEXTBOX", "IMG", "TABLE", "CHART"];
|
||||
public IReadOnlyList<string> ElementTypes { get; } =
|
||||
["TEXT", "TEXTBOX", "FLOWBOX", "DRAWBOX", "FLOWDRAWBOX", "IMG", "TABLE", "CHART"];
|
||||
public IReadOnlyList<string> ElementScopes { get; } = ["Seitenvorlage (fest)", "Content-Flow"];
|
||||
public ObservableCollection<DesignerPlaceholder> Placeholders { get; } =
|
||||
[
|
||||
@@ -87,6 +90,8 @@ public partial class DesignerViewModel : ObservableObject
|
||||
{
|
||||
TemplateId = "neue-vorlage"; TemplateName = "Neue Vorlage"; Description = "";
|
||||
PageWidth = 210; PageHeight = 297; Unit = "mm"; LayoutSource = EmptyStructuredLayout;
|
||||
UseContinuationLayout = false;
|
||||
ContinuationLayoutSource = "PAGE 210 297 mm\nFLOWBOX 20 20 170 257 $Brieftext size=11\n";
|
||||
Placeholders.Clear(); SelectedPlaceholder = null;
|
||||
MetadataItems.Clear(); MetadataItems.Add(new(TemplateMetadataKeys.Language, "de-DE"));
|
||||
MetadataItems.Add(new(TemplateMetadataKeys.ReportType, "letter"));
|
||||
@@ -105,6 +110,7 @@ public partial class DesignerViewModel : ObservableObject
|
||||
SchemaVersion = TemplateLoader.CurrentSchemaVersion,
|
||||
Id = TemplateId.Trim(), Name = TemplateName.Trim(), Description = Description.Trim(),
|
||||
PageSize = new((float)PageWidth, (float)PageHeight, Unit), LayoutFile = "layout.tpl",
|
||||
ContinuationLayoutFile = UseContinuationLayout ? "continuation.tpl" : null,
|
||||
Metadata = BuildMetadata(),
|
||||
Placeholders = BuildPlaceholders(),
|
||||
};
|
||||
@@ -116,21 +122,69 @@ public partial class DesignerViewModel : ObservableObject
|
||||
throw new InvalidDataException("Die ID darf nur ASCII-Buchstaben, Ziffern und Bindestriche enthalten.");
|
||||
if (string.IsNullOrWhiteSpace(manifest.Name)) throw new InvalidDataException("Der Vorlagenname fehlt.");
|
||||
var layout = new LayoutParser().Parse(LayoutSource);
|
||||
var continuationLayout = UseContinuationLayout ? new LayoutParser().Parse(ContinuationLayoutSource) : null;
|
||||
var issues = new List<ValidationIssue>();
|
||||
var layouts = continuationLayout is null ? new[] { layout } : new[] { layout, continuationLayout };
|
||||
var declared = manifest.Placeholders.Select(x => x.Name).ToHashSet(StringComparer.Ordinal);
|
||||
foreach (var used in TemplateLoader.UsedPlaceholders(layout).Where(x => !declared.Contains(x)))
|
||||
foreach (var used in layouts.SelectMany(TemplateLoader.UsedPlaceholders).Distinct(StringComparer.Ordinal)
|
||||
.Where(x => !declared.Contains(x)))
|
||||
issues.Add(new(ValidationSeverity.Error, $"Platzhalter „{used}“ ist nicht deklariert."));
|
||||
foreach (var path in layout.Elements.Select(x => x switch { BackgroundElement b => b.Path, ImageElement i => i.Path, _ => null }).Where(x => x is not null))
|
||||
foreach (var path in layouts.SelectMany(x => x.Elements).Select(x => x switch { BackgroundElement b => b.Path, ImageElement i => i.Path, _ => null }).Where(x => x is not null))
|
||||
if (!Assets.ContainsKey(path!)) issues.Add(new(ValidationSeverity.Error, $"Asset „{path}“ fehlt."));
|
||||
if (layout.UsesPageTemplates && continuationLayout is not null)
|
||||
issues.Add(new(ValidationSeverity.Error,
|
||||
"Layoutformat 3 enthält Folgeseiten als page-template und kann nicht zusätzlich continuation.tpl verwenden."));
|
||||
if (!layout.UsesPageTemplates)
|
||||
{
|
||||
var firstFlows = layout.Elements.Where(x => x is FlowBoxElement or FlowDrawBoxElement).ToList();
|
||||
if (firstFlows.Count > 1)
|
||||
issues.Add(new(ValidationSeverity.Error, "Ein Legacy-Layout darf höchstens eine FLOWBOX enthalten."));
|
||||
if (continuationLayout is not null)
|
||||
{
|
||||
var continuationFlows = continuationLayout.Elements.Where(x => x is FlowBoxElement or FlowDrawBoxElement).ToList();
|
||||
if (firstFlows.Count != 1 || continuationFlows.Count != 1)
|
||||
issues.Add(new(ValidationSeverity.Error, "Haupt- und Folgeseite müssen jeweils genau ein fließendes Element enthalten."));
|
||||
else if (firstFlows[0].GetType() != continuationFlows[0].GetType())
|
||||
issues.Add(new(ValidationSeverity.Error, "Haupt- und Folgeseite müssen denselben fließenden Elementtyp verwenden."));
|
||||
else if (firstFlows[0].X != continuationFlows[0].X || firstFlows[0].Y != continuationFlows[0].Y
|
||||
|| firstFlows[0].Width != continuationFlows[0].Width || firstFlows[0].Height != continuationFlows[0].Height)
|
||||
issues.Add(new(ValidationSeverity.Error, "Die FLOWBOX muss auf Haupt- und Folgeseiten dieselbe Position und Größe haben."));
|
||||
}
|
||||
}
|
||||
foreach (var issue in TemplateDataResolver.ValidateConstants(manifest))
|
||||
issues.Add(new(ValidationSeverity.Error, issue));
|
||||
if (issues.Count > 0) throw new TemplateValidationException(new(issues));
|
||||
return new(manifest, layout, new Dictionary<string, byte[]>(Assets));
|
||||
return new(manifest, layout, new Dictionary<string, byte[]>(Assets), ContinuationLayout: continuationLayout);
|
||||
}
|
||||
|
||||
public ITemplateDataProvider BuildDataProvider() => new DesignerDataProvider(Placeholders.ToDictionary(
|
||||
x => x.Name.Trim(), x => x.ToValue(), StringComparer.Ordinal));
|
||||
|
||||
public byte[] RenderCurrentPdf() =>
|
||||
new QuestTemplateRenderer().RenderToPdf(BuildLoaded(), BuildDataProvider());
|
||||
|
||||
public void ApplyPdfImport(PdfImportResult import)
|
||||
{
|
||||
TemplateId = import.Manifest.Id; TemplateName = import.Manifest.Name; Description = import.Manifest.Description;
|
||||
PageWidth = (decimal)import.Manifest.PageSize.Width; PageHeight = (decimal)import.Manifest.PageSize.Height;
|
||||
Unit = import.Manifest.PageSize.Unit; LayoutSource = import.LayoutSource;
|
||||
UseContinuationLayout = false;
|
||||
Placeholders.Clear();
|
||||
foreach (var definition in import.Manifest.Placeholders)
|
||||
{
|
||||
var candidate = import.Candidates.FirstOrDefault(x => x.Name.Equals(definition.Name, StringComparison.Ordinal));
|
||||
Placeholders.Add(new(definition.Name, definition.Type, definition.Required,
|
||||
candidate?.OriginalText ?? DesignerPlaceholder.SampleFor(definition.Type)));
|
||||
}
|
||||
SelectedPlaceholder = Placeholders.FirstOrDefault();
|
||||
Assets.Clear(); AssetItems.Clear();
|
||||
foreach (var asset in import.Assets) AddOrReplaceAsset(asset.Key, asset.Value, keepName: true);
|
||||
MetadataItems.Clear();
|
||||
foreach (var metadata in import.Manifest.Metadata) MetadataItems.Add(new(metadata.Key, metadata.Value));
|
||||
CanExport = false;
|
||||
SetStatus("PDF-Import übernommen. Bitte Vorschau, Platzhalter und Layout vor dem Speichern prüfen.", false);
|
||||
}
|
||||
|
||||
public DesignerPlaceholder AddPlaceholder()
|
||||
{
|
||||
var existing = Placeholders.Select(x => x.Name).ToHashSet(StringComparer.Ordinal);
|
||||
@@ -149,13 +203,16 @@ public partial class DesignerViewModel : ObservableObject
|
||||
SetStatus($"Platzhalter „{selected.Name}“ entfernt.", false);
|
||||
}
|
||||
|
||||
public void Load(LoadedTemplate template, string layoutSource)
|
||||
public void Load(LoadedTemplate template, string layoutSource, string? continuationLayoutSource = null)
|
||||
{
|
||||
if (!template.Layout.UsesPageTemplates)
|
||||
if (!template.Layout.UsesPageTemplates && continuationLayoutSource is null)
|
||||
layoutSource = MigrateLegacyLayout(layoutSource, template.Layout);
|
||||
TemplateId = template.Manifest.Id; TemplateName = template.Manifest.Name; Description = template.Manifest.Description;
|
||||
PageWidth = (decimal)template.Manifest.PageSize.Width; PageHeight = (decimal)template.Manifest.PageSize.Height;
|
||||
Unit = template.Manifest.PageSize.Unit; LayoutSource = layoutSource;
|
||||
UseContinuationLayout = !template.Layout.UsesPageTemplates
|
||||
&& template.Manifest.ContinuationLayoutFile is not null;
|
||||
ContinuationLayoutSource = continuationLayoutSource ?? "PAGE 210 297 mm\n";
|
||||
MetadataItems.Clear();
|
||||
foreach (var item in template.Manifest.Metadata.OrderBy(x => x.Key, StringComparer.OrdinalIgnoreCase))
|
||||
MetadataItems.Add(new(item.Key, item.Value));
|
||||
@@ -202,9 +259,9 @@ public partial class DesignerViewModel : ObservableObject
|
||||
return string.Join('\n', result);
|
||||
}
|
||||
|
||||
public void LoadAsNewProject(LoadedTemplate template, string layoutSource, string? newId = null)
|
||||
public void LoadAsNewProject(LoadedTemplate template, string layoutSource, string? continuationLayoutSource = null, string? newId = null)
|
||||
{
|
||||
Load(template, layoutSource);
|
||||
Load(template, layoutSource, continuationLayoutSource);
|
||||
TemplateId = newId ?? template.Manifest.Id + "-neu";
|
||||
TemplateName = template.Manifest.Name + " - Neu";
|
||||
CanExport = false;
|
||||
@@ -304,18 +361,27 @@ public partial class DesignerViewModel : ObservableObject
|
||||
{
|
||||
("TEXT", false) => $"TEXT {NewX} {NewY} {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
("TEXTBOX", false) => $"TEXTBOX {NewX} {NewY} {NewWidth} {NewHeight} {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
("FLOWBOX", false) => $"FLOWBOX {NewX} {NewY} {NewWidth} {NewHeight} {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
("DRAWBOX", false) => $"DRAWBOX {NewX} {NewY} {NewWidth} {NewHeight} {NewContent}{attrs}",
|
||||
("FLOWDRAWBOX", false) => $"FLOWDRAWBOX {NewX} {NewY} {NewWidth} {NewHeight} {NewContent}{attrs}",
|
||||
("IMG", false) => $"IMG {NewContent} {NewX} {NewY} {NewWidth} {NewHeight} scale={NormalizedImageScale()}%",
|
||||
("TABLE", false) => $"TABLE {NewX} {NewY} {NewWidth} {NewHeight} {NewContent}{attrs}",
|
||||
("CHART", false) => $"CHART {NewX} {NewY} {NewWidth} {NewHeight} {NewContent}{attrs}",
|
||||
("TEXT", true) => $"TEXT {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
("TEXTBOX", true) => $"TEXTBOX {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
("FLOWBOX", true) => $"FLOWBOX {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
("DRAWBOX", true) => $"DRAWBOX {NewContent} w={NewWidth} h={NewHeight}{attrs}",
|
||||
("FLOWDRAWBOX", true) => $"FLOWDRAWBOX {NewContent} w={NewWidth} h={NewHeight}{attrs}",
|
||||
("IMG", true) => $"IMG {NewContent} w={NewWidth} h={NewHeight} scale={NormalizedImageScale()}%{attrs}",
|
||||
("TABLE", true) => $"TABLE {NewContent}{attrs}",
|
||||
("CHART", true) => $"CHART {NewContent} h={NewHeight}{attrs}",
|
||||
_ => throw new InvalidOperationException("Unbekannter Elementtyp."),
|
||||
};
|
||||
LayoutSource = InsertIntoSection(LayoutSource, line, flowElement ? "content-flow" : "page-template",
|
||||
flowElement ? SelectedContentFlow : SelectedPageTemplate);
|
||||
var parsedLayout = new LayoutParser().Parse(LayoutSource);
|
||||
LayoutSource = parsedLayout.UsesPageTemplates
|
||||
? InsertIntoSection(LayoutSource, line, flowElement ? "content-flow" : "page-template",
|
||||
flowElement ? SelectedContentFlow : SelectedPageTemplate)
|
||||
: LayoutSource.TrimEnd() + Environment.NewLine + line + Environment.NewLine;
|
||||
CanExport = false; SetStatus("Element ergänzt. Vorschau zur Prüfung aktualisieren.", false);
|
||||
}
|
||||
|
||||
@@ -463,6 +529,12 @@ public partial class DesignerViewModel : ObservableObject
|
||||
TextElement text => $"TEXT {x} {y} {Content(text.Content, text.Placeholder, text.Format)}{Attributes(text.Attributes)}",
|
||||
TextBoxElement box => $"TEXTBOX {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
|
||||
+ $"{Content(box.Content, box.Placeholder, box.Format)}{Attributes(box.Attributes)}",
|
||||
FlowBoxElement box => $"FLOWBOX {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
|
||||
+ $"{Content(box.Content, box.Placeholder, box.Format)}{Attributes(box.Attributes)}",
|
||||
DrawBoxElement box => $"DRAWBOX {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
|
||||
+ $"${box.Placeholder}{Attributes(box.Attributes)}",
|
||||
FlowDrawBoxElement box => $"FLOWDRAWBOX {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
|
||||
+ $"${box.Placeholder}{Attributes(box.Attributes)}",
|
||||
TableElement table => $"TABLE {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
|
||||
+ $"${table.Placeholder}{Attributes(table.Attributes)}",
|
||||
ChartElement chart => $"CHART {x} {y} {FormatNumber(width)} {FormatNumber(height)} "
|
||||
@@ -683,11 +755,13 @@ public partial class DesignerPlaceholder : ObservableObject
|
||||
PlaceholderType.Image => new ImageValue([], "image/png"),
|
||||
PlaceholderType.Table => ParseTable(Sample),
|
||||
PlaceholderType.Chart => ParseChart(Sample),
|
||||
PlaceholderType.Drawing => SampleDrawing(),
|
||||
_ => new TextValue(Sample),
|
||||
};
|
||||
public static string SampleFor(PlaceholderType type) => type switch
|
||||
{ PlaceholderType.Date => DateTime.Today.ToString("yyyy-MM-dd"), PlaceholderType.Number => "42,5",
|
||||
PlaceholderType.Table => "Datum;Grund|01.09.;Krank", PlaceholderType.Chart => "Sep:2;Okt:3;Nov:1", _ => "Beispielwert" };
|
||||
PlaceholderType.Table => "Datum;Grund|01.09.;Krank", PlaceholderType.Chart => "Sep:2;Okt:3;Nov:1",
|
||||
PlaceholderType.Drawing => "Externe Zeichenbefehle", _ => "Beispielwert" };
|
||||
private static TableValue ParseTable(string value)
|
||||
{
|
||||
var lines = value.Split('|', StringSplitOptions.RemoveEmptyEntries);
|
||||
@@ -696,6 +770,10 @@ public partial class DesignerPlaceholder : ObservableObject
|
||||
}
|
||||
private static ChartValue ParseChart(string value) => new([new("Werte", value.Split(';', StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select((x, i) => { var parts = x.Split(':', 2); return new ChartPoint(parts[0], parts.Length == 2 && decimal.TryParse(parts[1], CultureInfo.InvariantCulture, out var y) ? y : i + 1); }).ToList())]);
|
||||
private static DrawingValue SampleDrawing() => new DrawingValue(
|
||||
[new DrawRectangle(0, 0, 80, 24, "#2563EB", 0.8f, "#EFF6FF"),
|
||||
new DrawString(4, 4, "Dynamischer Inhalt", 10, "#1E3A8A", Bold: true),
|
||||
new MoveTo(4, 19), new LineTo(76, 19, "#93C5FD", 0.6f)], 24);
|
||||
}
|
||||
|
||||
internal sealed class DesignerDataProvider(IReadOnlyDictionary<string, PlaceholderValue> values) : ITemplateDataProvider
|
||||
|
||||
Reference in New Issue
Block a user