Compare commits

1 Commits
v1.0.0 ... main

Author SHA1 Message Date
5a1a284505 Phase 6-07a: Fensterlokale Control-Menüs und Windows-Testkorrekturen
Control-Menüs öffnen für alle Fensterarten am [≡]-Symbol des aktiven
Fensters: eigener Auswahlzustand statt Kopplung an die Menüleiste,
bevorzugt unter der Titelzeile, bei Platzmangel nach oben aufgeklappt,
am rechten Rand nur so weit nach links wie nötig. Regressionstest über
TestBackend für Code-, Projekt-, Output-, Immediate-, Debug- und
Hilfefenster, maximiert, minimiert, Rand und Maus.

Nebenbefunde der Windows-Testausführung: Temporärdatei vor sync_all
schreibend öffnen (Zugriff verweigert), relative Projektverweise immer
mit / schreiben, zeilenendenneutrale Vergleiche in Referenzmatrix-,
Kompatibilitäts- und MAK-Beispieltests, DriveListBox-Zeichenbild
plattformneutral. Korpus-Sollausgaben per .gitattributes auf LF.

Specs ide-oberflaeche und ide-projekte synchronisiert, Change archiviert.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-08 11:02:40 +02:00
22 changed files with 572 additions and 107 deletions

2
.gitattributes vendored
View File

