diff --git a/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs b/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs index 1b485ce..492dcc9 100644 --- a/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs +++ b/LehrerApp.Desktop.Tests/ExamCoverSheetTests.cs @@ -204,6 +204,25 @@ public sealed class ExamCoverSheetTests : IDisposable Assert.Equal(new TextValue("G-Kurs"), Values(Niveau.E)["Exam.Course"]); } + // Der Vorlagen-Designer bietet genau diesen Katalog an - liefert die Hauptapp einen Namen + // nicht oder mit anderem Typ, scheitert die Vorlage erst beim Drucken. + [Fact] + public void Platzhalter_DeckenDenKatalogDesDesignersMitGleichenTypenAb() + { + var store = Store(); + Import(store, "deckblatt", "Deckblatt", TemplateReportTypes.ExamCover); + var template = store.Load(store.GetTemplates().Single()); + + var values = ExamCoverPlaceholderBuilder.Build(_exam, _group, "Chemie", _anna, null, false, + new GradingService(), template, new DateOnly(2026, 1, 1)); + + Assert.All(ExamCoverPlaceholders.All, known => + { + Assert.True(values.ContainsKey(known.Name), $"{known.Name} fehlt"); + Assert.Equal(known.Type, values[known.Name].Type); + }); + } + [Fact] public void TeacherSettings_SpeichertDasKuerzel() { diff --git a/LehrerApp.Desktop/Services/ExamPointsTableDrawingBuilder.cs b/LehrerApp.Desktop/Services/ExamPointsTableDrawingBuilder.cs index b665cad..b4427a5 100644 --- a/LehrerApp.Desktop/Services/ExamPointsTableDrawingBuilder.cs +++ b/LehrerApp.Desktop/Services/ExamPointsTableDrawingBuilder.cs @@ -1,82 +1,21 @@ -using System.Globalization; using LehrerApp.Core.Services; using LehrerApp.Templating; namespace LehrerApp.Desktop.Services; -/// Zeichnet die Punktetabelle des Klausur-Deckblatts (Aufgabe / Max. Punktzahl / -/// Punktzahl). Die Spalten haben eine feste Breite: passen nicht alle Aufgaben nebeneinander in die -/// Box der Vorlage, folgt darunter ein weiterer Tabellenblock, statt alles zu verkleinern. Gedacht -/// für eine FLOWDRAWBOX … fit=content, damit nachfolgende Elemente mitwandern. +/// Brücke von den Klausurdaten zur Punktetabelle des Deckblatts. Gezeichnet wird in +/// (Vorlagenbibliothek), damit der Vorlagen-Designer dieselbe +/// Tabelle in der Vorschau zeigt. public static class ExamPointsTableDrawingBuilder { - public const string PlaceholderName = "Exam.PointsTable"; + public const string PlaceholderName = ExamPointsTableDrawing.PlaceholderName; - private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE"); - - // Referenzentwurf in Millimetern, abgemessen an der Schulvorlage. - private const float LabelWidth = 38f, ColumnWidth = 20.5f; - private const float HeaderHeight = 5.5f, MaximumHeight = 8.5f, PointsHeight = 11.5f; - private const float BlockGap = 4f, Padding = 1.5f, Stroke = 0.2f, HeavyStroke = 0.45f; - private const float FontSize = 10f; - - /// Breite der deklarierten Box in der Maßeinheit der Vorlage. - /// 1 mm in der Maßeinheit der Vorlage (siehe - /// ). public static DrawingValue Build(IReadOnlyList columns, float contentWidth = 170, - float millimeterScale = 1) - { - if (columns.Count == 0) return new DrawingValue([], 0); - var k = millimeterScale; - var perBlock = ColumnsPerBlock(contentWidth, millimeterScale); - // Halbe Strichstärke Abstand zum Rand, sonst schneidet die Box die äußeren Linien an. - var origin = HeavyStroke / 2 * k; - var blockHeight = (HeaderHeight + MaximumHeight + PointsHeight) * k; - var commands = new List(); - var y = origin; - for (var start = 0; start < columns.Count; start += perBlock) - { - if (start > 0) y += BlockGap * k; - var block = columns.Skip(start).Take(perBlock).ToList(); - var maximumY = y + HeaderHeight * k; - var pointsY = maximumY + MaximumHeight * k; + float millimeterScale = 1) => + ExamPointsTableDrawing.Build( + columns.Select(c => new ExamPointsTableColumn(c.Number, c.MaxPoints, c.IsBonus, c.AchievedPoints)).ToList(), + contentWidth, millimeterScale); - Cell(commands, origin, y, LabelWidth * k, HeaderHeight * k, "Aufgabe", DrawingTextAlignment.AlignRight, false, k); - Cell(commands, origin, maximumY, LabelWidth * k, MaximumHeight * k, "Max. Punktzahl", DrawingTextAlignment.AlignRight, false, k); - Cell(commands, origin, pointsY, LabelWidth * k, PointsHeight * k, "Punktzahl", DrawingTextAlignment.AlignRight, true, k); - for (var index = 0; index < block.Count; index++) - { - var column = block[index]; - var x = origin + (LabelWidth + index * ColumnWidth) * k; - Cell(commands, x, y, ColumnWidth * k, HeaderHeight * k, - column.IsBonus ? "" : $"Nr. {column.Number}", DrawingTextAlignment.AlignCenter, false, k); - Cell(commands, x, maximumY, ColumnWidth * k, MaximumHeight * k, - (column.IsBonus ? "+" : "") + Format(column.MaxPoints), DrawingTextAlignment.AlignCenter, false, k); - Cell(commands, x, pointsY, ColumnWidth * k, PointsHeight * k, - column.AchievedPoints is { } achieved ? Format(achieved) : "", DrawingTextAlignment.AlignCenter, false, k); - } - commands.Add(new DrawLine(origin, pointsY, origin + (LabelWidth + block.Count * ColumnWidth) * k, pointsY, - StrokeWidth: HeavyStroke * k)); - y += blockHeight; - } - return new DrawingValue(commands, y + origin); - } - - /// Wie viele Aufgaben bei fester Spaltenbreite nebeneinander in die Box passen. public static int ColumnsPerBlock(float contentWidth, float millimeterScale = 1) => - Math.Max(1, (int)Math.Floor((contentWidth / millimeterScale - HeavyStroke - LabelWidth + 0.01f) / ColumnWidth)); - - private static void Cell(List commands, float x, float y, float width, float height, - string text, DrawingTextAlignment alignment, bool bold, float k) - { - commands.Add(new DrawRectangle(x, y, width, height, StrokeWidth: Stroke * k)); - if (text.Length == 0) return; - // DrawStringEx setzt die Grundlinie auf Y + Schriftgröße; so sitzt die Zeile optisch mittig. - var fontHeight = FontSize * UnitConverter.Points(1, "pt") / UnitConverter.Points(1, "mm") * k; - var textY = y + height / 2 - fontHeight * 0.64f; - commands.Add(new DrawStringEx(x + Padding * k, textY, y + height - textY, width - 2 * Padding * k, text, - alignment, FontSize, Bold: bold)); - } - - private static string Format(double value) => value.ToString("0.##", German); + ExamPointsTableDrawing.ColumnsPerBlock(contentWidth, millimeterScale); } diff --git a/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs b/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs index fd71e90..9ecbd12 100644 --- a/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs +++ b/LehrerApp.Desktop/Services/LetterPlaceholderBuilder.cs @@ -51,34 +51,8 @@ public static class LetterPlaceholderBuilder _ => false, }; - /// Tatsächliche DRAWBOX/FLOWDRAWBOX-Größe eines Platzhalters (in der Maßeinheit der - /// Vorlage) plus dem Umrechnungsfaktor "1mm in dieser Maßeinheit". DrawingValue-Builder wie - /// entwerfen ihr Raster in Millimetern; - /// ohne MillimeterScale bliebe das bei einer in "pt" (statt "mm") deklarierten Vorlage viel zu - /// klein (1 "mm-Einheit" würde als 1pt ≈ 0.35mm gerendert). IsFixed unterscheidet DRAWBOX (feste, - /// nicht umbrechende Box - Inhalt, der die Höhe sprengt, wird von QuestTemplateRenderer - /// stillschweigend am unteren Rand abgeschnitten statt umzubrechen) von FLOWDRAWBOX (fließt bei - /// Bedarf automatisch auf weitere Seiten, siehe DrawingElementRenderer.Slice) - nur bei - /// IsFixed=true darf/muss ein Builder seine Höhe an Height anpassen. - public readonly record struct DeclaredDrawingBox(float Width, float Height, float MillimeterScale, bool IsFixed); - + /// Siehe — dort liegt die Suche, damit auch der + /// Vorlagen-Designer sie nutzen kann. public static DeclaredDrawingBox? FindDeclaredDrawingBox(LoadedTemplate template, string placeholderName) => - FindDeclaredDrawingBox(template.Layout, placeholderName) ?? - (template.ContinuationLayout is not null ? FindDeclaredDrawingBox(template.ContinuationLayout, placeholderName) : null); - - private static DeclaredDrawingBox? FindDeclaredDrawingBox(TemplateLayout layout, string placeholderName) - { - var box = layout.Elements.Concat(layout.PageTemplates.SelectMany(p => p.Elements)) - .Concat(layout.ContentFlows.SelectMany(f => f.Elements)) - .Select(e => e switch - { - DrawBoxElement draw when draw.Placeholder == placeholderName => ((float Width, float Height, bool IsFixed)?)(draw.Width, draw.Height, true), - FlowDrawBoxElement flow when flow.Placeholder == placeholderName => (flow.Width, flow.Height, false), - _ => null, - }) - .FirstOrDefault(b => b is not null); - if (box is null) return null; - var millimeterScale = UnitConverter.Points(1, "mm") / UnitConverter.Points(1, layout.Unit); - return new DeclaredDrawingBox(box.Value.Width, box.Value.Height, millimeterScale, box.Value.IsFixed); - } + DrawingBoxLocator.Find(template, placeholderName); } diff --git a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs index c33907e..408b430 100644 --- a/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs +++ b/LehrerApp.Desktop/ViewModels/Students/CreateLetterDialogViewModel.cs @@ -157,9 +157,9 @@ public partial class CreateLetterDialogViewModel : ObservableObject /// Platzhalter, damit der Kalender/die Fehltagesliste die real verfügbare Fläche ausfüllen statt /// eine feste Breite zu raten (siehe LetterPlaceholderBuilder.FindDeclaredDrawingBox). Fällt auf /// 170mm zurück, wenn keine Vorlage gewählt ist oder die Box nicht gefunden wird. - private LetterPlaceholderBuilder.DeclaredDrawingBox DeclaredDrawingBox(string placeholderName) + private DeclaredDrawingBox DeclaredDrawingBox(string placeholderName) { - var fallback = new LetterPlaceholderBuilder.DeclaredDrawingBox(170, float.PositiveInfinity, 1, false); + var fallback = new DeclaredDrawingBox(170, float.PositiveInfinity, 1, false); if (SelectedTemplate is null) return fallback; try { return LetterPlaceholderBuilder.FindDeclaredDrawingBox(_templates.Load(SelectedTemplate.Model), placeholderName) ?? fallback; } catch (Exception ex) when (ex is IOException or InvalidDataException or TemplateValidationException) { return fallback; } diff --git a/LehrerApp.TemplateDesigner.Tests/ExamCoverDesignerTests.cs b/LehrerApp.TemplateDesigner.Tests/ExamCoverDesignerTests.cs new file mode 100644 index 0000000..baf2cec --- /dev/null +++ b/LehrerApp.TemplateDesigner.Tests/ExamCoverDesignerTests.cs @@ -0,0 +1,102 @@ +using LehrerApp.TemplateDesigner; +using LehrerApp.Templating; +using Xunit; + +namespace LehrerApp.TemplateDesigner.Tests; + +public sealed class ExamCoverDesignerTests +{ + [Fact] + public void SetUpExamCover_SetztVorlagentypUndErgaenztFehlendePlatzhalter() + { + var viewModel = new DesignerViewModel(); + viewModel.Reset(); + viewModel.Placeholders.Add(new("Exam.Title", PlaceholderType.Text, true, "Mein Titel")); + + viewModel.SetUpExamCover(); + viewModel.SetUpExamCover(); + + Assert.True(viewModel.IsExamCover); + Assert.Single(viewModel.MetadataItems, x => x.Name == TemplateMetadataKeys.ReportType); + Assert.Equal(ExamCoverPlaceholders.All.Count, viewModel.Placeholders.Count); + // Vorhandene Platzhalter bleiben samt Beispielwert erhalten. + Assert.Equal("Mein Titel", viewModel.Placeholders.Single(x => x.Name == "Exam.Title").Sample); + Assert.Equal("G-Kurs", viewModel.Placeholders.Single(x => x.Name == "Exam.Course").Sample); + Assert.Equal(PlaceholderType.Date, viewModel.Placeholders.Single(x => x.Name == "Exam.Date").Type); + Assert.Equal(TemplateReportTypes.ExamCover, + viewModel.BuildLoaded().Manifest.Metadata[TemplateMetadataKeys.ReportType]); + } + + [Fact] + public void SetUpExamCover_AbweichenderTyp_WirdGemeldetStattUeberschrieben() + { + var viewModel = new DesignerViewModel(); + viewModel.Reset(); + viewModel.Placeholders.Add(new("Exam.Date", PlaceholderType.Text, false, "18.12.")); + + viewModel.SetUpExamCover(); + + Assert.Equal(PlaceholderType.Text, viewModel.Placeholders.Single(x => x.Name == "Exam.Date").Type); + Assert.Contains("Exam.Date", viewModel.Status); + } + + [Fact] + public void Punktetabelle_WirdAlsFlowDrawBoxMitFitContentInDenContentFlowVorbereitet() + { + var viewModel = new DesignerViewModel(); + viewModel.Reset(); + viewModel.SelectedSpecialContent = viewModel.SpecialContents.Single(x => + x.PlaceholderName == ExamPointsTableDrawing.PlaceholderName); + + viewModel.PrepareSelectedSpecialContent(); + viewModel.AddElement(); + + Assert.Contains("FLOWDRAWBOX $Exam.PointsTable w=165 h=150 fit=content", viewModel.LayoutSource); + var layout = viewModel.BuildLoaded().Layout; + Assert.IsType(layout.ContentFlows.Single().Elements.Single()); + } + + [Fact] + public void Vorschau_ZeigtDieEchtePunktetabelleUndSprechendeBeispielwerte() + { + var viewModel = new DesignerViewModel(); + viewModel.Reset(); + viewModel.SetUpExamCover(); + viewModel.LayoutSource = """ + PAGE 210 297 mm + #pragma format-version 3 + #pragma page-template first + TEXT 20 20 "Fach: $Subject.Name $Exam.Course $Teacher.Abbreviation" size=12 + #pragma flow-slot body x=20 y=40 w=170 h=230 + #pragma end-page-template + #pragma content-flow body + FLOWDRAWBOX $Exam.PointsTable w=100 h=150 fit=content + TEXT "Gesamtpunkte: $Result.Points von $Exam.MaxPoints Punkte" size=11 gap=8 + #pragma end-content-flow + """; + + using var document = UglyToad.PdfPig.PdfDocument.Open(viewModel.RenderCurrentPdf()); + var text = document.GetPage(1).Text.Replace(" ", ""); + + Assert.Contains("Fach:ChemieG-KursHED", text); + Assert.Contains("Gesamtpunkte:52,5von62Punkte", text); + Assert.Contains("+3", text); + // 100 mm Boxbreite: drei Aufgaben je Block, also drei Blöcke für neun Spalten. + Assert.Equal(3, System.Text.RegularExpressions.Regex.Matches(text, "Max.Punktzahl").Count); + } + + [Fact] + public void Laden_EinerDeckblattVorlage_VerwendetSprechendeBeispielwerte() + { + var source = new DesignerViewModel(); + source.Reset(); + source.SetUpExamCover(); + var loaded = source.BuildLoaded(); + + var viewModel = new DesignerViewModel(); + viewModel.Load(loaded, source.LayoutSource); + + Assert.True(viewModel.IsExamCover); + Assert.Equal("Atombau und PSE", viewModel.Placeholders.Single(x => x.Name == "Exam.Title").Sample); + } +} diff --git a/LehrerApp.TemplateDesigner/DesignerViewModel.cs b/LehrerApp.TemplateDesigner/DesignerViewModel.cs index f10d462..b81e125 100644 --- a/LehrerApp.TemplateDesigner/DesignerViewModel.cs +++ b/LehrerApp.TemplateDesigner/DesignerViewModel.cs @@ -169,8 +169,51 @@ public partial class DesignerViewModel : ObservableObject 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 byte[] RenderCurrentPdf() + { + var loaded = BuildLoaded(); + return new QuestTemplateRenderer().RenderToPdf(loaded, FitSampleData(loaded, BuildDataProvider())); + } + + /// Beispieldaten, deren Aussehen von der Vorlage abhängt: die Punktetabelle des + /// Klausur-Deckblatts richtet ihre Blockbreite nach der im Layout deklarierten Box, genau wie + /// später in der Hauptapp. + private static ITemplateDataProvider FitSampleData(LoadedTemplate template, ITemplateDataProvider provider) + { + var values = provider.GetValues(); + if (values.GetValueOrDefault(ExamPointsTableDrawing.PlaceholderName) is not DrawingValue) return provider; + return new DesignerDataProvider(new Dictionary(values, StringComparer.Ordinal) + { [ExamPointsTableDrawing.PlaceholderName] = ExamCoverPlaceholders.SampleTable(template) }); + } + + public bool IsExamCover => MetadataItems.Any(x => + x.Name.Trim().Equals(TemplateMetadataKeys.ReportType, StringComparison.OrdinalIgnoreCase) + && x.Value.Trim().Equals(TemplateReportTypes.ExamCover, StringComparison.OrdinalIgnoreCase)); + + /// Macht das Projekt zur Klausur-Deckblatt-Vorlage: setzt den Vorlagentyp und legt + /// alle Platzhalter an, die die Hauptapp dafür befüllt (mit Beispielwerten für die Vorschau). + /// Vorhandene Platzhalter und das Layout bleiben unangetastet. + public void SetUpExamCover() + { + var reportType = MetadataItems.FirstOrDefault(x => + x.Name.Trim().Equals(TemplateMetadataKeys.ReportType, StringComparison.OrdinalIgnoreCase)); + if (reportType is null) MetadataItems.Add(new(TemplateMetadataKeys.ReportType, TemplateReportTypes.ExamCover)); + else reportType.Value = TemplateReportTypes.ExamCover; + + var conflicts = new List(); + var added = 0; + foreach (var known in ExamCoverPlaceholders.All) + { + var existing = Placeholders.FirstOrDefault(p => p.Name.Trim() == known.Name); + if (existing is null) { Placeholders.Add(new(known.Name, known.Type, false, known.Sample)); added++; } + else if (existing.Type != known.Type) conflicts.Add(known.Name); + } + CanExport = false; + if (conflicts.Count > 0) + SetStatus($"Als Klausur-Deckblatt eingerichtet, {added} Platzhalter ergänzt. Achtung, abweichender Typ bei: {string.Join(", ", conflicts)} — die Hauptapp liefert dafür einen anderen Typ.", true); + else + SetStatus($"Als Klausur-Deckblatt eingerichtet, {added} Platzhalter ergänzt. Die Punktetabelle steht unter „LehrerApp-Sonderinhalte“.", false); + } public void ApplyPdfImport(PdfImportResult import) { @@ -226,7 +269,8 @@ public partial class DesignerViewModel : ObservableObject NewContent = "$" + special.PlaceholderName; NewWidth = special.RecommendedWidth; NewHeight = special.RecommendedHeight; - NewAttributes = ""; + NewAttributes = special.RecommendedAttributes; + if (special.InContentFlow) NewElementScope = "Content-Flow"; CanExport = false; SetStatus($"„{special.DisplayName}“ vorbereitet. Position und Größe prüfen, dann ins Layout übernehmen.", false); } @@ -255,7 +299,7 @@ public partial class DesignerViewModel : ObservableObject Placeholders.Clear(); foreach (var placeholder in template.Manifest.Placeholders) Placeholders.Add(new(placeholder.Name, placeholder.Type, placeholder.Required, - placeholder.IsConstant ? placeholder.ConstantValue ?? "" : DesignerPlaceholder.SampleFor(placeholder.Type), + placeholder.IsConstant ? placeholder.ConstantValue ?? "" : DesignerPlaceholder.SampleFor(placeholder.Name, placeholder.Type), placeholder.IsConstant, placeholder.Bold, placeholder.Italic, placeholder.Underline)); SelectedPlaceholder = Placeholders.FirstOrDefault(); Assets.Clear(); AssetItems.Clear(); @@ -305,14 +349,13 @@ public partial class DesignerViewModel : ObservableObject public void PreviewTemplate(LoadedTemplate template) { - var provider = BuildSampleDataProvider(template.Manifest); - RenderPreviewPages(template, provider); + RenderPreviewPages(template, BuildSampleDataProvider(template.Manifest)); SetStatus($"Vorschau von „{template.Manifest.Name}“ mit {PreviewPages.Count} Seite(n).", false); } public void RenderPreviewPages(LoadedTemplate template, ITemplateDataProvider provider) { - var pages = new QuestTemplateRenderer().RenderPagesToPng(template, provider); + var pages = new QuestTemplateRenderer().RenderPagesToPng(template, FitSampleData(template, provider)); ClearPreviewPages(); for (var index = 0; index < pages.Count; index++) { @@ -331,7 +374,8 @@ public partial class DesignerViewModel : ObservableObject var canvasLayout = new TemplateLayout(loaded.Layout.Width, loaded.Layout.Height, loaded.Layout.Unit, pageTemplate.Elements); var canvasTemplate = new LoadedTemplate(loaded.Manifest, canvasLayout, loaded.Assets, loaded.SourceName); - var bytes = new QuestTemplateRenderer().RenderFirstPageToPng(canvasTemplate, BuildDataProvider()); + var bytes = new QuestTemplateRenderer().RenderFirstPageToPng(canvasTemplate, + FitSampleData(canvasTemplate, BuildDataProvider())); using var stream = new MemoryStream(bytes); _pageTemplatePreview?.Dispose(); _pageTemplatePreview = new Bitmap(stream); SelectedPreviewPage = null; PreviewImage = _pageTemplatePreview; @@ -385,7 +429,7 @@ public partial class DesignerViewModel : ObservableObject private static ITemplateDataProvider BuildSampleDataProvider(TemplateManifest manifest) => new DesignerDataProvider(manifest.Placeholders.ToDictionary(x => x.Name, - x => new DesignerPlaceholder(x.Name, x.Type, x.Required, DesignerPlaceholder.SampleFor(x.Type)).ToValue(), + x => new DesignerPlaceholder(x.Name, x.Type, x.Required, DesignerPlaceholder.SampleFor(x.Name, x.Type)).ToValue(), StringComparer.Ordinal)); public void AddElement() @@ -739,7 +783,8 @@ public sealed record DesignerPreviewPage(int Number, Bitmap Image) } public sealed record SpecialContentItem(string DisplayName, string PlaceholderName, string Description, - string RecommendedElementType, string RecommendedWidth, string RecommendedHeight); + string RecommendedElementType, string RecommendedWidth, string RecommendedHeight, + string RecommendedAttributes = "", bool InContentFlow = false); public static class SpecialContentCatalog { @@ -751,6 +796,9 @@ public static class SpecialContentCatalog new("Fehltage nach Datum", "Student.AbsenceDays", "Chronologische Liste mit Datum, Fehlzeit oder ganzem Fehltag sowie Entschuldigungsstatus.", "FLOWDRAWBOX", "170", "140"), + new("Punktetabelle (Klausur-Deckblatt)", ExamPointsTableDrawing.PlaceholderName, + "Aufgabe / Max. Punktzahl / Punktzahl mit festen Spaltenbreiten; bei vielen Aufgaben folgt ein weiterer Block, und die Elemente darunter im Content-Flow wandern mit. Die Breite bestimmt, wie viele Aufgaben nebeneinander passen (165 = sechs).", + "FLOWDRAWBOX", "165", "150", "fit=content", InContentFlow: true), ]; public static DrawingValue SampleDrawing(string placeholderName) => placeholderName switch @@ -776,6 +824,7 @@ public static class SpecialContentCatalog new DrawString(42, 27, "Ganzer Fehltag", 7, "#374151"), new DrawString(116, 27, "Entschuldigt", 7, "#2E7D32"), ], 38), + ExamPointsTableDrawing.PlaceholderName => ExamPointsTableDrawing.Build(ExamCoverPlaceholders.SampleColumns, 165), _ => DesignerPlaceholder.GenericSampleDrawing(), }; } @@ -837,6 +886,11 @@ public partial class DesignerPlaceholder : ObservableObject PlaceholderType.Drawing => SpecialContentCatalog.SampleDrawing(Name), _ => new TextValue(Sample), }; + /// Bekannte LehrerApp-Platzhalter (Klausur-Deckblatt) bekommen einen sprechenden + /// Beispielwert statt des allgemeinen je Typ. + public static string SampleFor(string name, PlaceholderType type) => + ExamCoverPlaceholders.Find(name.Trim()) is { } known && known.Type == type && type != PlaceholderType.Drawing + ? known.Sample : SampleFor(type); 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", diff --git a/LehrerApp.TemplateDesigner/MainWindow.axaml b/LehrerApp.TemplateDesigner/MainWindow.axaml index 628a136..fae264d 100644 --- a/LehrerApp.TemplateDesigner/MainWindow.axaml +++ b/LehrerApp.TemplateDesigner/MainWindow.axaml @@ -85,6 +85,8 @@