|
| 1 | +package de.visustruct.control; |
| 2 | + |
| 3 | +import java.awt.Color; |
| 4 | +import java.awt.Font; |
| 5 | +import java.awt.Graphics2D; |
| 6 | +import java.awt.font.FontRenderContext; |
| 7 | +import java.awt.geom.Rectangle2D; |
| 8 | +import java.util.Locale; |
| 9 | +import java.util.Optional; |
| 10 | + |
| 11 | +/** |
| 12 | + * Fachliche Diagramm-Textdarstellung: Schlüsselwörter fett, Rest normal (Monospace), |
| 13 | + * angelehnt an {@code DiagramCanvasView.keywordSplit} / {@code drawKeywordLine} in VisuStruct-SwiftUI. |
| 14 | + */ |
| 15 | +public final class DiagramKeywordText { |
| 16 | + |
| 17 | + private DiagramKeywordText() { |
| 18 | + } |
| 19 | + |
| 20 | + /** Ergebnis von {@link #splitKeywordLine(String)}. */ |
| 21 | + public static final class KeywordParts { |
| 22 | + public final String keyword; |
| 23 | + public final String rest; |
| 24 | + |
| 25 | + public KeywordParts(String keyword, String rest) { |
| 26 | + this.keyword = keyword; |
| 27 | + this.rest = rest; |
| 28 | + } |
| 29 | + } |
| 30 | + |
| 31 | + /** |
| 32 | + * Wie Swift {@code normalizedDoUntilConditionLines}: führendes {@code until} in der |
| 33 | + * Fußbedingung für die Anzeige zu {@code while} umformulieren (Speicher bleibt unverändert). |
| 34 | + */ |
| 35 | + public static String normalizeDoUntilDisplayLine(String line) { |
| 36 | + if (line == null) { |
| 37 | + return ""; |
| 38 | + } |
| 39 | + String t = line.trim(); |
| 40 | + String lower = t.toLowerCase(Locale.ROOT); |
| 41 | + if ("until".equals(lower)) { |
| 42 | + return ""; |
| 43 | + } |
| 44 | + if (lower.startsWith("until ")) { |
| 45 | + return t.substring(5).trim(); |
| 46 | + } |
| 47 | + if (lower.startsWith("until(")) { |
| 48 | + return t.substring(5); |
| 49 | + } |
| 50 | + return line; |
| 51 | + } |
| 52 | + |
| 53 | + /** |
| 54 | + * Erste Zeile mit führendem Schlüsselwort versehen (Swift {@code withLeadingKeyword}), |
| 55 | + * wenn noch nicht vorhanden. |
| 56 | + */ |
| 57 | + public static String ensureLeadingKeyword(String keyword, String line) { |
| 58 | + if (line == null) { |
| 59 | + return keyword + " "; |
| 60 | + } |
| 61 | + String trimmed = line.trim(); |
| 62 | + if (trimmed.isEmpty()) { |
| 63 | + return keyword + " "; |
| 64 | + } |
| 65 | + String lower = trimmed.toLowerCase(Locale.ROOT); |
| 66 | + String k = keyword.toLowerCase(Locale.ROOT); |
| 67 | + if (lower.equals(k) || lower.startsWith(k + " ")) { |
| 68 | + return line; |
| 69 | + } |
| 70 | + return keyword + " " + trimmed; |
| 71 | + } |
| 72 | + |
| 73 | + /** |
| 74 | + * Zeile für die Diagramm-Anzeige aufbereiten (Do-Until-Normalisierung + ggf. führendes Keyword). |
| 75 | + * |
| 76 | + * @param struktogrammTyp {@link Struktogramm#typAnweisung} … |
| 77 | + * @param lineIndex 0-basiert; führendes Keyword nur bei Zeile 0 |
| 78 | + */ |
| 79 | + public static String lineForDisplay(int struktogrammTyp, int lineIndex, String raw) { |
| 80 | + if (raw == null) { |
| 81 | + return ""; |
| 82 | + } |
| 83 | + String line = raw; |
| 84 | + if (struktogrammTyp == Struktogramm.typDoUntilSchleife) { |
| 85 | + line = normalizeDoUntilDisplayLine(line); |
| 86 | + } |
| 87 | + if (lineIndex != 0) { |
| 88 | + return line; |
| 89 | + } |
| 90 | + return switch (struktogrammTyp) { |
| 91 | + case Struktogramm.typVerzweigung -> ensureLeadingKeyword("if", line); |
| 92 | + case Struktogramm.typWhileSchleife -> ensureLeadingKeyword("while", line); |
| 93 | + case Struktogramm.typForSchleife -> ensureLeadingKeyword("for", line); |
| 94 | + case Struktogramm.typFallauswahl -> ensureLeadingKeyword("switch", line); |
| 95 | + case Struktogramm.typDoUntilSchleife -> ensureLeadingKeyword("while", line); |
| 96 | + default -> line; |
| 97 | + }; |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Zerlegt eine Zeile in fett zu zeichnendes Schlüsselwort und Rest (Swift {@code keywordSplit}). |
| 102 | + */ |
| 103 | + public static Optional<KeywordParts> splitKeywordLine(String line) { |
| 104 | + if (line == null || line.isEmpty()) { |
| 105 | + return Optional.empty(); |
| 106 | + } |
| 107 | + String trimmed = line.trim(); |
| 108 | + String lower = trimmed.toLowerCase(Locale.ROOT); |
| 109 | + for (String prefix : new String[] { "input:", "output:", "print:" }) { |
| 110 | + if (lower.equals(prefix)) { |
| 111 | + return Optional.of(new KeywordParts(trimmed.substring(0, prefix.length()), "")); |
| 112 | + } |
| 113 | + if (lower.startsWith(prefix + " ")) { |
| 114 | + String kw = trimmed.substring(0, prefix.length()); |
| 115 | + String rest = trimmed.substring(prefix.length()).trim(); |
| 116 | + return Optional.of(new KeywordParts(kw, rest)); |
| 117 | + } |
| 118 | + } |
| 119 | + int sp = indexOfFirstSpace(trimmed); |
| 120 | + if (sp < 0) { |
| 121 | + String w = trimmed; |
| 122 | + if (isBareKeyword(w)) { |
| 123 | + return Optional.of(new KeywordParts(w, "")); |
| 124 | + } |
| 125 | + return Optional.empty(); |
| 126 | + } |
| 127 | + String head = trimmed.substring(0, sp); |
| 128 | + if (!isBareKeyword(head)) { |
| 129 | + return Optional.empty(); |
| 130 | + } |
| 131 | + String rest = trimmed.substring(sp + 1).trim(); |
| 132 | + return Optional.of(new KeywordParts(head, rest)); |
| 133 | + } |
| 134 | + |
| 135 | + private static int indexOfFirstSpace(String s) { |
| 136 | + for (int i = 0; i < s.length(); i++) { |
| 137 | + if (Character.isWhitespace(s.charAt(i))) { |
| 138 | + return i; |
| 139 | + } |
| 140 | + } |
| 141 | + return -1; |
| 142 | + } |
| 143 | + |
| 144 | + private static boolean isBareKeyword(String w) { |
| 145 | + String lower = w.toLowerCase(Locale.ROOT); |
| 146 | + return lower.equals("if") || lower.equals("for") || lower.equals("while") |
| 147 | + || lower.equals("until") || lower.equals("switch") || lower.equals("do") |
| 148 | + || lower.equals("true") || lower.equals("false") || lower.equals("default"); |
| 149 | + } |
| 150 | + |
| 151 | + /** Gesamtbreite der Zeile (fett + normal) in px. */ |
| 152 | + public static int measureLineWidth(Graphics2D g, String line) { |
| 153 | + Optional<KeywordParts> parts = splitKeywordLine(line); |
| 154 | + if (parts.isEmpty()) { |
| 155 | + return (int) Math.ceil(stringWidth(g, g.getFont(), line)); |
| 156 | + } |
| 157 | + KeywordParts p = parts.get(); |
| 158 | + Font plain = stripBold(g.getFont()); |
| 159 | + Font bold = plain.deriveFont(Font.BOLD); |
| 160 | + double wKw = stringWidth(g, bold, p.keyword); |
| 161 | + double wRest = p.rest.isEmpty() ? 0 : stringWidth(g, plain, " " + p.rest); |
| 162 | + return (int) Math.ceil(wKw + wRest); |
| 163 | + } |
| 164 | + |
| 165 | + /** |
| 166 | + * Zeichnet eine Zeile mit optional fettem Schlüsselwort; {@code y} ist Basislinie wie bei {@link Graphics2D#drawString}. |
| 167 | + */ |
| 168 | + public static void drawKeywordAwareLine(Graphics2D g, Color color, int x, int y, String line) { |
| 169 | + g.setColor(color); |
| 170 | + Optional<KeywordParts> parts = splitKeywordLine(line); |
| 171 | + if (parts.isEmpty()) { |
| 172 | + g.drawString(line, x, y); |
| 173 | + return; |
| 174 | + } |
| 175 | + KeywordParts p = parts.get(); |
| 176 | + Font plain = stripBold(g.getFont()); |
| 177 | + Font bold = plain.deriveFont(Font.BOLD); |
| 178 | + g.setFont(bold); |
| 179 | + g.drawString(p.keyword, x, y); |
| 180 | + int x2 = x + (int) Math.ceil(stringWidth(g, bold, p.keyword)); |
| 181 | + if (!p.rest.isEmpty()) { |
| 182 | + g.setFont(plain); |
| 183 | + g.drawString(" " + p.rest, x2, y); |
| 184 | + } else { |
| 185 | + g.setFont(plain); |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + private static Font stripBold(Font f) { |
| 190 | + return f.deriveFont(f.getStyle() & ~Font.BOLD); |
| 191 | + } |
| 192 | + |
| 193 | + private static double stringWidth(Graphics2D g, Font font, String s) { |
| 194 | + FontRenderContext frc = g.getFontRenderContext(); |
| 195 | + Rectangle2D r = font.getStringBounds(s, frc); |
| 196 | + return r.getWidth(); |
| 197 | + } |
| 198 | +} |
0 commit comments