TemplateDesigner: Redesign des TemplateSkripts
This commit is contained in:
@@ -8,6 +8,7 @@ namespace LehrerApp.TemplateDesigner;
|
||||
|
||||
public partial class DesignerViewModel : ObservableObject
|
||||
{
|
||||
private Bitmap? _pageTemplatePreview;
|
||||
[ObservableProperty] private string _templateId = "elternbrief-standard";
|
||||
[ObservableProperty] private string _templateName = "Elternbrief Standard";
|
||||
[ObservableProperty] private string _description = "Briefvorlage mit Schul-Briefkopf";
|
||||
@@ -23,6 +24,10 @@ public partial class DesignerViewModel : ObservableObject
|
||||
[ObservableProperty] private string _newImageScale = "100";
|
||||
[ObservableProperty] private string _newContent = "$Brieftext";
|
||||
[ObservableProperty] private string _newAttributes = "size=11";
|
||||
[ObservableProperty] private string _newElementScope = "Seitenvorlage (fest)";
|
||||
[ObservableProperty] private string _selectedContentFlow = "body";
|
||||
[ObservableProperty] private string _newPageTemplateName = "continuation";
|
||||
[ObservableProperty] private string _newFlowName = "body";
|
||||
[ObservableProperty] private Bitmap? _previewImage;
|
||||
[ObservableProperty] private string _status = "Bereit.";
|
||||
[ObservableProperty] private string _statusColor = "#475569";
|
||||
@@ -39,10 +44,13 @@ public partial class DesignerViewModel : ObservableObject
|
||||
[ObservableProperty] private double _overlayPageWidth = 210;
|
||||
[ObservableProperty] private double _overlayPageHeight = 297;
|
||||
[ObservableProperty] private string _overlayPageUnit = "mm";
|
||||
[ObservableProperty] private string _selectedPageTemplate = "first";
|
||||
[ObservableProperty] private DesignerPreviewPage? _selectedPreviewPage;
|
||||
|
||||
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> ElementScopes { get; } = ["Seitenvorlage (fest)", "Content-Flow"];
|
||||
public ObservableCollection<DesignerPlaceholder> Placeholders { get; } =
|
||||
[
|
||||
new("Datum", PlaceholderType.Date, true, DateTime.Today.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
|
||||
@@ -59,6 +67,9 @@ public partial class DesignerViewModel : ObservableObject
|
||||
public Dictionary<string, byte[]> Assets { get; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
public ObservableCollection<DesignerAsset> AssetItems { get; } = [];
|
||||
public ObservableCollection<StarterTemplateItem> StarterTemplates { get; } = [];
|
||||
public ObservableCollection<string> PageTemplateNames { get; } = ["first"];
|
||||
public ObservableCollection<string> ContentFlowNames { get; } = ["body"];
|
||||
public ObservableCollection<DesignerPreviewPage> PreviewPages { get; } = [];
|
||||
public bool HasSelectedPlaceholder => SelectedPlaceholder is not null;
|
||||
public bool HasNoSelectedPlaceholder => SelectedPlaceholder is null;
|
||||
|
||||
@@ -70,10 +81,12 @@ public partial class DesignerViewModel : ObservableObject
|
||||
OnPropertyChanged(nameof(HasNoSelectedPlaceholder));
|
||||
}
|
||||
|
||||
partial void OnSelectedPreviewPageChanged(DesignerPreviewPage? value) => PreviewImage = value?.Image;
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
TemplateId = "neue-vorlage"; TemplateName = "Neue Vorlage"; Description = "";
|
||||
PageWidth = 210; PageHeight = 297; Unit = "mm"; LayoutSource = "PAGE 210 297 mm\n";
|
||||
PageWidth = 210; PageHeight = 297; Unit = "mm"; LayoutSource = EmptyStructuredLayout;
|
||||
Placeholders.Clear(); SelectedPlaceholder = null;
|
||||
MetadataItems.Clear(); MetadataItems.Add(new(TemplateMetadataKeys.Language, "de-DE"));
|
||||
MetadataItems.Add(new(TemplateMetadataKeys.ReportType, "letter"));
|
||||
@@ -81,7 +94,7 @@ public partial class DesignerViewModel : ObservableObject
|
||||
Assets.Clear(); AssetItems.Clear(); SelectedAsset = null;
|
||||
NewElementType = "TEXT"; NewX = "20"; NewY = "50"; NewWidth = "170"; NewHeight = "30";
|
||||
NewImageScale = "100"; NewContent = "$Brieftext"; NewAttributes = "size=11";
|
||||
PreviewImage?.Dispose(); PreviewImage = null; CanExport = false;
|
||||
ClearPreviewPages(); CanExport = false;
|
||||
OverlayMode = OverlayEditorMode.Measure; OverlayCoordinates = "x=– · y=–";
|
||||
SelectedOverlayElement = "Kein Element ausgewählt";
|
||||
SetStatus("Neues Projekt angelegt.", false);
|
||||
@@ -138,6 +151,8 @@ public partial class DesignerViewModel : ObservableObject
|
||||
|
||||
public void Load(LoadedTemplate template, string layoutSource)
|
||||
{
|
||||
if (!template.Layout.UsesPageTemplates)
|
||||
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;
|
||||
@@ -157,6 +172,36 @@ public partial class DesignerViewModel : ObservableObject
|
||||
CanExport = true; SetStatus($"„{template.Manifest.Name}“ geladen.", false);
|
||||
}
|
||||
|
||||
public static string MigrateLegacyLayout(string source, TemplateLayout layout)
|
||||
{
|
||||
if (layout.UsesPageTemplates) return source;
|
||||
var statements = source.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n')
|
||||
.Where(line =>
|
||||
{
|
||||
var trimmed = line.Trim();
|
||||
return trimmed.Length > 0 && !trimmed.StartsWith('#')
|
||||
&& !trimmed.StartsWith("PAGE ", StringComparison.OrdinalIgnoreCase);
|
||||
}).Select(line => line.Trim()).ToList();
|
||||
var margin = layout.Unit.Equals("mm", StringComparison.OrdinalIgnoreCase) ? 20f : layout.Width * 0.08f;
|
||||
var x = FormatNumber(margin); var y = FormatNumber(margin);
|
||||
var width = FormatNumber(Math.Max(1, layout.Width - margin * 2));
|
||||
var height = FormatNumber(Math.Max(1, layout.Height - margin * 2));
|
||||
var result = new List<string>
|
||||
{
|
||||
$"PAGE {FormatNumber(layout.Width)} {FormatNumber(layout.Height)} {layout.Unit}",
|
||||
"#pragma format-version 3", "", "#pragma page-template first",
|
||||
};
|
||||
result.AddRange(statements);
|
||||
result.AddRange([
|
||||
$"#pragma flow-slot body x={x} y={y} w={width} h={height}",
|
||||
"#pragma end-page-template", "", "#pragma page-template continuation",
|
||||
$"#pragma flow-slot body x={x} y={y} w={width} h={height}",
|
||||
"#pragma end-page-template", "", "#pragma content-flow body",
|
||||
"#pragma end-content-flow", "",
|
||||
]);
|
||||
return string.Join('\n', result);
|
||||
}
|
||||
|
||||
public void LoadAsNewProject(LoadedTemplate template, string layoutSource, string? newId = null)
|
||||
{
|
||||
Load(template, layoutSource);
|
||||
@@ -169,10 +214,44 @@ public partial class DesignerViewModel : ObservableObject
|
||||
public void PreviewTemplate(LoadedTemplate template)
|
||||
{
|
||||
var provider = BuildSampleDataProvider(template.Manifest);
|
||||
var bytes = new QuestTemplateRenderer().RenderFirstPageToPng(template, provider);
|
||||
RenderPreviewPages(template, provider);
|
||||
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);
|
||||
ClearPreviewPages();
|
||||
for (var index = 0; index < pages.Count; index++)
|
||||
{
|
||||
using var stream = new MemoryStream(pages[index]);
|
||||
PreviewPages.Add(new(index + 1, new Bitmap(stream)));
|
||||
}
|
||||
SelectedPreviewPage = PreviewPages.FirstOrDefault();
|
||||
}
|
||||
|
||||
public void PreviewSelectedPageTemplateCanvas()
|
||||
{
|
||||
var loaded = BuildLoaded();
|
||||
var pageTemplate = loaded.Layout.PageTemplates.FirstOrDefault(x =>
|
||||
x.Name.Equals(SelectedPageTemplate, StringComparison.OrdinalIgnoreCase));
|
||||
if (pageTemplate is null) return;
|
||||
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());
|
||||
using var stream = new MemoryStream(bytes);
|
||||
PreviewImage?.Dispose(); PreviewImage = new Bitmap(stream);
|
||||
SetStatus($"Vorschau von „{template.Manifest.Name}“.", false);
|
||||
_pageTemplatePreview?.Dispose(); _pageTemplatePreview = new Bitmap(stream);
|
||||
SelectedPreviewPage = null; PreviewImage = _pageTemplatePreview;
|
||||
SetStatus($"Bearbeitungsansicht des Seitentyps „{SelectedPageTemplate}“.", false);
|
||||
}
|
||||
|
||||
private void ClearPreviewPages()
|
||||
{
|
||||
_pageTemplatePreview?.Dispose(); _pageTemplatePreview = null;
|
||||
PreviewImage = null; SelectedPreviewPage = null;
|
||||
foreach (var page in PreviewPages) page.Image.Dispose();
|
||||
PreviewPages.Clear();
|
||||
}
|
||||
|
||||
public void SetStarterTemplates(IEnumerable<StarterTemplateItem> templates, string? selectedId = null)
|
||||
@@ -220,19 +299,84 @@ public partial class DesignerViewModel : ObservableObject
|
||||
public void AddElement()
|
||||
{
|
||||
var attrs = string.IsNullOrWhiteSpace(NewAttributes) ? "" : " " + NewAttributes.Trim();
|
||||
var line = NewElementType switch
|
||||
var flowElement = NewElementScope == "Content-Flow";
|
||||
var line = (NewElementType, flowElement) switch
|
||||
{
|
||||
"TEXT" => $"TEXT {NewX} {NewY} {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
"TEXTBOX" => $"TEXTBOX {NewX} {NewY} {NewWidth} {NewHeight} {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
"IMG" => $"IMG {NewContent} {NewX} {NewY} {NewWidth} {NewHeight} scale={NormalizedImageScale()}%",
|
||||
"TABLE" => $"TABLE {NewX} {NewY} {NewWidth} {NewHeight} {NewContent}{attrs}",
|
||||
"CHART" => $"CHART {NewX} {NewY} {NewWidth} {NewHeight} {NewContent}{attrs}",
|
||||
("TEXT", false) => $"TEXT {NewX} {NewY} {QuoteIfLiteral(NewContent)}{attrs}",
|
||||
("TEXTBOX", false) => $"TEXTBOX {NewX} {NewY} {NewWidth} {NewHeight} {QuoteIfLiteral(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}",
|
||||
("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 = LayoutSource.TrimEnd() + Environment.NewLine + line + Environment.NewLine;
|
||||
LayoutSource = InsertIntoSection(LayoutSource, line, flowElement ? "content-flow" : "page-template",
|
||||
flowElement ? SelectedContentFlow : SelectedPageTemplate);
|
||||
CanExport = false; SetStatus("Element ergänzt. Vorschau zur Prüfung aktualisieren.", false);
|
||||
}
|
||||
|
||||
public void AddPageTemplate()
|
||||
{
|
||||
var name = ValidateSectionName(NewPageTemplateName, "Seitentyp");
|
||||
if (PageTemplateNames.Contains(name, StringComparer.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException($"Seitentyp „{name}“ existiert bereits.");
|
||||
var block = $"#pragma page-template {name}\n#pragma end-page-template\n";
|
||||
var marker = LayoutSource.IndexOf("#pragma content-flow", StringComparison.OrdinalIgnoreCase);
|
||||
LayoutSource = marker < 0 ? LayoutSource.TrimEnd() + "\n\n" + block
|
||||
: LayoutSource.Insert(marker, block + "\n");
|
||||
SelectedPageTemplate = name; CanExport = false;
|
||||
SetStatus($"Seitentyp „{name}“ angelegt.", false);
|
||||
}
|
||||
|
||||
public void AddContentFlow()
|
||||
{
|
||||
var name = ValidateSectionName(NewFlowName, "Flow");
|
||||
if (ContentFlowNames.Contains(name, StringComparer.OrdinalIgnoreCase))
|
||||
throw new InvalidDataException($"Content-Flow „{name}“ existiert bereits.");
|
||||
LayoutSource = LayoutSource.TrimEnd() + $"\n\n#pragma content-flow {name}\n#pragma end-content-flow\n";
|
||||
SelectedContentFlow = name; CanExport = false;
|
||||
SetStatus($"Content-Flow „{name}“ angelegt. Lege nun gleichnamige Slots auf den Seitentypen an.", false);
|
||||
}
|
||||
|
||||
public void AddFlowSlot()
|
||||
{
|
||||
var name = ValidateSectionName(NewFlowName, "Flow-Slot");
|
||||
var line = $"#pragma flow-slot {name} x={NewX} y={NewY} w={NewWidth} h={NewHeight}";
|
||||
LayoutSource = InsertIntoSection(LayoutSource, line, "page-template", SelectedPageTemplate);
|
||||
CanExport = false;
|
||||
SetStatus($"Flow-Slot „{name}“ auf „{SelectedPageTemplate}“ angelegt.", false);
|
||||
}
|
||||
|
||||
private static string ValidateSectionName(string value, string label)
|
||||
{
|
||||
var name = value.Trim();
|
||||
if (name.Length == 0 || name.Any(c => !(char.IsAsciiLetterOrDigit(c) || c is '-' or '_')))
|
||||
throw new InvalidDataException($"{label}-Namen dürfen nur Buchstaben, Ziffern, - und _ enthalten.");
|
||||
return name;
|
||||
}
|
||||
|
||||
private static string InsertIntoSection(string source, string line, string section, string name)
|
||||
{
|
||||
var lines = source.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n').ToList();
|
||||
var start = lines.FindIndex(x =>
|
||||
{
|
||||
var tokens = x.Trim().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
|
||||
return tokens.Length >= 3 && tokens[0].Equals("#pragma", StringComparison.OrdinalIgnoreCase)
|
||||
&& tokens[1].Equals(section, StringComparison.OrdinalIgnoreCase)
|
||||
&& tokens[2].Equals(name, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
if (start < 0) throw new InvalidDataException($"{section} „{name}“ wurde nicht gefunden.");
|
||||
var endDirective = section == "content-flow" ? "end-content-flow" : "end-page-template";
|
||||
var end = lines.FindIndex(start + 1, x => x.Trim().Equals($"#pragma {endDirective}", StringComparison.OrdinalIgnoreCase));
|
||||
if (end < 0) throw new InvalidDataException($"{section} „{name}“ ist nicht geschlossen.");
|
||||
lines.Insert(end, line);
|
||||
return string.Join('\n', lines);
|
||||
}
|
||||
|
||||
public DesignerAsset ImportAsset(string sourceName, byte[] bytes) => AddOrReplaceAsset(sourceName, bytes, keepName: false);
|
||||
|
||||
public DesignerAsset ImportBackground(string sourceName, byte[] bytes)
|
||||
@@ -293,6 +437,19 @@ public partial class DesignerViewModel : ObservableObject
|
||||
public void ApplyElementGeometry(OverlayElementGeometry geometry)
|
||||
{
|
||||
var layout = new LayoutParser().Parse(LayoutSource);
|
||||
if (geometry.Keyword == "FLOW")
|
||||
{
|
||||
var slot = layout.PageTemplates.SelectMany(x => x.FlowSlots)
|
||||
.FirstOrDefault(x => x.Line == geometry.Line)
|
||||
?? throw new InvalidDataException($"Flow-Slot in Zeile {geometry.Line} wurde nicht gefunden.");
|
||||
var slotLines = LayoutSource.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n').ToList();
|
||||
slotLines[geometry.Line - 1] = $"#pragma flow-slot {slot.Name} x={FormatNumber(geometry.X)} "
|
||||
+ $"y={FormatNumber(geometry.Y)} w={FormatNumber(geometry.Width)} h={FormatNumber(geometry.Height)}";
|
||||
LayoutSource = string.Join('\n', slotLines); CanExport = false;
|
||||
SelectedOverlayElement = $"Flow {slot.Name} · Zeile {geometry.Line}";
|
||||
SetStatus($"Flow-Slot „{slot.Name}“ verschoben/skaliert.", false);
|
||||
return;
|
||||
}
|
||||
var element = layout.Elements.FirstOrDefault(x => x.Line == geometry.Line)
|
||||
?? throw new InvalidDataException($"Element in Zeile {geometry.Line} wurde nicht gefunden.");
|
||||
var lines = LayoutSource.Replace("\r\n", "\n", StringComparison.Ordinal).Split('\n').ToList();
|
||||
@@ -324,6 +481,20 @@ public partial class DesignerViewModel : ObservableObject
|
||||
{
|
||||
var page = new LayoutParser().Parse(value);
|
||||
OverlayPageWidth = page.Width; OverlayPageHeight = page.Height; OverlayPageUnit = page.Unit;
|
||||
var names = page.PageTemplates.Select(x => x.Name).ToList();
|
||||
if (names.Count == 0) names.Add("legacy");
|
||||
var selected = names.Contains(SelectedPageTemplate, StringComparer.OrdinalIgnoreCase)
|
||||
? SelectedPageTemplate : names[0];
|
||||
PageTemplateNames.Clear();
|
||||
foreach (var name in names) PageTemplateNames.Add(name);
|
||||
SelectedPageTemplate = selected;
|
||||
var flows = page.ContentFlows.Select(x => x.Name).ToList();
|
||||
if (flows.Count == 0) flows.Add("body");
|
||||
var selectedFlow = flows.Contains(SelectedContentFlow, StringComparer.OrdinalIgnoreCase)
|
||||
? SelectedContentFlow : flows[0];
|
||||
ContentFlowNames.Clear();
|
||||
foreach (var flow in flows) ContentFlowNames.Add(flow);
|
||||
SelectedContentFlow = selectedFlow;
|
||||
}
|
||||
catch (TemplateValidationException) { }
|
||||
}
|
||||
@@ -406,15 +577,44 @@ public partial class DesignerViewModel : ObservableObject
|
||||
{ Status = text; StatusColor = error ? "#B91C1C" : "#166534"; }
|
||||
private static string QuoteIfLiteral(string value) => value.StartsWith('$') ? value : $"\"{value.Replace("\"", "\\\"")}\"";
|
||||
|
||||
private const string DefaultLayout = """
|
||||
# Elternbrief Standard
|
||||
private const string EmptyStructuredLayout = """
|
||||
PAGE 210 297 mm
|
||||
#pragma format-version 3
|
||||
|
||||
#pragma page-template first
|
||||
#pragma flow-slot body x=20 y=30 w=170 h=247
|
||||
#pragma end-page-template
|
||||
|
||||
#pragma page-template continuation
|
||||
#pragma flow-slot body x=20 y=20 w=170 h=257
|
||||
#pragma end-page-template
|
||||
|
||||
#pragma content-flow body
|
||||
#pragma end-content-flow
|
||||
""";
|
||||
|
||||
private const string DefaultLayout = """
|
||||
# Elternbrief Standard · Format 3
|
||||
PAGE 210 297 mm
|
||||
#pragma format-version 3
|
||||
|
||||
#pragma page-template first
|
||||
TEXT 20 25 "Elternbrief" size=18 bold=true color=#1E3A8A
|
||||
TEXT 20 43 $Datum|dd.MM.yyyy size=10
|
||||
TEXT 20 55 $Empfaenger size=11
|
||||
TEXT 20 75 "Sehr geehrte/r $Anrede," size=11
|
||||
TEXTBOX 20 90 170 155 $Brieftext size=11 wrap=true
|
||||
TEXT 20 265 $LehrerName size=10 italic=true
|
||||
#pragma flow-slot body x=20 y=90 w=170 h=175
|
||||
#pragma end-page-template
|
||||
|
||||
#pragma page-template continuation
|
||||
#pragma flow-slot body x=20 y=25 w=170 h=240
|
||||
#pragma end-page-template
|
||||
|
||||
#pragma content-flow body
|
||||
TEXTBOX $Brieftext size=11 overflow=continue
|
||||
TEXT "Mit freundlichen Grüßen" size=10 gap=8 keep-with-next=true
|
||||
TEXT $LehrerName size=10 italic=true gap=4
|
||||
#pragma end-content-flow
|
||||
""";
|
||||
}
|
||||
|
||||
@@ -424,6 +624,11 @@ public partial class DesignerMetadata(string name, string value) : ObservableObj
|
||||
[ObservableProperty] private string _value = value;
|
||||
}
|
||||
|
||||
public sealed record DesignerPreviewPage(int Number, Bitmap Image)
|
||||
{
|
||||
public string Display => $"Dokumentseite {Number}";
|
||||
}
|
||||
|
||||
public partial class DesignerAsset(string name, int pixelWidth, int pixelHeight, long byteCount) : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private int _usageCount;
|
||||
|
||||
@@ -30,14 +30,18 @@ public sealed class LayoutOverlayEditor : Control
|
||||
AvaloniaProperty.Register<LayoutOverlayEditor, bool>(nameof(SnapToGrid), true);
|
||||
public static readonly StyledProperty<double> GridSizeProperty =
|
||||
AvaloniaProperty.Register<LayoutOverlayEditor, double>(nameof(GridSize), 1);
|
||||
public static readonly StyledProperty<string> PageTemplateNameProperty =
|
||||
AvaloniaProperty.Register<LayoutOverlayEditor, string>(nameof(PageTemplateName), "first");
|
||||
|
||||
private readonly Pen _normalPen = new(new SolidColorBrush(Color.Parse("#2563EB")), 1.5);
|
||||
private readonly Pen _selectedPen = new(new SolidColorBrush(Color.Parse("#DC2626")), 2.5);
|
||||
private readonly Pen _measurePen = new(new SolidColorBrush(Color.Parse("#D97706")), 2);
|
||||
private readonly Pen _gridPen = new(new SolidColorBrush(Color.FromArgb(55, 37, 99, 235)), 1);
|
||||
private readonly Pen _flowPen = new(new SolidColorBrush(Color.Parse("#7C3AED")), 2, dashStyle: DashStyle.Dash);
|
||||
private readonly IBrush _normalFill = new SolidColorBrush(Color.FromArgb(30, 37, 99, 235));
|
||||
private readonly IBrush _selectedFill = new SolidColorBrush(Color.FromArgb(35, 220, 38, 38));
|
||||
private readonly IBrush _handleFill = new SolidColorBrush(Color.Parse("#DC2626"));
|
||||
private readonly IBrush _flowFill = new SolidColorBrush(Color.FromArgb(38, 124, 58, 237));
|
||||
private readonly List<OverlayItem> _items = [];
|
||||
private OverlayItem? _selected;
|
||||
private Point? _pointerStartDsl;
|
||||
@@ -52,6 +56,7 @@ public sealed class LayoutOverlayEditor : Control
|
||||
public OverlayEditorMode Mode { get => GetValue(ModeProperty); set => SetValue(ModeProperty, value); }
|
||||
public bool SnapToGrid { get => GetValue(SnapToGridProperty); set => SetValue(SnapToGridProperty, value); }
|
||||
public double GridSize { get => GetValue(GridSizeProperty); set => SetValue(GridSizeProperty, value); }
|
||||
public string PageTemplateName { get => GetValue(PageTemplateNameProperty); set => SetValue(PageTemplateNameProperty, value); }
|
||||
|
||||
public event EventHandler<OverlayMeasurement>? MeasurementCompleted;
|
||||
public event EventHandler<OverlayElementGeometry>? ElementGeometryChanged;
|
||||
@@ -61,7 +66,8 @@ public sealed class LayoutOverlayEditor : Control
|
||||
static LayoutOverlayEditor()
|
||||
{
|
||||
AffectsRender<LayoutOverlayEditor>(PreviewImageProperty, LayoutSourceProperty, PageWidthProperty,
|
||||
PageHeightProperty, PageUnitProperty, ModeProperty, SnapToGridProperty, GridSizeProperty);
|
||||
PageHeightProperty, PageUnitProperty, ModeProperty, SnapToGridProperty, GridSizeProperty,
|
||||
PageTemplateNameProperty);
|
||||
}
|
||||
|
||||
public LayoutOverlayEditor() { Focusable = true; ClipToBounds = true; }
|
||||
@@ -80,7 +86,9 @@ public sealed class LayoutOverlayEditor : Control
|
||||
var geometry = ReferenceEquals(item, _selected) && _workingRectDsl is { } working ? working : item.Rect;
|
||||
var rect = ToControl(geometry, page);
|
||||
var selected = ReferenceEquals(item, _selected);
|
||||
context.DrawRectangle(selected ? _selectedFill : _normalFill, selected ? _selectedPen : _normalPen,
|
||||
var fill = selected ? _selectedFill : item.IsFlowSlot ? _flowFill : _normalFill;
|
||||
var pen = selected ? _selectedPen : item.IsFlowSlot ? _flowPen : _normalPen;
|
||||
context.DrawRectangle(fill, pen,
|
||||
rect, 2, 2);
|
||||
if (selected && item.Resizable)
|
||||
context.FillRectangle(_handleFill, new Rect(rect.Right - 6, rect.Bottom - 6, 12, 12), 2);
|
||||
@@ -188,7 +196,11 @@ public sealed class LayoutOverlayEditor : Control
|
||||
try
|
||||
{
|
||||
var layout = new LayoutParser().Parse(LayoutSource);
|
||||
foreach (var element in layout.Elements)
|
||||
var pageTemplate = layout.PageTemplates.FirstOrDefault(x =>
|
||||
x.Name.Equals(PageTemplateName, StringComparison.OrdinalIgnoreCase))
|
||||
?? layout.PageTemplates.FirstOrDefault();
|
||||
var visibleElements = pageTemplate?.Elements ?? layout.Elements;
|
||||
foreach (var element in visibleElements)
|
||||
{
|
||||
if (element is BackgroundElement) continue;
|
||||
var keyword = element switch
|
||||
@@ -201,8 +213,11 @@ public sealed class LayoutOverlayEditor : Control
|
||||
ImageElement image => (image.Width * ImageScale(image), image.Height * ImageScale(image), true),
|
||||
_ => ((double)element.Width, element.Height, true),
|
||||
};
|
||||
_items.Add(new(element.Line, keyword, new Rect(element.X, element.Y, width, height), resizable));
|
||||
_items.Add(new(element.Line, keyword, new Rect(element.X, element.Y, width, height), resizable, false));
|
||||
}
|
||||
if (pageTemplate is not null)
|
||||
foreach (var slot in pageTemplate.FlowSlots)
|
||||
_items.Add(new(slot.Line, "FLOW", new Rect(slot.X, slot.Y, slot.Width, slot.Height), true, true));
|
||||
if (_selected is not null)
|
||||
_selected = _items.FirstOrDefault(x => x.Line == _selected.Line);
|
||||
}
|
||||
@@ -243,7 +258,7 @@ public sealed class LayoutOverlayEditor : Control
|
||||
private double PointsToUnit(double points) => PageUnit.ToLowerInvariant() switch
|
||||
{ "mm" => points * 25.4 / 72, "cm" => points * 2.54 / 72, "in" => points / 72, _ => points };
|
||||
|
||||
private sealed record OverlayItem(int Line, string Keyword, Rect Rect, bool Resizable);
|
||||
private sealed record OverlayItem(int Line, string Keyword, Rect Rect, bool Resizable, bool IsFlowSlot);
|
||||
private enum DragKind { None, Measure, Move, Resize }
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<Border Grid.Row="1" Padding="18,12" Background="#172554">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel><TextBlock Text="Vorlagen-Designer" Foreground="White" FontWeight="Bold" FontSize="20"/>
|
||||
<TextBlock Text="Portable PDF-Briefpakete · Schema 1" Foreground="#BFDBFE" FontSize="12"/></StackPanel>
|
||||
<TextBlock Text="Portable PDF-Briefpakete · Layoutformat 3" Foreground="#BFDBFE" FontSize="12"/></StackPanel>
|
||||
<StackPanel Grid.Column="1" HorizontalAlignment="Right">
|
||||
<TextBlock x:Name="DocumentNameText" Foreground="White" FontWeight="SemiBold" HorizontalAlignment="Right"/>
|
||||
<TextBlock x:Name="DocumentPathText" Foreground="#BFDBFE" FontSize="11" HorizontalAlignment="Right"/>
|
||||
@@ -171,10 +171,53 @@
|
||||
</Grid>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Seiten & Flows">
|
||||
<ScrollViewer Padding="8">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock Text="Seitentypen" Classes="section"/>
|
||||
<TextBlock Text="Jeder Seitentyp beschreibt nur seine eigenen festen Elemente und Flow-Slots."
|
||||
TextWrapping="Wrap" Opacity="0.7" FontSize="12"/>
|
||||
<ComboBox ItemsSource="{Binding PageTemplateNames}" SelectedItem="{Binding SelectedPageTemplate, Mode=TwoWay}"/>
|
||||
<Grid ColumnDefinitions="*,8,Auto">
|
||||
<TextBox Text="{Binding NewPageTemplateName}" PlaceholderText="z. B. continuation"/>
|
||||
<Button Grid.Column="2" Content="Seitentyp anlegen" Click="OnAddPageTemplate"/>
|
||||
</Grid>
|
||||
<Separator/>
|
||||
<TextBlock Text="Content-Flows" Classes="section"/>
|
||||
<TextBlock Text="Ein Flow läuft automatisch in den gleichnamigen Slot der ersten und anschließend der Folgeseiten."
|
||||
TextWrapping="Wrap" Opacity="0.7" FontSize="12"/>
|
||||
<ComboBox ItemsSource="{Binding ContentFlowNames}" SelectedItem="{Binding SelectedContentFlow, Mode=TwoWay}"/>
|
||||
<Grid ColumnDefinitions="*,8,Auto">
|
||||
<TextBox Text="{Binding NewFlowName}" PlaceholderText="z. B. body"/>
|
||||
<Button Grid.Column="2" Content="Flow anlegen" Click="OnAddContentFlow"/>
|
||||
</Grid>
|
||||
<Separator/>
|
||||
<TextBlock Text="Flow-Slot auf ausgewählter Seite" Classes="section"/>
|
||||
<TextBlock Text="Die Bounding Box wird rechts violett gestrichelt dargestellt und kann dort verschoben und skaliert werden."
|
||||
TextWrapping="Wrap" Opacity="0.7" FontSize="12"/>
|
||||
<Grid ColumnDefinitions="*,6,*,6,*,6,*">
|
||||
<StackPanel><TextBlock Text="X" Classes="label"/><TextBox Text="{Binding NewX}"/></StackPanel>
|
||||
<StackPanel Grid.Column="2"><TextBlock Text="Y" Classes="label"/><TextBox Text="{Binding NewY}"/></StackPanel>
|
||||
<StackPanel Grid.Column="4"><TextBlock Text="Breite" Classes="label"/><TextBox Text="{Binding NewWidth}"/></StackPanel>
|
||||
<StackPanel Grid.Column="6"><TextBlock Text="Höhe" Classes="label"/><TextBox Text="{Binding NewHeight}"/></StackPanel>
|
||||
</Grid>
|
||||
<Button Content="Flow-Slot anlegen" HorizontalAlignment="Left" Click="OnAddFlowSlot"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Header="Elemente">
|
||||
<ScrollViewer Padding="8">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Text="Element hinzufügen" Classes="section"/>
|
||||
<StackPanel><TextBlock Text="Einfügen in" Classes="label"/>
|
||||
<ComboBox ItemsSource="{Binding ElementScopes}" SelectedItem="{Binding NewElementScope}"/></StackPanel>
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<StackPanel><TextBlock Text="Seitentyp für feste Elemente" Classes="label"/>
|
||||
<ComboBox ItemsSource="{Binding PageTemplateNames}" SelectedItem="{Binding SelectedPageTemplate}"/></StackPanel>
|
||||
<StackPanel Grid.Column="2"><TextBlock Text="Content-Flow für fließende Elemente" Classes="label"/>
|
||||
<ComboBox ItemsSource="{Binding ContentFlowNames}" SelectedItem="{Binding SelectedContentFlow}"/></StackPanel>
|
||||
</Grid>
|
||||
<StackPanel><TextBlock Text="Elementtyp" Classes="label"/>
|
||||
<ComboBox ItemsSource="{Binding ElementTypes}" SelectedItem="{Binding NewElementType}"/></StackPanel>
|
||||
<Grid ColumnDefinitions="*,6,*,6,*,6,*">
|
||||
@@ -192,8 +235,8 @@
|
||||
<Button Content="Element ins Layout übernehmen" Click="OnAddElement"/>
|
||||
<TextBlock Text="Tipp: Koordinaten können rechts im Messmodus direkt aus der Vorschau übernommen werden."
|
||||
TextWrapping="Wrap" Opacity="0.65" FontSize="11"/>
|
||||
<TextBlock Text="Bekannte Einschränkung: TEXTBOX-Überlauf wird in v1 nicht automatisch auf Folgeseiten verteilt."
|
||||
TextWrapping="Wrap" Foreground="#B45309" FontSize="12"/>
|
||||
<TextBlock Text="Elemente im Content-Flow verwenden Reihenfolge, gap und keep-with-next; ihre X/Y-Koordinaten werden nicht benötigt."
|
||||
TextWrapping="Wrap" Foreground="#166534" FontSize="12"/>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
@@ -210,11 +253,19 @@
|
||||
<Border Grid.Column="2" Background="#E2E8F0" Padding="18">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="Visueller Layout-Editor" Classes="section"/>
|
||||
<StackPanel><TextBlock Text="Visueller Layout-Editor" Classes="section"/>
|
||||
<ComboBox ItemsSource="{Binding PageTemplateNames}" SelectedItem="{Binding SelectedPageTemplate, Mode=TwoWay}"
|
||||
MinWidth="150" Margin="0,5,0,0"/>
|
||||
<Button Content="Seitentyp anzeigen" Margin="0,5,0,0" Click="OnPageTemplateSelectionChanged"/></StackPanel>
|
||||
<TextBlock Grid.Column="1" Text="{Binding OverlayCoordinates}" FontFamily="Monospace"
|
||||
FontSize="11" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="1" Spacing="7" Margin="0,10,0,0">
|
||||
<Grid ColumnDefinitions="Auto,8,*">
|
||||
<TextBlock Text="Vorschau" VerticalAlignment="Center" Classes="label"/>
|
||||
<ComboBox Grid.Column="2" ItemsSource="{Binding PreviewPages}" SelectedItem="{Binding SelectedPreviewPage}"
|
||||
DisplayMemberBinding="{Binding Display}" PlaceholderText="Noch nicht gerendert"/>
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<Button Grid.Column="0" Content="Koordinaten messen" Click="OnMeasureOverlayMode"/>
|
||||
<Button Grid.Column="2" Content="Elemente verschieben" Click="OnEditOverlayMode"/>
|
||||
@@ -232,6 +283,7 @@
|
||||
LayoutSource="{Binding LayoutSource, Mode=TwoWay}"
|
||||
PageWidth="{Binding OverlayPageWidth}" PageHeight="{Binding OverlayPageHeight}"
|
||||
PageUnit="{Binding OverlayPageUnit}" Mode="{Binding OverlayMode}"
|
||||
PageTemplateName="{Binding SelectedPageTemplate}"
|
||||
SnapToGrid="{Binding SnapOverlayToGrid}" GridSize="{Binding OverlayGridSize}"
|
||||
MeasurementCompleted="OnOverlayMeasurementCompleted"
|
||||
ElementGeometryChanged="OnOverlayElementGeometryChanged"
|
||||
|
||||
@@ -58,6 +58,18 @@ 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 OnAddPageTemplate(object? sender, RoutedEventArgs e)
|
||||
{ try { _viewModel.AddPageTemplate(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
|
||||
private void OnAddContentFlow(object? sender, RoutedEventArgs e)
|
||||
{ try { _viewModel.AddContentFlow(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
|
||||
private void OnAddFlowSlot(object? sender, RoutedEventArgs e)
|
||||
{ try { _viewModel.AddFlowSlot(); } catch (Exception ex) { _viewModel.SetStatus(ex.Message, true); } }
|
||||
private void OnPageTemplateSelectionChanged(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!IsLoaded) return;
|
||||
try { _viewModel.PreviewSelectedPageTemplateCanvas(); }
|
||||
catch (Exception ex) { _viewModel.SetStatus($"Seitentyp-Vorschau fehlgeschlagen: {ex.Message}", true); }
|
||||
}
|
||||
private void OnMeasureOverlayMode(object? sender, RoutedEventArgs e)
|
||||
{ _viewModel.OverlayMode = OverlayEditorMode.Measure; _viewModel.SelectedOverlayElement = "Messmodus aktiv"; }
|
||||
private void OnEditOverlayMode(object? sender, RoutedEventArgs e)
|
||||
@@ -109,10 +121,9 @@ public partial class MainWindow : Window
|
||||
{
|
||||
try
|
||||
{
|
||||
var bytes = new QuestTemplateRenderer().RenderFirstPageToPng(_viewModel.BuildLoaded(), _viewModel.BuildDataProvider());
|
||||
using var stream = new MemoryStream(bytes);
|
||||
_viewModel.PreviewImage?.Dispose(); _viewModel.PreviewImage = new Bitmap(stream);
|
||||
_viewModel.CanExport = true; _viewModel.SetStatus("Validierung erfolgreich. Vorschau ist aktuell.", false);
|
||||
_viewModel.RenderPreviewPages(_viewModel.BuildLoaded(), _viewModel.BuildDataProvider());
|
||||
_viewModel.CanExport = true;
|
||||
_viewModel.SetStatus($"Validierung erfolgreich. Vorschau enthält {_viewModel.PreviewPages.Count} Seite(n).", false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{ _viewModel.CanExport = false; _viewModel.SetStatus(ex.Message, true); }
|
||||
|
||||
Reference in New Issue
Block a user