Skip to content
This repository was archived by the owner on Nov 7, 2026. It is now read-only.

Commit fab9f8f

Browse files
committed
Merge branch 'simulation-controller' into palette-view
2 parents d852cb9 + 9febfde commit fab9f8f

24 files changed

Lines changed: 160 additions & 19 deletions

File tree

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/Canvas/DiagramCanvasView.swift‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ struct DiagramCanvasView: View {
213213
Color.clear
214214
.frame(width: cw, height: ch)
215215
.contentShape(Rectangle())
216+
.accessibilityHidden(true)
216217
.onTapGesture {
217218
selectedPath = nil
218219
// iPhone: Einfügemarke durch Tap auf leeren Bereich löschen.
@@ -237,13 +238,16 @@ struct DiagramCanvasView: View {
237238

238239
canvasView(layout: layout, cw: cw, ch: ch)
239240
.allowsHitTesting(false)
241+
// Gezeichnete Fläche; VoiceOver liest stattdessen die Blöcke darüber.
242+
.accessibilityHidden(true)
240243

241244
if let firstRoot = layout.elements.first {
242245
rootInsertBeforeFirstStrip(
243246
firstRootFrame: firstRoot.frame,
244247
canvasWidth: cw,
245248
canvasHeight: ch
246249
)
250+
.accessibilityHidden(true)
247251
}
248252

249253
ForEach(flat, id: \.pathKey) { node in
@@ -354,13 +358,30 @@ struct DiagramCanvasView: View {
354358
.draggable(DiagramDragPayload(raw: ElementPalette.moveDragIdentifier(path: node.idPath))) {
355359
nodeDragPreview(for: node)
356360
}
361+
// VoiceOver: ein Element pro Block (statt der unbeschrifteten Drop-Streifen darin).
362+
.accessibilityElement(children: .ignore)
363+
.accessibilityLabel(accessibilityLabel(for: node))
364+
.accessibilityAddTraits(selectedPath == node.idPath ? [.isButton, .isSelected] : .isButton)
365+
.accessibilityAction {
366+
selectedPath = node.idPath
367+
}
357368
.accessibilityHint(
358369
isPlaceholderNode
359370
? lang.tr("a11y_placeholder_drop")
360371
: lang.tr("a11y_node_drop")
361372
)
362373
}
363374

375+
/// Vorgelesener Name eines Blocks: Elementart und Text, z. B. „Anweisung: int x = 5“.
376+
private func accessibilityLabel(for node: VisuStructLayout.Node) -> String {
377+
// Schlüssel wie in der Palette: element_anweisung, element_verzweigung, …
378+
let kind = lang.tr("element_\(node.element.type)")
379+
let text = node.element.textLines
380+
.joined(separator: " ")
381+
.trimmingCharacters(in: .whitespacesAndNewlines)
382+
return text.isEmpty ? kind : "\(kind): \(text)"
383+
}
384+
364385
private func copyElement(at path: [Int]) {
365386
guard let el = try? VisuStructDocumentEditing.element(at: path, in: document) else { return }
366387
elementClipboard = el

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/ContentView.swift‎

Lines changed: 86 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,14 @@ struct ContentView: View {
5252
}
5353
// iPhone UX: Einfügemarke + Inspector einklappen beim Einfügen.
5454
@State private var insertTarget: DiagramInsertTarget? = nil
55-
@State private var hideInspectorOnPhone: Bool = false
55+
/// iPhone: Inspector startet eingeklappt, damit das Diagramm Platz hat.
56+
@State private var hideInspectorOnPhone: Bool = true
5657
// iPhone UX: Palette automatisch einklappen beim Text-Editieren.
57-
@State private var showPaletteOnPhone: Bool = true
58+
/// iPhone: Palette als Sheet über dem Editor (statt fest über der Navigationsleiste).
59+
@State private var showPaletteOnPhone: Bool = false
60+
/// iPhone: Diagrammtitel wird in einem kleinen Dialog bearbeitet statt in einem eigenen Feld über dem Diagramm.
61+
@State private var showPhoneTitleEditor: Bool = false
62+
@State private var phoneTitleDraft: String = ""
5863
// In-App Zwischenablage für Diagramm-Elemente (Copy/Cut/Paste).
5964
@State private var elementClipboard: VisuStructElement? = nil
6065
#if canImport(UIKit)
@@ -160,7 +165,7 @@ struct ContentView: View {
160165
.navigationSplitViewStyle(.balanced)
161166
} else {
162167
VStack(spacing: 0) {
163-
if !isPhone || showPaletteOnPhone {
168+
if !isPhone {
164169
paletteListPanel
165170
.frame(maxHeight: 300, alignment: .top)
166171
.background(Color(.secondarySystemGroupedBackground))
@@ -170,6 +175,14 @@ struct ContentView: View {
170175
mainEditorNavigationStack
171176
.frame(maxWidth: .infinity, maxHeight: .infinity)
172177
}
178+
.sheet(isPresented: phonePaletteSheetBinding) {
179+
paletteListPanel
180+
.environment(\.appLanguage, lang)
181+
.presentationDetents([.medium, .large])
182+
// Diagramm bleibt bedienbar, z. B. um eine Einfügestelle zu wählen.
183+
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
184+
.presentationDragIndicator(.visible)
185+
}
173186
}
174187
}
175188
#if canImport(UIKit)
@@ -329,6 +342,11 @@ struct ContentView: View {
329342
refreshLineEditor()
330343
DiagramCommandCenter.shared.update(file: diagramFileActions, edit: diagramEditActions)
331344
}
345+
.onChange(of: insertTarget) { _, newTarget in
346+
// iPhone: Einfügestelle gewählt → Palette öffnen, damit gleich ein Element gewählt werden kann.
347+
guard isPhone, newTarget != nil else { return }
348+
showPaletteOnPhone = true
349+
}
332350
.onChange(of: focusedField) { _, newVal in
333351
// iPhone: Sobald Textbearbeitung startet, Palette einklappen für mehr Platz.
334352
guard isPhone else { return }
@@ -686,21 +704,25 @@ struct ContentView: View {
686704
let blockCorner: CGFloat = horizontalSizeClass == .regular ? 14 : 12
687705

688706
return VStack(alignment: .leading, spacing: 0) {
689-
VStack(alignment: .leading, spacing: 8) {
690-
Text(lang.tr("diagram_title_label"))
691-
.font(horizontalSizeClass == .regular ? .caption : .subheadline)
692-
.foregroundStyle(.secondary)
693-
TextField(lang.tr("diagram_title_placeholder"), text: captionBinding)
694-
.font(horizontalSizeClass == .regular ? .subheadline : .body)
695-
.textFieldStyle(.roundedBorder)
696-
.textInputAutocapitalization(.never)
697-
.autocorrectionDisabled(true)
698-
.focused($focusedField, equals: .caption)
707+
if isPhone {
708+
phoneCaptionRow
709+
} else {
710+
VStack(alignment: .leading, spacing: 8) {
711+
Text(lang.tr("diagram_title_label"))
712+
.font(horizontalSizeClass == .regular ? .caption : .subheadline)
713+
.foregroundStyle(.secondary)
714+
TextField(lang.tr("diagram_title_placeholder"), text: captionBinding)
715+
.font(horizontalSizeClass == .regular ? .subheadline : .body)
716+
.textFieldStyle(.roundedBorder)
717+
.textInputAutocapitalization(.never)
718+
.autocorrectionDisabled(true)
719+
.focused($focusedField, equals: .caption)
720+
}
721+
.frame(maxWidth: .infinity, alignment: .leading)
722+
.padding(.horizontal, 14)
723+
.padding(.top, 8)
724+
.padding(.bottom, 6)
699725
}
700-
.frame(maxWidth: .infinity, alignment: .leading)
701-
.padding(.horizontal, 14)
702-
.padding(.top, 8)
703-
.padding(.bottom, 6)
704726
// Innerer Trennstrich — Titel zählt zum Diagramm-„Blatt“, nicht als losgelöster Bereich oberhalb.
705727
Rectangle()
706728
.fill(Color.primary.opacity(0.1))
@@ -740,6 +762,47 @@ struct ContentView: View {
740762
}
741763
}
742764

765+
/// iPhone: Titel als eine kompakte Zeile; Tippen öffnet einen Dialog zum Bearbeiten.
766+
private var phoneCaptionRow: some View {
767+
Button {
768+
phoneTitleDraft = document.caption
769+
showPhoneTitleEditor = true
770+
} label: {
771+
HStack(spacing: 6) {
772+
Text(document.caption.isEmpty ? lang.tr("diagram_title_placeholder") : document.caption)
773+
.font(.subheadline.weight(.semibold))
774+
.foregroundStyle(document.caption.isEmpty ? .secondary : .primary)
775+
.lineLimit(1)
776+
Image(systemName: "pencil")
777+
.font(.caption)
778+
.foregroundStyle(.secondary)
779+
}
780+
.frame(maxWidth: .infinity, alignment: .leading)
781+
.contentShape(Rectangle())
782+
}
783+
.buttonStyle(.plain)
784+
.accessibilityLabel(lang.tr("diagram_title_label"))
785+
.padding(.horizontal, 14)
786+
.padding(.vertical, 8)
787+
.alert(lang.tr("diagram_title_label"), isPresented: $showPhoneTitleEditor) {
788+
TextField(lang.tr("diagram_title_placeholder"), text: $phoneTitleDraft)
789+
.textInputAutocapitalization(.never)
790+
.autocorrectionDisabled(true)
791+
Button(lang.tr("phone_title_done")) {
792+
if phoneTitleDraft != document.caption {
793+
captionBinding.wrappedValue = phoneTitleDraft
794+
}
795+
}
796+
}
797+
}
798+
799+
private var phonePaletteSheetBinding: Binding<Bool> {
800+
Binding(
801+
get: { isPhone && showPaletteOnPhone },
802+
set: { showPaletteOnPhone = $0 }
803+
)
804+
}
805+
743806
// MARK: - Bindings (Dokument, Caption)
744807

745808
private var documentBinding: Binding<VisuStructDocument> {
@@ -1073,7 +1136,11 @@ struct ContentView: View {
10731136
insertTargetActive: insertTarget != nil,
10741137
hideInspectorOnPhone: hideInspectorOnPhone,
10751138
onCancelInsert: { insertTarget = nil },
1076-
onShowInspector: { hideInspectorOnPhone = false }
1139+
onShowInspector: { hideInspectorOnPhone = false },
1140+
onHideInspector: {
1141+
focusedField = nil
1142+
hideInspectorOnPhone = true
1143+
}
10771144
) {
10781145
inspectorPanel
10791146
}
@@ -1336,6 +1403,7 @@ struct ContentView: View {
13361403
private func insertPaletteItem(_ item: ElementPalette.PaletteItem) {
13371404
if isPhone {
13381405
hideInspectorOnPhone = true
1406+
showPaletteOnPhone = false
13391407
}
13401408
let el = ElementPalette.element(for: item, lang: lang)
13411409
let doc = document

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/Inspector/InspectorPanelViews.swift‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ struct InspectorPhoneChrome<Inspector: View>: View {
190190
let hideInspectorOnPhone: Bool
191191
let onCancelInsert: () -> Void
192192
let onShowInspector: () -> Void
193+
let onHideInspector: () -> Void
193194
@ViewBuilder let inspector: () -> Inspector
194195

195196
var body: some View {
@@ -211,7 +212,16 @@ struct InspectorPhoneChrome<Inspector: View>: View {
211212
}
212213
.buttonStyle(.bordered)
213214
} else {
214-
inspector()
215+
VStack(alignment: .trailing, spacing: 4) {
216+
Button {
217+
onHideInspector()
218+
} label: {
219+
Label(lang.tr("phone_hide_text_editor"), systemImage: "chevron.down")
220+
.font(.subheadline)
221+
}
222+
.buttonStyle(.borderless)
223+
inspector()
224+
}
215225
}
216226
}
217227
}

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/ar.lproj/Localizable.strings‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@
194194
/* iPhone: tap-to-insert */
195195
"phone_insert_cancel" = "إلغاء الإدراج";
196196
"phone_show_text_editor" = "تحرير النص";
197+
"phone_hide_text_editor" = "طيّ";
198+
"phone_title_done" = "تم";
197199

198200
/* Alerts */
199201
"err_save_failed_fmt" = "تعذر الحفظ: %@";

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/da.lproj/Localizable.strings‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,3 +140,5 @@
140140
"sim_notice_division_by_zero" = "Division med nul.";
141141
"sim_notice_whole_number_expected" = "Indtast et heltal.";
142142
"sim_notice_number_expected" = "Indtast et tal.";
143+
"phone_hide_text_editor" = "Skjul";
144+
"phone_title_done" = "Færdig";

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/de.lproj/Localizable.strings‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@
211211
/* iPhone: Einfügen per Tap */
212212
"phone_insert_cancel" = "Einfügen abbrechen";
213213
"phone_show_text_editor" = "Text bearbeiten";
214+
"phone_hide_text_editor" = "Einklappen";
215+
"phone_title_done" = "Fertig";
214216

215217
/* Meldungen */
216218
"err_save_failed_fmt" = "Speichern fehlgeschlagen: %@";

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/el.lproj/Localizable.strings‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,8 @@
191191
/* iPhone: tap-to-insert */
192192
"phone_insert_cancel" = "Ακύρωση εισαγωγής";
193193
"phone_show_text_editor" = "Επεξεργασία κειμένου";
194+
"phone_hide_text_editor" = "Σύμπτυξη";
195+
"phone_title_done" = "Τέλος";
194196

195197
/* Alerts */
196198
"err_save_failed_fmt" = "Δεν ήταν δυνατή η αποθήκευση: %@";

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/en.lproj/Localizable.strings‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,8 @@
211211
/* iPhone: tap-to-insert */
212212
"phone_insert_cancel" = "Cancel insert";
213213
"phone_show_text_editor" = "Edit text";
214+
"phone_hide_text_editor" = "Collapse";
215+
"phone_title_done" = "Done";
214216

215217
/* Alerts */
216218
"err_save_failed_fmt" = "Could not save: %@";

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/es.lproj/Localizable.strings‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@
194194
/* iPhone: tap-to-insert */
195195
"phone_insert_cancel" = "Cancelar inserción";
196196
"phone_show_text_editor" = "Editar texto";
197+
"phone_hide_text_editor" = "Contraer";
198+
"phone_title_done" = "Listo";
197199

198200
/* Alerts */
199201
"err_save_failed_fmt" = "No se pudo guardar: %@";

‎VisuStruct-iOS/VisuStructXcodeOpen/VisuStruct/fr.lproj/Localizable.strings‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@
194194
/* iPhone: tap-to-insert */
195195
"phone_insert_cancel" = "Annuler l’insertion";
196196
"phone_show_text_editor" = "Modifier le texte";
197+
"phone_hide_text_editor" = "Réduire";
198+
"phone_title_done" = "OK";
197199

198200
/* Alerts */
199201
"err_save_failed_fmt" = "Impossible d’enregistrer : %@";

0 commit comments

Comments
 (0)