@@ -1,2 +1,4 @@
# Rohbelege enthalten absichtlich Patch-Kontext und unveränderte Tool-Ausgaben. # Rohbelege enthalten absichtlich Patch-Kontext und unveränderte Tool-Ausgaben.
openspec/changes/**/evidence/** -whitespace openspec/changes/**/evidence/** -whitespace
# Sollausgaben des Kompatibilitaetskorpus sind LF-normiert; Tests vergleichen zeilenendenneutral.
tests/compat/*.out text eol=lf

View File

@@ -801,7 +801,10 @@ fn reference_matrix_is_complete_and_points_to_runnable_tests() {
assert!(!tests.is_empty()); assert!(!tests.is_empty());
for test in tests { for test in tests {
let (file, function) = test.split_once("::").unwrap(); let (file, function) = test.split_once("::").unwrap();
let source = fs::read_to_string(root.join(file)).unwrap(); // Quellen liegen je Checkout mit CRLF vor; die Suche ist zeilenendenneutral.
let source = fs::read_to_string(root.join(file))
.unwrap()
.replace("\r\n", "\n");
assert!( assert!(
source.contains(&format!("#[test]\nfn {function}(")), source.contains(&format!("#[test]\nfn {function}(")),
"{test}" "{test}"

View File

@@ -478,6 +478,9 @@ fn source_for_directives(path: &Path) -> String {
/// Snapshots vergleichen: erst das Textbild, dann die Attributebene. /// Snapshots vergleichen: erst das Textbild, dann die Attributebene.
/// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte. /// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte.
fn assert_output_matches(file: &str, want: &str, got: &str) { fn assert_output_matches(file: &str, want: &str, got: &str) {
// Sollausgaben werden je Checkout mit CRLF ausgeliefert; Snapshots sind LF.
let (want, got) = (want.replace("\r\n", "\n"), got.replace("\r\n", "\n"));
let (want, got) = (want.as_str(), got.as_str());
if want == got { if want == got {
return; return;
} }
@@ -688,8 +691,8 @@ fn tbc_isam_open_in_transaktion_hat_eine_harte_frist() {
); );
assert!(out.status.success(), "{out:?}"); assert!(out.status.success(), "{out:?}");
assert_eq!( assert_eq!(
String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stdout).replace("\r\n", "\n"),
include_str!("../../../tests/compat/isamopen.out") include_str!("../../../tests/compat/isamopen.out").replace("\r\n", "\n")
); );
} }

View File

@@ -409,7 +409,12 @@ pub fn prepare_publication(
} }
}; };
prepare(&temporary.0)?; prepare(&temporary.0)?;
fs::File::open(&temporary.0)?.sync_all()?; // Windows verlangt für FlushFileBuffers Schreibrechte; lesend geöffnet meldet sync_all
// „Zugriff verweigert“ (os error 5). Die Datei ist soeben angelegt und bleibt schreibbar.
fs::OpenOptions::new()
.write(true)
.open(&temporary.0)?
.sync_all()?;
ensure!(!cancelled(), "Export abgebrochen"); ensure!(!cancelled(), "Export abgebrochen");
Ok(PreparedPublication { Ok(PreparedPublication {
path: path.into(), path: path.into(),

View File

@@ -291,7 +291,7 @@ pub struct App {
pub active: u64, pub active: u64,
pub size: (u16, u16), pub size: (u16, u16),
pub menu: Option<(usize, usize)>, pub menu: Option<(usize, usize)>,
pub control_menu: bool, pub control_menu: Option<usize>,
pub dialog: Option<Dialog>, pub dialog: Option<Dialog>,
pub properties: bool, pub properties: bool,
pub value_focus: bool, pub value_focus: bool,
@@ -339,7 +339,7 @@ impl App {
active: 0, active: 0,
size, size,
menu: None, menu: None,
control_menu: false, control_menu: None,
dialog: None, dialog: None,
properties: false, properties: false,
value_focus: false, value_focus: false,
@@ -401,6 +401,30 @@ impl App {
WindowState::Normal => clamp(w.normal, area), WindowState::Normal => clamp(w.normal, area),
} }
} }
/// Rechteck des Control-Menü-Popups, für alle Fensterarten nach derselben Regel: linke
/// Spalte gleich Spalte des `[≡]`-Symbols, bevorzugt direkt unter der Titelzeile; passt es
/// dort nicht, klappt es nach oben und endet direkt über der Titelzeile; passt es beidseitig
/// nicht, gewinnt die Seite mit mehr Platz. Rechts wird nur so weit nach links gerückt, dass
/// es in die Arbeitsfläche passt und die Symbolspalte überdeckt bleibt.
pub fn control_menu_rect(&self, width: u16, height: u16) -> Rect {
let area = self.area();
let anchor = self.active_window().map(|w| self.rect(w)).unwrap_or(area);
let below = area.bottom().saturating_sub(anchor.y + 1);
let above = anchor.y.saturating_sub(area.y);
let (y, height) = if height <= below {
(anchor.y + 1, height)
} else if height <= above {
(anchor.y - height, height)
} else if below >= above {
(anchor.y + 1, below.max(3))
} else {
let h = above.max(3);
(anchor.y.saturating_sub(h), h)
};
let width = width.min(area.width);
let x = anchor.x.min(area.right().saturating_sub(width));
Rect::new(x, y, width, height)
}
fn add_window(&mut self, kind: WindowKind, normal: Rect) -> u64 { fn add_window(&mut self, kind: WindowKind, normal: Rect) -> u64 {
let id = self.next_window; let id = self.next_window;
self.next_window += 1; self.next_window += 1;
@@ -569,8 +593,7 @@ impl App {
self.message = reason; self.message = reason;
return; return;
} }
self.menu = None; self.close_menus();
self.control_menu = false;
if let Err(e) = self.action(command) { if let Err(e) = self.action(command) {
self.message = format!("{e:#}"); self.message = format!("{e:#}");
if self.mode == Mode::Designer { if self.mode == Mode::Designer {
@@ -792,8 +815,8 @@ impl App {
} }
} }
ControlMenu => { ControlMenu => {
self.control_menu = true; // Fensterlokal: kein Menüleistentitel wird ausgewählt.
self.menu = Some((0, 0)); self.control_menu = Some(0);
} }
FocusWindow(id) => { FocusWindow(id) => {
if self.windows.iter().any(|w| w.id == id) { if self.windows.iter().any(|w| w.id == id) {
@@ -1539,8 +1562,8 @@ impl App {
&& !self.program_focus() && !self.program_focus()
&& self.dialog.is_none() && self.dialog.is_none()
{ {
if let Some((m, i)) = self.menu { if let Some((entries, i)) = self.open_entries() {
if let Some((_, Some(command))) = self.menu_entries(m).get(i) { if let Some((_, Some(command))) = entries.get(i) {
self.help_menu_context(*command); self.help_menu_context(*command);
} }
} else { } else {
@@ -1549,7 +1572,7 @@ impl App {
return; return;
} }
if self.dialog.is_none() if self.dialog.is_none()
&& self.menu.is_none() && !self.menu_open()
&& self && self
.active_window() .active_window()
.is_some_and(|w| w.kind == WindowKind::Help) .is_some_and(|w| w.kind == WindowKind::Help)
@@ -1581,7 +1604,7 @@ impl App {
} }
_ => {} _ => {}
} }
if self.dialog.is_none() && self.menu.is_none() && self.program_focus() { if self.dialog.is_none() && !self.menu_open() && self.program_focus() {
if self.session.fullscreen { if self.session.fullscreen {
self.basic_events.push(Event::Mouse(mouse)); self.basic_events.push(Event::Mouse(mouse));
return; return;
@@ -1604,7 +1627,7 @@ impl App {
} }
} }
if mouse.kind == MouseEventKind::Down(MouseButton::Right) { if mouse.kind == MouseEventKind::Down(MouseButton::Right) {
if self.dialog.is_none() && self.menu.is_none() && self.options.right_help { if self.dialog.is_none() && !self.menu_open() && self.options.right_help {
self.execute(Command::Topic); self.execute(Command::Topic);
} }
return; return;
@@ -1619,7 +1642,7 @@ impl App {
if let Some(hit) = hit { if let Some(hit) = hit {
self.click(hit); self.click(hit);
} }
} else if self.dialog.is_none() && self.menu.is_none() { } else if self.dialog.is_none() && !self.menu_open() {
if let Some(Window { if let Some(Window {
kind: WindowKind::Code(v), kind: WindowKind::Code(v),
.. ..
@@ -1641,7 +1664,7 @@ impl App {
self.basic_events.push(event); self.basic_events.push(event);
} else if let Event::Paste(text) = event { } else if let Event::Paste(text) = event {
if self.dialog.is_none() if self.dialog.is_none()
&& self.menu.is_none() && !self.menu_open()
&& self && self
.active_window() .active_window()
.is_some_and(|w| w.kind == WindowKind::Immediate) .is_some_and(|w| w.kind == WindowKind::Immediate)
@@ -1649,7 +1672,7 @@ impl App {
self.debugger.immediate.push_str(&text); self.debugger.immediate.push_str(&text);
return; return;
} }
if self.dialog.is_none() && self.menu.is_none() && self.mode == Mode::Environment { if self.dialog.is_none() && !self.menu_open() && self.mode == Mode::Environment {
if let Err(e) = self.editor_insert(&text) { if let Err(e) = self.editor_insert(&text) {
self.message = e.to_string(); self.message = e.to_string();
} }
@@ -1676,19 +1699,17 @@ impl App {
} }
return; return;
} }
if self.menu.is_some() { if self.menu_open() {
match hit { match hit {
Hit::Menu(i) => self.menu = Some((i, 0)), Hit::Menu(i) => {
self.control_menu = None;
self.menu = Some((i, 0));
}
Hit::MenuItem(i) => { Hit::MenuItem(i) => {
if let Some((menu, _)) = self.menu { self.select_open(i);
self.menu = Some((menu, i));
self.menu_enter(); self.menu_enter();
} }
} _ => self.close_menus(),
_ => {
self.menu = None;
self.control_menu = false;
}
} }
return; return;
} }
@@ -1763,14 +1784,14 @@ impl App {
fn key(&mut self, key: KeyEvent) { fn key(&mut self, key: KeyEvent) {
if self.dialog.is_none() && key.code == K::F(1) { if self.dialog.is_none() && key.code == K::F(1) {
if key.modifiers.is_empty() { if key.modifiers.is_empty() {
if let Some((m, i)) = self.menu { if let Some((entries, i)) = self.open_entries() {
if let Some((_, Some(command))) = self.menu_entries(m).get(i) { if let Some((_, Some(command))) = entries.get(i) {
self.help_menu_context(*command); self.help_menu_context(*command);
return; return;
} }
} }
} }
if self.menu.is_none() { if !self.menu_open() {
if !self.help_key(key) { if !self.help_key(key) {
self.execute(if key.modifiers.contains(M::SHIFT) { self.execute(if key.modifiers.contains(M::SHIFT) {
Command::UsingHelp Command::UsingHelp
@@ -1787,7 +1808,7 @@ impl App {
} }
if self.editor.chord.is_some() if self.editor.chord.is_some()
&& self.dialog.is_none() && self.dialog.is_none()
&& self.menu.is_none() && !self.menu_open()
&& !self.program_focus() && !self.program_focus()
{ {
if let Err(e) = self.editor_key(key) { if let Err(e) = self.editor_key(key) {
@@ -1828,7 +1849,7 @@ impl App {
self.execute(Command::MenuBar); self.execute(Command::MenuBar);
return; return;
} }
if self.menu.is_some() { if self.menu_open() {
if key.modifiers.contains(M::ALT) { if key.modifiers.contains(M::ALT) {
if let K::Char(c) = key.code { if let K::Char(c) = key.code {
if let Some(i) = self if let Some(i) = self
@@ -1836,47 +1857,48 @@ impl App {
.iter() .iter()
.position(|m| m.mnemonic == c.to_ascii_lowercase()) .position(|m| m.mnemonic == c.to_ascii_lowercase())
{ {
self.control_menu = false; self.control_menu = None;
self.menu = Some((i, 0)); self.menu = Some((i, 0));
return; return;
} }
} }
} }
match key.code { match key.code {
K::Esc => { K::Esc => self.close_menus(),
self.menu = None; K::Left | K::Right => {
self.control_menu = false; // Nur die Menüleiste wechselt seitlich; das Control-Menü bleibt stehen.
} if let Some((m, _)) = self.menu {
K::Left | K::Right if !self.control_menu => {
let n = self.menus().len(); let n = self.menus().len();
let (m, _) = self.menu.unwrap();
self.menu = Some(( self.menu = Some((
(m + n + if key.code == K::Right { 1 } else { n - 1 }) % n, (m + n + if key.code == K::Right { 1 } else { n - 1 }) % n,
0, 0,
)); ));
} }
}
K::Up | K::Down => { K::Up | K::Down => {
let (m, i) = self.menu.unwrap(); if let Some((entries, i)) = self.open_entries() {
let n = self.menu_entries(m).len(); let n = entries.len();
if n > 0 { if n > 0 {
self.menu = self.select_open(
Some((m, (i + n + if key.code == K::Down { 1 } else { n - 1 }) % n)); (i + n + if key.code == K::Down { 1 } else { n - 1 }) % n,
);
}
} }
} }
K::Enter => self.menu_enter(), K::Enter => self.menu_enter(),
K::Char(c) => { K::Char(c) => {
let (m, _) = self.menu.unwrap(); if let Some((entries, _)) = self.open_entries() {
let entries = self.menu_entries(m);
if let Some(i) = entries.iter().position(|(label, _)| { if let Some(i) = entries.iter().position(|(label, _)| {
label label
.split_once('&') .split_once('&')
.and_then(|(_, s)| s.chars().next()) .and_then(|(_, s)| s.chars().next())
.is_some_and(|v| v.eq_ignore_ascii_case(&c)) .is_some_and(|v| v.eq_ignore_ascii_case(&c))
}) { }) {
self.menu = Some((m, i)); self.select_open(i);
self.menu_enter(); self.menu_enter();
} }
} }
}
_ => {} _ => {}
} }
return; return;
@@ -2123,17 +2145,40 @@ impl App {
} }
self.editor_key(key) self.editor_key(key)
} }
pub fn menu_entries(&self, index: usize) -> Vec<(String, Option<Command>)> { /// Einträge des fensterlokalen Control-Menüs, für alle Fensterarten gleich.
if self.control_menu { pub fn control_entries(&self) -> MenuEntries {
return vec![ vec![
("&Restore".into(), Some(Command::Restore)), ("&Restore".into(), Some(Command::Restore)),
("&Move".into(), Some(Command::MoveWindow)), ("&Move".into(), Some(Command::MoveWindow)),
("&Size".into(), Some(Command::SizeWindow)), ("&Size".into(), Some(Command::SizeWindow)),
("Mi&nimize".into(), Some(Command::Minimize)), ("Mi&nimize".into(), Some(Command::Minimize)),
("Ma&ximize".into(), Some(Command::Maximize)), ("Ma&ximize".into(), Some(Command::Maximize)),
("&Close".into(), Some(Command::CloseWindow)), ("&Close".into(), Some(Command::CloseWindow)),
]; ]
} }
/// Ist ein Menü (Menüleiste oder Control-Menü) geöffnet?
pub fn menu_open(&self) -> bool {
self.menu.is_some() || self.control_menu.is_some()
}
/// Einträge und Auswahl der gerade offenen Liste: Control-Menü hat Vorrang vor der Menüleiste.
pub fn open_entries(&self) -> Option<(MenuEntries, usize)> {
if let Some(i) = self.control_menu {
return Some((self.control_entries(), i));
}
self.menu.map(|(m, i)| (self.menu_entries(m), i))
}
fn select_open(&mut self, i: usize) {
if self.control_menu.is_some() {
self.control_menu = Some(i);
} else if let Some((m, _)) = self.menu {
self.menu = Some((m, i));
}
}
pub(crate) fn close_menus(&mut self) {
self.menu = None;
self.control_menu = None;
}
pub fn menu_entries(&self, index: usize) -> MenuEntries {
let menus = self.menus(); let menus = self.menus();
let mut entries = menus[index] let mut entries = menus[index]
.items .items
@@ -2151,13 +2196,15 @@ impl App {
entries entries
} }
fn menu_enter(&mut self) { fn menu_enter(&mut self) {
if let Some((m, i)) = self.menu { if let Some((entries, i)) = self.open_entries() {
if let Some((_, Some(c))) = self.menu_entries(m).get(i) { if let Some((_, Some(c))) = entries.get(i) {
self.execute(*c); self.execute(*c);
} }
} }
} }
} }
/// Beschriftungen und Befehle einer aufgeklappten Menüliste.
pub type MenuEntries = Vec<(String, Option<Command>)>;
pub fn clamp(r: Rect, area: Rect) -> Rect { pub fn clamp(r: Rect, area: Rect) -> Rect {
let width = r.width.max(12).min(area.width); let width = r.width.max(12).min(area.width);
let height = r.height.max(4).min(area.height); let height = r.height.max(4).min(area.height);

View File

@@ -1481,7 +1481,7 @@ impl App {
Ok(()) Ok(())
} }
pub(crate) fn design_mouse(&mut self, m: MouseEvent) -> Result<bool> { pub(crate) fn design_mouse(&mut self, m: MouseEvent) -> Result<bool> {
if self.mode != Mode::Designer || self.dialog.is_some() || self.menu.is_some() { if self.mode != Mode::Designer || self.dialog.is_some() || self.menu_open() {
return Ok(false); return Ok(false);
} }
if let Some((property, color)) = self.designer.paint { if let Some((property, color)) = self.designer.paint {

View File

@@ -796,8 +796,7 @@ impl App {
} }
pub(crate) fn help_menu_context(&mut self, command: Command) { pub(crate) fn help_menu_context(&mut self, command: Command) {
let target = command_target(command, self.mode == Mode::Designer); let target = command_target(command, self.mode == Mode::Designer);
self.menu = None; self.close_menus();
self.control_menu = false;
self.help_show(); self.help_show();
match Catalog::embedded().resolve("", target) { match Catalog::embedded().resolve("", target) {
Ok((p, b)) => self.help.visit(p, b, None), Ok((p, b)) => self.help.visit(p, b, None),

View File

@@ -64,7 +64,7 @@ impl App {
); );
return; return;
} }
if self.session.fullscreen && self.dialog.is_none() && self.menu.is_none() { if self.session.fullscreen && self.dialog.is_none() && !self.menu_open() {
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), area); f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), area);
return; return;
} }
@@ -317,7 +317,7 @@ impl App {
if active if active
&& expansion.is_none() && expansion.is_none()
&& self.dialog.is_none() && self.dialog.is_none()
&& self.menu.is_none() && !self.menu_open()
{ {
let before = &doc.code()[..v.cursor.min(doc.code().len())]; let before = &doc.code()[..v.cursor.min(doc.code().len())];
let row = before.bytes().filter(|c| *c == b'\n').count(); let row = before.bytes().filter(|c| *c == b'\n').count();
@@ -523,14 +523,17 @@ impl App {
x += r.width; x += r.width;
} }
} }
if let Some((menu, selected)) = self.menu { if let Some((entries, selected)) = self.open_entries() {
let entries = self.menu_entries(menu);
let width = (entries.iter().map(|(s, _)| s.width()).max().unwrap_or(12) as u16 + 4) let width = (entries.iter().map(|(s, _)| s.width()).max().unwrap_or(12) as u16 + 4)
.min(area.width); .min(area.width);
// Control-Menü hängt am Fenstersymbol, Menüleistenmenüs unter ihrem Titel.
let (x, y, visible) = if self.control_menu.is_some() {
let r = self.control_menu_rect(width, entries.len() as u16 + 2);
(r.x, r.y, r.height.saturating_sub(2).max(1) as usize)
} else {
let menu = self.menu.map(|(m, _)| m).unwrap_or(0);
let menus = self.menus(); let menus = self.menus();
let x = if self.control_menu { let x = if menus[menu].title == "Help" {
self.active_window().map(|w| self.rect(w).x).unwrap_or(0)
} else if menus[menu].title == "Help" {
area.width - width area.width - width
} else { } else {
menus menus
@@ -540,9 +543,10 @@ impl App {
.sum::<u16>() .sum::<u16>()
.min(area.width - width) .min(area.width - width)
}; };
let visible = (area.height - 4) as usize; (x, 1, (area.height - 4) as usize)
};
let start = selected.saturating_sub(visible - 1); let start = selected.saturating_sub(visible - 1);
let rect = Rect::new(x, 1, width, (entries.len().min(visible) + 2) as u16); let rect = Rect::new(x, y, width, (entries.len().min(visible) + 2) as u16);
f.render_widget(Clear, rect); f.render_widget(Clear, rect);
f.render_widget(Block::bordered().style(style(self, 0)), rect); f.render_widget(Block::bordered().style(style(self, 0)), rect);
for (i, (text, c)) in entries.iter().enumerate().skip(start).take(visible) { for (i, (text, c)) in entries.iter().enumerate().skip(start).take(visible) {
@@ -554,7 +558,7 @@ impl App {
} else { } else {
style(self, 0) style(self, 0)
}; };
let r = Rect::new(x + 1, 2 + (i - start) as u16, width - 2, 1); let r = Rect::new(x + 1, y + 1 + (i - start) as u16, width - 2, 1);
let label = if disabled.is_some() { let label = if disabled.is_some() {
format!("× {text}") format!("× {text}")
} else if *c == Some(Command::SyntaxChecking) && self.options.syntax_checking { } else if *c == Some(Command::SyntaxChecking) && self.options.syntax_checking {

View File

@@ -302,7 +302,7 @@ fn windows_restore_geometry_and_resize_without_document_loss() {
assert!(r.right() <= 80 && r.bottom() <= 24); assert!(r.right() <= 80 && r.bottom() <= 24);
} }
key(&mut app, K::Char('-'), M::ALT); key(&mut app, K::Char('-'), M::ALT);
assert!(app.control_menu); assert!(app.control_menu.is_some());
plain(&mut app, K::Esc); plain(&mut app, K::Esc);
key(&mut app, K::F(4), M::CONTROL); key(&mut app, K::F(4), M::CONTROL);
assert_eq!(app.windows.len(), 2); assert_eq!(app.windows.len(), 2);
@@ -1180,3 +1180,182 @@ fn theme_windows_erase_desktop_and_underlying_windows_on_every_redraw() {
} }
} }
} }
/// Sucht die Zelle, an der `text` in der Ausgabe beginnt.
fn find_text(b: &ratatui::buffer::Buffer, text: &str) -> Option<(u16, u16)> {
let chars: Vec<String> = text.chars().map(|c| c.to_string()).collect();
for y in 0..b.area.height {
for x in 0..b.area.width.saturating_sub(chars.len() as u16) {
if chars
.iter()
.enumerate()
.all(|(i, c)| b[(x + i as u16, y)].symbol() == c)
{
return Some((x, y));
}
}
}
None
}
/// Öffnet das Control-Menü per Alt+Minus und prüft die Anheftregel: linke Spalte gleich
/// Symbolspalte (oder so weit links, dass es passt und das Symbol überdeckt), Rahmen grenzt
/// unten oder oben an die Titelzeile, vollständig in der Arbeitsfläche, kein Menüleistentitel
/// hervorgehoben. Liefert das Rechteck des Aufklappbereichs.
fn assert_control_menu_attached(app: &mut App, context: &str) -> ratatui::layout::Rect {
let r = app.rect(app.active_window().unwrap());
let area = app.area();
key(app, K::Char('-'), M::ALT);
assert!(app.control_menu.is_some(), "{context}: Control-Menü offen");
assert_eq!(
app.menu, None,
"{context}: Menüleiste darf nicht ausgewählt sein"
);
let (_, b) = draw(app);
assert_eq!(
(b[(1, 0)].fg, b[(1, 0)].bg),
(tb_ide::render::dos(0), tb_ide::render::dos(7)),
"{context}: „File“ darf nicht hervorgehoben sein"
);
let (rx, ry) = find_text(&b, "Restore").unwrap_or_else(|| panic!("{context}: Restore"));
let (_, cy) = find_text(&b, "Close").unwrap_or_else(|| panic!("{context}: Close"));
let (x, top, bottom) = (rx - 1, ry - 1, cy + 1);
let mut right = x + 1;
while right < b.area.width && b[(right, top)].symbol() != "" {
right += 1;
}
let popup = ratatui::layout::Rect::new(x, top, right + 1 - x, bottom + 1 - top);
assert_eq!(b[(x, top)].symbol(), "", "{context}: linke obere Ecke");
assert_eq!(b[(x, bottom)].symbol(), "", "{context}: linke untere Ecke");
assert!(
popup.y >= area.y && popup.bottom() <= area.bottom() && popup.right() <= area.right(),
"{context}: Popup {popup:?} außerhalb der Arbeitsfläche {area:?}"
);
assert!(
popup.x <= r.x && r.x < popup.right(),
"{context}: Popup {popup:?} überdeckt die Symbolspalte von {r:?} nicht"
);
if r.x + popup.width <= area.right() {
assert_eq!(popup.x, r.x, "{context}: linke Spalte gleich Symbolspalte");
} else {
assert_eq!(
popup.right(),
area.right(),
"{context}: nur so weit links wie nötig"
);
}
if popup.height <= area.bottom() - (r.y + 1) {
assert_eq!(popup.y, r.y + 1, "{context}: direkt unter der Titelzeile");
} else {
assert_eq!(
popup.bottom(),
r.y,
"{context}: nach oben aufgeklappt bis an die Titelzeile"
);
}
popup
}
#[test]
fn control_menus_attach_to_the_symbol_of_every_window() {
let t = Temp::new();
let mut app = t.app();
let area = app.area();
let code = app.active;
let p = assert_control_menu_attached(&mut app, "Code");
let r = app.rect(app.active_window().unwrap());
assert_eq!((p.x, p.y), (r.x, r.y + 1));
plain(&mut app, K::Esc);
assert!(app.control_menu.is_none());
assert_eq!(app.active, code);
plain(&mut app, K::F(6));
assert_eq!(app.active_window().unwrap().kind, WindowKind::Project);
assert_control_menu_attached(&mut app, "Project");
plain(&mut app, K::Esc);
menu(&mut app, Command::Output);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Output);
assert_control_menu_attached(&mut app, "Output");
plain(&mut app, K::Esc);
menu(&mut app, Command::Immediate);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Immediate);
assert_control_menu_attached(&mut app, "Immediate");
plain(&mut app, K::Esc);
menu(&mut app, Command::Debug);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Debug);
assert_control_menu_attached(&mut app, "Debug");
plain(&mut app, K::Esc);
menu(&mut app, Command::HelpWindow);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Help);
assert_control_menu_attached(&mut app, "Help");
plain(&mut app, K::Esc);
assert_eq!(app.active_window().unwrap().kind, WindowKind::Help);
menu(&mut app, Command::Immediate);
key(&mut app, K::F(10), M::CONTROL);
let p = assert_control_menu_attached(&mut app, "Maximized");
assert_eq!((p.x, p.y), (area.x, area.y + 1));
plain(&mut app, K::Esc);
key(&mut app, K::F(9), M::CONTROL);
assert_eq!(app.active_window().unwrap().state, WindowState::Minimized);
let r = app.rect(app.active_window().unwrap());
let p = assert_control_menu_attached(&mut app, "Minimized");
assert_eq!(
(p.x, p.bottom()),
(r.x, r.y),
"minimiertes Fenster klappt nach oben"
);
plain(&mut app, K::Esc);
key(&mut app, K::F(5), M::CONTROL);
key(&mut app, K::F(8), M::CONTROL);
for _ in 0..20 {
key(&mut app, K::Left, M::CONTROL);
}
plain(&mut app, K::Enter);
key(&mut app, K::F(7), M::CONTROL);
for _ in 0..30 {
key(&mut app, K::Right, M::CONTROL);
}
plain(&mut app, K::Enter);
let r = app.rect(app.active_window().unwrap());
assert_eq!(r.right(), area.right());
let p = assert_control_menu_attached(&mut app, "Rechter Rand");
assert!(p.x < r.x && p.right() == area.right());
plain(&mut app, K::Esc);
// Normales Fenster am unteren Rand: zu wenig Platz unter der Titelzeile, klappt nach oben.
key(&mut app, K::F(8), M::CONTROL);
for _ in 0..20 {
key(&mut app, K::Up, M::CONTROL);
}
plain(&mut app, K::Enter);
key(&mut app, K::F(7), M::CONTROL);
for _ in 0..30 {
key(&mut app, K::Down, M::CONTROL);
}
plain(&mut app, K::Enter);
let r = app.rect(app.active_window().unwrap());
assert_eq!(r.bottom(), area.bottom());
let p = assert_control_menu_attached(&mut app, "Unterer Rand");
assert_eq!(p.bottom(), r.y, "klappt nach oben");
plain(&mut app, K::Esc);
// Maus: Klick auf [≡] öffnet identisch, Klick auf einen Eintrag führt ihn aus.
key(&mut app, K::F(5), M::CONTROL);
draw(&mut app);
let r = app.rect(app.active_window().unwrap());
click(&mut app, r.x, r.y);
assert!(app.control_menu.is_some());
assert_eq!(app.menu, None);
let (_, b) = draw(&mut app);
let (mx, my) = find_text(&b, "Minimize").unwrap();
click(&mut app, mx, my);
assert!(app.control_menu.is_none());
assert_eq!(app.active_window().unwrap().state, WindowState::Minimized);
}

View File

@@ -3243,6 +3243,8 @@ End
m.set(8, selected, PropertyValue::Integer(0)).unwrap(); m.set(8, selected, PropertyValue::Integer(0)).unwrap();
m.object_method(11, "PRINT", vec![PropertyValue::String("Bild".into())]) m.object_method(11, "PRINT", vec![PropertyValue::String("Bild".into())])
.unwrap(); .unwrap();
// Die DriveListBox zeigt den Wurzeltrenner der Plattform (`/` bzw. `\`).
let drive = format!("{}", std::path::MAIN_SEPARATOR);
let snapshots = [ let snapshots = [
(1, "<OK> "), (1, "<OK> "),
(2, "Text "), (2, "Text "),
@@ -3256,7 +3258,7 @@ End
(10, ""), (10, ""),
(11, "┌──────┐"), (11, "┌──────┐"),
(13, "┌──────┐"), (13, "┌──────┐"),
(14, "/ ▼ "), (14, drive.as_str()),
(15, "┌──────┐"), (15, "┌──────┐"),
(18, ""), (18, ""),
]; ];

View File

@@ -240,19 +240,23 @@ pub fn relative_path(base: &Path, target: &Path) -> std::io::Result<String> {
) )
}); });
} }
let mut result = PathBuf::new(); // Relative Verweise werden plattformneutral mit `/` geschrieben, damit ein unter
// Windows gespeichertes Projekt auch unter Linux und macOS lädt; beide Plattformen
// lösen `/` beim Laden auf.
let mut parts: Vec<String> = Vec::new();
for _ in common..a.len() { for _ in common..a.len() {
result.push(".."); parts.push("..".into());
} }
for c in &b[common..] { for c in &b[common..] {
result.push(c.as_os_str()); let s = c.as_os_str().to_str().ok_or_else(|| {
}
result.to_str().map(str::to_owned).ok_or_else(|| {
std::io::Error::new( std::io::Error::new(
std::io::ErrorKind::InvalidData, std::io::ErrorKind::InvalidData,
"Projektpfad ist nicht UTF-8", "Projektpfad ist nicht UTF-8",
) )
}) })?;
parts.push(s.to_owned());
}
Ok(parts.join("/"))
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]

View File

@@ -162,7 +162,8 @@ fn documented_mak_example_is_accepted_by_the_shared_loader() {
t.write("main.bas", "END\n"); t.write("main.bas", "END\n");
t.write("lib.bas", ""); t.write("lib.bas", "");
t.write("form.frm", "VERSION 1.00\nBegin Form F\nEnd\n"); t.write("form.frm", "VERSION 1.00\nBegin Form F\nEnd\n");
let docs = include_str!("../../../docs/dateiformate.md"); // Die Dokumentation liegt je Checkout mit CRLF vor; der Beispielblock wird LF-neutral gesucht.
let docs = include_str!("../../../docs/dateiformate.md").replace("\r\n", "\n");
let example = docs let example = docs
.split("```mak\n") .split("```mak\n")
.nth(1) .nth(1)

View File

@@ -240,7 +240,7 @@ Umbruch oder Scrollen abgeschnitten.
| Spin | `▲` / `▼` oder `◄ ►` | ebenso in Fokusfarben | ebenso | | Spin | `▲` / `▼` oder `◄ ►` | ebenso in Fokusfarben | ebenso |
| PictureBox | `┌──────┐` / `│Text │` / `└──────┘` | ebenso in Fokusfarben | ebenso | | PictureBox | `┌──────┐` / `│Text │` / `└──────┘` | ebenso in Fokusfarben | ebenso |
| DirListBox | `┌──────┐` / `│[dir] │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso | | DirListBox | `┌──────┐` / `│[dir] │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso |
| DriveListBox | `[ / ▼]` | ebenso in Fokusfarben | ebenso | | DriveListBox | `[ / ▼]` (unter Windows `[ \ ▼]`, Wurzeltrenner der Plattform) | ebenso in Fokusfarben | ebenso |
| FileListBox | `┌──────┐` / `│a.bas │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso | | FileListBox | `┌──────┐` / `│a.bas │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso |
| Menu | ` Datei Bearbeiten ` | gewählter Titel/Eintrag in Fokusfarben | Text in Farbe 8 | | Menu | ` Datei Bearbeiten ` | gewählter Titel/Eintrag in Fokusfarben | Text in Farbe 8 |
| Timer | keine sichtbaren Zellen | keine sichtbaren Zellen | keine sichtbaren Zellen | | Timer | keine sichtbaren Zellen | keine sichtbaren Zellen | keine sichtbaren Zellen |

View File

@@ -55,6 +55,13 @@ Buchstaben wählen, Esc bricht ab, „…" kündigt Dialog an.
**Ctrl+F4** schließen, **Ctrl+F7** verschieben, **Ctrl+F8** Größe, **Ctrl+F4** schließen, **Ctrl+F7** verschieben, **Ctrl+F8** Größe,
**Ctrl+F9** minimieren, **Ctrl+F10** maximieren, **Ctrl+F5** **Ctrl+F9** minimieren, **Ctrl+F10** maximieren, **Ctrl+F5**
wiederherstellen, **Alt+Minus** Control-Menü; „Arrange All" kachelt. wiederherstellen, **Alt+Minus** Control-Menü; „Arrange All" kachelt.
- Control-Menü (Restore, Move, Size, Minimize, Maximize, Close): fensterlokal
für alle Fensterarten gleich, per Alt+Minus oder Klick auf „[≡]" des aktiven
Fensters. Es hängt am Symbol: linke Spalte gleich Symbolspalte, bevorzugt
direkt unter der Titelzeile; fehlt dort Platz (z. B. minimiertes Fenster am
unteren Rand), klappt es nach oben bis an die Titelzeile; am rechten Rand
rückt es nur so weit nach links, dass es in die Arbeitsfläche passt. Die
Menüleiste zeigt dabei keinen ausgewählten Titel.
- **Project Window** (Start: rechts angedockt): Buttons „Form"/„Code" - **Project Window** (Start: rechts angedockt): Buttons „Form"/„Code"
(Enter öffnet, „Form" wechselt in den Designer) + Modulliste (.FRM/.BAS). (Enter öffnet, „Form" wechselt in den Designer) + Modulliste (.FRM/.BAS).
- **Immediate Window** über Alt+W,I — nicht permanent sichtbar (anders als QB). - **Immediate Window** über Alt+W,I — nicht permanent sichtbar (anders als QB).

View File

@@ -0,0 +1,34 @@
## Context
`App` hält ein einziges Menüfeld `menu: Option<(usize, usize)>` für die Menüleiste (Menüindex, Eintrag) und ein Flag `control_menu: bool`. `Command::ControlMenu` setzt `control_menu = true` und `menu = Some((0, 0))`. Dadurch gilt in `render.rs` der Menüleistentitel mit Index 0 („File“) als ausgewählt und wird hervorgehoben. Der Aufklappbereich wird in `render.rs` mit `Rect::new(x, 1, …)` gezeichnet; `x` ist bei Control-Menü nur die linke Fensterkante, `y` ist fest die Zeile unter der Menüleiste. Das Popup steht deshalb nicht am `[≡]`-Symbol des Fensters, sondern wandert an den oberen Bildschirmrand und überdeckt fremde Fenster.
Die Tastenverteilung im Menüzustand blockiert Links/Rechts, wenn `control_menu` gesetzt ist, erlaubt aber Alt+Mnemonic-Wechsel in die Menüleiste. Der Hit-Bereich für `[≡]` existiert bereits nur für das aktive Fenster (`Hit::Command(Command::ControlMenu)` auf `Rect::new(rect.x, rect.y, 3, 1)`).
Beobachtung aus der Validierung: siehe `evidence/befund-2026-09-08.md`.
## Goals / Non-Goals
**Goals:** Control-Menü als fensterlokales Popup am Symbol; keine sichtbare oder logische Kopplung an einen Menüleistentitel; bestehende Tasten- und Mausbedienung unverändert; prüfbare Renderposition.
**Non-Goals:** Neues Menüsystem, Änderung der Menüleisten-Aufklappposition, neue Befehle im Control-Menü, Control-Menü für inaktive Fenster, Änderung des Themes.
## Decisions
1. Den Control-Menü-Zustand von der Menüleiste trennen: `control_menu` trägt die Auswahl selbst (z. B. `Option<usize>` für den gewählten Eintrag) statt `menu = Some((0, 0))` zu missbrauchen. `menu` bleibt bei offenem Control-Menü `None`. Damit hebt die Menüleiste nichts hervor, ohne dass der Renderer Sonderfälle braucht. Die gemeinsame Tastenlogik (Auf/Ab, Enter, Mnemonic, Esc) wird über eine kleine Abstraktion „offene Liste“ geteilt, die beide Zustände bedient; keine zweite Kopie der Eintragsnavigation.
2. Eine einzige Platzierungsfunktion für alle `WindowKind`-Varianten (Code, Project, Output, Immediate, Debug, Calls, Help, Toolbox, Palette, MenuDesign). Ankerpunkt ist die Zelle des `[≡]`-Symbols im aktuellen Fensterrechteck `self.rect(w)`: Popup-x = `rect.x`, bevorzugt Popup-y = `rect.y + 1` (erste Zeile unter der Titelzeile). Das gilt für Normal, Maximized (Rechteck = Arbeitsfläche) und Minimized (Titelzeile des minimierten Fensters am unteren Rand). Bei Alt+Minus und Mausklick identisch, da beide `Command::ControlMenu` ausführen. Kein fensterartspezifischer Sonderfall.
3. Anheftregel bei Platzmangel, in dieser Reihenfolge: (a) Passt die Popup-Höhe nicht zwischen `rect.y + 1` und die Statuszeile, klappt das Popup nach oben: Popup-Unterkante = `rect.y - 1`, Popup-y = `rect.y - Höhe`. Minimierte Fenster liegen laut `rect()` immer zwei Zeilen über der Statuszeile und treffen damit stets diesen Fall. (b) Passt es auch oben nicht vollständig, wird die Richtung mit mehr Platz gewählt und die vorhandene Scroll-Logik für Einträge greift; die Titelzeile des eigenen Fensters wird nie überdeckt. (c) Ragt es rechts hinaus, wird x nur so weit reduziert, dass `x + Breite <= area.right()`; da die Breite größer als drei Zellen ist, bleibt die Symbolspalte `rect.x..rect.x+3` überdeckt. Die visuelle Verbindung zum Symbol bleibt in allen Fällen: eine Popup-Kante grenzt an die Titelzeile, und die Symbolspalte liegt innerhalb der Popup-Breite.
4. Zeichenreihenfolge: Das Popup wird nach allen Fenstern und vor der Statuszeile mit `Clear` gezeichnet, so wie heute die Menüleisten-Popups; es überdeckt Fensterinhalt lokal, nicht die Menüleiste.
5. Alt+Mnemonic aus dem offenen Control-Menü wechselt weiterhin in die Menüleiste (bestehendes Verhalten); dabei wird das Control-Menü geschlossen. Links/Rechts bleiben im Control-Menü ohne Wirkung. Esc schließt das Control-Menü, der Fokus bleibt im Fenster, die Statuszeile zeigt die Fensterkürzel.
6. Nachweis über TestBackend für mehrere Fensterarten (mindestens Code, Project, Output, Immediate): Nach Alt+Minus und `draw` wird geprüft, dass der Rahmen des Popups in Zeile `rect.y + 1` ab Spalte `rect.x` beginnt, dass die Zelle „File“ in Zeile 0 den normalen Menüstil trägt, dass bei einem minimierten Fenster (Ctrl+F9) die Popup-Unterkante in `rect.y - 1` liegt und die linke Spalte `rect.x` ist, dass ein per Ctrl+F7 an den rechten Rand verschobenes Fenster ein Popup mit `x + Breite <= area.right()` und `x <= rect.x` erhält, und dass nach Ctrl+F10 (Maximize) das Popup unter der Titelzeile des maximierten Rechtecks liegt. Keine rein textuellen Snapshots.
7. Nebenbefunde der Windows-Testausführung werden im selben Change behoben, ohne die Fensterlogik zu berühren: `prepare_publication` öffnet die Temporärdatei vor `sync_all` schreibend; `relative_path` schreibt Verweise immer mit `/`, weil `\` unter Linux/macOS kein Trenner ist und beide Plattformen `/` auflösen; zeilenendenabhängige Testvergleiche normalisieren CRLF. Details und Zahlen in `evidence/befund-2026-09-08.md`.
## Risks / Trade-offs
- Trennung des Zustands berührt alle Stellen, die `self.menu.is_some()` als „ein Menü ist offen“ lesen (Tasten- und Mausverteilung) → gezielt in eine Hilfsfunktion `menu_open()` zusammenführen, damit Klick außerhalb und Esc beide Zustände schließen.
- Bei sehr kleinem Terminal passt das Popup weder unter noch über die Titelzeile → Richtung mit mehr Platz und vorhandene Eintrags-Scroll-Logik; das eigene Symbol bleibt sichtbar, ein Teil der Einträge ist dann nur per Auf/Ab erreichbar.
- Hilfe-Referenz und Tutorial nennen Alt+Minus, aber keinen Ort → Abschnitt 2 der `ide-referenz.md` um einen Satz ergänzen, keine Änderung der Hilfeseiten nötig.
## Migration Plan
Keine Datenmigration. Änderung in `app.rs`, `render.rs`, Test in `tests/app.rs`, ein Satz in `docs/ide-referenz.md`. Rücknahme durch Revert des Commits; keine gespeicherten Zustände betroffen.

View File

@@ -0,0 +1,64 @@
# Befund: Fenstermenü falsch positioniert und „File“ hervorgehoben
Datum: 08.09.2026, Validierung Phase 6 (Change 07), Windows Terminal / cmd.exe, `target\debug\tb`.
## Beobachtung (Screenshot Run-Fenster)
- Aktives Fenster: Run-Fenster (Magenta-Titel), Position etwa Spalte 3, Zeile 6.
- Nach Öffnen des Fenstermenüs erscheint der Aufklappbereich (Restore, Move, Size, Minimize, Maximize, Close) in Zeile 1 direkt unter der Menüleiste, beginnend an der linken Fensterkante, und überdeckt den Titel des dahinterliegenden Codefensters.
- Gleichzeitig ist der Hauptmenüeintrag „File“ in der Menüleiste hervorgehoben, obwohl kein Menüleistenmenü geöffnet wurde.
Das Run-Fenster ist nur das Beispiel; der Fehler betrifft alle Fenster gleich, da Zustand und Renderpfad des Control-Menüs fensterartunabhängig sind.
## Erwartung
- Aufklappbereich an das Symbol `[≡]` des aktiven Fensters angehängt: linke Spalte = Symbolspalte, bevorzugt unmittelbar unter der Titelzeile.
- Reicht der Platz unten nicht (z. B. minimiertes Fenster am unteren Rand), klappt das Menü nach oben und grenzt mit seiner Unterkante an die Titelzeile; es bleibt immer vollständig im Terminalbereich und sichtbar mit dem Fenster verbunden.
- Gleiche Regel für alle Fensterarten.
- Kein hervorgehobener Menüleistentitel.
## Ursache im Code (Stand 6e742d9)
- `crates/tb-ide/src/app.rs`, `Command::ControlMenu`: setzt `self.menu = Some((0, 0))` und damit den Menüleistenindex 0 („File“) als ausgewählt.
- `crates/tb-ide/src/render.rs`, Menüleiste: hebt den Titel `i` hervor, wenn `self.menu == Some((i, _))`; bei Control-Menü also „File“.
- `crates/tb-ide/src/render.rs`, Popup: `Rect::new(x, 1, …)` mit `x` = linke Fensterkante; die Zeile ist fest 1 statt Titelzeile des Fensters + 1.
## Korrektur (08.09.2026)
- `crates/tb-ide/src/app.rs`: `control_menu` trägt jetzt die eigene Auswahl (`Option<usize>`); `Command::ControlMenu` setzt `menu` nicht mehr. Gemeinsame Eintragsnavigation über `open_entries`/`select_open`, `menu_open()` ersetzt alle „ein Menü ist offen“-Abfragen. Neue Platzierungsfunktion `control_menu_rect` für alle Fensterarten: linke Spalte = Symbolspalte, bevorzugt unter der Titelzeile, sonst nach oben aufgeklappt bis an die Titelzeile, rechts nur so weit nach links wie nötig.
- `crates/tb-ide/src/render.rs`: Popup zeichnet die offene Liste an der berechneten Position; Menüleiste hebt nur bei echter Menüleistenauswahl hervor.
- `crates/tb-ide/tests/app.rs`: `control_menus_attach_to_the_symbol_of_every_window` prüft Code-, Projekt-, Output- und Immediate-Fenster, maximiert, minimiert (klappt nach oben), an den rechten Rand verschoben (rückt nach links) sowie Mausklick auf `[≡]` und auf einen Eintrag; vor der Korrektur schlug der Test mit `menu == Some((0, 0))` fehl.
## Nebenbefund: Export-Tests unter Windows
Bei der Testausführung scheiterten auf unverändertem Stand 6e742d9 sechs Export-Tests (`tb-export` und `tb-ide`) mit „Zugriff verweigert (os error 5)“. Ursache in `crates/tb-export/src/lib.rs`, `prepare_publication`: Die Temporärdatei wurde nur lesend geöffnet und dann `sync_all` aufgerufen; Windows verlangt für `FlushFileBuffers` Schreibrechte. Korrektur: schreibend öffnen. Auf Linux war das Verhalten unauffällig, daher blieb es in den Gitea-Läufen unentdeckt. Für Change 07 (Aufgabe 2.2/2.4) bedeutet das: Windows-Zielprüfungen des Exports müssen mit dieser Korrektur wiederholt werden.
## Nebenbefunde: Windows-Checkout mit CRLF und Pfadtrennern
Der vollständige Workspace-Lauf auf dem Windows-Prüfsystem (core.autocrlf=true) zeigte weitere plattformabhängige Fehler auf unverändertem Stand:
- `crates/tb-cli/src/ide_acceptance.rs`, Referenzmatrix: suchte `#[test]\nfn …(` in Quelldateien, die im Checkout CRLF tragen. Korrektur: Suche zeilenendenneutral.
- `crates/tb-cli/tests/compat.rs`: `release-formular` und `isamopen` verglichen CRLF-Sollausgaben direkt mit LF-Snapshots (die Hauptschleife normalisierte bereits, die beiden Einzeltests nicht). Korrektur: `assert_output_matches` und der `isamopen`-Vergleich normalisieren; `.gitattributes` erklärt `tests/compat/*.out` zu LF, wie der bestehende Kommentar es bereits behauptete.
- `crates/tb-vm/tests/project_io.rs`: suchte den Block ```` ```mak\n ```` in `docs/dateiformate.md` (CRLF im Checkout). Korrektur: zeilenendenneutral.
- `crates/tb-vm/src/project_io.rs`, `relative_path`: schrieb relative Projektverweise mit dem Plattformtrenner (`..\lib.tbl`), die drei `save_as`-Tests in `crates/tb-ide/tests/documents.rs` erwarten `/`. Unter Linux/macOS ist `\` kein Trenner, ein unter Windows gespeichertes Projekt wäre dort nicht ladbar. Korrektur: Verweise werden immer mit `/` geschrieben; beide Plattformen lösen `/` beim Laden auf.
- `crates/tb-ui/src/forms.rs`, Zeichenbildtest: erwartete für die DriveListBox fest `/ ▼`; die Implementierung zeigt absichtlich den Wurzeltrenner der Plattform (`std::path::MAIN_SEPARATOR`, unter Windows `\`). Korrektur: Test plattformneutral, `docs/forms-referenz.md` nennt beide Bilder.
Diese Befunde sind für Change 07 (Plattformmatrix, Aufgabe 2.2/2.4) relevant: Die Windows-Zielprüfung muss mit diesem Stand wiederholt werden. Der vollständige Workspace-Lauf unter Windows (cargo test --workspace --no-fail-fast) bestand nach den Korrekturen; die Zahlen stehen unten.
## Testlauf Windows (08.09.2026, Arbeitsstand auf 6e742d9)
`cargo fmt --all -- --check` sauber, `cargo clippy -p tb-ide --tests` ohne Warnung.
| Lauf | Testziele | bestanden | fehlgeschlagen | ignoriert |
|---|---|---|---|---|
| Vor den Nebenbefund-Korrekturen | 42 | 636 | 12 | 4 |
| `cargo test --workspace --no-fail-fast`, final | 42 | 637 | 0 | 4 |
## Manuelle Gegenprobe (08.09.2026)
Der Benutzer hat die Korrektur im realen Windows Terminal geprüft und als erfolgreich gemeldet: Control-Menüs öffnen fensterlokal am `[≡]`-Symbol des aktiven Fensters, die Menüleiste zeigt keinen ausgewählten Titel mehr.
## Status
Behoben. Automatisiert und manuell nachgewiesen; Übergabe an Change 07 Aufgabe 2.3 als erledigter Fachbefund.

View File

@@ -0,0 +1,28 @@
## Why
Die Validierung in Phase 6 (Change 07) zeigt am 08.09.2026 eine UI-Regression beim Öffnen von Fenstermenüs (Control-Menüs) aller Fenster, exemplarisch am Run-Fenster: Das Menü erscheint links oben unter der Menüleiste statt am Control-Menü-Symbol `[≡]` des Fensters, und gleichzeitig wird der Hauptmenüeintrag „File“ als aktiv hervorgehoben. Ursache ist, dass das Control-Menü den Zustand der Menüleiste (Index 0 = File) wiederverwendet und der Renderer den Aufklappbereich fest in Zeile 1 platziert. Die bestehende Spezifikation fordert nur, dass ein Control-Menü existiert und per Alt+Minus bedienbar ist; Ort und Abgrenzung zur Menüleiste sind nicht festgelegt. Diese Lücke wird hier geschlossen.
## What Changes
- Control-Menüs werden für alle Fensterarten nach einer einzigen Regel fensterlokal spezifiziert: Der Aufklappbereich SHALL an das `[≡]`-Symbol des aktiven Fensters angehängt sein, linke Spalte gleich Symbolspalte, und bevorzugt direkt unter der Titelzeile erscheinen, unabhängig davon, ob das Fenster normal, maximiert oder minimiert ist.
- Reicht der Platz unterhalb nicht aus (etwa bei minimierten Fenstern am unteren Rand), SHALL das Menü nach oben aufklappen und mit seiner untersten Zeile an die Titelzeile grenzen. Reicht der Platz rechts nicht aus, SHALL es nur so weit nach links rücken, dass es passt und die Symbolspalte weiter überdeckt. Es SHALL immer vollständig in der Arbeitsfläche liegen und sichtbar mit dem Fenster verbunden bleiben.
- Ein offenes Control-Menü MUST NOT einen Titel der Menüleiste hervorheben oder als Menüleisten-Auswahl gelten; Menüleiste und Control-Menü sind getrennte Zustände mit gemeinsamer Tastenführung (Auf/Ab, Enter, Mnemonic, Esc).
- Maus auf `[≡]` und Alt+Minus SHALL identisch wirken; Esc oder ein Klick außerhalb SHALL das Control-Menü schließen und den Fokus im Fenster belassen.
- Regressionsnachweis über TestBackend-Renderprüfung für mehrere Fensterarten: Position relativ zum Fensterrechteck, Aufklappen nach oben bei minimiertem Fenster, Linksverschiebung am rechten Rand, maximiertes Fenster, kein hervorgehobener Menüleistentitel.
## Capabilities
### New Capabilities
Keine.
### Modified Capabilities
- `ide-oberflaeche`: Requirement „MDI-Fenster und Fokus“ um die für alle Fensterarten gleiche Anheft- und Aufklappregel des Control-Menüs und dessen Abgrenzung von der Menüleiste präzisieren.
- `ide-projekte`: Requirement „Verlustfreies Speichern und Wechseln“ um plattformneutrale relative Verweise mit `/` ergänzen (Nebenbefund der Windows-Testausführung).
## Impact
Betroffen sind `crates/tb-ide/src/app.rs` (Zustand `control_menu`/`menu`, Tastenverteilung), `crates/tb-ide/src/render.rs` (Menüleisten-Hervorhebung, Popup-Position) und `crates/tb-ide/tests/app.rs`. Keine Änderung an Befehlen, Kürzeln, Optionen oder Dokumentformaten; `docs/ide-referenz.md` Abschnitt 2 wird um den Ort des Control-Menüs ergänzt. Als Nebenbefunde der Testausführung unter Windows werden zusätzlich `crates/tb-export/src/lib.rs` (Temporärdatei vor `sync_all` schreibend öffnen), `crates/tb-vm/src/project_io.rs` (relative Projektverweise immer mit `/`) sowie die zeilenendenabhängigen Vergleiche in `tb-cli`- und `tb-vm`-Tests korrigiert, damit der vollständige Workspace-Lauf auf allen Plattformen besteht.
Eigenständiger Korrektur-Change innerhalb Phase 6, ausgelöst durch die Validierung in 07. Er ist Voraussetzung für 07 Aufgabe 2.3 (Fachbefunde beheben, null offene Befunde); 07 bleibt Eigentümer der Phasenabnahme.

View File

@@ -0,0 +1,30 @@
## MODIFIED Requirements
### Requirement: MDI-Fenster und Fokus
Fenster SHALL Titelleiste, Control-Menü, Rahmen und bei Bedarf Scrollbalken erhalten. New Window SHALL eine zusätzliche Ansicht erzeugen; Arrange All SHALL geöffnete Fenster kacheln. F6/Ctrl+F6, Shift+F6, Ctrl+F4, Ctrl+F7/F8/F9/F10/F5 und Alt+Minus SHALL Wechseln, Schließen, Verschieben, Skalieren, Minimieren, Maximieren, Wiederherstellen und Control-Menü bedienen. Die Window-Liste SHALL das aktive Codefenster markieren. Projektbuttons Form/Code und Enter SHALL die entsprechende Ansicht öffnen.
Das Control-Menü SHALL für alle Fensterarten (Code, Projekt, Output/Run, Immediate, Debug, Calls, Hilfe, Toolbox, Palette, Menüdesign) nach derselben Regel fensterlokal erscheinen. Es SHALL sich nur für das aktive Fenster öffnen, per Alt+Minus und per Klick auf das Control-Menü-Symbol `[≡]` identisch. Der Aufklappbereich SHALL an das Symbol angehängt sein: seine linke Spalte SHALL der Spalte des Symbols in der Titelleiste des aktuellen Fensterrechtecks entsprechen (normal, maximiert oder minimiert), und er SHALL bevorzugt unmittelbar unter der Titelzeile beginnen. Reicht der Platz unterhalb bis zur Statuszeile nicht aus, SHALL der Aufklappbereich stattdessen nach oben aufklappen, sodass seine unterste Zeile unmittelbar über der Titelzeile endet. Reicht der Platz rechts nicht aus, SHALL er nur so weit nach links verschoben werden, dass er in die Arbeitsfläche passt und die Symbolspalte weiterhin überdeckt. Der Aufklappbereich SHALL stets vollständig innerhalb der Arbeitsfläche zwischen Menüleiste und Statuszeile liegen und MUST NOT die Titelzeile des eigenen Fensters überdecken. Ein offenes Control-Menü MUST NOT einen Titel der Menüleiste hervorheben oder als Menüleistenauswahl gelten; Auf/Ab, Enter, Mnemonics und Esc SHALL die Einträge bedienen, Esc und ein Klick außerhalb SHALL das Control-Menü schließen und den Fokus im Fenster belassen.
#### Scenario: Fenster wiederherstellen
- **WHEN** ein Codefenster verschoben, skaliert, minimiert, maximiert und wiederhergestellt wird
- **THEN** bleiben Dokument, Cursor und vorherige Normalgeometrie erhalten und alle Bedienelemente erreichbar
#### Scenario: Dialogfokus
- **WHEN** ein modaler Dialog geöffnet und anschließend abgebrochen wird
- **THEN** empfängt nur der Dialog die Eingaben und danach erhält das vorher aktive Fenster den Fokus zurück
#### Scenario: Control-Menü am Fenstersymbol
- **WHEN** ein beliebiges Fenster (etwa Run, Code oder Projekt) aktiv ist, unter seiner Titelzeile genug Platz besteht und der Benutzer Alt+Minus drückt oder auf `[≡]` in dessen Titelleiste klickt
- **THEN** erscheint das Control-Menü mit Restore, Move, Size, Minimize, Maximize und Close direkt unterhalb der Titelzeile, mit linker Spalte gleich der Symbolspalte, kein Titel der Menüleiste ist hervorgehoben, und Esc schließt es, ohne den Fokus zu ändern
#### Scenario: Control-Menü klappt nach oben
- **WHEN** das aktive Fenster minimiert am unteren Rand liegt oder so weit nach unten verschoben wurde, dass das Control-Menü unterhalb der Titelzeile nicht mehr in die Arbeitsfläche passt
- **THEN** öffnet sich das Control-Menü oberhalb der Titelzeile, seine unterste Zeile grenzt an die Titelzeile, seine linke Spalte ist die Symbolspalte, und alle Einträge liegen innerhalb der Arbeitsfläche
#### Scenario: Control-Menü am rechten Rand und bei maximiertem Fenster
- **WHEN** das aktive Fenster so weit rechts liegt, dass das Control-Menü über die Arbeitsfläche hinausragen würde, oder das Fenster maximiert ist
- **THEN** wird das Control-Menü nur so weit nach links gerückt, dass es in die Arbeitsfläche passt und die Symbolspalte weiterhin überdeckt; bei maximiertem Fenster öffnet es sich unter der Titelzeile des maximierten Rechtecks
#### Scenario: Gleiche Regel für alle Fensterarten
- **WHEN** nacheinander Code-, Projekt-, Output-, Immediate-, Debug- und Hilfefenster aktiviert und ihr Control-Menü geöffnet wird
- **THEN** ergibt sich die Position jedes Menüs ausschließlich aus dem aktuellen Rechteck und der Titelzeile des jeweils aktiven Fensters nach derselben Anheft- und Aufklappregel

View File

@@ -0,0 +1,16 @@
## MODIFIED Requirements
### Requirement: Verlustfreies Speichern und Wechseln
Save File, Save File As, Save Project sowie Projektwechsel und IDE-Ende SHALL Änderungen mit Speichern/Verwerfen/Abbrechen behandeln. Fehlgeschlagenes Speichern MUST den bearbeiteten Stand erhalten und darf die letzte gültige Zieldatei nicht beschädigen. Externe Dateiänderungen und bestehende Save-As-Ziele SHALL vor Überschreiben erkennbar sein und eine ausdrückliche Wahl erfordern. Erst erfolgreich gespeicherte Dokumente SHALL als gespeichert gelten. Relative Mitglieder- und Include-Verweise in geschriebenen MAK- und BAS-Dateien SHALL plattformneutral mit `/` als Trenner geschrieben werden; das Laden SHALL solche Verweise auf allen Zielplattformen auflösen.
#### Scenario: Schreibfehler im Projekt
- **WHEN** ein Mitglied erfolgreich gespeichert wird, ein weiteres aber nicht geschrieben werden kann
- **THEN** bleibt das zweite als geändert offen, der Fehler nennt den Pfad und das Projekt wird nicht als vollständig gespeichert oder geschlossen gemeldet
#### Scenario: Save As in anderes Verzeichnis
- **WHEN** ein Projekt unter einem neuen Pfad gespeichert wird
- **THEN** werden relative Mitgliederverweise so angepasst, dass dieselben Dateien geladen werden; ein fehlgeschlagener Vorgang lässt den bisherigen Projektpfad aktiv
#### Scenario: Projekt zwischen Plattformen austauschen
- **WHEN** ein Projekt unter Windows in ein Unterverzeichnis gespeichert wird und die Projektdatei anschließend unter Linux oder macOS geöffnet wird
- **THEN** enthalten die geschriebenen Verweise `../` statt `..\`, und beide Plattformen laden dieselben Mitglieder und Bibliotheken

View File

@@ -0,0 +1,15 @@
## 1. Ursache und Nachweis vorbereiten
- [x] 1.1 Befund mit frischem Start reproduzieren (Run-Fenster aktiv, Alt+Minus sowie Klick auf `[≡]`) und die fehlerhafte Position und die „File“-Hervorhebung als fehlschlagende TestBackend-Prüfung in `crates/tb-ide/tests/app.rs` festhalten; die Prüfung muss die Popup-Position relativ zu `app.rect(aktives Fenster)` und den Stil der Menüleistenzelle „File“ auswerten.
## 2. Zustand trennen und Position korrigieren
- [x] 2.1 In `crates/tb-ide/src/app.rs` den Control-Menü-Zustand von `menu` lösen: `Command::ControlMenu` darf `menu` nicht setzen; Auswahl, Auf/Ab, Enter, Mnemonic und Esc für das Control-Menü über die gemeinsame Eintragsnavigation bedienen; Klick außerhalb und Esc schließen beide Zustände; Alt+Mnemonic wechselt in die Menüleiste und schließt das Control-Menü.
- [x] 2.2 In `crates/tb-ide/src/render.rs` eine für alle Fensterarten gemeinsame Platzierungsfunktion für das Control-Menü-Popup einführen: linke Spalte = Symbolspalte `rect.x`, bevorzugt unter der Titelzeile (`rect.y + 1`), bei fehlendem Platz unten nach oben aufklappen mit Unterkante `rect.y - 1`, bei fehlendem Platz rechts nur so weit nach links rücken, dass das Popup passt und die Symbolspalte überdeckt bleibt; das Popup liegt immer vollständig in der Arbeitsfläche und überdeckt nie die eigene Titelzeile; Menüleiste hebt bei offenem Control-Menü keinen Titel hervor; Hit-Bereiche der Einträge an die berechnete Position anpassen.
- [x] 2.3 Prüfung aus 1.1 grün bekommen und um Fälle ergänzen: Code-, Projekt-, Output- und Immediate-Fenster mit derselben Regel, minimiertes Fenster (Ctrl+F9) klappt nach oben, per Ctrl+F7 an den rechten Rand verschobenes Fenster rückt nach links, maximiertes Fenster (Ctrl+F10), Mausklick auf `[≡]` und auf einen Eintrag; bestehende Tests zu Fenster- und Menübedienung bleiben bestanden.
## 3. Dokumentation und Übergabe
- [x] 3.1 `docs/ide-referenz.md` Abschnitt 2 um den Ort des Control-Menüs ergänzen; Hilfekatalog-/Linktests bleiben bestanden.
- [x] 3.2 Manuelle Gegenprobe im realen Terminal (Windows Terminal) mit Run-, Code-, Projekt-, Immediate- und Hilfefenster in normalem, minimiertem und maximiertem Zustand sowie am rechten Rand; Vorher-/Nachher-Beobachtung in `evidence/` festhalten und Befund an Change 07 Aufgabe 2.3 als behoben übergeben.
- [x] 3.3 Nebenbefunde der Windows-Testausführung beheben (siehe `evidence/befund-2026-09-08.md`): `prepare_publication` in `crates/tb-export/src/lib.rs` öffnete die Temporärdatei lesend vor `sync_all` („Zugriff verweigert“, sechs Export-Tests); `relative_path` in `crates/tb-vm/src/project_io.rs` schrieb Projektverweise mit Plattformtrenner statt `/` (drei `save_as`-Tests); Referenzmatrix-, Kompatibilitäts- und MAK-Beispieltests verglichen CRLF-Checkouts nicht zeilenendenneutral. Vollständiger Workspace-Lauf unter Windows muss bestehen.

View File

@@ -27,6 +27,8 @@ Alle Befehle aus docs/ide-referenz.md SHALL im jeweils beschriebenen Menü ersch
### Requirement: MDI-Fenster und Fokus ### Requirement: MDI-Fenster und Fokus
Fenster SHALL Titelleiste, Control-Menü, Rahmen und bei Bedarf Scrollbalken erhalten. New Window SHALL eine zusätzliche Ansicht erzeugen; Arrange All SHALL geöffnete Fenster kacheln. F6/Ctrl+F6, Shift+F6, Ctrl+F4, Ctrl+F7/F8/F9/F10/F5 und Alt+Minus SHALL Wechseln, Schließen, Verschieben, Skalieren, Minimieren, Maximieren, Wiederherstellen und Control-Menü bedienen. Die Window-Liste SHALL das aktive Codefenster markieren. Projektbuttons Form/Code und Enter SHALL die entsprechende Ansicht öffnen. Fenster SHALL Titelleiste, Control-Menü, Rahmen und bei Bedarf Scrollbalken erhalten. New Window SHALL eine zusätzliche Ansicht erzeugen; Arrange All SHALL geöffnete Fenster kacheln. F6/Ctrl+F6, Shift+F6, Ctrl+F4, Ctrl+F7/F8/F9/F10/F5 und Alt+Minus SHALL Wechseln, Schließen, Verschieben, Skalieren, Minimieren, Maximieren, Wiederherstellen und Control-Menü bedienen. Die Window-Liste SHALL das aktive Codefenster markieren. Projektbuttons Form/Code und Enter SHALL die entsprechende Ansicht öffnen.
Das Control-Menü SHALL für alle Fensterarten (Code, Projekt, Output/Run, Immediate, Debug, Calls, Hilfe, Toolbox, Palette, Menüdesign) nach derselben Regel fensterlokal erscheinen. Es SHALL sich nur für das aktive Fenster öffnen, per Alt+Minus und per Klick auf das Control-Menü-Symbol `[≡]` identisch. Der Aufklappbereich SHALL an das Symbol angehängt sein: seine linke Spalte SHALL der Spalte des Symbols in der Titelleiste des aktuellen Fensterrechtecks entsprechen (normal, maximiert oder minimiert), und er SHALL bevorzugt unmittelbar unter der Titelzeile beginnen. Reicht der Platz unterhalb bis zur Statuszeile nicht aus, SHALL der Aufklappbereich stattdessen nach oben aufklappen, sodass seine unterste Zeile unmittelbar über der Titelzeile endet. Reicht der Platz rechts nicht aus, SHALL er nur so weit nach links verschoben werden, dass er in die Arbeitsfläche passt und die Symbolspalte weiterhin überdeckt. Der Aufklappbereich SHALL stets vollständig innerhalb der Arbeitsfläche zwischen Menüleiste und Statuszeile liegen und MUST NOT die Titelzeile des eigenen Fensters überdecken. Ein offenes Control-Menü MUST NOT einen Titel der Menüleiste hervorheben oder als Menüleistenauswahl gelten; Auf/Ab, Enter, Mnemonics und Esc SHALL die Einträge bedienen, Esc und ein Klick außerhalb SHALL das Control-Menü schließen und den Fokus im Fenster belassen.
#### Scenario: Fenster wiederherstellen #### Scenario: Fenster wiederherstellen
- **WHEN** ein Codefenster verschoben, skaliert, minimiert, maximiert und wiederhergestellt wird - **WHEN** ein Codefenster verschoben, skaliert, minimiert, maximiert und wiederhergestellt wird
- **THEN** bleiben Dokument, Cursor und vorherige Normalgeometrie erhalten und alle Bedienelemente erreichbar - **THEN** bleiben Dokument, Cursor und vorherige Normalgeometrie erhalten und alle Bedienelemente erreichbar
@@ -35,6 +37,22 @@ Fenster SHALL Titelleiste, Control-Menü, Rahmen und bei Bedarf Scrollbalken erh
- **WHEN** ein modaler Dialog geöffnet und anschließend abgebrochen wird - **WHEN** ein modaler Dialog geöffnet und anschließend abgebrochen wird
- **THEN** empfängt nur der Dialog die Eingaben und danach erhält das vorher aktive Fenster den Fokus zurück - **THEN** empfängt nur der Dialog die Eingaben und danach erhält das vorher aktive Fenster den Fokus zurück
#### Scenario: Control-Menü am Fenstersymbol
- **WHEN** ein beliebiges Fenster (etwa Run, Code oder Projekt) aktiv ist, unter seiner Titelzeile genug Platz besteht und der Benutzer Alt+Minus drückt oder auf `[≡]` in dessen Titelleiste klickt
- **THEN** erscheint das Control-Menü mit Restore, Move, Size, Minimize, Maximize und Close direkt unterhalb der Titelzeile, mit linker Spalte gleich der Symbolspalte, kein Titel der Menüleiste ist hervorgehoben, und Esc schließt es, ohne den Fokus zu ändern
#### Scenario: Control-Menü klappt nach oben
- **WHEN** das aktive Fenster minimiert am unteren Rand liegt oder so weit nach unten verschoben wurde, dass das Control-Menü unterhalb der Titelzeile nicht mehr in die Arbeitsfläche passt
- **THEN** öffnet sich das Control-Menü oberhalb der Titelzeile, seine unterste Zeile grenzt an die Titelzeile, seine linke Spalte ist die Symbolspalte, und alle Einträge liegen innerhalb der Arbeitsfläche
#### Scenario: Control-Menü am rechten Rand und bei maximiertem Fenster
- **WHEN** das aktive Fenster so weit rechts liegt, dass das Control-Menü über die Arbeitsfläche hinausragen würde, oder das Fenster maximiert ist
- **THEN** wird das Control-Menü nur so weit nach links gerückt, dass es in die Arbeitsfläche passt und die Symbolspalte weiterhin überdeckt; bei maximiertem Fenster öffnet es sich unter der Titelzeile des maximierten Rechtecks
#### Scenario: Gleiche Regel für alle Fensterarten
- **WHEN** nacheinander Code-, Projekt-, Output-, Immediate-, Debug- und Hilfefenster aktiviert und ihr Control-Menü geöffnet wird
- **THEN** ergibt sich die Position jedes Menüs ausschließlich aus dem aktuellen Rechteck und der Titelzeile des jeweils aktiven Fensters nach derselben Anheft- und Aufklappregel
### Requirement: Farben und Statusanzeige ### Requirement: Farben und Statusanzeige
Die IDE SHALL das anhand belegter VBDOS-Ansichten dokumentierte Farbschema aus docs/ide-referenz.md verwenden, insbesondere dunkelblaue Codefläche mit heller Schrift, weiße aktive Titel auf dunklem Magenta, graue Menüs und schwarze Statusschrift auf Cyan. Alle IDE-Farben SHALL als feste 24-Bit-RGB-Werte ausgegeben werden, unabhängig von TERM, COLORTERM, NO_COLOR und der ANSI-Palette. Ein Rückfall auf Index- oder Profilfarben MUST NOT erfolgen; die IDE MUST NOT die globale Terminalpalette verändern. Fensterflächen SHALL vor dem Zeichnen vorhandene Desktopzeichen und verdeckte Fensterinhalte löschen, sodass unbeschriebene Fläche und Texthintergrund dieselbe Farbe besitzen. Die Statuszeile SHALL kontextuelle klickbare Kürzel und Zeile/Spalte im Format 00001:001 zeigen. Display SHALL Vorder-/Hintergrund aus 16 Farben, Desktop-Füllzeichen und Tabweite ändern können. Bestehende gespeicherte Farbauswahlen SHALL gültig bleiben und MUST NOT stillschweigend überschrieben werden. Die IDE SHALL das anhand belegter VBDOS-Ansichten dokumentierte Farbschema aus docs/ide-referenz.md verwenden, insbesondere dunkelblaue Codefläche mit heller Schrift, weiße aktive Titel auf dunklem Magenta, graue Menüs und schwarze Statusschrift auf Cyan. Alle IDE-Farben SHALL als feste 24-Bit-RGB-Werte ausgegeben werden, unabhängig von TERM, COLORTERM, NO_COLOR und der ANSI-Palette. Ein Rückfall auf Index- oder Profilfarben MUST NOT erfolgen; die IDE MUST NOT die globale Terminalpalette verändern. Fensterflächen SHALL vor dem Zeichnen vorhandene Desktopzeichen und verdeckte Fensterinhalte löschen, sodass unbeschriebene Fläche und Texthintergrund dieselbe Farbe besitzen. Die Statuszeile SHALL kontextuelle klickbare Kürzel und Zeile/Spalte im Format 00001:001 zeigen. Display SHALL Vorder-/Hintergrund aus 16 Farben, Desktop-Füllzeichen und Tabweite ändern können. Bestehende gespeicherte Farbauswahlen SHALL gültig bleiben und MUST NOT stillschweigend überschrieben werden.

View File

@@ -36,7 +36,7 @@ Mehrere Codefenster und der Formular-Designer SHALL denselben Dokumentstand benu
- **THEN** zeigt das andere denselben neuen Text mit eigener Cursorposition; Schließen einer Ansicht verwirft das Dokument nicht - **THEN** zeigt das andere denselben neuen Text mit eigener Cursorposition; Schließen einer Ansicht verwirft das Dokument nicht
### Requirement: Verlustfreies Speichern und Wechseln ### Requirement: Verlustfreies Speichern und Wechseln
Save File, Save File As, Save Project sowie Projektwechsel und IDE-Ende SHALL Änderungen mit Speichern/Verwerfen/Abbrechen behandeln. Fehlgeschlagenes Speichern MUST den bearbeiteten Stand erhalten und darf die letzte gültige Zieldatei nicht beschädigen. Externe Dateiänderungen und bestehende Save-As-Ziele SHALL vor Überschreiben erkennbar sein und eine ausdrückliche Wahl erfordern. Erst erfolgreich gespeicherte Dokumente SHALL als gespeichert gelten. Save File, Save File As, Save Project sowie Projektwechsel und IDE-Ende SHALL Änderungen mit Speichern/Verwerfen/Abbrechen behandeln. Fehlgeschlagenes Speichern MUST den bearbeiteten Stand erhalten und darf die letzte gültige Zieldatei nicht beschädigen. Externe Dateiänderungen und bestehende Save-As-Ziele SHALL vor Überschreiben erkennbar sein und eine ausdrückliche Wahl erfordern. Erst erfolgreich gespeicherte Dokumente SHALL als gespeichert gelten. Relative Mitglieder- und Include-Verweise in geschriebenen MAK- und BAS-Dateien SHALL plattformneutral mit `/` als Trenner geschrieben werden; das Laden SHALL solche Verweise auf allen Zielplattformen auflösen.
#### Scenario: Schreibfehler im Projekt #### Scenario: Schreibfehler im Projekt
- **WHEN** ein Mitglied erfolgreich gespeichert wird, ein weiteres aber nicht geschrieben werden kann - **WHEN** ein Mitglied erfolgreich gespeichert wird, ein weiteres aber nicht geschrieben werden kann
@@ -46,6 +46,10 @@ Save File, Save File As, Save Project sowie Projektwechsel und IDE-Ende SHALL Ä
- **WHEN** ein Projekt unter einem neuen Pfad gespeichert wird - **WHEN** ein Projekt unter einem neuen Pfad gespeichert wird
- **THEN** werden relative Mitgliederverweise so angepasst, dass dieselben Dateien geladen werden; ein fehlgeschlagener Vorgang lässt den bisherigen Projektpfad aktiv - **THEN** werden relative Mitgliederverweise so angepasst, dass dieselben Dateien geladen werden; ein fehlgeschlagener Vorgang lässt den bisherigen Projektpfad aktiv
#### Scenario: Projekt zwischen Plattformen austauschen
- **WHEN** ein Projekt unter Windows in ein Unterverzeichnis gespeichert wird und die Projektdatei anschließend unter Linux oder macOS geöffnet wird
- **THEN** enthalten die geschriebenen Verweise `../` statt `..\`, und beide Plattformen laden dieselben Mitglieder und Bibliotheken
### Requirement: Formularimport und Textaustausch ### Requirement: Formularimport und Textaustausch
Die IDE SHALL bestehende FRM-Leser und die kanonische Textausgabe gemäß forms-dateiformat verwenden. Binäre FRM-Originale MUST beim Import erhalten bleiben; die bearbeitete Textfassung SHALL über ein ausdrücklich gewähltes Ziel gespeichert werden. Load Text und Save Text SHALL Text in das aktive Codedokument einfügen beziehungsweise dessen Auswahl oder gesamten Inhalt exportieren, ohne die Projektmitgliedschaft unbemerkt zu ändern. Die IDE SHALL bestehende FRM-Leser und die kanonische Textausgabe gemäß forms-dateiformat verwenden. Binäre FRM-Originale MUST beim Import erhalten bleiben; die bearbeitete Textfassung SHALL über ein ausdrücklich gewähltes Ziel gespeichert werden. Load Text und Save Text SHALL Text in das aktive Codedokument einfügen beziehungsweise dessen Auswahl oder gesamten Inhalt exportieren, ohne die Projektmitgliedschaft unbemerkt zu ändern.