feat: Feldeditor für Autofelder im TemplateDesigner
CI / build-and-test (push) Canceled after 0s

This commit is contained in:
2026-09-15 11:07:37 +02:00
parent 09cbbf1b77
commit 0b5cfc5522
4 changed files with 128 additions and 3 deletions
@@ -48,12 +48,14 @@ public partial class DesignerViewModel : ObservableObject
[ObservableProperty] private string _overlayPageUnit = "mm";
[ObservableProperty] private string _selectedPageTemplate = "first";
[ObservableProperty] private DesignerPreviewPage? _selectedPreviewPage;
[ObservableProperty] private SpecialContentItem? _selectedSpecialContent;
public IReadOnlyList<string> Units { get; } = ["mm", "cm", "pt", "in"];
public IReadOnlyList<PlaceholderType> PlaceholderTypes { get; } = Enum.GetValues<PlaceholderType>();
public IReadOnlyList<string> ElementTypes { get; } =
["TEXT", "TEXTBOX", "FLOWBOX", "DRAWBOX", "FLOWDRAWBOX", "IMG", "TABLE", "CHART"];
public IReadOnlyList<string> ElementScopes { get; } = ["Seitenvorlage (fest)", "Content-Flow"];
public IReadOnlyList<SpecialContentItem> SpecialContents { get; } = SpecialContentCatalog.Items;
public ObservableCollection<DesignerPlaceholder> Placeholders { get; } =
[
new("Datum", PlaceholderType.Date, true, DateTime.Today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
@@ -78,7 +80,11 @@ public partial class DesignerViewModel : ObservableObject
public bool HasSelectedPlaceholder => SelectedPlaceholder is not null;
public bool HasNoSelectedPlaceholder => SelectedPlaceholder is null;
public DesignerViewModel() => SelectedPlaceholder = Placeholders.FirstOrDefault();
public DesignerViewModel()
{
SelectedPlaceholder = Placeholders.FirstOrDefault();
SelectedSpecialContent = SpecialContents.FirstOrDefault();
}
partial void OnSelectedPlaceholderChanged(DesignerPlaceholder? value)
{
@@ -198,6 +204,32 @@ public partial class DesignerViewModel : ObservableObject
return placeholder;
}
/// <summary>Übernimmt einen lesbar benannten LehrerApp-Sonderinhalt in das normale
/// Elementformular und legt seine Drawing-Deklaration an, falls sie noch fehlt.</summary>
public void PrepareSelectedSpecialContent()
{
if (SelectedSpecialContent is not { } special)
throw new InvalidOperationException("Bitte zuerst einen Sonderinhalt auswählen.");
var placeholder = Placeholders.FirstOrDefault(p => p.Name == special.PlaceholderName);
if (placeholder is not null && placeholder.Type != PlaceholderType.Drawing)
throw new InvalidDataException(
$"Der vorhandene Platzhalter „{special.PlaceholderName}“ hat nicht den Typ Drawing.");
if (placeholder is null)
{
placeholder = new DesignerPlaceholder(special.PlaceholderName, PlaceholderType.Drawing,
false, special.Description);
Placeholders.Add(placeholder);
}
SelectedPlaceholder = placeholder;
NewElementType = special.RecommendedElementType;
NewContent = "$" + special.PlaceholderName;
NewWidth = special.RecommendedWidth;
NewHeight = special.RecommendedHeight;
NewAttributes = "";
CanExport = false;
SetStatus($"„{special.DisplayName}“ vorbereitet. Position und Größe prüfen, dann ins Layout übernehmen.", false);
}
public void RemoveSelectedPlaceholder()
{
if (SelectedPlaceholder is not { } selected) return;
@@ -703,6 +735,48 @@ public sealed record DesignerPreviewPage(int Number, Bitmap Image)
public string Display => $"Dokumentseite {Number}";
}
public sealed record SpecialContentItem(string DisplayName, string PlaceholderName, string Description,
string RecommendedElementType, string RecommendedWidth, string RecommendedHeight);
public static class SpecialContentCatalog
{
public static IReadOnlyList<SpecialContentItem> Items { get; } =
[
new("Anwesenheitskalender", "Student.AttendanceCalendar",
"Monatskalender mit farbigen Anwesenheitsmarkern; Zeitraum und Größe werden beim Erstellen des Briefs gewählt.",
"DRAWBOX", "170", "90"),
new("Fehltage nach Datum", "Student.AbsenceDays",
"Chronologische Liste mit Datum, Fehlzeit oder ganzem Fehltag sowie Entschuldigungsstatus.",
"FLOWDRAWBOX", "170", "140"),
];
public static DrawingValue SampleDrawing(string placeholderName) => placeholderName switch
{
"Student.AttendanceCalendar" => new DrawingValue(
[
new DrawString(2, 2, "Anwesenheit · Erika Beispiel", 10, "#1F2937", Bold: true),
new DrawRectangle(2, 14, 22, 12, "#D1D5DB", .4f, "#FFFFFF"),
new DrawString(9, 16, "12", 7, "#374151"),
new DrawRectangle(26, 14, 22, 12, "#C62828", .4f, "#C62828"),
new DrawString(34, 16, "U", 7, "#FFFFFF", Bold: true),
new DrawRectangle(50, 14, 22, 12, "#2E7D32", .4f, "#2E7D32"),
new DrawString(58, 16, "E", 7, "#FFFFFF", Bold: true),
], 28),
"Student.AbsenceDays" => new DrawingValue(
[
new DrawString(2, 2, "Fehltage · Erika Beispiel", 10, "#1F2937", Bold: true),
new DrawRectangle(2, 14, 166, 11, "#CBD5E1", .4f, "#F3F4F6"),
new DrawString(4, 15, "Datum", 7, "#374151", Bold: true),
new DrawString(42, 15, "Umfang", 7, "#374151", Bold: true),
new DrawString(116, 15, "Status", 7, "#374151", Bold: true),
new DrawString(4, 27, "03.09.2026", 7, "#374151"),
new DrawString(42, 27, "Ganzer Fehltag", 7, "#374151"),
new DrawString(116, 27, "Entschuldigt", 7, "#2E7D32"),
], 38),
_ => DesignerPlaceholder.GenericSampleDrawing(),
};
}
public partial class DesignerAsset(string name, int pixelWidth, int pixelHeight, long byteCount) : ObservableObject
{
[ObservableProperty] private int _usageCount;
@@ -757,7 +831,7 @@ public partial class DesignerPlaceholder : ObservableObject
PlaceholderType.Image => new ImageValue([], "image/png"),
PlaceholderType.Table => ParseTable(Sample),
PlaceholderType.Chart => ParseChart(Sample),
PlaceholderType.Drawing => SampleDrawing(),
PlaceholderType.Drawing => SpecialContentCatalog.SampleDrawing(Name),
_ => new TextValue(Sample),
};
public static string SampleFor(PlaceholderType type) => type switch
@@ -772,7 +846,7 @@ 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(
internal static DrawingValue GenericSampleDrawing() => 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);
@@ -213,6 +213,21 @@
<ScrollViewer Padding="8">
<StackPanel Spacing="12">
<TextBlock Text="Element hinzufügen" Classes="section"/>
<Border BorderBrush="#93C5FD" BorderThickness="1" Background="#EFF6FF"
CornerRadius="6" Padding="12">
<StackPanel Spacing="7">
<TextBlock Text="LehrerApp-Sonderinhalte" FontWeight="SemiBold" Foreground="#1E3A8A"/>
<TextBlock Text="Kein technischer Platzhaltername nötig: Inhalt auswählen und übernehmen."
FontSize="11" Foreground="#475569" TextWrapping="Wrap"/>
<ComboBox ItemsSource="{Binding SpecialContents}"
SelectedItem="{Binding SelectedSpecialContent, Mode=TwoWay}"
DisplayMemberBinding="{Binding DisplayName}"/>
<TextBlock Text="{Binding SelectedSpecialContent.Description}" FontSize="11"
Foreground="#475569" TextWrapping="Wrap"/>
<Button Content="Für Layout vorbereiten" HorizontalAlignment="Left"
Click="OnPrepareSpecialContent"/>
</StackPanel>
</Border>
<StackPanel><TextBlock Text="Einfügen in" Classes="label"/>
<ComboBox ItemsSource="{Binding ElementScopes}" SelectedItem="{Binding NewElementScope}"/></StackPanel>
<Grid ColumnDefinitions="*,8,*">
@@ -58,6 +58,8 @@ public partial class MainWindow : Window
}
private void OnAddElement(object? sender, RoutedEventArgs e)
{ try { _viewModel.AddElement(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
private void OnPrepareSpecialContent(object? sender, RoutedEventArgs e)
{ try { _viewModel.PrepareSelectedSpecialContent(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
private void OnAddPageTemplate(object? sender, RoutedEventArgs e)
{ try { _viewModel.AddPageTemplate(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
private void OnAddContentFlow(object? sender, RoutedEventArgs e)