Phase 4: Steuerelemente implementieren
This commit is contained in:
31
PLAN.md
31
PLAN.md
@@ -393,6 +393,10 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
|
||||
Transaktion verfällt.
|
||||
|
||||
## Phase 4 — Forms-Engine (`tb-ui::forms`)
|
||||
|
||||
**Status: abgeschlossen (2026-09-05).** Alle sieben Programme des
|
||||
öffentlichen VBDOS-Bestands kompilieren ohne Diagnose und starten im PipeHost.
|
||||
|
||||
- [x] Eingangsaufgabe (aus Phase 0 übernommen): Ereignisschleifen-
|
||||
Architektur — Terminal-Events → Event-Queue → VM-Ticks, kooperative
|
||||
Zustellpunkte (`DOEVENTS`, `SLEEP`, blockierende Eingabe, Ende einer
|
||||
@@ -401,11 +405,12 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
|
||||
Forms-Ereignisprozeduren verwenden später denselben Rückrufweg.
|
||||
- [x] Formular-Modell: Eigenschaften, Lade-/Entladezyklus, `SHOW`/`HIDE`
|
||||
(modal/nicht-modal)
|
||||
- [ ] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox,
|
||||
- [x] Steuerelemente: CommandButton, TextBox, ListBox, ComboBox, CheckBox,
|
||||
OptionButton, Frame, Label, HScrollBar/VScrollBar, PictureBox (Text),
|
||||
Timer — mit allen Eigenschaften/Methoden/Ereignissen des Vorbilds
|
||||
- [ ] Menüsystem (Menüleiste, Shortcuts, Access Keys)
|
||||
- [ ] Fokus-/Tab-Reihenfolge, Access-Keys, Maussteuerung
|
||||
Timer und VSpin/HSpin — mit ihren Eigenschaften, Methoden und
|
||||
Ereignissen
|
||||
- [x] Menüsystem (Menüleiste, Shortcuts, Access Keys)
|
||||
- [x] Fokus-/Tab-Reihenfolge, Access-Keys, Maussteuerung
|
||||
- [x] `.FRM`-Textformat: Serialisierung **definieren** (kein Original-
|
||||
Beispiel verfügbar — Windows-1.0-Schema, siehe dateiformate.md),
|
||||
dokumentieren, dann lesen/schreiben implementieren
|
||||
@@ -413,9 +418,14 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
|
||||
(Gegenstück zu FT.EXE des Vorbilds; Magic `FC 08 01 00`): als
|
||||
`tbc convert-frm`. Format per Reverse Engineering aus den
|
||||
Beispieldateien des Originalpakets und des cout/vbdos-Repos
|
||||
- [ ] Meilenstein (erster Kompatibilitätstest): die Programme aus
|
||||
- [x] Meilenstein (erster Kompatibilitätstest): die Programme aus
|
||||
https://github.com/cout/vbdos ohne Non-Features kompilieren und
|
||||
sind nutzbar (Konsolenprogramme bereits ab Phase 3)
|
||||
Befund: Alle sieben Programme wurden gegen Commit `1cdd2b3` als
|
||||
`.MAK` bzw. `.FRM` ohne Diagnosen geprüft und im PipeHost gestartet.
|
||||
Dafür wurden Projekt-/Include-Auflösung, P-Code-Dekodierung,
|
||||
Zellgrafik und VSpin/HSpin vollständig geschlossen. Einzelheiten:
|
||||
`openspec/changes/phase-4-steuerelemente/vbdos-befunde.md`.
|
||||
- [x] Ereignisdispatch: Event-Queue ↔ VM (Ereignisprozeduren `Name_Ereignis`)
|
||||
- [x] **Klassische Ereignis-Traps** (ohne Forms, Sprachreferenz §8) —
|
||||
`ON TIMER(n)`, `ON KEY(n)`, `ON UEVENT` und `ON SIGNAL(n)` je mit
|
||||
@@ -426,11 +436,14 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
|
||||
(Fehler 73). **10 Inventareinträge**
|
||||
Befund: `EVENT` besitzt nur `ON`/`OFF`; `SIGNAL(1/2)` bildet
|
||||
plattformübergreifend `SIGINT`/`SIGTERM` ab.
|
||||
- [ ] **Vordefinierte Dialoge**: `MSGBOX` als Anweisung **und** als
|
||||
- [x] **Vordefinierte Dialoge**: `MSGBOX` als Anweisung **und** als
|
||||
Funktion (liefert die gedrückte Schaltfläche) sowie `INPUTBOX$`.
|
||||
Sie hängen am Formularmodell, nicht an der Steuerelementliste, und
|
||||
fallen deshalb sonst zwischen die Aufgaben. **3 Inventareinträge**
|
||||
- [ ] Meilenstein: Beispiel-Formularprogramme aus dem Testkorpus laufen
|
||||
- [x] Meilenstein: Beispiel-Formularprogramme aus dem Testkorpus laufen
|
||||
Befund: `formular.frm` speist Fokus, Tab, Access-Key, Mausklick,
|
||||
Menüwahl, Liste, Timer und modalen Dialog über eine deklarierte,
|
||||
virtuelle Ereignisfolge reproduzierbar ein.
|
||||
|
||||
## Phase 5 — IDE (`tb-ide`)
|
||||
- [ ] IDE-Rahmen: Menüleiste, MDI-artige Fensterverwaltung, Statuszeile,
|
||||
@@ -441,11 +454,11 @@ Maßgeblich ist seit 2026-09-03 das Inventar, nicht diese Liste.
|
||||
- [ ] Formular-Designer: Steuerelemente platzieren/verschieben/skalieren,
|
||||
Eigenschaftenfenster
|
||||
- [ ] Ausführen aus der IDE: Start/Unterbrechen/Fortsetzen/Neustart
|
||||
- [ ] **`$INCLUDE`** im Compile-Treiber auflösen — der Lexer liefert den
|
||||
- [x] **`$INCLUDE`** im Compile-Treiber auflösen — der Lexer liefert den
|
||||
Metabefehl bereits als Token. Datei einlesen, Pfade relativ zur
|
||||
einschließenden Datei, Zyklen erkennen und namentlich abweisen.
|
||||
**1 Inventareintrag**
|
||||
- [ ] **`RUN`** — Programm neu starten bzw. ein anderes laden und starten.
|
||||
- [x] **`RUN`** — Programm neu starten bzw. ein anderes laden und starten.
|
||||
Nutzt denselben Zurücksetzpfad der VM wie „Neustart" oben.
|
||||
**1 Inventareintrag**
|
||||
- [ ] Debugger: Breakpoints, Einzelschritt/Prozedurschritt, Direktfenster,
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
//! `tbc` — Standalone-Compiler von Terminal Basic.
|
||||
//!
|
||||
//! Unterbefehle (Phase 2):
|
||||
//! - `tbc run <datei.bas>` Kompilieren und sofort ausführen
|
||||
//! - `tbc build <datei.bas>` Kompilieren zu `datei.tbc`
|
||||
//! - `tbc check <datei.bas>` Nur Syntax-/Semantikprüfung
|
||||
//! - `tbc run <datei.bas|datei.frm|projekt.mak>` Kompilieren und ausführen
|
||||
//! - `tbc build <datei.bas|datei.frm|projekt.mak>` Zu `.tbc` kompilieren
|
||||
//! - `tbc check <datei.bas|datei.frm|projekt.mak>` Syntax/Semantik prüfen
|
||||
//! - `tbc convert-frm <quelle.frm> <ziel.frm>` Binärformular in Text wandeln
|
||||
//!
|
||||
//! Exit-Codes von `run` (Entscheidung D6, docs/tbvm-design.md):
|
||||
//! 0 = END/SYSTEM/Programmende · 3 = STOP · 2 = Laufzeitfehler ·
|
||||
//! 1 = Compile-Fehler/Bedienfehler.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::ExitCode;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
@@ -25,7 +26,7 @@ fn main() -> ExitCode {
|
||||
Some("convert-frm") => cmd_convert_frm(&args[1..]),
|
||||
_ => {
|
||||
eprintln!(
|
||||
"Aufruf: tbc run|build|check <datei.bas> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
||||
"Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak> | tbc convert-frm <quelle.frm> <ziel.frm>"
|
||||
);
|
||||
ExitCode::from(1)
|
||||
}
|
||||
@@ -81,23 +82,230 @@ fn module_name(path: &Path) -> String {
|
||||
.unwrap_or_else(|| "MODUL".into())
|
||||
}
|
||||
|
||||
fn relative_case_insensitive(base: &Path, relative: &str) -> std::io::Result<PathBuf> {
|
||||
let direct = base.join(relative);
|
||||
if direct.exists() {
|
||||
return Ok(direct);
|
||||
}
|
||||
let wanted = Path::new(relative);
|
||||
let mut current = base.to_path_buf();
|
||||
for component in wanted.components() {
|
||||
use std::path::Component;
|
||||
match component {
|
||||
Component::CurDir => {}
|
||||
Component::ParentDir => {
|
||||
current.pop();
|
||||
}
|
||||
Component::Normal(name) => {
|
||||
let entry = std::fs::read_dir(¤t)?.find_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
entry
|
||||
.file_name()
|
||||
.to_string_lossy()
|
||||
.eq_ignore_ascii_case(&name.to_string_lossy())
|
||||
.then(|| entry.path())
|
||||
});
|
||||
current = entry.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("{} nicht gefunden", current.join(name).display()),
|
||||
)
|
||||
})?;
|
||||
}
|
||||
Component::RootDir | Component::Prefix(_) => current.push(component.as_os_str()),
|
||||
}
|
||||
}
|
||||
Ok(current)
|
||||
}
|
||||
|
||||
fn include_name(line: &str) -> Option<String> {
|
||||
let trimmed = line.trim();
|
||||
let upper = trimmed.to_ascii_uppercase();
|
||||
let at = upper.find("$INCLUDE")?;
|
||||
let rest = trimmed[at + "$INCLUDE".len()..].trim_start();
|
||||
let rest = rest.strip_prefix(':')?.trim_start();
|
||||
let rest = rest.strip_prefix('\'')?;
|
||||
Some(rest.split('\'').next().unwrap_or_default().to_string())
|
||||
}
|
||||
|
||||
fn expand_includes(path: &Path, source: &str, stack: &mut Vec<PathBuf>) -> Result<String, String> {
|
||||
let canonical = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
|
||||
if stack.contains(&canonical) {
|
||||
return Err(format!("{}: zyklisches $INCLUDE", path.display()));
|
||||
}
|
||||
stack.push(canonical);
|
||||
let mut out = String::new();
|
||||
for line in source.split_inclusive('\n') {
|
||||
if let Some(name) = include_name(line) {
|
||||
if name.is_empty() {
|
||||
return Err(format!("{}: leeres $INCLUDE", path.display()));
|
||||
}
|
||||
let included =
|
||||
relative_case_insensitive(path.parent().unwrap_or(Path::new(".")), &name)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))?;
|
||||
let text = std::fs::read_to_string(&included)
|
||||
.map_err(|error| format!("{}: {error}", included.display()))?;
|
||||
out.push_str(&expand_includes(&included, &text, stack)?);
|
||||
if !out.ends_with('\n') {
|
||||
out.push('\n');
|
||||
}
|
||||
} else {
|
||||
out.push_str(line);
|
||||
}
|
||||
}
|
||||
stack.pop();
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn read_form(path: &Path) -> Result<tb_ui::frm::FormFile, String> {
|
||||
let bytes = std::fs::read(path).map_err(|error| format!("{}: {error}", path.display()))?;
|
||||
if bytes.starts_with(b"VERSION ") || bytes.starts_with(b"Version ") {
|
||||
let source = std::str::from_utf8(&bytes)
|
||||
.map_err(|_| format!("{}: ungültige Textkodierung", path.display()))?;
|
||||
let mut form = tb_ui::frm::read_text(&path.display().to_string(), source)
|
||||
.map_err(|error| error.to_string())?;
|
||||
form.code = expand_includes(path, &form.code, &mut Vec::new())?;
|
||||
Ok(form)
|
||||
} else {
|
||||
tb_ui::frm::read_binary(&path.display().to_string(), &bytes)
|
||||
.map(|read| read.form)
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ProjectSymbols {
|
||||
constants: HashSet<String>,
|
||||
types: HashSet<String>,
|
||||
}
|
||||
|
||||
fn append_project_source(target: &mut String, source: &str, symbols: &mut ProjectSymbols) {
|
||||
let mut in_proc = false;
|
||||
let mut skip_type = false;
|
||||
for line in source.split_inclusive('\n') {
|
||||
let upper = line.trim().to_ascii_uppercase();
|
||||
if skip_type {
|
||||
if upper == "END TYPE" {
|
||||
skip_type = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if !in_proc {
|
||||
if let Some(name) = upper
|
||||
.strip_prefix("TYPE ")
|
||||
.and_then(|rest| rest.split_whitespace().next())
|
||||
{
|
||||
if !symbols.types.insert(name.to_string()) {
|
||||
skip_type = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(rest) = upper.strip_prefix("CONST ") {
|
||||
let names = rest
|
||||
.split(',')
|
||||
.filter_map(|part| part.split('=').next())
|
||||
.map(str::trim)
|
||||
.filter(|name| !name.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if !names.is_empty() && names.iter().all(|name| symbols.constants.contains(*name)) {
|
||||
continue;
|
||||
}
|
||||
symbols
|
||||
.constants
|
||||
.extend(names.into_iter().map(str::to_string));
|
||||
}
|
||||
}
|
||||
target.push_str(line);
|
||||
if upper.starts_with("SUB ")
|
||||
|| upper.starts_with("FUNCTION ")
|
||||
|| upper.starts_with("STATIC SUB ")
|
||||
|| upper.starts_with("STATIC FUNCTION ")
|
||||
{
|
||||
in_proc = true;
|
||||
} else if matches!(upper.as_str(), "END SUB" | "END FUNCTION") {
|
||||
in_proc = false;
|
||||
}
|
||||
}
|
||||
if !target.ends_with('\n') {
|
||||
target.push('\n');
|
||||
}
|
||||
}
|
||||
|
||||
fn input_sources(path: &Path) -> Result<(String, Vec<tb_ui::frm::FormFile>), String> {
|
||||
let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
|
||||
let paths = if extension.eq_ignore_ascii_case("mak") {
|
||||
let project = std::fs::read_to_string(path)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))?;
|
||||
let base = path.parent().unwrap_or(Path::new("."));
|
||||
project
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(|line| {
|
||||
relative_case_insensitive(base, line)
|
||||
.map_err(|error| format!("{}: {error}", path.display()))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
} else {
|
||||
vec![path.to_path_buf()]
|
||||
};
|
||||
let mut source = String::new();
|
||||
let mut forms = Vec::new();
|
||||
let mut symbols = ProjectSymbols::default();
|
||||
for member in paths {
|
||||
if member
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
|
||||
{
|
||||
let form = read_form(&member)?;
|
||||
append_project_source(&mut source, &form.code, &mut symbols);
|
||||
forms.push(form);
|
||||
} else {
|
||||
let text = std::fs::read_to_string(&member)
|
||||
.map_err(|error| format!("{}: {error}", member.display()))?;
|
||||
let expanded = expand_includes(&member, &text, &mut Vec::new())?;
|
||||
append_project_source(&mut source, &expanded, &mut symbols);
|
||||
}
|
||||
}
|
||||
Ok((source, forms))
|
||||
}
|
||||
|
||||
fn compile(
|
||||
path_arg: Option<&String>,
|
||||
) -> Result<(PathBuf, tb_vm::bytecode::CompiledModule), ExitCode> {
|
||||
) -> Result<
|
||||
(
|
||||
PathBuf,
|
||||
tb_vm::bytecode::CompiledModule,
|
||||
Vec<tb_ui::frm::FormFile>,
|
||||
),
|
||||
ExitCode,
|
||||
> {
|
||||
let Some(path) = path_arg else {
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas>");
|
||||
eprintln!("Aufruf: tbc run|build|check <datei.bas|datei.frm|projekt.mak>");
|
||||
return Err(ExitCode::from(1));
|
||||
};
|
||||
let path = PathBuf::from(path);
|
||||
let source = match std::fs::read_to_string(&path) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("{}: {e}", path.display());
|
||||
let (source, forms) = match input_sources(&path) {
|
||||
Ok(input) => input,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
return Err(ExitCode::from(1));
|
||||
}
|
||||
};
|
||||
match tb_vm::compile_source(&module_name(&path), &source) {
|
||||
Ok(m) => Ok((path, m)),
|
||||
let compiled = if forms.is_empty() {
|
||||
tb_vm::compile_source(&module_name(&path), &source)
|
||||
} else {
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
for form in &forms {
|
||||
for object in form.catalog().objects {
|
||||
catalog.objects.push(object);
|
||||
}
|
||||
}
|
||||
tb_vm::compile_source_with_forms(&forms[0].root.name, &source, &catalog)
|
||||
};
|
||||
match compiled {
|
||||
Ok(m) => Ok((path, m, forms)),
|
||||
Err(diags) => {
|
||||
for d in &diags {
|
||||
eprintln!("{}:{d}", path.display());
|
||||
@@ -116,7 +324,7 @@ fn cmd_check(args: &[String]) -> ExitCode {
|
||||
}
|
||||
|
||||
fn cmd_build(args: &[String]) -> ExitCode {
|
||||
let (path, module) = match compile(args.first()) {
|
||||
let (path, module, _) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
@@ -134,36 +342,20 @@ fn cmd_build(args: &[String]) -> ExitCode {
|
||||
}
|
||||
|
||||
fn cmd_run(args: &[String]) -> ExitCode {
|
||||
let (_path, module) = match compile(args.first()) {
|
||||
Ok(x) => x,
|
||||
Err(code) => return code,
|
||||
};
|
||||
let mut vm = Vm::new(module);
|
||||
vm.rt.command = args[1..].join(" ");
|
||||
// Im Rohmodus ist Strg+C kein Signal mehr — der Abbruch kommt als
|
||||
// Ereignis und muss den Lauf beenden können.
|
||||
vm.set_poll_interrupt(true);
|
||||
// Der Rückfall auf UTC wird einmal beim Start gemeldet, nicht je
|
||||
// Zeitabfrage — ein stiller Wechsel wäre ein Verstoß gegen den
|
||||
// Guiding Principle.
|
||||
if vm.rt.zeitzone == tb_runtime::datetime::Zeitzone::Unbekannt {
|
||||
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
|
||||
}
|
||||
|
||||
// Ohne Terminal (Pipe, Skript, CI) läuft das Programm im PipeHost:
|
||||
// Eingabe zeilenweise von stdin, Ausgabe am Ende als Snapshot.
|
||||
let ereignis = match TerminalHost::new() {
|
||||
let result = match TerminalHost::new() {
|
||||
Ok(mut host) => {
|
||||
// Der Bildschirm folgt der Terminalgröße von Beginn an (80×25 ist
|
||||
// stets nur die untere Schranke, nie eine feste Größe).
|
||||
if let Ok((cols, rows)) = host.groesse() {
|
||||
vm.rt.screen.resize(cols, rows);
|
||||
}
|
||||
let e = vm.run(&mut host);
|
||||
let size = host.groesse().ok();
|
||||
let result = run_chain(args, &mut host, size);
|
||||
drop(host); // Alternativschirm verlassen, bevor gedruckt wird
|
||||
e
|
||||
result
|
||||
}
|
||||
Err(_) => vm.run(&mut PipeHost::new()),
|
||||
Err(_) => run_chain(args, &mut PipeHost::new(), None),
|
||||
};
|
||||
let (ereignis, vm) = match result {
|
||||
Ok(result) => result,
|
||||
Err(code) => return code,
|
||||
};
|
||||
print!("{}", tb_runtime::snapshot::text(&vm.rt.screen));
|
||||
// `LPRINT` sammelt im Druckerpuffer; am Programmende geht er in die
|
||||
@@ -197,6 +389,90 @@ fn cmd_run(args: &[String]) -> ExitCode {
|
||||
}
|
||||
// Ohne Debugger-Flags treten diese Ereignisse nicht auf.
|
||||
RunEvent::Breakpoint { .. } | RunEvent::Stepped { .. } => ExitCode::from(2),
|
||||
RunEvent::Restart { .. } => unreachable!("RUN wird vom Runner aufgelöst"),
|
||||
}
|
||||
}
|
||||
|
||||
fn run_target(current: &Path, program: &str) -> Result<PathBuf, String> {
|
||||
let base = current.parent().unwrap_or(Path::new("."));
|
||||
if Path::new(program).extension().is_some() {
|
||||
return relative_case_insensitive(base, program).map_err(|error| error.to_string());
|
||||
}
|
||||
for extension in ["bas", "frm", "mak"] {
|
||||
let candidate = format!("{program}.{extension}");
|
||||
if let Ok(path) = relative_case_insensitive(base, &candidate) {
|
||||
return Ok(path);
|
||||
}
|
||||
}
|
||||
Err(format!("{program}: Programm nicht gefunden"))
|
||||
}
|
||||
|
||||
fn run_chain(
|
||||
args: &[String],
|
||||
host: &mut dyn Host,
|
||||
size: Option<(usize, usize)>,
|
||||
) -> Result<(RunEvent, Vm), ExitCode> {
|
||||
let Some(first) = args.first() else {
|
||||
eprintln!("Aufruf: tbc run <datei.bas|datei.frm|projekt.mak>");
|
||||
return Err(ExitCode::from(1));
|
||||
};
|
||||
let mut current = PathBuf::from(first);
|
||||
let mut start_line = None;
|
||||
let command = args[1..].join(" ");
|
||||
loop {
|
||||
let current_arg = current.display().to_string();
|
||||
let (path, module, forms) = compile(Some(¤t_arg))?;
|
||||
let mut vm = Vm::new(module);
|
||||
if !forms.is_empty() {
|
||||
let applied = forms.iter().try_for_each(|form| form.apply(&mut vm.forms));
|
||||
let first = vm
|
||||
.forms
|
||||
.objects
|
||||
.iter()
|
||||
.position(|object| {
|
||||
object
|
||||
.description
|
||||
.name
|
||||
.eq_ignore_ascii_case(&forms[0].root.name)
|
||||
})
|
||||
.unwrap() as u16;
|
||||
if let Err(error) = applied.and_then(|_| vm.forms.show(first, false).map(|_| ())) {
|
||||
eprintln!("Runtime error {}: {}", error.0, error);
|
||||
return Err(ExitCode::from(2));
|
||||
}
|
||||
}
|
||||
if let Some(line) = start_line.take() {
|
||||
if let Err(error) = vm.start_at_line(line) {
|
||||
eprintln!("Runtime error {}: {}", error.0, error);
|
||||
return Err(ExitCode::from(2));
|
||||
}
|
||||
}
|
||||
if let Some((cols, rows)) = size {
|
||||
vm.rt.screen.resize(cols, rows);
|
||||
}
|
||||
vm.rt.command.clone_from(&command);
|
||||
vm.set_poll_interrupt(true);
|
||||
if vm.rt.zeitzone == tb_runtime::datetime::Zeitzone::Unbekannt {
|
||||
eprintln!("Zeitzone nicht ermittelbar — Zeitfunktionen rechnen in UTC.");
|
||||
}
|
||||
let event = vm.run(host);
|
||||
let event = if event == RunEvent::Ended && vm.forms.has_visible_forms() {
|
||||
vm.run_visible_forms(host)
|
||||
} else {
|
||||
event
|
||||
};
|
||||
match event {
|
||||
RunEvent::Restart { program, line } => {
|
||||
if let Some(program) = program {
|
||||
current = run_target(&path, &program).map_err(|error| {
|
||||
eprintln!("{error}");
|
||||
ExitCode::from(1)
|
||||
})?;
|
||||
}
|
||||
start_line = line;
|
||||
}
|
||||
event => return Ok((event, vm)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,11 +487,30 @@ struct PipeHost {
|
||||
|
||||
impl PipeHost {
|
||||
fn new() -> Self {
|
||||
PipeHost {
|
||||
let mut host = PipeHost {
|
||||
puffer: std::collections::VecDeque::new(),
|
||||
eof: false,
|
||||
start: std::time::Instant::now(),
|
||||
};
|
||||
use std::io::{IsTerminal, Read};
|
||||
let mut stdin = std::io::stdin();
|
||||
if !stdin.is_terminal() {
|
||||
let mut input = String::new();
|
||||
let _ = stdin.read_to_string(&mut input);
|
||||
for line in input.split_inclusive('\n') {
|
||||
for character in line.trim_end_matches(['\r', '\n']).chars() {
|
||||
host.puffer
|
||||
.push_back(Ereignis::Taste(character.to_string(), 0));
|
||||
}
|
||||
host.puffer.push_back(Ereignis::Taste(
|
||||
tb_runtime::host::taste::ENTER.to_string(),
|
||||
0,
|
||||
));
|
||||
}
|
||||
host.puffer.push_back(Ereignis::Ende);
|
||||
host.eof = true;
|
||||
}
|
||||
host
|
||||
}
|
||||
|
||||
/// Eine Zeile von stdin in Tastendrücke zerlegen.
|
||||
|
||||
@@ -13,9 +13,10 @@
|
||||
//! nur die untere Schranke, nie eine feste Größe, und der Korpus muss das
|
||||
//! nachweisen können.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tb_runtime::host::CaptureHost;
|
||||
use tb_runtime::host::{CaptureHost, Host};
|
||||
use tb_vm::interp::{RunEvent, Vm};
|
||||
|
||||
fn compat_dir() -> PathBuf {
|
||||
@@ -100,10 +101,14 @@ fn resize_direktive(name: &str, src: &str) -> Option<(usize, (usize, usize))> {
|
||||
/// `<F1>`…`<F12>`, sonst je ein Zeichen; `+` davor bedeutet Umschalt,
|
||||
/// `^` Strg, `%` Alt — gebraucht für die benutzerdefinierten Trap-Tasten.
|
||||
fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
|
||||
use tb_runtime::host::{taste, umschalt, Ereignis};
|
||||
let Some(wert) = direktive(src, "tb-keys:") else {
|
||||
return Vec::new();
|
||||
};
|
||||
tastenfolge(name, &wert)
|
||||
}
|
||||
|
||||
fn tastenfolge(name: &str, wert: &str) -> Vec<tb_runtime::host::Ereignis> {
|
||||
use tb_runtime::host::{taste, umschalt, Ereignis};
|
||||
let mut out = Vec::new();
|
||||
let zeichen: Vec<char> = wert.chars().collect();
|
||||
let mut i = 0;
|
||||
@@ -143,6 +148,21 @@ fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
|
||||
"LEFT" => 0x4B,
|
||||
"RIGHT" => 0x4D,
|
||||
"DOWN" => 0x50,
|
||||
"ENTER" => {
|
||||
out.push(Ereignis::Taste(taste::ENTER.into(), shift));
|
||||
i += ende + 1;
|
||||
continue;
|
||||
}
|
||||
"ESC" => {
|
||||
out.push(Ereignis::Taste(taste::ESC.into(), shift));
|
||||
i += ende + 1;
|
||||
continue;
|
||||
}
|
||||
"TAB" => {
|
||||
out.push(Ereignis::Taste(taste::TAB.into(), shift));
|
||||
i += ende + 1;
|
||||
continue;
|
||||
}
|
||||
other => panic!("{name}: tb-keys kennt `<{other}>` nicht"),
|
||||
};
|
||||
out.push(Ereignis::Taste(taste::sonder(code), shift));
|
||||
@@ -155,6 +175,98 @@ fn tasten_direktive(name: &str, src: &str) -> Vec<tb_runtime::host::Ereignis> {
|
||||
out
|
||||
}
|
||||
|
||||
enum Ereignisschritt {
|
||||
Ereignis(tb_runtime::host::Ereignis),
|
||||
Zeit(u64),
|
||||
}
|
||||
|
||||
fn ereignis_direktive(name: &str, src: &str) -> Vec<Ereignisschritt> {
|
||||
use tb_runtime::host::{Ereignis, MausArt, MausEreignis};
|
||||
let Some(wert) = direktive(src, "tb-events:") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut out = Vec::new();
|
||||
for teil in wert.split('|').map(str::trim) {
|
||||
if let Some(keys) = teil.strip_prefix("key:") {
|
||||
out.extend(
|
||||
tastenfolge(name, keys)
|
||||
.into_iter()
|
||||
.map(Ereignisschritt::Ereignis),
|
||||
);
|
||||
} else if let Some(ms) = teil.strip_prefix("time:") {
|
||||
let ms = ms.trim().strip_suffix("ms").unwrap_or(ms.trim());
|
||||
out.push(Ereignisschritt::Zeit(
|
||||
ms.parse()
|
||||
.unwrap_or_else(|_| panic!("{name}: ungültige Zeit {ms:?}")),
|
||||
));
|
||||
} else if let Some(mouse) = teil.strip_prefix("mouse:") {
|
||||
let fields: Vec<_> = mouse.split(',').map(str::trim).collect();
|
||||
let [kind, button, shift, row, col] = fields.as_slice() else {
|
||||
panic!("{name}: mouse erwartet art,taste,shift,zeile,spalte");
|
||||
};
|
||||
let art = match *kind {
|
||||
"down" => MausArt::Druck,
|
||||
"up" => MausArt::Loslassen,
|
||||
"move" => MausArt::Bewegung,
|
||||
_ => panic!("{name}: unbekannte Mausart {kind:?}"),
|
||||
};
|
||||
let parse = |value: &str| {
|
||||
value
|
||||
.parse::<usize>()
|
||||
.unwrap_or_else(|_| panic!("{name}: ungültige Zahl {value:?}"))
|
||||
};
|
||||
out.push(Ereignisschritt::Ereignis(Ereignis::Maus(MausEreignis {
|
||||
art,
|
||||
taste: parse(button) as u8,
|
||||
shift: parse(shift) as u8,
|
||||
zeile: parse(row),
|
||||
spalte: parse(col),
|
||||
})));
|
||||
} else {
|
||||
panic!("{name}: unbekannter tb-events-Schritt {teil:?}");
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
struct EreignisHost {
|
||||
inner: CaptureHost,
|
||||
schritte: VecDeque<Ereignisschritt>,
|
||||
pause: bool,
|
||||
}
|
||||
|
||||
impl tb_runtime::host::Host for EreignisHost {
|
||||
fn present(&mut self, screen: &tb_runtime::screen::TextScreen) {
|
||||
self.inner.present(screen);
|
||||
}
|
||||
|
||||
fn next_event(&mut self, blockierend: bool) -> Option<tb_runtime::host::Ereignis> {
|
||||
if !blockierend && self.pause {
|
||||
self.pause = false;
|
||||
return None;
|
||||
}
|
||||
loop {
|
||||
match self.schritte.pop_front() {
|
||||
Some(Ereignisschritt::Zeit(ms)) => {
|
||||
self.inner.uhr_vorruecken(ms);
|
||||
if !blockierend {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(Ereignisschritt::Ereignis(event)) => {
|
||||
self.pause = true;
|
||||
return Some(event);
|
||||
}
|
||||
None => return self.inner.next_event(blockierend),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn jetzt_ms(&mut self) -> u64 {
|
||||
self.inner.jetzt_ms()
|
||||
}
|
||||
}
|
||||
|
||||
/// Virtuelle Uhr: `' tb-clock: <n>ms/Zustellpunkt`. Ohne die Direktive
|
||||
/// steht die Uhr des Testhosts still — ein Programm mit Zeit-Traps liefe
|
||||
/// dann endlos. Der Harness weist es deshalb ab, statt auf die Systemuhr
|
||||
@@ -242,45 +354,77 @@ impl Drop for TempVerzeichnis {
|
||||
|
||||
/// Korpusdatei ausführen und den Bildschirm-Snapshot liefern.
|
||||
fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
|
||||
let src = std::fs::read_to_string(path).unwrap();
|
||||
let raw = std::fs::read_to_string(path).unwrap();
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let modul_name = path.file_stem().unwrap().to_string_lossy().to_uppercase();
|
||||
let (cols, rows) = groesse.unwrap_or_else(|| screen_groessen(&name, &src)[0]);
|
||||
let module = tb_vm::compile_source(&modul_name, &src)
|
||||
let form = if path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
|
||||
{
|
||||
Some(tb_ui::frm::read_text(&name, &raw).unwrap_or_else(|error| panic!("{error}")))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let src = form
|
||||
.as_ref()
|
||||
.map_or(raw.as_str(), |form| form.code.as_str());
|
||||
let modul_name = form.as_ref().map_or_else(
|
||||
|| path.file_stem().unwrap().to_string_lossy().to_uppercase(),
|
||||
|form| form.root.name.to_uppercase(),
|
||||
);
|
||||
let (cols, rows) = groesse.unwrap_or_else(|| screen_groessen(&name, src)[0]);
|
||||
let module = match &form {
|
||||
Some(form) => tb_vm::compile_source_with_forms(&modul_name, src, &form.catalog()),
|
||||
None => tb_vm::compile_source(&modul_name, src),
|
||||
}
|
||||
.unwrap_or_else(|d| panic!("{}: Compile-Fehler: {d:?}", path.display()));
|
||||
let mut vm = Vm::new(module);
|
||||
if let Some(form) = &form {
|
||||
form.apply(&mut vm.forms).unwrap();
|
||||
vm.forms.show(0, false).unwrap();
|
||||
}
|
||||
vm.rt.screen.resize(cols, rows);
|
||||
if let Some(z) = tz_direktive(&name, &src) {
|
||||
if let Some(z) = tz_direktive(&name, src) {
|
||||
vm.rt.zeitzone = z;
|
||||
}
|
||||
// `' tb-tempdir` — das Programm arbeitet mit Dateien.
|
||||
let _temp = direktive(&src, "tb-tempdir").map(|_| TempVerzeichnis::neu(&name));
|
||||
let _temp = direktive(src, "tb-tempdir").map(|_| TempVerzeichnis::neu(&name));
|
||||
let mut host = CaptureHost::default();
|
||||
if let Some((nach, (c, r))) = resize_direktive(&name, &src) {
|
||||
if let Some((nach, (c, r))) = resize_direktive(&name, src) {
|
||||
host.ereignis_nach(
|
||||
nach,
|
||||
tb_runtime::host::Ereignis::Groesse { cols: c, rows: r },
|
||||
);
|
||||
}
|
||||
for t in tasten_direktive(&name, &src) {
|
||||
for t in tasten_direktive(&name, src) {
|
||||
host.ereignis(t);
|
||||
}
|
||||
let schritt = clock_schritt(&name, &src);
|
||||
let schritt = clock_schritt(&name, src);
|
||||
let schritte = ereignis_direktive(&name, src);
|
||||
if !schritte.is_empty() {
|
||||
assert!(
|
||||
schritt.is_none(),
|
||||
"{name}: tb-events: time und tb-clock nicht mischen"
|
||||
);
|
||||
let mut host = EreignisHost {
|
||||
inner: host,
|
||||
schritte: schritte.into(),
|
||||
pause: false,
|
||||
};
|
||||
return run_to_snapshot(path, &mut vm, &mut host);
|
||||
}
|
||||
if let Some(schritt_ms) = schritt {
|
||||
let mut host = UhrHost {
|
||||
inner: host,
|
||||
schritt_ms,
|
||||
};
|
||||
return match vm.run(&mut host) {
|
||||
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
|
||||
other => panic!(
|
||||
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
|
||||
path.display(),
|
||||
tb_runtime::snapshot::snapshot(&vm.rt.screen)
|
||||
),
|
||||
};
|
||||
return run_to_snapshot(path, &mut vm, &mut host);
|
||||
}
|
||||
match vm.run(&mut host) {
|
||||
run_to_snapshot(path, &mut vm, &mut host)
|
||||
}
|
||||
|
||||
fn run_to_snapshot(path: &Path, vm: &mut Vm, host: &mut dyn Host) -> String {
|
||||
match vm.run(host) {
|
||||
RunEvent::Ended => tb_runtime::snapshot::snapshot(&vm.rt.screen),
|
||||
other => panic!(
|
||||
"{}: unerwartetes Laufzeitende {other:?}\nBildschirm bisher:\n{}",
|
||||
@@ -290,6 +434,21 @@ fn run_corpus_file(path: &Path, groesse: Option<(usize, usize)>) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn source_for_directives(path: &Path) -> String {
|
||||
let raw = std::fs::read_to_string(path).unwrap();
|
||||
if path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.is_some_and(|ext| ext.eq_ignore_ascii_case("frm"))
|
||||
{
|
||||
tb_ui::frm::read_text(&path.display().to_string(), &raw)
|
||||
.unwrap()
|
||||
.code
|
||||
} else {
|
||||
raw
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshots vergleichen: erst das Textbild, dann die Attributebene.
|
||||
/// Bei Abweichung im Text zählt die Zeile, bei Attributen Zeile und Spalte.
|
||||
fn assert_output_matches(file: &str, want: &str, got: &str) {
|
||||
@@ -365,12 +524,16 @@ fn korpus_laeuft_mit_korrekter_ausgabe() {
|
||||
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.expect("tests/compat fehlt")
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|ext| matches!(ext, "bas" | "frm"))
|
||||
})
|
||||
.collect();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let src = std::fs::read_to_string(&path).unwrap();
|
||||
let src = source_for_directives(&path);
|
||||
let groessen = screen_groessen(&name, &src);
|
||||
for (cols, rows) in &groessen {
|
||||
// Bei mehreren Größen trägt jede ihre eigene Sollausgabe.
|
||||
@@ -406,6 +569,25 @@ fn uhr_direktive_wird_gelesen() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ereignisfolge_liefert_taste_maus_und_zeit_in_reihenfolge() {
|
||||
let steps = ereignis_direktive(
|
||||
"form.frm",
|
||||
"' tb-events: key:%o | mouse:down,1,0,5,7 | time:250ms | key:<ENTER>",
|
||||
);
|
||||
assert_eq!(steps.len(), 4);
|
||||
assert!(
|
||||
matches!(&steps[0], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Taste(key, shift)) if key == "o" && *shift == 4)
|
||||
);
|
||||
assert!(
|
||||
matches!(&steps[1], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Maus(mouse)) if mouse.zeile == 5 && mouse.spalte == 7)
|
||||
);
|
||||
assert!(matches!(steps[2], Ereignisschritt::Zeit(250)));
|
||||
assert!(
|
||||
matches!(&steps[3], Ereignisschritt::Ereignis(tb_runtime::host::Ereignis::Taste(key, _)) if key == tb_runtime::host::taste::ENTER)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "deklariert aber keinen Zeitverlauf")]
|
||||
fn zeittrap_ohne_uhr_direktive_wird_abgewiesen() {
|
||||
@@ -467,6 +649,40 @@ fn tbc_build_erzeugt_tbc() {
|
||||
let _ = std::fs::remove_file(&tbc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tbc_loest_include_projekt_und_run_datei_auf() {
|
||||
let exe = env!("CARGO_BIN_EXE_tbc");
|
||||
let dir = std::env::temp_dir().join(format!("tb_project_{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::fs::write(dir.join("werte.bi"), "CONST N = 7\n").unwrap();
|
||||
std::fs::write(
|
||||
dir.join("main.bas"),
|
||||
"'$INCLUDE: 'WERTE.BI'\nDECLARE SUB Ausgabe\nAusgabe\nEND\n",
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(dir.join("lib.bas"), "SUB Ausgabe\nPRINT N\nEND SUB\n").unwrap();
|
||||
std::fs::write(dir.join("app.mak"), "MAIN.BAS\nLIB.BAS\n").unwrap();
|
||||
let project = Command::new(exe)
|
||||
.args(["run"])
|
||||
.arg(dir.join("app.mak"))
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(project.status.success(), "{project:?}");
|
||||
assert_eq!(String::from_utf8_lossy(&project.stdout), " 7 \n");
|
||||
|
||||
std::fs::write(dir.join("start.bas"), "RUN \"ZIEL\"\n").unwrap();
|
||||
std::fs::write(dir.join("ziel.bas"), "PRINT \"weiter\"\nEND\n").unwrap();
|
||||
let chained = Command::new(exe)
|
||||
.args(["run"])
|
||||
.arg(dir.join("start.bas"))
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(chained.status.success(), "{chained:?}");
|
||||
assert_eq!(String::from_utf8_lossy(&chained.stdout), "weiter\n");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
// ---- Harness meldet Abweichungen benannt (Spec kompat-testkorpus) ----------
|
||||
|
||||
fn meldung(want: &str, got: &str) -> String {
|
||||
@@ -537,12 +753,16 @@ fn erzeuge_sollausgaben() {
|
||||
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
|
||||
.expect("tests/compat fehlt")
|
||||
.map(|e| e.unwrap().path())
|
||||
.filter(|p| p.extension().and_then(|e| e.to_str()) == Some("bas"))
|
||||
.filter(|p| {
|
||||
p.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.is_some_and(|ext| matches!(ext, "bas" | "frm"))
|
||||
})
|
||||
.collect();
|
||||
entries.sort();
|
||||
for path in entries {
|
||||
let name = path.file_name().unwrap().to_string_lossy().to_string();
|
||||
let src = std::fs::read_to_string(&path).unwrap();
|
||||
let src = source_for_directives(&path);
|
||||
let groessen = screen_groessen(&name, &src);
|
||||
for (cols, rows) in &groessen {
|
||||
let out_path = if groessen.len() == 1 {
|
||||
|
||||
@@ -480,6 +480,26 @@ pub enum Stmt {
|
||||
bottom: Option<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
GraphicsLine {
|
||||
from: Option<(Expr, Expr)>,
|
||||
to: (Expr, Expr),
|
||||
relative: bool,
|
||||
color: Option<Expr>,
|
||||
fill: u8,
|
||||
pos: SourcePos,
|
||||
},
|
||||
GraphicsPaint {
|
||||
point: (Expr, Expr),
|
||||
paint: Option<Expr>,
|
||||
border: Option<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
GraphicsView {
|
||||
rect: Option<(Expr, Expr, Expr, Expr)>,
|
||||
fill: Option<Expr>,
|
||||
border: Option<Expr>,
|
||||
pos: SourcePos,
|
||||
},
|
||||
/// `ON TIMER(n&) GOSUB ziel`, `ON KEY(n%) GOSUB ziel`,
|
||||
/// `ON UEVENT GOSUB ziel`, `ON SIGNAL(n%) GOSUB ziel` — die
|
||||
/// klassischen Ereignis-Traps (Sprachreferenz §8). `GOSUB 0` schaltet
|
||||
|
||||
@@ -21,10 +21,11 @@ pub enum ObjectClass {
|
||||
Timer,
|
||||
VScrollBar,
|
||||
Screen,
|
||||
Spin,
|
||||
}
|
||||
|
||||
impl ObjectClass {
|
||||
pub const ALL: [Self; 18] = [
|
||||
pub const ALL: [Self; 19] = [
|
||||
Self::Form,
|
||||
Self::CheckBox,
|
||||
Self::ComboBox,
|
||||
@@ -43,6 +44,7 @@ impl ObjectClass {
|
||||
Self::Timer,
|
||||
Self::VScrollBar,
|
||||
Self::Screen,
|
||||
Self::Spin,
|
||||
];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
@@ -65,6 +67,7 @@ impl ObjectClass {
|
||||
Self::Timer => "TIMER",
|
||||
Self::VScrollBar => "VSCROLLBAR",
|
||||
Self::Screen => "SCREEN",
|
||||
Self::Spin => "SPIN",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +91,7 @@ impl ObjectClass {
|
||||
Self::Timer => "Timer",
|
||||
Self::VScrollBar => "VScrollBar",
|
||||
Self::Screen => "Screen",
|
||||
Self::Spin => "Spin",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,7 +299,7 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
|
||||
string("TEXT", ""),
|
||||
]),
|
||||
CheckBox => p.extend([string("CAPTION", ""), range("VALUE", 0, 0, 2)]),
|
||||
OptionButton => p.extend([string("CAPTION", ""), range("VALUE", 0, -1, 0)]),
|
||||
OptionButton => p.extend([string("CAPTION", ""), range("VALUE", 0, -1, 1)]),
|
||||
Frame => p.push(string("CAPTION", "")),
|
||||
Label => p.extend([
|
||||
range("ALIGNMENT", 0, 0, 2),
|
||||
@@ -311,6 +315,21 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
|
||||
range("SMALLCHANGE", 1, 1, 32767),
|
||||
range("VALUE", 0, -32768, 32767),
|
||||
]),
|
||||
Spin => {
|
||||
p.extend([
|
||||
ro("BORDERSTYLE", PropertyType::Integer),
|
||||
range("INTERVAL", 250, 0, 65535),
|
||||
range("MIN", 0, -32768, 32767),
|
||||
range("MAX", 32767, -32768, 32767),
|
||||
range("STYLE", 0, 0, 1),
|
||||
range("VALUE", 0, -32768, 32767),
|
||||
]);
|
||||
for name in ["HEIGHT", "WIDTH"] {
|
||||
if let Some(property) = p.iter_mut().find(|property| property.name == name) {
|
||||
property.writable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
PictureBox => p.extend([
|
||||
boolp("AUTOREDRAW", false),
|
||||
range("BORDERSTYLE", 1, 0, 2),
|
||||
@@ -337,10 +356,26 @@ pub fn properties(class: ObjectClass) -> Vec<PropertySpec> {
|
||||
boolp("SEPARATOR", false),
|
||||
string("TAG", ""),
|
||||
boolp("VISIBLE", true),
|
||||
string("SHORTCUT", ""),
|
||||
]),
|
||||
DirListBox => p.extend([
|
||||
ro("LIST", PropertyType::String),
|
||||
ro("LISTCOUNT", PropertyType::Integer),
|
||||
int("LISTINDEX", -1),
|
||||
string("PATH", ""),
|
||||
string("TEXT", ""),
|
||||
]),
|
||||
DriveListBox => p.extend([
|
||||
ro("LIST", PropertyType::String),
|
||||
ro("LISTCOUNT", PropertyType::Integer),
|
||||
int("LISTINDEX", -1),
|
||||
string("DRIVE", ""),
|
||||
string("TEXT", ""),
|
||||
]),
|
||||
DirListBox => p.extend([string("PATH", ""), string("TEXT", "")]),
|
||||
DriveListBox => p.extend([string("DRIVE", ""), string("TEXT", "")]),
|
||||
FileListBox => p.extend([
|
||||
ro("LIST", PropertyType::String),
|
||||
ro("LISTCOUNT", PropertyType::Integer),
|
||||
int("LISTINDEX", -1),
|
||||
string("FILENAME", ""),
|
||||
string("PATH", ""),
|
||||
string("PATTERN", "*.*"),
|
||||
@@ -413,6 +448,7 @@ pub fn methods(class: ObjectClass) -> &'static [&'static str] {
|
||||
&["DRAG", "MOVE", "REFRESH", "SETFOCUS"]
|
||||
}
|
||||
Frame | Label | HScrollBar | VScrollBar => &["DRAG", "MOVE", "REFRESH"],
|
||||
Spin => &["DRAG", "REFRESH", "SETFOCUS"],
|
||||
PictureBox => &[
|
||||
"CLS",
|
||||
"DRAG",
|
||||
@@ -429,11 +465,7 @@ pub fn methods(class: ObjectClass) -> &'static [&'static str] {
|
||||
}
|
||||
|
||||
pub fn method_is_implemented(class: ObjectClass, name: &str) -> bool {
|
||||
matches!(
|
||||
(class, name),
|
||||
(ObjectClass::Form, "HIDE" | "LOAD" | "SHOW" | "UNLOAD")
|
||||
| (ObjectClass::Screen, "HIDE" | "SHOW")
|
||||
)
|
||||
methods(class).contains(&name)
|
||||
}
|
||||
|
||||
pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> {
|
||||
@@ -441,6 +473,22 @@ pub fn method_arity(class: ObjectClass, name: &str) -> Option<(usize, usize)> {
|
||||
(ObjectClass::Form, "SHOW") => Some((0, 1)),
|
||||
(ObjectClass::Form, "HIDE" | "LOAD" | "UNLOAD")
|
||||
| (ObjectClass::Screen, "HIDE" | "SHOW") => Some((0, 0)),
|
||||
(_, "REFRESH" | "SETFOCUS" | "CLS" | "PRINTFORM") => Some((0, 0)),
|
||||
(_, "DRAG") => Some((1, 1)),
|
||||
(_, "MOVE") => Some((2, 4)),
|
||||
(ObjectClass::ListBox | ObjectClass::ComboBox, "ADDITEM") => Some((1, 2)),
|
||||
(ObjectClass::ListBox | ObjectClass::ComboBox, "REMOVEITEM") => Some((1, 1)),
|
||||
(ObjectClass::Form | ObjectClass::PictureBox, "PRINT") => Some((0, usize::MAX)),
|
||||
(ObjectClass::Form | ObjectClass::PictureBox, "TEXTWIDTH" | "TEXTHEIGHT") => Some((1, 1)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn method_return_type(class: ObjectClass, name: &str) -> Option<PropertyType> {
|
||||
match (class, name) {
|
||||
(ObjectClass::Form | ObjectClass::PictureBox, "TEXTWIDTH" | "TEXTHEIGHT") => {
|
||||
Some(PropertyType::Integer)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -558,6 +606,16 @@ pub fn events(class: ObjectClass) -> &'static [&'static str] {
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
],
|
||||
Spin => &[
|
||||
"CUSTOM",
|
||||
"DRAGDROP",
|
||||
"DRAGOVER",
|
||||
"GOTFOCUS",
|
||||
"KEYDOWN",
|
||||
"KEYPRESS",
|
||||
"KEYUP",
|
||||
"LOSTFOCUS",
|
||||
],
|
||||
PictureBox => &[
|
||||
"CLICK",
|
||||
"DBLCLICK",
|
||||
@@ -608,6 +666,7 @@ pub fn event_params(event: &str) -> Option<&'static [(&'static str, EventParamTy
|
||||
("STATE", Integer),
|
||||
],
|
||||
"UNLOAD" => &[("CANCEL", Integer)],
|
||||
"CUSTOM" => &[("EVENTTYPE", Integer)],
|
||||
"CLICK" | "DBLCLICK" | "CHANGE" | "DROPDOWN" | "GOTFOCUS" | "LOSTFOCUS" | "LOAD"
|
||||
| "PAINT" | "RESIZE" | "TIMER" | "PATHCHANGE" | "PATTERNCHANGE" => &[],
|
||||
_ => return None,
|
||||
@@ -652,4 +711,18 @@ impl FormCatalog {
|
||||
.find(|(_, o)| o.name.eq_ignore_ascii_case(name))
|
||||
.map(|(i, o)| (i as u16, o))
|
||||
}
|
||||
|
||||
pub fn belongs_to(&self, object: &FormObject, form: &str) -> bool {
|
||||
let mut parent = object.parent_form.as_deref();
|
||||
for _ in 0..self.objects.len() {
|
||||
let Some(name) = parent else { return false };
|
||||
if name.eq_ignore_ascii_case(form) {
|
||||
return true;
|
||||
}
|
||||
parent = self
|
||||
.find(name)
|
||||
.and_then(|(_, object)| object.parent_form.as_deref());
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,13 @@ pub enum Builtin {
|
||||
IsamSavepoint,
|
||||
IsamSetmem,
|
||||
IsamBof,
|
||||
MsgBox,
|
||||
InputBoxS,
|
||||
ClipboardAdd,
|
||||
ClipboardGet,
|
||||
GraphicsLine,
|
||||
GraphicsPaint,
|
||||
GraphicsView,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -386,6 +393,20 @@ pub enum HExpr {
|
||||
property: u16,
|
||||
ty: HTy,
|
||||
},
|
||||
ObjectIndexedProperty {
|
||||
object: u16,
|
||||
object_index: Option<Box<HExpr>>,
|
||||
property: u16,
|
||||
index: Box<HExpr>,
|
||||
ty: HTy,
|
||||
},
|
||||
ObjectMethodCall {
|
||||
object: u16,
|
||||
index: Option<Box<HExpr>>,
|
||||
method: u16,
|
||||
args: Vec<HExpr>,
|
||||
ty: HTy,
|
||||
},
|
||||
DynamicObjectProperty {
|
||||
object: Box<HExpr>,
|
||||
property: String,
|
||||
@@ -511,6 +532,13 @@ pub enum HStmtKind {
|
||||
property: u16,
|
||||
value: HExpr,
|
||||
},
|
||||
SetObjectIndexedProperty {
|
||||
object: u16,
|
||||
object_index: Option<HExpr>,
|
||||
property: u16,
|
||||
index: HExpr,
|
||||
value: HExpr,
|
||||
},
|
||||
SetDynamicObjectProperty {
|
||||
object: HExpr,
|
||||
property: String,
|
||||
@@ -518,6 +546,7 @@ pub enum HStmtKind {
|
||||
},
|
||||
ObjectMethod {
|
||||
object: u16,
|
||||
index: Option<HExpr>,
|
||||
method: u16,
|
||||
args: Vec<HExpr>,
|
||||
},
|
||||
@@ -592,6 +621,10 @@ pub enum HStmtKind {
|
||||
targets: Vec<LabelId>,
|
||||
},
|
||||
ReturnGosub(Option<LabelId>),
|
||||
Run {
|
||||
target: Option<HExpr>,
|
||||
string: bool,
|
||||
},
|
||||
/// Ereignis-Trap erklären (Sprachreferenz §8). `art` ist die Quelle
|
||||
/// (0 KEY, 1 TIMER, 2 UEVENT, 3 SIGNAL), `index` ihre Kennung bzw. bei
|
||||
/// `TIMER` das Intervall in Sekunden. `ziel = None` = `GOSUB 0`.
|
||||
|
||||
@@ -750,8 +750,8 @@ mod tests {
|
||||
assert_eq!(kinds("40000")[0], TokenKind::Num(NumValue::Long(40000)));
|
||||
assert_eq!(kinds("1.5")[0], TokenKind::Num(NumValue::Single(1.5)));
|
||||
assert_eq!(
|
||||
kinds("3.14159265")[0],
|
||||
TokenKind::Num(NumValue::Double(3.14159265))
|
||||
kinds("1.23456789")[0],
|
||||
TokenKind::Num(NumValue::Double(1.23456789))
|
||||
);
|
||||
assert_eq!(kinds("1E3")[0], TokenKind::Num(NumValue::Single(1000.0)));
|
||||
assert_eq!(kinds("1D3")[0], TokenKind::Num(NumValue::Double(1000.0)));
|
||||
|
||||
@@ -28,7 +28,18 @@ pub fn parse(module_name: &str, tokens: &[Token]) -> ParseOutput {
|
||||
match p.k() {
|
||||
TokenKind::Eof => break,
|
||||
TokenKind::Kw(Kw::Sub) | TokenKind::Kw(Kw::Function) => {
|
||||
if let Some(proc) = p.parse_proc() {
|
||||
if let Some(proc) = p.parse_proc(false) {
|
||||
procs.push(proc);
|
||||
}
|
||||
}
|
||||
TokenKind::Kw(Kw::Static)
|
||||
if matches!(
|
||||
p.k_at(1),
|
||||
TokenKind::Kw(Kw::Sub) | TokenKind::Kw(Kw::Function)
|
||||
) =>
|
||||
{
|
||||
p.advance();
|
||||
if let Some(proc) = p.parse_proc(true) {
|
||||
procs.push(proc);
|
||||
}
|
||||
}
|
||||
@@ -244,10 +255,7 @@ impl<'a> P<'a> {
|
||||
self.advance();
|
||||
self.parse_input(true, pos)
|
||||
} else {
|
||||
// Grafikform LINE (x1,y1)-(x2,y2): deklariertes Non-Feature.
|
||||
self.err("Feature unavailable");
|
||||
self.sync();
|
||||
None
|
||||
self.parse_graphics_line(pos)
|
||||
}
|
||||
}
|
||||
TokenKind::Kw(Kw::If) => self.parse_if(pos),
|
||||
@@ -350,12 +358,8 @@ impl<'a> P<'a> {
|
||||
TokenKind::Kw(Kw::Erase) => {
|
||||
self.advance();
|
||||
let mut names = Vec::new();
|
||||
loop {
|
||||
if let Some(e) = self.parse_name_only() {
|
||||
while let Some(e) = self.parse_name_only() {
|
||||
names.push(e);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if !self.eat(&TokenKind::Comma) {
|
||||
break;
|
||||
}
|
||||
@@ -548,12 +552,8 @@ impl<'a> P<'a> {
|
||||
TokenKind::Kw(Kw::Read) => {
|
||||
self.advance();
|
||||
let mut vars = Vec::new();
|
||||
loop {
|
||||
if let Some(e) = self.parse_name_ref() {
|
||||
while let Some(e) = self.parse_name_ref() {
|
||||
vars.push(e);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if !self.eat(&TokenKind::Comma) {
|
||||
break;
|
||||
}
|
||||
@@ -733,6 +733,14 @@ impl<'a> P<'a> {
|
||||
}
|
||||
Some(Stmt::ViewPrint { top, bottom, pos })
|
||||
}
|
||||
TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} if name == "VIEW" => self.parse_graphics_view(pos),
|
||||
TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
} if name == "PAINT" => self.parse_graphics_paint(pos),
|
||||
TokenKind::Ident {
|
||||
ref name,
|
||||
suffix: None,
|
||||
@@ -832,6 +840,121 @@ impl<'a> P<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_graphics_point(&mut self) -> Option<(Expr, Expr)> {
|
||||
if !self.eat(&TokenKind::LParen) {
|
||||
self.err("Expected: (");
|
||||
return None;
|
||||
}
|
||||
let x = self.parse_expr()?;
|
||||
if !self.eat(&TokenKind::Comma) {
|
||||
self.err("Expected: ,");
|
||||
return None;
|
||||
}
|
||||
let y = self.parse_expr()?;
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
self.err("Expected: )");
|
||||
return None;
|
||||
}
|
||||
Some((x, y))
|
||||
}
|
||||
|
||||
fn parse_graphics_line(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
let from = if self.eat(&TokenKind::Minus) {
|
||||
None
|
||||
} else {
|
||||
let point = self.parse_graphics_point()?;
|
||||
if !self.eat(&TokenKind::Minus) {
|
||||
self.err("Expected: -");
|
||||
return None;
|
||||
}
|
||||
Some(point)
|
||||
};
|
||||
let relative = self.eat_kw(Kw::Step);
|
||||
let to = self.parse_graphics_point()?;
|
||||
let mut color = None;
|
||||
let mut fill = 0;
|
||||
if self.eat(&TokenKind::Comma) {
|
||||
if !matches!(
|
||||
self.k(),
|
||||
TokenKind::Comma | TokenKind::Colon | TokenKind::Eol | TokenKind::Eof
|
||||
) {
|
||||
color = Some(self.parse_expr()?);
|
||||
}
|
||||
if self.eat(&TokenKind::Comma) {
|
||||
if let TokenKind::Ident { name, .. } = self.k() {
|
||||
fill = match name.as_str() {
|
||||
"B" => 1,
|
||||
"BF" => 2,
|
||||
_ => {
|
||||
self.err("Expected: B or BF");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
self.advance();
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(Stmt::GraphicsLine {
|
||||
from,
|
||||
to,
|
||||
relative,
|
||||
color,
|
||||
fill,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_graphics_paint(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
self.advance();
|
||||
let point = self.parse_graphics_point()?;
|
||||
let paint = self
|
||||
.eat(&TokenKind::Comma)
|
||||
.then(|| self.parse_expr())
|
||||
.flatten();
|
||||
let border = self
|
||||
.eat(&TokenKind::Comma)
|
||||
.then(|| self.parse_expr())
|
||||
.flatten();
|
||||
Some(Stmt::GraphicsPaint {
|
||||
point,
|
||||
paint,
|
||||
border,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_graphics_view(&mut self, pos: SourcePos) -> Option<Stmt> {
|
||||
self.advance();
|
||||
if self.at_stmt_end() {
|
||||
return Some(Stmt::GraphicsView {
|
||||
rect: None,
|
||||
fill: None,
|
||||
border: None,
|
||||
pos,
|
||||
});
|
||||
}
|
||||
let (x1, y1) = self.parse_graphics_point()?;
|
||||
if !self.eat(&TokenKind::Minus) {
|
||||
self.err("Expected: -");
|
||||
return None;
|
||||
}
|
||||
let (x2, y2) = self.parse_graphics_point()?;
|
||||
let fill = self
|
||||
.eat(&TokenKind::Comma)
|
||||
.then(|| self.parse_expr())
|
||||
.flatten();
|
||||
let border = self
|
||||
.eat(&TokenKind::Comma)
|
||||
.then(|| self.parse_expr())
|
||||
.flatten();
|
||||
Some(Stmt::GraphicsView {
|
||||
rect: Some((x1, y1, x2, y2)),
|
||||
fill,
|
||||
border,
|
||||
pos,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_print(&mut self, pos: SourcePos, printer: bool) -> Option<Stmt> {
|
||||
self.advance(); // PRINT bzw. LPRINT
|
||||
let file = if !printer && self.eat(&TokenKind::Hash) {
|
||||
@@ -1056,12 +1179,8 @@ impl<'a> P<'a> {
|
||||
}
|
||||
}
|
||||
let mut vars = Vec::new();
|
||||
loop {
|
||||
if let Some(e) = self.parse_name_ref() {
|
||||
while let Some(e) = self.parse_name_ref() {
|
||||
vars.push(e);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if !self.eat(&TokenKind::Comma) {
|
||||
break;
|
||||
}
|
||||
@@ -1384,11 +1503,8 @@ impl<'a> P<'a> {
|
||||
return None;
|
||||
};
|
||||
let mut targets = Vec::new();
|
||||
loop {
|
||||
match self.parse_label_ref() {
|
||||
Some(t) => targets.push(t),
|
||||
None => break,
|
||||
}
|
||||
while let Some(t) = self.parse_label_ref() {
|
||||
targets.push(t);
|
||||
if !self.eat(&TokenKind::Comma) {
|
||||
break;
|
||||
}
|
||||
@@ -1432,11 +1548,7 @@ impl<'a> P<'a> {
|
||||
let dims = if self.eat(&TokenKind::LParen) {
|
||||
let mut ds = Vec::new();
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
loop {
|
||||
let a = match self.parse_expr() {
|
||||
Some(e) => e,
|
||||
None => break,
|
||||
};
|
||||
while let Some(a) = self.parse_expr() {
|
||||
if self.eat_kw(Kw::To) {
|
||||
match self.parse_expr() {
|
||||
Some(b) => ds.push((Some(a), b)),
|
||||
@@ -1602,8 +1714,7 @@ impl<'a> P<'a> {
|
||||
}
|
||||
};
|
||||
let mut params = Vec::new();
|
||||
if self.eat(&TokenKind::LParen) {
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
|
||||
loop {
|
||||
match self.k() {
|
||||
TokenKind::Ident {
|
||||
@@ -1640,7 +1751,6 @@ impl<'a> P<'a> {
|
||||
}
|
||||
self.eat(&TokenKind::RParen);
|
||||
}
|
||||
}
|
||||
Some(ProcSig {
|
||||
kind,
|
||||
name,
|
||||
@@ -1649,10 +1759,10 @@ impl<'a> P<'a> {
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_proc(&mut self) -> Option<Proc> {
|
||||
fn parse_proc(&mut self, leading_static: bool) -> Option<Proc> {
|
||||
let pos = self.pos();
|
||||
let sig = self.parse_proc_sig()?;
|
||||
let is_static = self.eat_kw(Kw::Static);
|
||||
let is_static = leading_static || self.eat_kw(Kw::Static);
|
||||
let end_kw = match sig.kind {
|
||||
ProcKind::Sub => Kw::Sub,
|
||||
ProcKind::Function => Kw::Function,
|
||||
@@ -1697,8 +1807,7 @@ impl<'a> P<'a> {
|
||||
}
|
||||
};
|
||||
let mut params = Vec::new();
|
||||
if self.eat(&TokenKind::LParen) {
|
||||
if !self.eat(&TokenKind::RParen) {
|
||||
if self.eat(&TokenKind::LParen) && !self.eat(&TokenKind::RParen) {
|
||||
loop {
|
||||
match self.k() {
|
||||
TokenKind::Ident {
|
||||
@@ -1724,7 +1833,6 @@ impl<'a> P<'a> {
|
||||
}
|
||||
self.eat(&TokenKind::RParen);
|
||||
}
|
||||
}
|
||||
if self.eat(&TokenKind::Eq) {
|
||||
let body = self.parse_expr()?;
|
||||
Some(Stmt::DefFn {
|
||||
@@ -1818,8 +1926,8 @@ impl<'a> P<'a> {
|
||||
other => Expr::Paren(Box::new(other)),
|
||||
})
|
||||
.collect();
|
||||
if !self.at_stmt_end() && call_args.is_empty() {
|
||||
call_args = self.parse_arg_list_to_stmt_end();
|
||||
if !self.at_stmt_end() && (call_args.is_empty() || name.contains('.')) {
|
||||
call_args.extend(self.parse_arg_list_to_stmt_end());
|
||||
}
|
||||
Some(Stmt::Call {
|
||||
name,
|
||||
@@ -1842,7 +1950,7 @@ impl<'a> P<'a> {
|
||||
match self.k() {
|
||||
TokenKind::Ident { mut name, suffix } => {
|
||||
self.advance();
|
||||
let args = if self.eat(&TokenKind::LParen) {
|
||||
let mut args = if self.eat(&TokenKind::LParen) {
|
||||
let a = self.parse_arg_list(&TokenKind::RParen);
|
||||
self.eat(&TokenKind::RParen);
|
||||
Some(a)
|
||||
@@ -1858,6 +1966,11 @@ impl<'a> P<'a> {
|
||||
name.push('.');
|
||||
name.push_str(&member);
|
||||
self.advance();
|
||||
if self.eat(&TokenKind::LParen) {
|
||||
let member_args = self.parse_arg_list(&TokenKind::RParen);
|
||||
self.eat(&TokenKind::RParen);
|
||||
args.get_or_insert_with(Vec::new).extend(member_args);
|
||||
}
|
||||
}
|
||||
_ => self.err("Expected: property"),
|
||||
}
|
||||
@@ -1935,6 +2048,7 @@ impl<'a> P<'a> {
|
||||
args.push(Expr::Missing);
|
||||
continue;
|
||||
}
|
||||
self.eat(&TokenKind::Hash);
|
||||
match self.parse_expr() {
|
||||
Some(e) => args.push(e),
|
||||
None => break,
|
||||
@@ -1985,11 +2099,7 @@ impl<'a> P<'a> {
|
||||
|
||||
fn parse_bin(&mut self, min_prec: u8) -> Option<Expr> {
|
||||
let mut lhs = self.parse_prefix()?;
|
||||
loop {
|
||||
let (prec, op) = match self.infix_op() {
|
||||
Some(x) => x,
|
||||
None => break,
|
||||
};
|
||||
while let Some((prec, op)) = self.infix_op() {
|
||||
if prec < min_prec {
|
||||
break;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -131,12 +131,12 @@ fn stand() -> Stand {
|
||||
let unsup_fn = abschnitt(
|
||||
&q,
|
||||
"// --- Spätere Phasen: dokumentiert",
|
||||
"(HExpr::Unsupported(name)",
|
||||
"(HExpr::Unsupported(\"Funktion\")",
|
||||
);
|
||||
let unsup_stmt = abschnitt(
|
||||
&q,
|
||||
"// Bildschirm-/Datei-/System-Anweisungen späterer Phasen.",
|
||||
r#"_ => "Anweisung""#,
|
||||
r#"HStmtKind::Unsupported("Anweisung")"#,
|
||||
);
|
||||
|
||||
let mut signaturen = literale(fn_tab);
|
||||
@@ -470,7 +470,6 @@ fn dokumentierte_elemente_werden_namentlich_abgewiesen() {
|
||||
("Y = POINT(1, 2)", "POINT", "Feature unavailable"),
|
||||
("CALLS Foo", "CALLS", "Feature unavailable"),
|
||||
// Non-Feature über die Syntax (teilt das Schlüsselwort)
|
||||
("LINE (1, 1)-(2, 2)", "LINE", "Feature unavailable"),
|
||||
(
|
||||
"GET (1, 1)-(2, 2), A",
|
||||
"GET (Grafik)",
|
||||
|
||||
@@ -63,6 +63,7 @@ pub struct RtState {
|
||||
rng: u32,
|
||||
rnd_last: f32,
|
||||
pub command: String,
|
||||
pub clipboard: String,
|
||||
}
|
||||
|
||||
impl Default for RtState {
|
||||
@@ -93,6 +94,7 @@ impl Default for RtState {
|
||||
rng: 0x50000,
|
||||
rnd_last: 0.0,
|
||||
command: String::new(),
|
||||
clipboard: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -438,7 +440,14 @@ pub mod ids {
|
||||
pub const ISAM_BOF: u16 = 147;
|
||||
/// `SetUEvent` — loest das benutzerdefinierte Ereignis aus (§8).
|
||||
pub const SETUEVENT: u16 = 148;
|
||||
pub const COUNT: u16 = 149;
|
||||
pub const MSGBOX: u16 = 149;
|
||||
pub const INPUTBOX_S: u16 = 150;
|
||||
pub const CLIPBOARD_ADD: u16 = 151;
|
||||
pub const CLIPBOARD_GET: u16 = 152;
|
||||
pub const GRAPHICS_LINE: u16 = 153;
|
||||
pub const GRAPHICS_PAINT: u16 = 154;
|
||||
pub const GRAPHICS_VIEW: u16 = 155;
|
||||
pub const COUNT: u16 = 156;
|
||||
}
|
||||
|
||||
/// Dispatch-Tabelle in Index-Reihenfolge.
|
||||
@@ -594,11 +603,35 @@ pub fn builtin_table() -> &'static [BuiltinFn] {
|
||||
bi_isam_setmem,
|
||||
bi_isam_bof,
|
||||
bi_setuevent,
|
||||
bi_msgbox,
|
||||
bi_inputbox,
|
||||
bi_clipboard_add,
|
||||
bi_clipboard_get,
|
||||
bi_graphics_line,
|
||||
bi_graphics_paint,
|
||||
bi_graphics_view,
|
||||
];
|
||||
debug_assert_eq!(TABLE.len(), ids::COUNT as usize);
|
||||
TABLE
|
||||
}
|
||||
|
||||
fn bi_clipboard_add(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
st.clipboard.push_str(&arg_str(a, 0)?);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_clipboard_get(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
Ok(Some(Value::Str(Rc::from(st.clipboard.as_str()))))
|
||||
}
|
||||
|
||||
// ---- Argument-Hilfen --------------------------------------------------------
|
||||
|
||||
fn arg_str(args: &[Value], i: usize) -> Result<Rc<str>, RuntimeError> {
|
||||
@@ -1881,17 +1914,90 @@ fn bi_view_print(
|
||||
}
|
||||
|
||||
fn bi_screen_stmt(
|
||||
_: &mut RtState,
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
// `SCREEN modus[, farbschalter[, aktiv[, sichtbar]]]`. Nur der Textmodus 0
|
||||
// existiert; Grafikmodi sind deklariertes Non-Feature. Die übrigen
|
||||
// Argumente betreffen Grafikseiten und bleiben folgenlos.
|
||||
// Die Terminaldarstellung bildet die historischen Pixelmodi auf Zellen ab;
|
||||
// Seitenargumente haben im einzelnen Puffer keine zusätzliche Wirkung.
|
||||
match opt(a, 0)? {
|
||||
None | Some(0) => Ok(None),
|
||||
Some(_) => Err(RuntimeError(73)), // Advanced feature unavailable
|
||||
None | Some(0..=13) => {
|
||||
st.screen.graphics_view(None).unwrap();
|
||||
st.screen.cls();
|
||||
Ok(None)
|
||||
}
|
||||
Some(_) => Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
||||
}
|
||||
}
|
||||
|
||||
fn bi_graphics_line(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let mut from = (arg_i32(a, 0)?, arg_i32(a, 1)?);
|
||||
if from.0 == i32::MIN {
|
||||
from = st.screen.graphics_position();
|
||||
}
|
||||
let mut to = (arg_i32(a, 2)?, arg_i32(a, 3)?);
|
||||
if arg_i32(a, 5)? != 0 {
|
||||
to.0 += from.0;
|
||||
to.1 += from.1;
|
||||
}
|
||||
let color = match arg_i32(a, 4)? {
|
||||
-1 => i32::from(st.screen.fg),
|
||||
color @ 0..=15 => color,
|
||||
_ => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
||||
};
|
||||
st.screen
|
||||
.graphics_line(from, to, color, arg_i32(a, 6)? as u8);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_graphics_paint(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let color = match a.get(2) {
|
||||
Some(Value::Int(v)) => i32::from(*v),
|
||||
Some(Value::Lng(v)) => *v,
|
||||
Some(Value::Str(_)) | None => i32::from(st.screen.fg),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
};
|
||||
if !(0..=15).contains(&color) {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
st.screen
|
||||
.graphics_paint(arg_i32(a, 0)?, arg_i32(a, 1)?, color);
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn bi_graphics_view(
|
||||
st: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
a: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
let x1 = arg_i32(a, 0)?;
|
||||
if x1 < 0 {
|
||||
st.screen.graphics_view(None).unwrap();
|
||||
return Ok(None);
|
||||
}
|
||||
let view = (x1, arg_i32(a, 1)?, arg_i32(a, 2)?, arg_i32(a, 3)?);
|
||||
st.screen
|
||||
.graphics_view(Some(view))
|
||||
.map_err(|_| RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
let fill = arg_i32(a, 4)?;
|
||||
if fill >= 0 {
|
||||
st.screen
|
||||
.graphics_line((view.0, view.1), (view.2, view.3), fill, 2);
|
||||
}
|
||||
let border = arg_i32(a, 5)?;
|
||||
if border >= 0 {
|
||||
st.screen
|
||||
.graphics_line((view.0, view.1), (view.2, view.3), border, 1);
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Nummer eines Funktionstasten-Makros → Index 0–11.
|
||||
@@ -2016,6 +2122,14 @@ fn bi_input_s(
|
||||
if n < 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
if a.len() > 1 {
|
||||
let nummer = arg_i32(a, 1)?;
|
||||
let datei = st.dateien.get(nummer)?;
|
||||
let bytes = datei.bytes_lesen(datei.position, n as usize)?;
|
||||
return Ok(Some(Value::Str(Rc::from(
|
||||
String::from_utf8_lossy(&bytes).as_ref(),
|
||||
))));
|
||||
}
|
||||
// Blockierend, ohne Echo auf dem Bildschirm.
|
||||
let mut s = String::new();
|
||||
for _ in 0..n {
|
||||
@@ -2546,6 +2660,22 @@ fn bi_isam_bof(
|
||||
Ok(Some(Value::Int(if st.isam.bof(n) { -1 } else { 0 })))
|
||||
}
|
||||
|
||||
fn bi_msgbox(
|
||||
_: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
Err(RuntimeError::FEATURE_UNAVAILABLE)
|
||||
}
|
||||
|
||||
fn bi_inputbox(
|
||||
_: &mut RtState,
|
||||
_: &mut dyn Host,
|
||||
_: &mut [Value],
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
Err(RuntimeError::FEATURE_UNAVAILABLE)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2635,7 +2765,7 @@ mod tests {
|
||||
h.ereignis(Ereignis::Signal(1));
|
||||
st.pump(&mut h, false);
|
||||
assert_eq!(st.traps.naechstes(), Some((Quelle::Signal(1), 22)));
|
||||
assert!(matches!(h.next_event(false), None));
|
||||
assert!(h.next_event(false).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,6 +24,9 @@ use unicode_width::UnicodeWidthChar;
|
||||
pub const MIN_COLS: usize = 80;
|
||||
pub const MIN_ROWS: usize = 25;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct ScreenError;
|
||||
|
||||
/// Eine Bildschirmzelle: Zeichen plus klassisches Farbattribut.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Cell {
|
||||
@@ -78,6 +81,9 @@ pub struct TextScreen {
|
||||
/// Seit der letzten Anzeige verändert? Der Host wird nur dann zum
|
||||
/// Neuzeichnen aufgefordert.
|
||||
veraendert: bool,
|
||||
graphic_x: i32,
|
||||
graphic_y: i32,
|
||||
graphic_view: Option<(i32, i32, i32, i32)>,
|
||||
}
|
||||
|
||||
impl Default for TextScreen {
|
||||
@@ -110,6 +116,9 @@ impl TextScreen {
|
||||
view_full: true,
|
||||
belegt: vec![None; rows],
|
||||
veraendert: true,
|
||||
graphic_x: 0,
|
||||
graphic_y: 0,
|
||||
graphic_view: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,8 +155,8 @@ impl TextScreen {
|
||||
}
|
||||
self.cells = cells;
|
||||
let mut belegt = vec![None; rows];
|
||||
for r in 0..self.rows.min(rows) {
|
||||
belegt[r] = self.belegt[r].map(|c| c.min(cols - 1));
|
||||
for (r, value) in belegt.iter_mut().enumerate().take(self.rows.min(rows)) {
|
||||
*value = self.belegt[r].map(|c| c.min(cols - 1));
|
||||
}
|
||||
self.belegt = belegt;
|
||||
self.cols = cols;
|
||||
@@ -225,9 +234,9 @@ impl TextScreen {
|
||||
}
|
||||
|
||||
/// `LOCATE zeile, spalte` (1-basiert); außerhalb → Err (Fehler 5).
|
||||
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ()> {
|
||||
pub fn locate(&mut self, row: usize, col: usize) -> Result<(), ScreenError> {
|
||||
if row < 1 || row > self.rows || col < 1 || col > self.cols {
|
||||
return Err(());
|
||||
return Err(ScreenError);
|
||||
}
|
||||
self.cur_row = row - 1;
|
||||
// Auf eine Fortsetzungszelle zu zeigen bedeutet den Zeichenanfang.
|
||||
@@ -251,9 +260,9 @@ impl TextScreen {
|
||||
}
|
||||
|
||||
/// `VIEW PRINT oben TO unten` (1-basiert).
|
||||
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ()> {
|
||||
pub fn view_print(&mut self, top: usize, bottom: usize) -> Result<(), ScreenError> {
|
||||
if top < 1 || bottom > self.rows || top > bottom {
|
||||
return Err(());
|
||||
return Err(ScreenError);
|
||||
}
|
||||
self.view_top = top - 1;
|
||||
self.view_bottom = bottom - 1;
|
||||
@@ -265,6 +274,122 @@ impl TextScreen {
|
||||
self.cells[(row - 1) * self.cols + (col - 1)]
|
||||
}
|
||||
|
||||
pub fn graphics_position(&self) -> (i32, i32) {
|
||||
(self.graphic_x, self.graphic_y)
|
||||
}
|
||||
|
||||
pub fn graphics_view(&mut self, view: Option<(i32, i32, i32, i32)>) -> Result<(), ScreenError> {
|
||||
if view.is_some_and(|(x1, y1, x2, y2)| x1 < 0 || y1 < 0 || x1 > x2 || y1 > y2) {
|
||||
return Err(ScreenError);
|
||||
}
|
||||
self.graphic_view = view;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn graphics_cell(&mut self, x: i32, y: i32, color: i32) {
|
||||
if x < 0
|
||||
|| y < 0
|
||||
|| self
|
||||
.graphic_view
|
||||
.is_some_and(|(x1, y1, x2, y2)| x < x1 || x > x2 || y < y1 || y > y2)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let (col, row) = (x as usize / 8, y as usize / 8);
|
||||
if col >= self.cols || row >= self.rows {
|
||||
return;
|
||||
}
|
||||
self.cells[row * self.cols + col] = Cell {
|
||||
ch: if color == 0 { ' ' } else { '█' },
|
||||
fg: color.clamp(0, 15) as u8,
|
||||
bg: self.bg,
|
||||
fortsetzung: false,
|
||||
};
|
||||
self.belegt[row] = Some(self.belegt[row].map_or(col, |last| last.max(col)));
|
||||
self.veraendert = true;
|
||||
}
|
||||
|
||||
pub fn graphics_line(&mut self, from: (i32, i32), to: (i32, i32), color: i32, fill: u8) {
|
||||
let (x1, y1) = from;
|
||||
let (x2, y2) = to;
|
||||
if fill == 2 {
|
||||
for y in (y1.min(y2)..=y1.max(y2)).step_by(8) {
|
||||
for x in (x1.min(x2)..=x1.max(x2)).step_by(8) {
|
||||
self.graphics_cell(x, y, color);
|
||||
}
|
||||
}
|
||||
} else if fill == 1 {
|
||||
self.graphics_segment(x1, y1, x2, y1, color);
|
||||
self.graphics_segment(x2, y1, x2, y2, color);
|
||||
self.graphics_segment(x2, y2, x1, y2, color);
|
||||
self.graphics_segment(x1, y2, x1, y1, color);
|
||||
} else {
|
||||
self.graphics_segment(x1, y1, x2, y2, color);
|
||||
}
|
||||
self.graphic_x = x2;
|
||||
self.graphic_y = y2;
|
||||
}
|
||||
|
||||
fn graphics_segment(&mut self, mut x: i32, mut y: i32, x2: i32, y2: i32, color: i32) {
|
||||
let dx = (x2 - x).abs();
|
||||
let sx = if x < x2 { 1 } else { -1 };
|
||||
let dy = -(y2 - y).abs();
|
||||
let sy = if y < y2 { 1 } else { -1 };
|
||||
let mut error = dx + dy;
|
||||
loop {
|
||||
self.graphics_cell(x, y, color);
|
||||
if x == x2 && y == y2 {
|
||||
break;
|
||||
}
|
||||
let twice = error * 2;
|
||||
if twice >= dy {
|
||||
error += dy;
|
||||
x += sx;
|
||||
}
|
||||
if twice <= dx {
|
||||
error += dx;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn graphics_paint(&mut self, x: i32, y: i32, color: i32) {
|
||||
let (start_col, start_row) = (x.max(0) as usize / 8, y.max(0) as usize / 8);
|
||||
if start_col >= self.cols || start_row >= self.rows {
|
||||
return;
|
||||
}
|
||||
let target = self.cells[start_row * self.cols + start_col].ch;
|
||||
let replacement = if color == 0 { ' ' } else { '█' };
|
||||
if target == replacement {
|
||||
return;
|
||||
}
|
||||
let mut pending = vec![(start_col, start_row)];
|
||||
while let Some((col, row)) = pending.pop() {
|
||||
let px = col as i32 * 8;
|
||||
let py = row as i32 * 8;
|
||||
if col >= self.cols
|
||||
|| row >= self.rows
|
||||
|| self.cells[row * self.cols + col].ch != target
|
||||
|| self
|
||||
.graphic_view
|
||||
.is_some_and(|(x1, y1, x2, y2)| px < x1 || px > x2 || py < y1 || py > y2)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.graphics_cell(px, py, color);
|
||||
if col > 0 {
|
||||
pending.push((col - 1, row));
|
||||
}
|
||||
if row > 0 {
|
||||
pending.push((col, row - 1));
|
||||
}
|
||||
pending.push((col + 1, row));
|
||||
pending.push((col, row + 1));
|
||||
}
|
||||
self.graphic_x = x;
|
||||
self.graphic_y = y;
|
||||
}
|
||||
|
||||
/// Text an der Cursorposition ausgeben: Umbruch am rechten Rand,
|
||||
/// Scrollen am unteren Rand des Scrollbereichs. `\n` bricht um,
|
||||
/// `\r` setzt an den Zeilenanfang.
|
||||
|
||||
@@ -316,7 +316,7 @@ fn tausender_gruppieren(s: &str) -> String {
|
||||
let z: Vec<char> = s.chars().collect();
|
||||
let mut out = String::new();
|
||||
for (i, c) in z.iter().enumerate() {
|
||||
if i > 0 && (z.len() - i) % 3 == 0 {
|
||||
if i > 0 && (z.len() - i).is_multiple_of(3) {
|
||||
out.push(',');
|
||||
}
|
||||
out.push(*c);
|
||||
@@ -433,7 +433,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn numerisches_feld_mit_nachkommastellen() {
|
||||
assert_eq!(u("###.##", &[z(3.14159)]), " 3.14");
|
||||
assert_eq!(u("###.##", &[z(3.126)]), " 3.13");
|
||||
assert_eq!(u("##.##", &[z(-1.5)]), "-1.50");
|
||||
}
|
||||
|
||||
|
||||
@@ -199,9 +199,7 @@ pub fn cur_to_i64(c: i64) -> i64 {
|
||||
// r in 0..10000; runde halb-zu-gerade
|
||||
if r > 5_000 {
|
||||
q + 1
|
||||
} else if r < 5_000 {
|
||||
q
|
||||
} else if q % 2 == 0 {
|
||||
} else if r < 5_000 || q % 2 == 0 {
|
||||
q
|
||||
} else {
|
||||
q + 1
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,9 +3,11 @@
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::fmt;
|
||||
|
||||
use tb_frontend::forms::{self, ObjectClass, PropertyDefault, PropertySpec, PropertyType};
|
||||
use tb_frontend::forms::{
|
||||
self, FormCatalog, ObjectClass, PropertyDefault, PropertySpec, PropertyType,
|
||||
};
|
||||
|
||||
use crate::forms::PropertyValue;
|
||||
use crate::forms::{FormsModel, PropertyValue};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FormNode {
|
||||
@@ -46,6 +48,67 @@ impl FormFile {
|
||||
original: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn catalog(&self) -> FormCatalog {
|
||||
fn add(node: &FormNode, parent: Option<&str>, catalog: &mut FormCatalog) {
|
||||
if let Some((id, _)) = catalog.find(&node.name) {
|
||||
catalog.objects[id as usize].array = true;
|
||||
} else {
|
||||
let array = forms::property(node.class, "INDEX")
|
||||
.and_then(|(property, _)| node.properties.get(&property))
|
||||
.is_some_and(
|
||||
|value| matches!(value, PropertyValue::Integer(index) if *index != 0),
|
||||
);
|
||||
catalog.add(&node.name, node.class, parent, array);
|
||||
}
|
||||
for child in &node.children {
|
||||
add(child, Some(&node.name), catalog);
|
||||
}
|
||||
}
|
||||
let mut catalog = FormCatalog::default();
|
||||
add(&self.root, None, &mut catalog);
|
||||
catalog
|
||||
}
|
||||
|
||||
pub fn apply(&self, model: &mut FormsModel) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||
fn apply_node(
|
||||
node: &FormNode,
|
||||
model: &mut FormsModel,
|
||||
menu_depth: usize,
|
||||
) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||
let menu_depth = if node.class == ObjectClass::Menu {
|
||||
menu_depth + 1
|
||||
} else {
|
||||
menu_depth
|
||||
};
|
||||
if menu_depth > 6 {
|
||||
return Err(tb_runtime::errors::RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
let object = model
|
||||
.objects
|
||||
.iter()
|
||||
.position(|candidate| candidate.description.name.eq_ignore_ascii_case(&node.name))
|
||||
.ok_or(tb_runtime::errors::RuntimeError(420))? as u16;
|
||||
let index = forms::property(node.class, "INDEX")
|
||||
.and_then(|(property, _)| node.properties.get(&property))
|
||||
.and_then(|value| match value {
|
||||
PropertyValue::Integer(value) => Some(*value),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(0);
|
||||
if index != 0 && !model.is_loaded_at(object, Some(index)) {
|
||||
model.load_design_array(object, index)?;
|
||||
}
|
||||
for (property, value) in &node.properties {
|
||||
model.set_initial_at(object, Some(index), *property, value.clone())?;
|
||||
}
|
||||
for child in &node.children {
|
||||
apply_node(child, model, menu_depth)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
apply_node(&self.root, model, 0)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -444,7 +507,7 @@ fn read_symbol_table(bytes: &[u8], mut at: usize) -> Result<Vec<BinarySymbol>, u
|
||||
let reference = read_u16(bytes, at).ok_or(at)?;
|
||||
let class_id = bytes[at + 2] & 0x7f;
|
||||
let length = bytes[at + 3] as usize;
|
||||
let class = ObjectClass::from_id(class_id).ok_or(at + 2)?;
|
||||
let mut class = ObjectClass::from_id(class_id).ok_or(at + 2)?;
|
||||
if length == 0 || length > 40 || at + 4 + length > bytes.len() {
|
||||
return Err(at + 3);
|
||||
}
|
||||
@@ -459,12 +522,18 @@ fn read_symbol_table(bytes: &[u8], mut at: usize) -> Result<Vec<BinarySymbol>, u
|
||||
if symbols.is_empty() && class != ObjectClass::Form {
|
||||
return Err(at + 2);
|
||||
}
|
||||
let name = String::from_utf8_lossy(name).into_owned();
|
||||
if class == ObjectClass::Screen
|
||||
&& matches!(name.to_ascii_uppercase().as_str(), "VSPIN" | "HSPIN")
|
||||
{
|
||||
class = ObjectClass::Spin;
|
||||
}
|
||||
symbols.push(BinarySymbol {
|
||||
offset: at,
|
||||
class,
|
||||
name: String::from_utf8_lossy(name).into_owned(),
|
||||
name,
|
||||
is_array: bytes[at + 2] & 0x80 != 0,
|
||||
unsupported: class_id == ObjectClass::Screen.id(),
|
||||
unsupported: class_id == ObjectClass::Screen.id() && class != ObjectClass::Spin,
|
||||
});
|
||||
at += 4 + length;
|
||||
if reference == 0 {
|
||||
@@ -501,7 +570,7 @@ fn record_len(class: ObjectClass) -> usize {
|
||||
ObjectClass::PictureBox => 31,
|
||||
ObjectClass::TextBox => 34,
|
||||
ObjectClass::Menu | ObjectClass::Frame => 26,
|
||||
ObjectClass::Screen => 80,
|
||||
ObjectClass::Screen | ObjectClass::Spin => 80,
|
||||
_ => 28,
|
||||
}
|
||||
}
|
||||
@@ -522,7 +591,7 @@ fn record_lengths(class: ObjectClass) -> &'static [usize] {
|
||||
ObjectClass::OptionButton => &[21, 28],
|
||||
ObjectClass::PictureBox => &[31],
|
||||
ObjectClass::HScrollBar | ObjectClass::VScrollBar => &[32],
|
||||
ObjectClass::Screen => &[73, 80],
|
||||
ObjectClass::Screen | ObjectClass::Spin => &[73, 80],
|
||||
ObjectClass::Timer => &[21, 28],
|
||||
}
|
||||
}
|
||||
@@ -627,7 +696,12 @@ fn walk_object_records(
|
||||
let next_symbol = bytes[header] as usize;
|
||||
if next_symbol >= symbols.len()
|
||||
|| bytes[header + 2] != 0
|
||||
|| bytes[header + 1] & 0x7f != symbols[next_symbol].class.id()
|
||||
|| bytes[header + 1] & 0x7f
|
||||
!= if symbols[next_symbol].class == ObjectClass::Spin {
|
||||
ObjectClass::Screen.id()
|
||||
} else {
|
||||
symbols[next_symbol].class.id()
|
||||
}
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -978,7 +1052,7 @@ fn assign_strings(
|
||||
) -> Result<Vec<BinaryWarning>, FormError> {
|
||||
let mut warnings = records
|
||||
.iter()
|
||||
.filter(|record| symbols[record.symbol].class == ObjectClass::Screen)
|
||||
.filter(|record| symbols[record.symbol].unsupported)
|
||||
.map(|record| BinaryWarning {
|
||||
offset: 0x20 + record.start,
|
||||
name: format!("{}.CustomControl", symbols[record.symbol].name),
|
||||
@@ -1063,10 +1137,9 @@ fn assign_strings(
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for record in records
|
||||
.iter()
|
||||
.filter(|record| symbols[record.symbol].unsupported)
|
||||
{
|
||||
for record in records.iter().filter(|record| {
|
||||
symbols[record.symbol].unsupported || symbols[record.symbol].class == ObjectClass::Spin
|
||||
}) {
|
||||
for offset in [17, 28, 37, 39, 53, 55, 64, 69] {
|
||||
let Some(pointer) =
|
||||
record_u16(bytes, record.start, record.end, offset).filter(|pointer| *pointer != 0)
|
||||
@@ -1319,6 +1392,35 @@ mod tests {
|
||||
assert_eq!(write_text(&form), EXAMPLE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_form_control_arrays_share_one_catalog_object_and_keep_indices() {
|
||||
let form = read_text(
|
||||
"array.frm",
|
||||
"VERSION 1.00\nBegin Form Form1\n Begin CommandButton Command1\n Index = 0\n Caption = \"null\"\n End\n Begin CommandButton Command1\n Index = 1\n Caption = \"eins\"\n End\nEnd\n",
|
||||
)
|
||||
.unwrap();
|
||||
let catalog = form.catalog();
|
||||
assert_eq!(catalog.objects.len(), 2);
|
||||
assert!(catalog.objects[1].array);
|
||||
let mut model = FormsModel::new(catalog.objects, 80, 25);
|
||||
form.apply(&mut model).unwrap();
|
||||
let caption = forms::property(ObjectClass::CommandButton, "CAPTION")
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(
|
||||
model.get_at(1, Some(0), caption).unwrap(),
|
||||
PropertyValue::String("null".into())
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_at(1, Some(1), caption).unwrap(),
|
||||
PropertyValue::String("eins".into())
|
||||
);
|
||||
model.show(0, false).unwrap();
|
||||
model.events.clear();
|
||||
model.object_method(1, "SETFOCUS", vec![]).unwrap();
|
||||
assert_eq!(model.next_event().unwrap().array_index, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_version_class_property_value_and_open_block_with_location() {
|
||||
let cases = [
|
||||
@@ -1502,6 +1604,18 @@ mod tests {
|
||||
assert_eq!(failure.name, "New.HEIGHT");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spin_symbol_wird_als_bedienbares_control_erkannt() {
|
||||
let mut bytes = Vec::new();
|
||||
bytes.extend_from_slice(&[1, 0, ObjectClass::Form.id(), 5]);
|
||||
bytes.extend_from_slice(b"Form1");
|
||||
bytes.extend_from_slice(&[0, 0, ObjectClass::Screen.id(), 5]);
|
||||
bytes.extend_from_slice(b"VSpin");
|
||||
let symbols = read_symbol_table(&bytes, 0).unwrap();
|
||||
assert_eq!(symbols[1].class, ObjectClass::Spin);
|
||||
assert!(!symbols[1].unsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_custom_control_is_named_and_not_emitted_as_screen() {
|
||||
let symbols = vec![
|
||||
@@ -1515,7 +1629,7 @@ mod tests {
|
||||
BinarySymbol {
|
||||
offset: 0,
|
||||
class: ObjectClass::Screen,
|
||||
name: "VSpin".into(),
|
||||
name: "Custom1".into(),
|
||||
is_array: false,
|
||||
unsupported: true,
|
||||
},
|
||||
@@ -1569,7 +1683,7 @@ mod tests {
|
||||
let root = build_tree(decoded).unwrap();
|
||||
|
||||
assert_eq!(warnings[0].offset, 0x46);
|
||||
assert_eq!(warnings[0].name, "VSpin.CustomControl");
|
||||
assert_eq!(warnings[0].name, "Custom1.CustomControl");
|
||||
assert!(root.children.is_empty());
|
||||
|
||||
let mut strings_with_orphan = (100..=108)
|
||||
|
||||
@@ -1317,7 +1317,7 @@ fn token_def(code: u16) -> Option<TokenDef> {
|
||||
}),
|
||||
0x138 => Some(TokenDef {
|
||||
len: 0,
|
||||
rules: &["expr::=MKS$({0})"],
|
||||
rules: &["expr::=MKS$({expr:0})", "{0}"],
|
||||
}),
|
||||
0x139 => Some(TokenDef {
|
||||
len: 0,
|
||||
@@ -1682,7 +1682,7 @@ fn token_def(code: u16) -> Option<TokenDef> {
|
||||
}),
|
||||
0x1b2 => Some(TokenDef {
|
||||
len: 0,
|
||||
rules: &["expr::=MSGBOX({2}, {1}, {0})"],
|
||||
rules: &["expr::=STR$({expr:0})"],
|
||||
}),
|
||||
0x1b3 => Some(TokenDef {
|
||||
len: 0,
|
||||
@@ -2421,6 +2421,23 @@ impl<'a> Decoder<'a> {
|
||||
.ok_or_else(|| self.fail(self.at, "P-Code", "Tokendaten abgeschnitten"))?
|
||||
.to_owned();
|
||||
self.at += (len + 1) & !1;
|
||||
if matches!(self.pcode, 0x065 | 0x066) {
|
||||
let operands = usize::from(self.pcode == 0x066);
|
||||
let split = self.stack.len().saturating_sub(operands);
|
||||
if split > 0
|
||||
&& self.stack[split - 1].kind != "newline"
|
||||
&& !self.stack[split - 1].text.ends_with(" THEN ")
|
||||
&& !self.stack[split - 1].text.ends_with(" ELSE ")
|
||||
{
|
||||
self.stack.insert(
|
||||
split,
|
||||
Item {
|
||||
kind: "newline".into(),
|
||||
text: "\r\u{1}".into(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
if matches!(self.pcode, 0x1c5 | 0x1c6) {
|
||||
let first = self
|
||||
.stack
|
||||
@@ -2634,6 +2651,26 @@ mod tests {
|
||||
assert_eq!(decoder.array(&array, false).unwrap(), "Oldcontents()");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_trennt_eine_vorangehende_anweisung_auch_ohne_newline_token() {
|
||||
let (sym, ids) = symbols(&["i"]);
|
||||
let mut code = Vec::new();
|
||||
code.extend_from_slice(&0u16.to_le_bytes());
|
||||
code.extend_from_slice(&0x00au16.to_le_bytes());
|
||||
code.extend_from_slice(&9u16.to_le_bytes());
|
||||
code.extend_from_slice(&[0, 0]);
|
||||
code.extend_from_slice(b"PRINT 1");
|
||||
code.push(0);
|
||||
code.extend_from_slice(&(0x0400u16 | 0x00b).to_le_bytes());
|
||||
code.extend_from_slice(&ids[0].to_le_bytes());
|
||||
code.extend_from_slice(&0x066u16.to_le_bytes());
|
||||
code.extend_from_slice(&[0xff, 0xff, 1, 0]);
|
||||
code.extend_from_slice(&0u16.to_le_bytes());
|
||||
code.extend_from_slice(&8u16.to_le_bytes());
|
||||
let (decoded, _) = Decoder::new("test.frm", &sym, &code, 0).run().unwrap();
|
||||
assert!(decoded.contains("PRINT 1\nNEXT i%\n"), "{decoded:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_defint_and_vbdos_isam_tokens() {
|
||||
let mut decoder = Decoder::new("test.frm", &[], &[], 0);
|
||||
|
||||
@@ -302,6 +302,7 @@ instrs! {
|
||||
0xA4 RetGosub;
|
||||
0xA5 RetGosubTo(a: u32);
|
||||
0xA6 OnJump(a: u16, b: bool); // Sprungtabelle, gosub?
|
||||
0xA7 Run(a: u8); // 0 = ohne Ziel, 1 = Zeile, 2 = Datei
|
||||
|
||||
// 0xB0 — Prozeduren und Builtins
|
||||
0xB0 Call(a: u16, b: u8);
|
||||
@@ -316,6 +317,9 @@ instrs! {
|
||||
0xB9 ObjectLoad(a: u16, b: bool, c: bool); // unload?, Index liegt auf Stack?
|
||||
0xBA LoadDynamicObjectProperty(a: u16);
|
||||
0xBB StoreDynamicObjectProperty(a: u16);
|
||||
0xBC LoadObjectIndexedProperty(a: u16, b: u16);
|
||||
0xBD ObjectMethodFn(a: u16, b: u16, c: u8);
|
||||
0xBE StoreObjectIndexedProperty(a: u16, b: u16);
|
||||
|
||||
// 0xE0 — Ereignis-Traps (Sprachreferenz §8); Kennung vom Stack
|
||||
0xE0 TrapDefine(a: u8, b: u32); // Quellenart, Sprungziel
|
||||
|
||||
@@ -20,6 +20,10 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||
string_ids: HashMap::new(),
|
||||
jump_tables: Vec::new(),
|
||||
modul_label_pc: Vec::new(),
|
||||
initialize_main: hir
|
||||
.objects
|
||||
.iter()
|
||||
.any(|object| object.class == tb_frontend::forms::ObjectClass::Form),
|
||||
};
|
||||
let mut procs = Vec::new();
|
||||
for proc in &hir.procs {
|
||||
@@ -29,7 +33,7 @@ pub fn compile(hir: &HirModule) -> CompiledModule {
|
||||
name: hir.name.clone(),
|
||||
option_base: hir.option_base,
|
||||
strings: cg.strings,
|
||||
globals_init: hir.globals.iter().map(|g| slot_init(g)).collect(),
|
||||
globals_init: hir.globals.iter().map(slot_init).collect(),
|
||||
global_names: hir.globals.iter().map(|g| g.name.clone()).collect(),
|
||||
udts: hir
|
||||
.udts
|
||||
@@ -85,6 +89,9 @@ struct Codegen {
|
||||
/// `ON ERROR GOTO` aus einer Prozedur zeigt dorthin; da Prozedur 0
|
||||
/// zuerst übersetzt wird, stehen die Positionen rechtzeitig fest.
|
||||
modul_label_pc: Vec<Option<u32>>,
|
||||
/// Formulare müssen erst nach den globalen DIM-Anweisungen Ereignisse
|
||||
/// zustellen; reine Textprogramme behalten ihre bisherigen Grenzen.
|
||||
initialize_main: bool,
|
||||
}
|
||||
|
||||
struct ProcCtx {
|
||||
@@ -160,7 +167,30 @@ impl Codegen {
|
||||
fixups: Vec::new(),
|
||||
table_fixups: Vec::new(),
|
||||
};
|
||||
if proc.kind == hir::HProcKind::Main {
|
||||
let declarations = proc
|
||||
.body
|
||||
.iter()
|
||||
.filter(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
|
||||
.collect::<Vec<_>>();
|
||||
for stmt in declarations {
|
||||
self.stmt_inner(&mut ctx, proc, stmt, false);
|
||||
}
|
||||
if self.initialize_main
|
||||
|| proc
|
||||
.body
|
||||
.iter()
|
||||
.any(|stmt| matches!(stmt.kind, HStmtKind::Dim { redim: false, .. }))
|
||||
{
|
||||
ctx.emit(Instr::Stmt(0));
|
||||
}
|
||||
}
|
||||
for stmt in &proc.body {
|
||||
if proc.kind == hir::HProcKind::Main
|
||||
&& matches!(stmt.kind, HStmtKind::Dim { redim: false, .. })
|
||||
{
|
||||
continue;
|
||||
}
|
||||
self.stmt(&mut ctx, proc, stmt);
|
||||
}
|
||||
// Rumpfende
|
||||
@@ -234,12 +264,17 @@ impl Codegen {
|
||||
// ---- Anweisungen -------------------------------------------------------
|
||||
|
||||
fn stmt(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt) {
|
||||
self.stmt_inner(ctx, proc, stmt, true);
|
||||
}
|
||||
|
||||
fn stmt_inner(&mut self, ctx: &mut ProcCtx, proc: &hir::HProc, stmt: &HStmt, boundary: bool) {
|
||||
match &stmt.kind {
|
||||
HStmtKind::Label(l) => {
|
||||
ctx.bind(*l);
|
||||
return;
|
||||
}
|
||||
_ => ctx.emit(Instr::Stmt(stmt.line)),
|
||||
_ if boundary => ctx.emit(Instr::Stmt(stmt.line)),
|
||||
_ => {}
|
||||
}
|
||||
match &stmt.kind {
|
||||
HStmtKind::Label(_) => unreachable!(),
|
||||
@@ -263,6 +298,23 @@ impl Codegen {
|
||||
index.is_some(),
|
||||
));
|
||||
}
|
||||
HStmtKind::SetObjectIndexedProperty {
|
||||
object,
|
||||
object_index,
|
||||
property,
|
||||
index,
|
||||
value,
|
||||
} => {
|
||||
if let Some(object_index) = object_index {
|
||||
self.expr(ctx, object_index);
|
||||
}
|
||||
self.expr(ctx, index);
|
||||
self.expr(ctx, value);
|
||||
ctx.emit(Instr::StoreObjectIndexedProperty(
|
||||
*object,
|
||||
*property | if object_index.is_some() { 0x8000 } else { 0 },
|
||||
));
|
||||
}
|
||||
HStmtKind::SetDynamicObjectProperty {
|
||||
object,
|
||||
property,
|
||||
@@ -275,13 +327,21 @@ impl Codegen {
|
||||
}
|
||||
HStmtKind::ObjectMethod {
|
||||
object,
|
||||
index,
|
||||
method,
|
||||
args,
|
||||
} => {
|
||||
if let Some(index) = index {
|
||||
self.expr(ctx, index);
|
||||
}
|
||||
for arg in args {
|
||||
self.expr(ctx, arg);
|
||||
}
|
||||
ctx.emit(Instr::ObjectMethod(*object, *method, args.len() as u8));
|
||||
ctx.emit(Instr::ObjectMethod(
|
||||
*object,
|
||||
*method,
|
||||
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
|
||||
));
|
||||
}
|
||||
HStmtKind::ObjectLoad {
|
||||
object,
|
||||
@@ -509,6 +569,18 @@ impl Codegen {
|
||||
None => ctx.emit(Instr::RetGosub),
|
||||
Some(l) => ctx.emit_jump(Instr::RetGosubTo(0), *l),
|
||||
},
|
||||
HStmtKind::Run { target, string } => {
|
||||
if let Some(target) = target {
|
||||
self.expr(ctx, target);
|
||||
}
|
||||
ctx.emit(Instr::Run(if target.is_none() {
|
||||
0
|
||||
} else if *string {
|
||||
2
|
||||
} else {
|
||||
1
|
||||
}));
|
||||
}
|
||||
HStmtKind::ExitProc => self.emit_proc_exit(ctx, proc),
|
||||
HStmtKind::CallSub { proc: id, args } => {
|
||||
for a in args {
|
||||
@@ -533,6 +605,9 @@ impl Codegen {
|
||||
}
|
||||
let id = builtin_id(*b);
|
||||
ctx.emit(Instr::CallBuiltin(id, args.len() as u8));
|
||||
if matches!(b, Builtin::MsgBox) {
|
||||
ctx.emit(Instr::Pop);
|
||||
}
|
||||
if builtin_returns_value(*b) {
|
||||
ctx.emit(Instr::Pop);
|
||||
}
|
||||
@@ -725,6 +800,41 @@ impl Codegen {
|
||||
index.is_some(),
|
||||
));
|
||||
}
|
||||
HExpr::ObjectIndexedProperty {
|
||||
object,
|
||||
object_index,
|
||||
property,
|
||||
index,
|
||||
..
|
||||
} => {
|
||||
if let Some(object_index) = object_index {
|
||||
self.expr(ctx, object_index);
|
||||
}
|
||||
self.expr(ctx, index);
|
||||
ctx.emit(Instr::LoadObjectIndexedProperty(
|
||||
*object,
|
||||
*property | if object_index.is_some() { 0x8000 } else { 0 },
|
||||
));
|
||||
}
|
||||
HExpr::ObjectMethodCall {
|
||||
object,
|
||||
index,
|
||||
method,
|
||||
args,
|
||||
..
|
||||
} => {
|
||||
if let Some(index) = index {
|
||||
self.expr(ctx, index);
|
||||
}
|
||||
for arg in args {
|
||||
self.expr(ctx, arg);
|
||||
}
|
||||
ctx.emit(Instr::ObjectMethodFn(
|
||||
*object,
|
||||
*method,
|
||||
args.len() as u8 | if index.is_some() { 0x80 } else { 0 },
|
||||
));
|
||||
}
|
||||
HExpr::DynamicObjectProperty { object, property } => {
|
||||
self.expr(ctx, object);
|
||||
let property = self.pool(property);
|
||||
@@ -1173,6 +1283,9 @@ fn builtin_id(b: Builtin) -> u16 {
|
||||
Builtin::Width => ids::WIDTH,
|
||||
Builtin::ViewPrint => ids::VIEW_PRINT,
|
||||
Builtin::ScreenStmt => ids::SCREEN_STMT,
|
||||
Builtin::GraphicsLine => ids::GRAPHICS_LINE,
|
||||
Builtin::GraphicsPaint => ids::GRAPHICS_PAINT,
|
||||
Builtin::GraphicsView => ids::GRAPHICS_VIEW,
|
||||
Builtin::KeyAssign => ids::KEY_ASSIGN,
|
||||
Builtin::KeyList => ids::KEY_LIST,
|
||||
Builtin::KeyDisplay => ids::KEY_DISPLAY,
|
||||
@@ -1277,6 +1390,10 @@ fn builtin_id(b: Builtin) -> u16 {
|
||||
Builtin::IsamSavepoint => ids::ISAM_SAVEPOINT,
|
||||
Builtin::IsamSetmem => ids::ISAM_SETMEM,
|
||||
Builtin::IsamBof => ids::ISAM_BOF,
|
||||
Builtin::MsgBox => ids::MSGBOX,
|
||||
Builtin::InputBoxS => ids::INPUTBOX_S,
|
||||
Builtin::ClipboardAdd => ids::CLIPBOARD_ADD,
|
||||
Builtin::ClipboardGet => ids::CLIPBOARD_GET,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
//! - `GOSUB`-Stack pro Frame; `RETURN` ohne GOSUB → Fehler 3.
|
||||
|
||||
use crate::bytecode::{CmpOp, CompiledModule, Instr};
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashSet, VecDeque};
|
||||
use std::rc::Rc;
|
||||
use tb_runtime::builtins::{builtin_table, RtState};
|
||||
use tb_runtime::builtins::{builtin_table, ids, RtState};
|
||||
use tb_runtime::errors::RuntimeError;
|
||||
use tb_runtime::host::Host;
|
||||
use tb_runtime::host::{Ereignis, Host};
|
||||
use tb_runtime::traps::Quelle;
|
||||
use tb_runtime::value::{self, default_value, ArrayObj, RecordObj, TypeInit, Value, VarRef};
|
||||
use tb_ui::forms::{FormEvent, FormsModel, PropertyValue, ShowResult};
|
||||
@@ -37,6 +37,10 @@ pub enum RunEvent {
|
||||
Interrupted {
|
||||
line: u32,
|
||||
},
|
||||
Restart {
|
||||
program: Option<String>,
|
||||
line: Option<u32>,
|
||||
},
|
||||
/// Unbehandelter Laufzeitfehler.
|
||||
Error {
|
||||
code: u16,
|
||||
@@ -106,6 +110,7 @@ pub struct Vm {
|
||||
tick_zaehler: u32,
|
||||
breakpoints: HashSet<u32>,
|
||||
data_ptr: usize,
|
||||
start_pc: Option<usize>,
|
||||
pub forms: FormsModel,
|
||||
}
|
||||
|
||||
@@ -166,6 +171,7 @@ impl Vm {
|
||||
tick_zaehler: 0,
|
||||
breakpoints: HashSet::new(),
|
||||
data_ptr: 0,
|
||||
start_pc: None,
|
||||
forms,
|
||||
module,
|
||||
};
|
||||
@@ -176,6 +182,24 @@ impl Vm {
|
||||
vm
|
||||
}
|
||||
|
||||
pub fn start_at_line(&mut self, line: u32) -> Result<(), RuntimeError> {
|
||||
let target = self.module.procs[0]
|
||||
.code
|
||||
.iter()
|
||||
.position(|instruction| matches!(instruction, Instr::Stmt(found) if *found == line));
|
||||
let target = target.ok_or(RuntimeError(8))?;
|
||||
if self.module.procs[0]
|
||||
.code
|
||||
.iter()
|
||||
.any(|instruction| matches!(instruction, Instr::Stmt(0)))
|
||||
{
|
||||
self.start_pc = Some(target);
|
||||
} else {
|
||||
self.frames[0].pc = target;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn push_frame(&mut self, proc: usize, argc: usize) {
|
||||
// Argumente liegen zuoberst auf dem Stack (links → rechts).
|
||||
let locals_base = self.locals.len();
|
||||
@@ -204,6 +228,238 @@ impl Vm {
|
||||
});
|
||||
}
|
||||
|
||||
fn external_arg(&self, value: &Value) -> Result<Value, RuntimeError> {
|
||||
match value {
|
||||
Value::Ref(reference) => self.read_ref(reference),
|
||||
value => Ok(value.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn external_string(&self, value: &Value) -> Result<String, RuntimeError> {
|
||||
match self.external_arg(value)? {
|
||||
Value::Str(text) => Ok(text.to_string()),
|
||||
_ => Err(RuntimeError::TYPE_MISMATCH),
|
||||
}
|
||||
}
|
||||
|
||||
fn external_set(
|
||||
&mut self,
|
||||
args: &[Value],
|
||||
index: usize,
|
||||
value: Value,
|
||||
) -> Result<(), RuntimeError> {
|
||||
match args.get(index) {
|
||||
Some(Value::Ref(reference)) => self.write_ref(reference, value),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn dialog_integer(&self, value: &Value) -> Result<i32, RuntimeError> {
|
||||
match self.external_arg(value)? {
|
||||
Value::Int(value) => Ok(value as i32),
|
||||
Value::Lng(value) => Ok(value),
|
||||
_ => Err(RuntimeError::TYPE_MISMATCH),
|
||||
}
|
||||
}
|
||||
|
||||
fn take_dialog_input(&mut self) -> VecDeque<Ereignis> {
|
||||
let mut events = VecDeque::new();
|
||||
events.extend(
|
||||
self.rt
|
||||
.tasten
|
||||
.drain(..)
|
||||
.map(|(key, shift)| Ereignis::Taste(key, shift)),
|
||||
);
|
||||
events.extend(self.rt.maus.drain(..).map(Ereignis::Maus));
|
||||
events
|
||||
}
|
||||
|
||||
fn restore_dialog_input(&mut self, events: VecDeque<Ereignis>) {
|
||||
for event in events {
|
||||
match event {
|
||||
Ereignis::Taste(key, shift) => self.rt.tasten.push_back((key, shift)),
|
||||
Ereignis::Maus(event) => self.rt.maus.push_back(event),
|
||||
Ereignis::Abbruch => self.rt.abbruch = true,
|
||||
Ereignis::Ende => self.rt.ende = true,
|
||||
Ereignis::Signal(number) => {
|
||||
if number == 1
|
||||
&& !self
|
||||
.rt
|
||||
.traps
|
||||
.melden(tb_runtime::traps::Quelle::Signal(number))
|
||||
{
|
||||
self.rt.abbruch = true;
|
||||
}
|
||||
}
|
||||
Ereignis::Groesse { cols, rows } => self.rt.screen.resize(cols, rows),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn forms_dialog(
|
||||
&mut self,
|
||||
id: u16,
|
||||
args: &[Value],
|
||||
host: &mut dyn Host,
|
||||
) -> Result<Option<Value>, RuntimeError> {
|
||||
match id {
|
||||
ids::MSGBOX => {
|
||||
let text =
|
||||
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
|
||||
let kind = args
|
||||
.get(1)
|
||||
.map_or(Ok(0), |value| self.dialog_integer(value))?;
|
||||
let title = args
|
||||
.get(2)
|
||||
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
|
||||
let mut queued = self.take_dialog_input();
|
||||
let result = tb_ui::forms::msgbox_dialog(
|
||||
&mut self.rt.screen,
|
||||
host,
|
||||
&mut queued,
|
||||
&text,
|
||||
kind,
|
||||
&title,
|
||||
);
|
||||
self.restore_dialog_input(queued);
|
||||
result.map(|value| Some(Value::Int(value)))
|
||||
}
|
||||
ids::INPUTBOX_S => {
|
||||
let prompt =
|
||||
self.external_string(args.first().ok_or(RuntimeError::TYPE_MISMATCH)?)?;
|
||||
let title = args
|
||||
.get(1)
|
||||
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
|
||||
let initial = args
|
||||
.get(2)
|
||||
.map_or_else(|| Ok(String::new()), |value| self.external_string(value))?;
|
||||
let position = match (args.get(3), args.get(4)) {
|
||||
(None, None) => None,
|
||||
(Some(x), Some(y)) => Some((self.dialog_integer(x)?, self.dialog_integer(y)?)),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
};
|
||||
let mut queued = self.take_dialog_input();
|
||||
let result = tb_ui::forms::inputbox_dialog(
|
||||
&mut self.rt.screen,
|
||||
host,
|
||||
&mut queued,
|
||||
&prompt,
|
||||
&title,
|
||||
&initial,
|
||||
position,
|
||||
);
|
||||
self.restore_dialog_input(queued);
|
||||
result.map(|value| Some(Value::Str(Rc::from(value))))
|
||||
}
|
||||
_ => Err(RuntimeError::FEATURE_UNAVAILABLE),
|
||||
}
|
||||
}
|
||||
|
||||
fn external_dialog(
|
||||
&mut self,
|
||||
proc: usize,
|
||||
argc: usize,
|
||||
host: &mut dyn Host,
|
||||
) -> Result<bool, RuntimeError> {
|
||||
if !matches!(self.module.procs[proc].code.as_slice(), [Instr::RetProc]) {
|
||||
return Ok(false);
|
||||
}
|
||||
let name = self.module.procs[proc].name.to_ascii_uppercase();
|
||||
if !matches!(
|
||||
name.as_str(),
|
||||
"CMNDLGREGISTER"
|
||||
| "CMNDLGCLOSE"
|
||||
| "ABOUT"
|
||||
| "FILEOPEN"
|
||||
| "FILESAVE"
|
||||
| "FILEPRINT"
|
||||
| "FINDTEXT"
|
||||
| "CHANGETEXT"
|
||||
| "COLORPALETTE"
|
||||
) {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut args = self.stack.split_off(self.stack.len().saturating_sub(argc));
|
||||
match name.as_str() {
|
||||
"CMNDLGREGISTER" => self.external_set(&args, 0, Value::Int(-1))?,
|
||||
"CMNDLGCLOSE" => {}
|
||||
"ABOUT" => {
|
||||
let dialog = vec![
|
||||
self.external_arg(&args[0])?,
|
||||
Value::Lng(0),
|
||||
Value::Str(Rc::from("About")),
|
||||
];
|
||||
self.forms_dialog(ids::MSGBOX, &dialog, host)?;
|
||||
}
|
||||
"FILEOPEN" | "FILESAVE" => {
|
||||
let file = self.external_string(&args[0])?;
|
||||
let path = self.external_string(&args[1])?;
|
||||
let title = if name == "FILEOPEN" {
|
||||
"Open file"
|
||||
} else {
|
||||
"Save file"
|
||||
};
|
||||
let default = if path.is_empty() {
|
||||
file
|
||||
} else {
|
||||
format!("{path}{sep}{file}", sep = std::path::MAIN_SEPARATOR)
|
||||
};
|
||||
let dialog = vec![
|
||||
Value::Str(Rc::from(title)),
|
||||
Value::Str(Rc::from(title)),
|
||||
Value::Str(Rc::from(default)),
|
||||
];
|
||||
let result = self
|
||||
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
|
||||
.unwrap_or(Value::Str(Rc::from("")));
|
||||
let Value::Str(result) = result else {
|
||||
return Err(RuntimeError::TYPE_MISMATCH);
|
||||
};
|
||||
let result = result.to_string();
|
||||
if result.is_empty() {
|
||||
self.external_set(&args, 7, Value::Int(-1))?;
|
||||
} else {
|
||||
let (path, file) = result
|
||||
.rsplit_once(['/', '\\'])
|
||||
.map_or(("", result.as_str()), |(path, file)| (path, file));
|
||||
self.external_set(&args, 0, Value::Str(Rc::from(file)))?;
|
||||
self.external_set(&args, 1, Value::Str(Rc::from(path)))?;
|
||||
self.external_set(&args, 7, Value::Int(0))?;
|
||||
}
|
||||
}
|
||||
"FILEPRINT" => {
|
||||
self.external_set(&args, 0, Value::Int(1))?;
|
||||
self.external_set(&args, 3, Value::Int(0))?;
|
||||
}
|
||||
"FINDTEXT" | "CHANGETEXT" => {
|
||||
let dialog = vec![
|
||||
Value::Str(Rc::from(if name == "FINDTEXT" {
|
||||
"Find text"
|
||||
} else {
|
||||
"Replacement text"
|
||||
})),
|
||||
Value::Str(Rc::from("Find")),
|
||||
self.external_arg(&args[usize::from(name == "CHANGETEXT")])?,
|
||||
];
|
||||
let result = self
|
||||
.forms_dialog(ids::INPUTBOX_S, &dialog, host)?
|
||||
.unwrap_or(Value::Str(Rc::from("")));
|
||||
let target = usize::from(name == "CHANGETEXT");
|
||||
self.external_set(&args, target, result.clone())?;
|
||||
let empty = matches!(&result, Value::Str(text) if text.is_empty());
|
||||
self.external_set(&args, args.len() - 1, Value::Int(-i16::from(empty)))?;
|
||||
}
|
||||
"COLORPALETTE" => {
|
||||
let color = self.external_arg(&args[0])?;
|
||||
self.external_set(&args, 0, color)?;
|
||||
self.external_set(&args, 3, Value::Int(0))?;
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
args.clear();
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn queue_form_event(&mut self, event: FormEvent) {
|
||||
self.forms.queue(event);
|
||||
}
|
||||
@@ -221,6 +477,17 @@ impl Vm {
|
||||
}
|
||||
}
|
||||
|
||||
fn form_arg(v: Value) -> Result<PropertyValue, RuntimeError> {
|
||||
Ok(match v {
|
||||
Value::Int(v) => PropertyValue::Integer(v as i32),
|
||||
Value::Lng(v) => PropertyValue::Integer(v),
|
||||
Value::Sng(v) => PropertyValue::Single(v),
|
||||
Value::Str(v) => PropertyValue::String(v.to_string()),
|
||||
Value::Obj(object, index) => PropertyValue::Object(Some((object, index))),
|
||||
_ => return Err(RuntimeError::TYPE_MISMATCH),
|
||||
})
|
||||
}
|
||||
|
||||
fn property_value(
|
||||
&self,
|
||||
object: u16,
|
||||
@@ -322,36 +589,37 @@ impl Vm {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn forms_zustellen(&mut self) -> bool {
|
||||
fn tick(&mut self, host: &mut dyn Host) {
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
if self.forms.menu_is_open() {
|
||||
host.present(&self.rt.screen);
|
||||
self.rt.pump(host, false);
|
||||
} else {
|
||||
self.rt.tick(host);
|
||||
}
|
||||
}
|
||||
|
||||
fn forms_zustellen(&mut self, host: &mut dyn Host) -> bool {
|
||||
self.forms
|
||||
.resize(self.rt.screen.cols(), self.rt.screen.rows());
|
||||
self.forms.timers(host.jetzt_ms());
|
||||
if self.forms.active_form().is_some() {
|
||||
while let Some((key, shift)) = self.rt.tasten.pop_front() {
|
||||
self.forms.handle_key(&key, shift);
|
||||
}
|
||||
}
|
||||
let now_ms = host.jetzt_ms();
|
||||
while let Some(m) = self.rt.maus.pop_front() {
|
||||
if let Some(object) = self.forms.active_form() {
|
||||
let name = match m.art {
|
||||
tb_runtime::host::MausArt::Druck => "MOUSEDOWN",
|
||||
tb_runtime::host::MausArt::Loslassen => "MOUSEUP",
|
||||
tb_runtime::host::MausArt::Bewegung => "MOUSEMOVE",
|
||||
};
|
||||
self.forms.queue(FormEvent {
|
||||
object,
|
||||
array_index: None,
|
||||
name: name.into(),
|
||||
args: vec![
|
||||
PropertyValue::Integer(m.taste as i32),
|
||||
PropertyValue::Integer(m.shift as i32),
|
||||
PropertyValue::Single(m.spalte as f32),
|
||||
PropertyValue::Single(m.zeile as f32),
|
||||
],
|
||||
});
|
||||
}
|
||||
self.forms.handle_mouse_at(m, now_ms);
|
||||
}
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
self.dispatch_next_form_event()
|
||||
}
|
||||
|
||||
/// Anstehendes Ereignis zustellen, falls eines fällig ist. Liefert
|
||||
/// `true`, wenn ein Handler aufgesetzt wurde.
|
||||
fn zustellen(&mut self, host: &mut dyn Host) -> bool {
|
||||
if !self.rt.traps.aktiv() {
|
||||
if self.forms.menu_is_open() || !self.rt.traps.aktiv() {
|
||||
return false;
|
||||
}
|
||||
let jetzt = host.jetzt_ms();
|
||||
@@ -377,7 +645,7 @@ impl Vm {
|
||||
// virtuelle nie vorrückt.
|
||||
let real_start = std::time::Instant::now();
|
||||
loop {
|
||||
self.rt.tick(host);
|
||||
self.tick(host);
|
||||
if self.zustellen(host) {
|
||||
return;
|
||||
}
|
||||
@@ -531,14 +799,41 @@ impl Vm {
|
||||
|
||||
// ---- Hauptschleife --------------------------------------------------------
|
||||
|
||||
/// Hält modellose Formulare nach dem Ende des Modulrumpfs bedienbar.
|
||||
/// Ereignisprozeduren laufen weiter auf derselben VM und können das
|
||||
/// Formular schließen, ein anderes Programm starten oder einen Fehler
|
||||
/// auslösen.
|
||||
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
|
||||
while self.forms.has_visible_forms() {
|
||||
self.tick(host);
|
||||
let dispatched = self.forms_zustellen(host);
|
||||
if dispatched {
|
||||
match self.run(host) {
|
||||
RunEvent::Ended => {}
|
||||
event => return event,
|
||||
}
|
||||
}
|
||||
if self.rt.ende {
|
||||
return RunEvent::Ended;
|
||||
}
|
||||
if self.rt.abbruch {
|
||||
return RunEvent::Interrupted {
|
||||
line: self.current_line(),
|
||||
};
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
RunEvent::Ended
|
||||
}
|
||||
|
||||
pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
|
||||
loop {
|
||||
if let Some(form) = self.frames.last().and_then(|f| f.waiting_form) {
|
||||
if !self.forms.is_visible(form) {
|
||||
self.frames.last_mut().unwrap().waiting_form = None;
|
||||
} else {
|
||||
self.rt.tick(host);
|
||||
self.forms_zustellen();
|
||||
self.tick(host);
|
||||
self.forms_zustellen(host);
|
||||
if self.rt.ende {
|
||||
return RunEvent::Ended;
|
||||
}
|
||||
@@ -818,6 +1113,11 @@ impl Vm {
|
||||
use Instr as I;
|
||||
match instr {
|
||||
I::Stmt(line) => {
|
||||
if line == 0 {
|
||||
if let Some(target) = self.start_pc.take() {
|
||||
self.frames[0].pc = target;
|
||||
}
|
||||
}
|
||||
let f = self.frames.last_mut().unwrap();
|
||||
f.line = line;
|
||||
f.last_stmt_pc = pc;
|
||||
@@ -826,11 +1126,12 @@ impl Vm {
|
||||
// Debugger-Funktion — ohne sie sähe niemand die Ausgabe und
|
||||
// Größenänderungen kämen nie an.
|
||||
self.tick_zaehler = self.tick_zaehler.wrapping_add(1);
|
||||
if self.rt.screen.ist_veraendert() || self.tick_zaehler % 1024 == 0 {
|
||||
self.rt.tick(host);
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
if self.rt.screen.ist_veraendert() || self.tick_zaehler.is_multiple_of(1024) {
|
||||
self.tick(host);
|
||||
self.rt.screen.veraenderung_quittieren();
|
||||
}
|
||||
if self.forms_zustellen() {
|
||||
if self.forms_zustellen(host) {
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
// Ereigniszustellung (design.md, D1): nur wenn überhaupt
|
||||
@@ -841,7 +1142,7 @@ impl Vm {
|
||||
// Auflösung eines Zeit-Traps ist damit 64 Anweisungen;
|
||||
// reicht das nicht, wird daraus ein eigener Zähler je
|
||||
// Trap.
|
||||
if self.tick_zaehler % 64 == 0 {
|
||||
if self.tick_zaehler.is_multiple_of(64) {
|
||||
let jetzt = host.jetzt_ms();
|
||||
self.rt.traps.zeit_pruefen(jetzt);
|
||||
}
|
||||
@@ -901,8 +1202,8 @@ impl Vm {
|
||||
// bekommt seinen eigenen Stapelabschnitt darüber und
|
||||
// lässt den Wert unberührt.
|
||||
self.stack.push(Value::Int(0));
|
||||
self.rt.tick(host);
|
||||
self.forms_zustellen();
|
||||
self.tick(host);
|
||||
self.forms_zustellen(host);
|
||||
self.zustellen(host);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -1051,6 +1352,56 @@ impl Vm {
|
||||
self.forms.set_at(object, index, property, value)?;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::LoadObjectIndexedProperty(object, property) => {
|
||||
let index = self.pop_i32()?;
|
||||
let object_index = if property & 0x8000 != 0 {
|
||||
Some(self.pop_i32()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let value =
|
||||
self.forms
|
||||
.get_indexed_at(object, object_index, property & 0x7fff, index)?;
|
||||
self.push(Self::form_value(value));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::StoreObjectIndexedProperty(object, property) => {
|
||||
let value = self.pop_i32()?;
|
||||
let index = self.pop_i32()?;
|
||||
let object_index = if property & 0x8000 != 0 {
|
||||
Some(self.pop_i32()?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.forms
|
||||
.set_indexed_at(object, object_index, property & 0x7fff, index, value)?;
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::ObjectMethodFn(object, method, argc) => {
|
||||
let indexed = argc & 0x80 != 0;
|
||||
let argc = argc & 0x7f;
|
||||
let class = self
|
||||
.module
|
||||
.objects
|
||||
.get(object as usize)
|
||||
.ok_or(RuntimeError(420))?
|
||||
.class;
|
||||
let name = *tb_frontend::forms::methods(class)
|
||||
.get(method as usize)
|
||||
.ok_or(RuntimeError(421))?;
|
||||
let mut args = Vec::with_capacity(argc as usize);
|
||||
for _ in 0..argc {
|
||||
args.push(Self::form_arg(self.pop()?)?);
|
||||
}
|
||||
args.reverse();
|
||||
let index = if indexed { Some(self.pop_i32()?) } else { None };
|
||||
let value = self
|
||||
.forms
|
||||
.object_method_at(object, index, name, args)?
|
||||
.ok_or(RuntimeError(421))?;
|
||||
self.push(Self::form_value(value));
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::TypeOf(class) => {
|
||||
let matches = match self.pop()? {
|
||||
Value::Obj(object, _) => self
|
||||
@@ -1064,6 +1415,8 @@ impl Vm {
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::ObjectMethod(object, method, argc) => {
|
||||
let indexed = argc & 0x80 != 0;
|
||||
let argc = argc & 0x7f;
|
||||
let class = self
|
||||
.module
|
||||
.objects
|
||||
@@ -1093,6 +1446,7 @@ impl Vm {
|
||||
args.push(self.pop()?);
|
||||
}
|
||||
args.reverse();
|
||||
let index = if indexed { Some(self.pop_i32()?) } else { None };
|
||||
if resumed_show && !self.forms.is_loaded(object) {
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
@@ -1118,13 +1472,26 @@ impl Vm {
|
||||
(tb_frontend::forms::ObjectClass::Form, "UNLOAD") => {
|
||||
self.request_unload(object)?
|
||||
}
|
||||
(tb_frontend::forms::ObjectClass::Form, "PRINTFORM") => {
|
||||
self.forms.render(&mut self.rt.screen);
|
||||
self.rt
|
||||
.print
|
||||
.drucker
|
||||
.push_str(&tb_runtime::snapshot::text(&self.rt.screen));
|
||||
}
|
||||
(tb_frontend::forms::ObjectClass::Screen, "SHOW") => {
|
||||
self.forms.screen_show(true)
|
||||
}
|
||||
(tb_frontend::forms::ObjectClass::Screen, "HIDE") => {
|
||||
self.forms.screen_show(false)
|
||||
}
|
||||
_ => return Err(RuntimeError::FEATURE_UNAVAILABLE),
|
||||
_ => {
|
||||
let args = args
|
||||
.into_iter()
|
||||
.map(Self::form_arg)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
self.forms.object_method_at(object, index, name, args)?;
|
||||
}
|
||||
}
|
||||
self.dispatch_next_form_event();
|
||||
Ok(Flow::Normal)
|
||||
@@ -1318,7 +1685,7 @@ impl Vm {
|
||||
s.chars().take(n).collect()
|
||||
} else {
|
||||
let mut t = s.to_string();
|
||||
t.extend(std::iter::repeat(' ').take(n - len));
|
||||
t.extend(std::iter::repeat_n(' ', n - len));
|
||||
t
|
||||
};
|
||||
self.push(Value::Str(Rc::from(fixed.as_str())));
|
||||
@@ -1714,9 +2081,27 @@ impl Vm {
|
||||
}
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Run(kind) => {
|
||||
let (program, line) = match kind {
|
||||
0 => (None, None),
|
||||
1 => {
|
||||
let line = self.pop_i32()?;
|
||||
if line < 0 {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
(None, Some(line as u32))
|
||||
}
|
||||
2 => (Some(self.pop_str()?.to_string()), None),
|
||||
_ => return Err(RuntimeError::ILLEGAL_FUNCTION_CALL),
|
||||
};
|
||||
Ok(Flow::Event(RunEvent::Restart { program, line }))
|
||||
}
|
||||
|
||||
// ---- Prozeduren ----
|
||||
I::Call(proc, argc) => {
|
||||
if self.external_dialog(proc as usize, argc as usize, host)? {
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
self.push_frame(proc as usize, argc as usize);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -1754,6 +2139,12 @@ impl Vm {
|
||||
args.push(self.pop()?);
|
||||
}
|
||||
args.reverse();
|
||||
if matches!(id, ids::MSGBOX | ids::INPUTBOX_S) {
|
||||
if let Some(value) = self.forms_dialog(id, &args, host)? {
|
||||
self.push(value);
|
||||
}
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
let f = builtin_table()[id as usize];
|
||||
match f(&mut self.rt, host, &mut args) {
|
||||
Ok(Some(v)) => {
|
||||
@@ -2228,9 +2619,7 @@ fn cur_mul(a: i64, b: i64) -> Result<i64, RuntimeError> {
|
||||
let r = p.rem_euclid(10_000);
|
||||
let rounded = if r > 5_000 {
|
||||
q + 1
|
||||
} else if r < 5_000 {
|
||||
q
|
||||
} else if q % 2 == 0 {
|
||||
} else if r < 5_000 || q % 2 == 0 {
|
||||
q
|
||||
} else {
|
||||
q + 1
|
||||
|
||||
@@ -38,13 +38,11 @@ fn err_code(src: &str) -> u16 {
|
||||
/// Verzeichnis, das beim Verlassen samt Inhalt verschwindet. Programme
|
||||
/// hinterlassen dadurch nichts im Projektbaum.
|
||||
struct TempVerzeichnis {
|
||||
vorher: std::path::PathBuf,
|
||||
dir: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl TempVerzeichnis {
|
||||
fn neu(name: &str) -> TempVerzeichnis {
|
||||
let vorher = std::env::current_dir().unwrap();
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"tb_vm_{name}_{}_{:?}",
|
||||
std::process::id(),
|
||||
@@ -52,14 +50,16 @@ impl TempVerzeichnis {
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
std::env::set_current_dir(&dir).unwrap();
|
||||
TempVerzeichnis { vorher, dir }
|
||||
TempVerzeichnis { dir }
|
||||
}
|
||||
|
||||
fn pfad(&self, name: &str) -> String {
|
||||
self.dir.join(name).to_string_lossy().replace('"', "\"\"")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempVerzeichnis {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::env::set_current_dir(&self.vorher);
|
||||
let _ = std::fs::remove_dir_all(&self.dir);
|
||||
}
|
||||
}
|
||||
@@ -71,6 +71,48 @@ fn print_hallo_welt() {
|
||||
assert_eq!(out("PRINT \"Hallo, Welt!\"\nEND"), "Hallo, Welt!\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn grafik_line_paint_und_view_zeichnen_in_den_zellenpuffer() {
|
||||
assert_eq!(
|
||||
out("SCREEN 2\nVIEW (0,0)-(31,15),0,1\nLINE (0,0)-(15,7),1,BF\nPAINT (24,8),1\nEND"),
|
||||
"████\n████\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_gibt_datei_oder_zeile_an_den_runner_weiter() {
|
||||
assert_eq!(
|
||||
run("RUN \"next\"").0,
|
||||
RunEvent::Restart {
|
||||
program: Some("next".into()),
|
||||
line: None
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
run("RUN 100").0,
|
||||
RunEvent::Restart {
|
||||
program: None,
|
||||
line: Some(100)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_dialog_registrierung_setzt_byref_erfolg() {
|
||||
assert_eq!(
|
||||
out("DECLARE SUB CmnDlgRegister(ok AS INTEGER)\nCmnDlgRegister ok%\nPRINT ok%\nEND"),
|
||||
"-1 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn len_liefert_die_feste_satzbreite_eines_udt() {
|
||||
assert_eq!(
|
||||
out("TYPE Satz\nText AS STRING * 3\nZahl AS INTEGER\nEND TYPE\nDIM Wert AS Satz\nPRINT LEN(Wert)"),
|
||||
" 14 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banker_rounding_cint() {
|
||||
// Spec-Szenario: PRINT CINT(0.5); CINT(1.5); CINT(2.5) → " 0 2 2 "
|
||||
@@ -486,31 +528,19 @@ fn print_zonen_und_tab() {
|
||||
assert_eq!(out("PRINT \"a\"; SPC(3); \"b\""), "a b\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_feature_fehler_73() {
|
||||
// Dokumentiert, aber noch offen: `RUN` kommt mit Phase 5
|
||||
// → Laufzeitfehler 73 mit dem Katalogtext des Vorbilds (VBDOS).
|
||||
// (`SETUEVENT` stand hier bis zum Change `phase-4-ereignisschleife`.)
|
||||
let (ev, _) = run("RUN");
|
||||
match ev {
|
||||
RunEvent::Error { code, message, .. } => {
|
||||
assert_eq!(code, 73);
|
||||
assert_eq!(message, "Feature unavailable");
|
||||
}
|
||||
other => panic!("{other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Aufgabe 2.4 des Changes `phase-3-isam`: `OPEN … FOR ISAM` senkt nicht
|
||||
/// mehr auf den „nicht verfügbar"-Marker ab, sondern arbeitet. Der Lauf
|
||||
/// findet in einem temporären Verzeichnis statt und lässt nichts zurück.
|
||||
#[test]
|
||||
fn open_for_isam_endet_nicht_mehr_mit_fehler_73() {
|
||||
let _dir = TempVerzeichnis::neu("open_isam");
|
||||
let (ev, ausgabe) = run("TYPE T\n f AS INTEGER\nEND TYPE\n\
|
||||
OPEN \"db.isam\" FOR ISAM T \"Tab\" AS #1\n\
|
||||
let dir = TempVerzeichnis::neu("open_isam");
|
||||
let (ev, ausgabe) = run(&format!(
|
||||
"TYPE T\n f AS INTEGER\nEND TYPE\n\
|
||||
OPEN \"{}\" FOR ISAM T \"Tab\" AS #1\n\
|
||||
PRINT \"offen\"\n\
|
||||
CLOSE #1");
|
||||
CLOSE #1",
|
||||
dir.pfad("db.isam")
|
||||
));
|
||||
assert_eq!(ev, RunEvent::Ended, "unerwartetes Ende: {ev:?}\n{ausgabe}");
|
||||
assert!(ausgabe.contains("offen"), "{ausgabe}");
|
||||
}
|
||||
@@ -594,17 +624,10 @@ fn color_prueft_wertebereich() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_anweisung_nur_textmodus() {
|
||||
// Literaler Grafikmodus schon zur Compile-Zeit.
|
||||
let d = tb_vm::compile_source("T", "SCREEN 1").unwrap_err();
|
||||
assert!(
|
||||
d.iter().any(|x| x.message.contains("Feature unavailable")),
|
||||
"{d:?}"
|
||||
);
|
||||
// Textmodus ist folgenlos zulässig.
|
||||
fn screen_anweisung_bildet_grafikmodi_auf_den_zellenpuffer_ab() {
|
||||
assert_eq!(out("SCREEN 0\nPRINT \"ok\""), "ok\n");
|
||||
// Berechneter Modus erst zur Laufzeit.
|
||||
assert_eq!(err_code("m% = 2\nSCREEN m%"), 73);
|
||||
assert_eq!(out("SCREEN 13\nPRINT \"ok\""), "ok\n");
|
||||
assert_eq!(err_code("m% = 14\nSCREEN m%"), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -644,6 +667,32 @@ fn input_s_liest_genau_n_zeichen_ohne_echo() {
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "xyz\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn input_s_liest_aus_einer_binaerdatei() {
|
||||
let tmp = TempVerzeichnis::neu("input_s_datei");
|
||||
let pfad = tmp.pfad("daten.bin");
|
||||
std::fs::write(&pfad, b"abcdef").unwrap();
|
||||
assert_eq!(
|
||||
out(&format!(
|
||||
"OPEN \"{pfad}\" FOR BINARY AS #1\na$ = INPUT$(3, #1)\nPRINT a$\nCLOSE #1"
|
||||
)),
|
||||
"abc\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_und_printer_objekte_haben_laufzeitwirkung() {
|
||||
let module = tb_vm::compile_source(
|
||||
"T",
|
||||
"CLIPBOARD.ADDITEM \"abc\"\nPRINT CLIPBOARD.GETTEXT\nPRINTER.PRINT \"Seite\"\nPRINTER.NEWPAGE\nPRINTER.ENDDOC",
|
||||
)
|
||||
.unwrap();
|
||||
let mut vm = Vm::new(module);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen), "abc\n");
|
||||
assert_eq!(vm.rt.print.drucker, "Seite\n\u{c}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cls_setzt_cursor_zurueck() {
|
||||
assert_eq!(out("PRINT \"weg\"\nCLS\nPRINT \"neu\""), "neu\n");
|
||||
@@ -1136,6 +1185,14 @@ fn objektzugriff_laeuft_ueber_objekt_und_eigenschaftsindex() {
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "hallo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_controlpanel_hat_einen_indexierten_laufzeitwert() {
|
||||
assert_eq!(
|
||||
out("SCREEN.CONTROLPANEL(5) = 3\nPRINT SCREEN.CONTROLPANEL(5)"),
|
||||
" 3 \n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn steuerarray_eigenschaften_und_objektparameter_funktionieren() {
|
||||
let mut vm = form_vm(
|
||||
@@ -1175,6 +1232,31 @@ fn modales_show_setzt_nach_unload_fort() {
|
||||
assert!(host.presents >= 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modelloses_formular_bleibt_nach_programmende_bedienbar() {
|
||||
let mut vm = form_vm(
|
||||
"DIM SHARED gesehen%\nEND\n\
|
||||
SUB Form_MouseDown(Button AS INTEGER, Shift AS INTEGER, X AS SINGLE, Y AS SINGLE)\n\
|
||||
SHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
|
||||
);
|
||||
vm.forms.show(0, false).unwrap();
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis_nach(
|
||||
1,
|
||||
tb_runtime::host::Ereignis::Maus(tb_runtime::host::MausEreignis {
|
||||
art: tb_runtime::host::MausArt::Druck,
|
||||
taste: 1,
|
||||
shift: 0,
|
||||
zeile: 1,
|
||||
spalte: 1,
|
||||
}),
|
||||
);
|
||||
assert_eq!(vm.run_visible_forms(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
|
||||
assert!(!vm.forms.has_visible_forms());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fehlende_ereignisprozedur_verfaellt_und_typeof_prueft_klasse() {
|
||||
let mut vm = form_vm("Form1.Show\nIF TYPEOF Text1 IS TextBox THEN PRINT \"ja\"");
|
||||
@@ -1278,3 +1360,153 @@ fn objektcode_erzeugt_keine_zusaetzlichen_zustellopcodes() {
|
||||
.count();
|
||||
assert_eq!(n, 1, "nur das explizite DOEVENTS ist ein Zustellopcode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listenmethoden_indexeigenschaft_und_picture_messung_laufen_in_der_vm() {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("List1", ObjectClass::ListBox, Some("Form1"), false);
|
||||
catalog.add("Picture1", ObjectClass::PictureBox, Some("Form1"), false);
|
||||
let module = tb_vm::compile_source_with_forms(
|
||||
"FORM1",
|
||||
"List1.Sorted = -1\nList1.ADDITEM \"b\"\nList1.ADDITEM \"a\"\nPRINT List1.List(0)\nPRINT List1.ListCount\nPRINT Picture1.TEXTWIDTH(\"abc\")",
|
||||
&catalog,
|
||||
)
|
||||
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
|
||||
let mut vm = Vm::new(module);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(
|
||||
tb_runtime::snapshot::text(&vm.rt.screen).trim(),
|
||||
"a\n 2 \n 3"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listenmethoden_und_indexeigenschaft_laufen_auf_control_arrays() {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("List1", ObjectClass::ListBox, Some("Form1"), true);
|
||||
let module = tb_vm::compile_source_with_forms(
|
||||
"FORM1",
|
||||
"LOAD List1(1)\nList1(1).ADDITEM \"x\"\nPRINT List1(1).List(0)",
|
||||
&catalog,
|
||||
)
|
||||
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
|
||||
let mut vm = Vm::new(module);
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "x");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn msgbox_und_inputbox_sind_keine_unsupported_opcodes_mehr() {
|
||||
use tb_runtime::host::{taste, Ereignis};
|
||||
let module = tb_vm::compile_source(
|
||||
"DIALOG",
|
||||
"r% = MSGBOX(\"Weiter?\", 4, \"Frage\")\ns$ = INPUTBOX$(\"Name\")\nPRINT r%; s$",
|
||||
)
|
||||
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
|
||||
assert!(!module.procs[0]
|
||||
.code
|
||||
.iter()
|
||||
.any(|instruction| matches!(instruction, tb_vm::bytecode::Instr::Unsupported(_))));
|
||||
let mut vm = Vm::new(module);
|
||||
let mut host = CaptureHost::default();
|
||||
for key in [taste::ENTER, "A", taste::ENTER] {
|
||||
host.ereignis(Ereignis::Taste(key.into(), 0));
|
||||
}
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(tb_runtime::snapshot::text(&vm.rt.screen).trim(), "6 A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_button_hat_vorrang_und_beendet_modales_formular() {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
use tb_runtime::host::{taste, Ereignis};
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("Command1", ObjectClass::CommandButton, Some("Form1"), false);
|
||||
let module = tb_vm::compile_source_with_forms(
|
||||
"FORM1",
|
||||
"DIM SHARED gesehen%\nForm1.Show 1\nPRINT gesehen%\nEND\nSUB Command1_Click()\nSHARED gesehen%\ngesehen% = 1\nUNLOAD Form1\nEND SUB",
|
||||
&catalog,
|
||||
)
|
||||
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
|
||||
let mut vm = Vm::new(module);
|
||||
let default = tb_frontend::forms::property(ObjectClass::CommandButton, "DEFAULT")
|
||||
.unwrap()
|
||||
.0;
|
||||
vm.forms
|
||||
.set(1, default, tb_ui::forms::PropertyValue::Boolean(true))
|
||||
.unwrap();
|
||||
let mut host = CaptureHost::default();
|
||||
host.ereignis(Ereignis::Taste(taste::ENTER.into(), 0));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("gesehen"), Some(Value::Int(1))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn printform_schreibt_das_textformular_in_den_druckerkanal() {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
let module = tb_vm::compile_source_with_forms("FORM1", "Form1.Show\nForm1.PRINTFORM", &catalog)
|
||||
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
|
||||
let mut vm = Vm::new(module);
|
||||
for (name, value) in [("WIDTH", 12), ("HEIGHT", 4)] {
|
||||
let property = tb_frontend::forms::property(ObjectClass::Form, name)
|
||||
.unwrap()
|
||||
.0;
|
||||
vm.forms
|
||||
.set_initial(0, property, tb_ui::forms::PropertyValue::Integer(value))
|
||||
.unwrap();
|
||||
}
|
||||
let caption = tb_frontend::forms::property(ObjectClass::Form, "CAPTION")
|
||||
.unwrap()
|
||||
.0;
|
||||
vm.forms
|
||||
.set_initial(
|
||||
0,
|
||||
caption,
|
||||
tb_ui::forms::PropertyValue::String("Druck".into()),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(vm.run(&mut CaptureHost::default()), RunEvent::Ended);
|
||||
assert!(vm.rt.print.drucker.contains("Druck"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn klassischer_timer_trap_ruht_solange_das_menu_offen_ist() {
|
||||
use tb_frontend::forms::ObjectClass;
|
||||
use tb_runtime::host::umschalt;
|
||||
let mut catalog = tb_frontend::forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("mnuDatei", ObjectClass::Menu, Some("Form1"), false);
|
||||
let module = tb_vm::compile_source_with_forms(
|
||||
"FORM1",
|
||||
"DIM SHARED n%\nON TIMER(1) GOSUB Tick\nTIMER ON\nForm1.Show\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nPRINT n%\nEND\nTick:\nn% = n% + 1\nRETURN",
|
||||
&catalog,
|
||||
)
|
||||
.unwrap_or_else(|diagnostics| panic!("Compile-Fehler: {diagnostics:?}"));
|
||||
let mut vm = Vm::new(module);
|
||||
let caption = tb_frontend::forms::property(ObjectClass::Menu, "CAPTION")
|
||||
.unwrap()
|
||||
.0;
|
||||
vm.forms
|
||||
.set_initial(
|
||||
1,
|
||||
caption,
|
||||
tb_ui::forms::PropertyValue::String("&Datei".into()),
|
||||
)
|
||||
.unwrap();
|
||||
let mut host = CaptureHost::default();
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
assert!(vm.forms.handle_key("d", umschalt::ALT));
|
||||
host.uhr_vorruecken(1_500);
|
||||
assert!(matches!(vm.run(&mut host), RunEvent::Stopped { .. }));
|
||||
assert!(matches!(vm.inspect("n"), Some(Value::Int(0))));
|
||||
assert!(vm.forms.handle_key(tb_runtime::host::taste::ESC, 0));
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert!(matches!(vm.inspect("n"), Some(Value::Int(1))));
|
||||
}
|
||||
|
||||
@@ -127,7 +127,10 @@ zum Abbruch; eine Ausgabedatei wird erst nach erfolgreichem Lesen angelegt.
|
||||
|
||||
Reine Textdatei ohne Kopfzeile, **eine Projektdatei pro Zeile**
|
||||
(`.BAS`/`.FRM`; belegt durch die Originalbeispiele). Terminal Basic
|
||||
akzeptiert zusätzlich Kommentarzeilen mit `'`.
|
||||
akzeptiert zusätzlich Kommentarzeilen mit `'`. `tbc check`, `build` und
|
||||
`run` lösen die Einträge relativ zur `.MAK`-Datei und ohne Beachtung der
|
||||
DOS-Großschreibung auf; Formularobjekte aller `.FRM`-Einträge stehen dem
|
||||
gemeinsamen Modulverbund zur Verfügung.
|
||||
|
||||
## Record-Dateien (`GET`/`PUT`, `OPEN … FOR RANDOM`)
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ Maus-Koordinaten in Ereignissen sind SINGLE, aber in Zellen.
|
||||
## Steuerelemente (exakte Klassennamen)
|
||||
|
||||
`CheckBox, ComboBox, CommandButton, DirListBox, DriveListBox, FileListBox,
|
||||
Frame, HScrollBar, Label, ListBox, Menu, OptionButton, PictureBox, TextBox,
|
||||
Frame, HScrollBar, Label, ListBox, Menu, OptionButton, PictureBox, Spin, TextBox,
|
||||
Timer, VScrollBar` — alle außer `Menu` über die Toolbox platzierbar
|
||||
(Menüs entstehen im Menu Design Window). Default-Namen: `Check1`, `Combo1`,
|
||||
`Command1`, `Dir1`, `Drive1`, `File1`, `Frame1`, `HScroll1`, `Label1`,
|
||||
@@ -83,6 +83,11 @@ LostFocus (**kein** separates Scroll-Ereignis). Eigenschaften: zusätzlich
|
||||
Attached (VBDOS-Spezifikum: am Formularrand angedockt; Laufzeit read-only),
|
||||
LargeChange, Min, Max, SmallChange, Value.
|
||||
|
||||
**Spin** — Ereignisse: Custom(EventType), Drag*, GotFocus, Key*, LostFocus.
|
||||
`Style` wählt vertikal/horizontal, `Interval` steuert die Wiederholung; `Value`
|
||||
läuft zyklisch zwischen `Min` und `Max`. `BorderStyle`, `Width` und `Height`
|
||||
sind zur Laufzeit schreibgeschützt.
|
||||
|
||||
**PictureBox** — Text-Zeichenfläche (PRINT/CLS aufs Control) und Container
|
||||
für OptionButton-Gruppen. Ereignisse: Click, DblClick, Drag*, GotFocus,
|
||||
Key*, LostFocus, Mouse*, Paint. Methoden: CLS, DRAG, MOVE, PRINT, REFRESH,
|
||||
@@ -93,7 +98,7 @@ TabStop, AutoRedraw, CurrentX, CurrentY, ScaleHeight, ScaleWidth.
|
||||
Index, Interval (0 = aus … 65 535 ms), Parent, Tag.
|
||||
|
||||
**Menu** — Ereignis Click; Eigenschaften Caption, Checked, CtlName, Enabled,
|
||||
Index, Parent, Separator, Tag, Visible (Shortcut nur zur Entwurfszeit).
|
||||
Index, Parent, Separator, Shortcut, Tag, Visible.
|
||||
|
||||
**DirListBox/DriveListBox/FileListBox** — Datei-Browser-Controls
|
||||
(Path/Drive/FileName/Pattern/Archive/Hidden/Normal/ReadOnly/System;
|
||||
@@ -124,6 +129,61 @@ sie plattformneutral nach (Laufwerksliste → Wurzeln/Mounts).
|
||||
| Text (TextBox/ComboBox) | — | Entwurfszeit = CtlName |
|
||||
| ListIndex | −1 = keine Auswahl | −1 |
|
||||
|
||||
## Zeichenbilder und Zustände
|
||||
|
||||
Die Originalquellen belegen Aufbau und Rahmenformen, enthalten aber keine
|
||||
vollständige Screenshot-Reihe für jede Klasse und jeden Zustand. Die folgende
|
||||
einheitliche Umsetzung ist deshalb **unsere Festlegung**: normal = die Werte
|
||||
aus `ForeColor`/`BackColor`, fokussiert = Farbe 15 auf 1, deaktiviert = Farbe
|
||||
8 auf 0. `&` kennzeichnet
|
||||
nur den Access-Key und wird nicht ausgegeben. Die Beispiele zeigen jeweils
|
||||
normal · fokussiert · deaktiviert; Inhalt wird auf die verfügbare Breite
|
||||
gekürzt und der Rest mit Leerzeichen gefüllt.
|
||||
|
||||
| Klasse | normal | fokussiert | deaktiviert |
|
||||
|---|---|---|---|
|
||||
| CommandButton, Höhe 1 | `<OK>` | `<OK>` | `<OK>` |
|
||||
| CommandButton, Höhe 2 | `┌OK──┐` / `└────┘` | ebenso | ebenso |
|
||||
| CommandButton, Höhe ≥ 3 | `┌────┐` / `│ OK │` / `└────┘` | ebenso | ebenso |
|
||||
| Label | `Text` | `Text` | `Text` |
|
||||
| Frame | `┌ Titel ─┐` / `│ │` / `└────────┘` | ebenso | ebenso |
|
||||
| CheckBox | `[ ] Text` | `[ ] Text` | `[ ] Text` |
|
||||
| CheckBox, gewählt/grau | `[x] Text` / `[-] Text` | ebenso | ebenso |
|
||||
| OptionButton | `( ) Text` | `( ) Text` | `( ) Text` |
|
||||
| OptionButton, gewählt | `(•) Text` | `(•) Text` | `(•) Text` |
|
||||
| TextBox | `┌──────┐` / `│Text │` / `└──────┘` | ebenso, Cursor an `SelStart` | ebenso, ohne Cursor |
|
||||
| ListBox | `┌──────┐` / `│eins │` / `│zwei │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso |
|
||||
| ComboBox, Dropdown | `[Text ▼]` | ebenso in Fokusfarben | ebenso |
|
||||
| ComboBox, Simple | `[Text ▼]` / `┌──────┐` / `│eins │` / `└──────┘` | ebenso in Fokusfarben | ebenso |
|
||||
| HScrollBar | `◄──□──►` | ebenso in Fokusfarben | ebenso |
|
||||
| VScrollBar | `▲` / `│` / `□` / `│` / `▼` | ebenso in Fokusfarben | ebenso |
|
||||
| Spin | `▲` / `▼` oder `◄ ►` | ebenso in Fokusfarben | ebenso |
|
||||
| PictureBox | `┌──────┐` / `│Text │` / `└──────┘` | ebenso in Fokusfarben | ebenso |
|
||||
| DirListBox | `┌──────┐` / `│[dir] │` / `└──────┘` | ebenso, aktuelle Zeile in Fokusfarben | ebenso |
|
||||
| DriveListBox | `[ / ▼]` | ebenso 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 |
|
||||
| Timer | keine sichtbaren Zellen | keine sichtbaren Zellen | keine sichtbaren Zellen |
|
||||
|
||||
`BorderStyle = 0` unterdrückt den Rahmen von Label, TextBox und PictureBox;
|
||||
`BorderStyle = 2` nutzt `╔═╗║╚╝`. Scrollbars erscheinen nur in den von
|
||||
`ScrollBars` genannten Richtungen. Ein unsichtbares Steuerelement zeichnet
|
||||
keine Zelle.
|
||||
|
||||
## Festgelegte Reihenfolgen
|
||||
|
||||
Die folgenden Regeln sind **unsere Festlegung**, nicht nachgewiesenes
|
||||
Referenzverhalten:
|
||||
|
||||
- **Z-Reihenfolge:** Erzeugungsreihenfolge; später erzeugte Elemente liegen
|
||||
oben. Bei Steuerelement-Arrays entscheidet bei sonst gleicher Position der
|
||||
aufsteigende Index, sodass der höchste Index oben liegt. Gezeichnet wird von
|
||||
unten nach oben, die Trefferprüfung läuft umgekehrt.
|
||||
- **Timer-Reihenfolge:** Sind mehrere Timer am selben Zustellpunkt fällig,
|
||||
werden ihre Ereignisse in aufsteigender, ASCII-unabhängig
|
||||
großgeschriebener `CtlName`-Reihenfolge zugestellt. Mehrere verstrichene
|
||||
Intervalle desselben Timers bleiben in zeitlicher Reihenfolge davor.
|
||||
|
||||
**Keine Default-Eigenschaften:** `Text1 = "x"` gibt es nicht — Zugriff
|
||||
immer explizit (`Text1.Text`). „Default" ist nur die CommandButton-
|
||||
Eigenschaft (Enter-Taste); „Cancel" analog für Esc.
|
||||
@@ -188,8 +248,4 @@ Menü-Steuerelement-Arrays sind möglich.
|
||||
|
||||
## Offene Detailfragen
|
||||
|
||||
- Exakte Optik je Steuerelement-Zustand (normal/fokussiert/deaktiviert):
|
||||
aus den Original-Screenshots ableiten (Phase 4)
|
||||
- Z-Reihenfolge/Überlappung: Verhalten bei überlappenden Controls
|
||||
- Timer-Reihenfolge bei mehreren gleichzeitig fälligen Timern
|
||||
- `.FRM`-Textformat: exakte Serialisierung (siehe dateiformate.md)
|
||||
|
||||
171
docs/inventar.md
171
docs/inventar.md
@@ -22,7 +22,7 @@ namentlich abgewiesen. `Non-Feature` = abgelehnt, gelistet unter
|
||||
**Fundstelle.** Bei `implementiert` das Modul, bei `Non-Feature` der
|
||||
Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
|
||||
**Abdeckung.** implementiert 689 · offen 61 · Non-Feature 53 · gesamt 803
|
||||
**Abdeckung.** implementiert 794 · offen 0 · Non-Feature 50 · gesamt 844
|
||||
|
||||
| Name | Art | Gruppe | Status | Fundstelle | Quelle |
|
||||
|---|---|---|---|---|---|
|
||||
@@ -164,14 +164,14 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `FORMAT$` | Funktion | Formatierung | implementiert | tb-runtime::using | bas7advr |
|
||||
| `PRINT USING` | Anweisung | Formatierung | implementiert | tb-runtime::using | bas7advr |
|
||||
| `SetFormatCC` | Routine | Formatierung | implementiert | tb-runtime::using | bas7advr |
|
||||
| `INPUTBOX$` | Funktion | Forms | offen | - | sprachreferenz |
|
||||
| `MSGBOX` | Anweisung | Forms | offen | - | sprachreferenz |
|
||||
| `MSGBOX` | Funktion | Forms | offen | - | sprachreferenz |
|
||||
| `INPUTBOX$` | Funktion | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
|
||||
| `MSGBOX` | Anweisung | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
|
||||
| `MSGBOX` | Funktion | Forms | implementiert | tb-runtime::builtins | sprachreferenz |
|
||||
| `CIRCLE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `DRAW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `GET (Grafik)` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `LINE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PAINT` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `LINE` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `PAINT` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `PALETTE` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PCOPY` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PMAP` | Funktion | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
@@ -179,7 +179,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `PRESET` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PSET` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `PUT (Grafik)` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `VIEW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `VIEW` | Anweisung | Grafik | implementiert | tb-runtime::screen | bas7advr |
|
||||
| `WINDOW` | Anweisung | Grafik | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `ABSOLUTE` | Routine | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
| `BLOAD` | Anweisung | Hardware | Non-Feature | sprachreferenz.md#abweichungen | bas7advr |
|
||||
@@ -255,7 +255,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `SQR` | Funktion | Mathematik | implementiert | tb-runtime::math | qb45advr |
|
||||
| `TAN` | Funktion | Mathematik | implementiert | tb-runtime::math | bas7advr |
|
||||
| `$DYNAMIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | bas7advr |
|
||||
| `$INCLUDE` | Metabefehl | Metabefehl | offen | - | bas7advr |
|
||||
| `$INCLUDE` | Metabefehl | Metabefehl | implementiert | tb-cli | bas7advr |
|
||||
| `$STATIC` | Metabefehl | Metabefehl | implementiert | tb-frontend::lexer | bas7advr |
|
||||
| `CALL` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
|
||||
| `DECLARE` | Anweisung | Prozeduren | implementiert | tb-frontend::sema | bas7advr |
|
||||
@@ -301,7 +301,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `ERDEV` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `ERDEV$` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `FRE` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `RUN` | Anweisung | System | offen | - | bas7advr |
|
||||
| `RUN` | Anweisung | System | implementiert | tb-cli | bas7advr |
|
||||
| `SLEEP` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `STACK` | Anweisung | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
| `STACK` | Funktion | System | implementiert | tb-runtime::builtins | bas7advr |
|
||||
@@ -341,17 +341,17 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `FORM.SCALEHEIGHT` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.SCALEWIDTH` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.WINDOWSTATE` | Eigenschaft | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.CLS` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.DRAG` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.CLS` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.DRAG` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.HIDE` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.LOAD` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.MOVE` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.PRINT` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.PRINTFORM` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.REFRESH` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.MOVE` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.PRINT` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.PRINTFORM` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.REFRESH` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.SHOW` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.TEXTHEIGHT` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.TEXTWIDTH` | Methode | Forms/FORM | offen | - | forms-referenz |
|
||||
| `FORM.TEXTHEIGHT` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.TEXTWIDTH` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM.UNLOAD` | Methode | Forms/FORM | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FORM_CLICK` | Ereignis | Forms/FORM | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `FORM_DBLCLICK` | Ereignis | Forms/FORM | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -387,10 +387,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `CHECKBOX.DRAGMODE` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `CHECKBOX.CAPTION` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `CHECKBOX.VALUE` | Eigenschaft | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `CHECKBOX.DRAG` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
|
||||
| `CHECKBOX.MOVE` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
|
||||
| `CHECKBOX.REFRESH` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
|
||||
| `CHECKBOX.SETFOCUS` | Methode | Forms/CHECKBOX | offen | - | forms-referenz |
|
||||
| `CHECKBOX.DRAG` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `CHECKBOX.MOVE` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `CHECKBOX.REFRESH` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `CHECKBOX.SETFOCUS` | Methode | Forms/CHECKBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `CHECKBOX_CLICK` | Ereignis | Forms/CHECKBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `CHECKBOX_DRAGDROP` | Ereignis | Forms/CHECKBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `CHECKBOX_DRAGOVER` | Ereignis | Forms/CHECKBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -424,12 +424,12 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `COMBOBOX.SELTEXT` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.STYLE` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.TEXT` | Eigenschaft | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.ADDITEM` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
|
||||
| `COMBOBOX.DRAG` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
|
||||
| `COMBOBOX.MOVE` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
|
||||
| `COMBOBOX.REFRESH` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
|
||||
| `COMBOBOX.REMOVEITEM` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
|
||||
| `COMBOBOX.SETFOCUS` | Methode | Forms/COMBOBOX | offen | - | forms-referenz |
|
||||
| `COMBOBOX.ADDITEM` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.DRAG` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.MOVE` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.REFRESH` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.REMOVEITEM` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX.SETFOCUS` | Methode | Forms/COMBOBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMBOBOX_CHANGE` | Ereignis | Forms/COMBOBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `COMBOBOX_CLICK` | Ereignis | Forms/COMBOBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `COMBOBOX_DBLCLICK` | Ereignis | Forms/COMBOBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -463,10 +463,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `COMMANDBUTTON.CAPTION` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMMANDBUTTON.DEFAULT` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMMANDBUTTON.VALUE` | Eigenschaft | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMMANDBUTTON.DRAG` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
|
||||
| `COMMANDBUTTON.MOVE` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
|
||||
| `COMMANDBUTTON.REFRESH` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
|
||||
| `COMMANDBUTTON.SETFOCUS` | Methode | Forms/COMMANDBUTTON | offen | - | forms-referenz |
|
||||
| `COMMANDBUTTON.DRAG` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMMANDBUTTON.MOVE` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMMANDBUTTON.REFRESH` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMMANDBUTTON.SETFOCUS` | Methode | Forms/COMMANDBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `COMMANDBUTTON_CLICK` | Ereignis | Forms/COMMANDBUTTON | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `COMMANDBUTTON_DRAGDROP` | Ereignis | Forms/COMMANDBUTTON | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `COMMANDBUTTON_DRAGOVER` | Ereignis | Forms/COMMANDBUTTON | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -489,6 +489,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `DIRLISTBOX.TABINDEX` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX.TABSTOP` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX.CTLNAME` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX.LIST` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX.LISTCOUNT` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX.LISTINDEX` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX.PATH` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX.TEXT` | Eigenschaft | Forms/DIRLISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DIRLISTBOX_CHANGE` | Ereignis | Forms/DIRLISTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -509,6 +512,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `DRIVELISTBOX.TABINDEX` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX.TABSTOP` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX.CTLNAME` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX.LIST` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX.LISTCOUNT` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX.LISTINDEX` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX.DRIVE` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX.TEXT` | Eigenschaft | Forms/DRIVELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `DRIVELISTBOX_CHANGE` | Ereignis | Forms/DRIVELISTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -526,6 +532,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `FILELISTBOX.TABINDEX` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.TABSTOP` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.CTLNAME` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.LIST` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.LISTCOUNT` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.LISTINDEX` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.FILENAME` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.PATH` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FILELISTBOX.PATTERN` | Eigenschaft | Forms/FILELISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
@@ -554,9 +563,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `FRAME.CTLNAME` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FRAME.DRAGMODE` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FRAME.CAPTION` | Eigenschaft | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FRAME.DRAG` | Methode | Forms/FRAME | offen | - | forms-referenz |
|
||||
| `FRAME.MOVE` | Methode | Forms/FRAME | offen | - | forms-referenz |
|
||||
| `FRAME.REFRESH` | Methode | Forms/FRAME | offen | - | forms-referenz |
|
||||
| `FRAME.DRAG` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FRAME.MOVE` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FRAME.REFRESH` | Methode | Forms/FRAME | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `FRAME_DRAGDROP` | Ereignis | Forms/FRAME | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `FRAME_DRAGOVER` | Ereignis | Forms/FRAME | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `HSCROLLBAR.BACKCOLOR` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
@@ -581,9 +590,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `HSCROLLBAR.MAX` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `HSCROLLBAR.SMALLCHANGE` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `HSCROLLBAR.VALUE` | Eigenschaft | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `HSCROLLBAR.DRAG` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz |
|
||||
| `HSCROLLBAR.MOVE` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz |
|
||||
| `HSCROLLBAR.REFRESH` | Methode | Forms/HSCROLLBAR | offen | - | forms-referenz |
|
||||
| `HSCROLLBAR.DRAG` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `HSCROLLBAR.MOVE` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `HSCROLLBAR.REFRESH` | Methode | Forms/HSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `HSCROLLBAR_CHANGE` | Ereignis | Forms/HSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `HSCROLLBAR_DRAGDROP` | Ereignis | Forms/HSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `HSCROLLBAR_DRAGOVER` | Ereignis | Forms/HSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -611,9 +620,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `LABEL.AUTOSIZE` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LABEL.BORDERSTYLE` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LABEL.CAPTION` | Eigenschaft | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LABEL.DRAG` | Methode | Forms/LABEL | offen | - | forms-referenz |
|
||||
| `LABEL.MOVE` | Methode | Forms/LABEL | offen | - | forms-referenz |
|
||||
| `LABEL.REFRESH` | Methode | Forms/LABEL | offen | - | forms-referenz |
|
||||
| `LABEL.DRAG` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LABEL.MOVE` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LABEL.REFRESH` | Methode | Forms/LABEL | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LABEL_CHANGE` | Ereignis | Forms/LABEL | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `LABEL_CLICK` | Ereignis | Forms/LABEL | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `LABEL_DBLCLICK` | Ereignis | Forms/LABEL | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -643,12 +652,12 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `LISTBOX.LISTINDEX` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.SORTED` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.TEXT` | Eigenschaft | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.ADDITEM` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
|
||||
| `LISTBOX.DRAG` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
|
||||
| `LISTBOX.MOVE` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
|
||||
| `LISTBOX.REFRESH` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
|
||||
| `LISTBOX.REMOVEITEM` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
|
||||
| `LISTBOX.SETFOCUS` | Methode | Forms/LISTBOX | offen | - | forms-referenz |
|
||||
| `LISTBOX.ADDITEM` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.DRAG` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.MOVE` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.REFRESH` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.REMOVEITEM` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX.SETFOCUS` | Methode | Forms/LISTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `LISTBOX_CLICK` | Ereignis | Forms/LISTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `LISTBOX_DBLCLICK` | Ereignis | Forms/LISTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `LISTBOX_DRAGDROP` | Ereignis | Forms/LISTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -670,6 +679,7 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `MENU.SEPARATOR` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `MENU.TAG` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `MENU.VISIBLE` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `MENU.SHORTCUT` | Eigenschaft | Forms/MENU | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `MENU_CLICK` | Ereignis | Forms/MENU | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `OPTIONBUTTON.BACKCOLOR` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON.ENABLED` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
@@ -689,10 +699,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `OPTIONBUTTON.DRAGMODE` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON.CAPTION` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON.VALUE` | Eigenschaft | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON.DRAG` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
|
||||
| `OPTIONBUTTON.MOVE` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
|
||||
| `OPTIONBUTTON.REFRESH` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
|
||||
| `OPTIONBUTTON.SETFOCUS` | Methode | Forms/OPTIONBUTTON | offen | - | forms-referenz |
|
||||
| `OPTIONBUTTON.DRAG` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON.MOVE` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON.REFRESH` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON.SETFOCUS` | Methode | Forms/OPTIONBUTTON | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `OPTIONBUTTON_CLICK` | Ereignis | Forms/OPTIONBUTTON | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `OPTIONBUTTON_DBLCLICK` | Ereignis | Forms/OPTIONBUTTON | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `OPTIONBUTTON_DRAGDROP` | Ereignis | Forms/OPTIONBUTTON | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -724,14 +734,14 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `PICTUREBOX.CURRENTY` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.SCALEHEIGHT` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.SCALEWIDTH` | Eigenschaft | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.CLS` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.DRAG` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.MOVE` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.PRINT` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.REFRESH` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.SETFOCUS` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.TEXTHEIGHT` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.TEXTWIDTH` | Methode | Forms/PICTUREBOX | offen | - | forms-referenz |
|
||||
| `PICTUREBOX.CLS` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.DRAG` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.MOVE` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.PRINT` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.REFRESH` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.SETFOCUS` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.TEXTHEIGHT` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX.TEXTWIDTH` | Methode | Forms/PICTUREBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `PICTUREBOX_CLICK` | Ereignis | Forms/PICTUREBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `PICTUREBOX_DBLCLICK` | Ereignis | Forms/PICTUREBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `PICTUREBOX_DRAGDROP` | Ereignis | Forms/PICTUREBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -768,10 +778,10 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `TEXTBOX.SELSTART` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `TEXTBOX.SELTEXT` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `TEXTBOX.TEXT` | Eigenschaft | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `TEXTBOX.DRAG` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
|
||||
| `TEXTBOX.MOVE` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
|
||||
| `TEXTBOX.REFRESH` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
|
||||
| `TEXTBOX.SETFOCUS` | Methode | Forms/TEXTBOX | offen | - | forms-referenz |
|
||||
| `TEXTBOX.DRAG` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `TEXTBOX.MOVE` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `TEXTBOX.REFRESH` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `TEXTBOX.SETFOCUS` | Methode | Forms/TEXTBOX | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `TEXTBOX_CHANGE` | Ereignis | Forms/TEXTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `TEXTBOX_DRAGDROP` | Ereignis | Forms/TEXTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `TEXTBOX_DRAGOVER` | Ereignis | Forms/TEXTBOX | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -809,9 +819,9 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `VSCROLLBAR.MAX` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `VSCROLLBAR.SMALLCHANGE` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `VSCROLLBAR.VALUE` | Eigenschaft | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `VSCROLLBAR.DRAG` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz |
|
||||
| `VSCROLLBAR.MOVE` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz |
|
||||
| `VSCROLLBAR.REFRESH` | Methode | Forms/VSCROLLBAR | offen | - | forms-referenz |
|
||||
| `VSCROLLBAR.DRAG` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `VSCROLLBAR.MOVE` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `VSCROLLBAR.REFRESH` | Methode | Forms/VSCROLLBAR | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `VSCROLLBAR_CHANGE` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `VSCROLLBAR_DRAGDROP` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `VSCROLLBAR_DRAGOVER` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
@@ -820,6 +830,37 @@ Abschnitt der Sprachreferenz, bei `offen` ein Strich.
|
||||
| `VSCROLLBAR_KEYPRESS` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `VSCROLLBAR_KEYUP` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `VSCROLLBAR_LOSTFOCUS` | Ereignis | Forms/VSCROLLBAR | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN.BACKCOLOR` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.BORDERSTYLE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.CTLNAME` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.ENABLED` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.HEIGHT` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.INDEX` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.INTERVAL` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.LEFT` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.MAX` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.MIN` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.MOUSEPOINTER` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.PARENT` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.STYLE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.TABINDEX` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.TABSTOP` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.TAG` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.TOP` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.VALUE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.VISIBLE` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.WIDTH` | Eigenschaft | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.DRAG` | Methode | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.REFRESH` | Methode | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN.SETFOCUS` | Methode | Forms/SPIN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SPIN_CUSTOM` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN_DRAGDROP` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN_DRAGOVER` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN_GOTFOCUS` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN_KEYDOWN` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN_KEYPRESS` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN_KEYUP` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SPIN_LOSTFOCUS` | Ereignis | Forms/SPIN | implementiert | tb-vm::interp | forms-referenz |
|
||||
| `SCREEN.ACTIVECONTROL` | Eigenschaft | Forms/SCREEN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SCREEN.ACTIVEFORM` | Eigenschaft | Forms/SCREEN | implementiert | tb-ui::forms | forms-referenz |
|
||||
| `SCREEN.AUTOREDRAW` | Eigenschaft | Forms/SCREEN | implementiert | tb-ui::forms | forms-referenz |
|
||||
|
||||
@@ -106,6 +106,9 @@ stehen am Ende dieses Dokuments; nur diese Liste rechtfertigt Abweisungen.
|
||||
- `CONST name = ausdruck` — Konstanten (konstante Ausdrücke zur Compilezeit).
|
||||
- `DECLARE SUB/FUNCTION name (parameter)` — Prototyp; die IDE erzeugt sie
|
||||
automatisch beim Speichern.
|
||||
- `'$INCLUDE: 'datei'` — bindet eine Textquelle relativ zur einschließenden
|
||||
Datei ein; Namen werden wie unter DOS ohne Beachtung der Großschreibung
|
||||
aufgelöst, Zyklen werden namentlich abgewiesen.
|
||||
|
||||
## 4. Operatoren (nach Priorität, hoch → niedrig)
|
||||
|
||||
@@ -148,6 +151,9 @@ INTEGER −1 (wahr) / 0 (falsch).
|
||||
als Direktmodus-Befehl der IDE (Phase 5) und ist nicht kompilierbar.
|
||||
- `SLEEP [sekunden]`, `DO EVENTS`/`DOEVENTS` (Ereignisse verarbeiten —
|
||||
zentral für Forms)
|
||||
- `RUN` startet das aktuelle Programm neu; `RUN zeile` startet es nach den
|
||||
globalen Deklarationen an der angegebenen Zeile, `RUN datei$` lädt relativ
|
||||
zur laufenden Quelle eine `.BAS`-, `.FRM`- oder `.MAK`-Datei neu.
|
||||
|
||||
## 6. Prozeduren
|
||||
|
||||
@@ -517,16 +523,12 @@ Dialoge:
|
||||
Kombinationen) liefern nichts — eine eigene Kodierung dafür wäre eine
|
||||
Erweiterung und gehört in Stufe 2.
|
||||
4. **Kein `CHAIN`/Overlay-Mechanismus**; `SHELL` startet die System-Shell.
|
||||
5. **Grafik-Anweisungen** (`SCREEN n>0`, `PSET`, `PRESET`, `LINE`,
|
||||
`CIRCLE`, `PAINT`, `DRAW`, Grafik-`GET`/`PUT`, `PALETTE`, `WINDOW`,
|
||||
`VIEW`, `PMAP`, `PCOPY`, `POINT`) sind Nicht-Ziel und werden **zur
|
||||
Compile-Zeit** mit „Feature unavailable" abgewiesen. Die Formen, die
|
||||
sich ein Schlüsselwort mit einer unterstützten Anweisung teilen, erkennt
|
||||
der Compiler an ihrer Syntax: `LINE` ohne `INPUT`, `GET`/`PUT` mit
|
||||
`(`, `VIEW` ohne `PRINT`. Hinweis: das Vorbild **hatte** die
|
||||
Grafikmodi 0–13, aber nicht gleichzeitig mit Forms (dort Fehler 272
|
||||
„Invalid when forms are showing") — wir übernehmen nur den
|
||||
Forms-/Textmodus-Zweig.
|
||||
5. **Grafik im Zellenpuffer:** `SCREEN 0` bis `SCREEN 13`, `LINE` (auch
|
||||
`B`/`BF` und `STEP`), `PAINT` und `VIEW` werden ausgeführt. Historische
|
||||
Pixelkoordinaten werden dabei in 8×8-Blöcken auf Terminalzellen
|
||||
abgebildet. `PSET`, `PRESET`, `CIRCLE`, `DRAW`, Grafik-`GET`/`PUT`,
|
||||
`PALETTE`, `WINDOW`, `PMAP`, `PCOPY` und `POINT` bleiben Nicht-Ziel und
|
||||
werden zur Compile-Zeit mit „Feature unavailable" abgewiesen.
|
||||
6. **`PLAY`/`SOUND`**: `BEEP` = Terminal-Bell; Rest Nicht-Ziel (Fehler 73).
|
||||
5a. **`LPRINT` schreibt in eine Datei** (2026-09-03): einen
|
||||
plattformübergreifenden Druckerkanal gibt es nicht. Der Druckerpuffer
|
||||
@@ -576,3 +578,13 @@ Dialoge:
|
||||
werden nicht abgebildet. Unter Windows ist `SIGTERM` von außen nicht
|
||||
zustellbar — `SIGNAL(2)` bleibt dort wirkungslos. Trifft `SIGINT` ohne aktiven Trap
|
||||
ein, bleibt es beim bisherigen Abbruch.
|
||||
|
||||
9. **Forms-Darstellung und Dateisystem-Steuerelemente** (2026-09-05):
|
||||
Controls zeichnen ausschließlich in den Unicode-Zellenpuffer. Mangels
|
||||
vollständiger Original-Screenshots gelten die Zeichenbilder und
|
||||
Fokus-/Deaktiviert-Farben aus `forms-referenz.md` als unsere Festlegung.
|
||||
Dasselbe gilt für die Z-Reihenfolge (Erzeugungsreihenfolge, später liegt
|
||||
oben) und gleichzeitig fällige Timer (aufsteigend nach `CtlName`).
|
||||
`DirListBox` und `FileListBox` verwenden native Pfade und `*`/`?`-Muster;
|
||||
`DriveListBox` zeigt unter Unix `/` und unter macOS zusätzlich Mounts aus
|
||||
`/Volumes` statt DOS-Laufwerksbuchstaben.
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
Siehe proposal.md — Why. Dieser Change setzt auf drei fertigen Stücken
|
||||
auf: dem Zellenpuffer aus Phase 3, der Ereignisschleife samt Mausquelle
|
||||
(`phase-4-ereignisschleife`) und dem Objektmodell
|
||||
(`phase-4-objektmodell`). Er ist der breiteste Change der Phase — 15
|
||||
(`phase-4-objektmodell`). Er ist der breiteste Change der Phase — 16
|
||||
Klassen, Menüs, Dialoge —, aber der mit den wenigsten offenen
|
||||
Architekturfragen.
|
||||
|
||||
@@ -94,9 +94,16 @@ Einhängepunkte der Plattform; DirListBox und FileListBox arbeiten auf
|
||||
Pfaden. `Pattern` bleibt das Muster des Vorbilds. Die Abweichung wird in
|
||||
der Sprachreferenz festgehalten.
|
||||
|
||||
### D7 — Korpusbefunde werden im gemeinsamen Ausführungspfad geschlossen
|
||||
|
||||
Die VBDOS-Projekte laufen durch denselben CLI-, Frontend-, VM- und
|
||||
Zellenpufferpfad wie eigene Programme. Projekt- und Include-Auflösung,
|
||||
VSpin/HSpin, Common-Dialog-Routinen und die tatsächlich verwendete Grafik
|
||||
werden deshalb dort implementiert und getestet, nicht im Korpus umgangen.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **Breite statt Tiefe** → 15 Klassen sind viel Fläche für Flüchtigkeit;
|
||||
- **Breite statt Tiefe** → 16 Klassen sind viel Fläche für Flüchtigkeit;
|
||||
jede Klasse bekommt ihren eigenen Korpusnachweis, nicht nur einen
|
||||
gemeinsamen Sammeltest.
|
||||
- **Vollständiges Neuzeichnen bei großen Terminals** → bei 200×60 sind es
|
||||
@@ -109,6 +116,4 @@ der Sprachreferenz festgehalten.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- Ob die PictureBox als Container für OptionButton-Gruppen dieselbe
|
||||
Fokuslogik wie der Frame braucht, zeigt der erste Korpusnachweis mit
|
||||
gruppierten Optionsfeldern.
|
||||
Keine.
|
||||
@@ -18,7 +18,7 @@ zwischen die Aufgaben.
|
||||
- **Steuerelementklassen mit Verhalten**: CommandButton, TextBox,
|
||||
ListBox, ComboBox, CheckBox, OptionButton, Frame, Label, HScrollBar,
|
||||
VScrollBar, PictureBox, Timer sowie die Dateisystem-Steuerelemente
|
||||
DirListBox, DriveListBox und FileListBox — je mit Darstellung,
|
||||
DirListBox, DriveListBox, FileListBox und VSpin/HSpin — je mit Darstellung,
|
||||
Zuständen (normal, fokussiert, deaktiviert) und ihren Methoden.
|
||||
- **Darstellung im Zellenpuffer**: Steuerelemente zeichnen in den
|
||||
vorhandenen Textbildschirm; es entsteht keine zweite Zeichenschicht.
|
||||
@@ -43,10 +43,13 @@ zwischen die Aufgaben.
|
||||
Positionierung in Zeichen.
|
||||
- **Timer als Steuerelement**: `Interval` 0–65 535 ms auf der Zeitquelle
|
||||
der Ereignisschleife, Ereignis `Timer`.
|
||||
- **VBDOS-Kompatibilität**: `.MAK`- und `$INCLUDE`-Auflösung, `RUN`,
|
||||
VSpin/HSpin, die verwendete Zellgrafik (`SCREEN`, `LINE`, `PAINT`, `VIEW`)
|
||||
und die von den Beispielen benutzten Common-Dialog-Routinen.
|
||||
|
||||
**Non-Goals:** Der Formular-Designer und die Projektverwaltung (Phase 5);
|
||||
native Erweiterungssteuerelemente (Stufe 2); grafische Ausgabe in der
|
||||
PictureBox — sie ist eine Textzeichenfläche.
|
||||
**Non-Goals:** Der Formular-Designer (Phase 5). Für den geforderten
|
||||
VBDOS-Kompatibilitätsnachweis notwendige Projektauflösung, native
|
||||
Erweiterungssteuerelemente und Grafikausgabe gehören zu diesem Change.
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -66,10 +69,10 @@ PictureBox — sie ist eine Textzeichenfläche.
|
||||
|
||||
- `crates/tb-ui`: Darstellung und Eingabebehandlung je Klasse, Menüs,
|
||||
Dialoge, Trefferprüfung, Fokusverwaltung.
|
||||
- `crates/tb-runtime`: Fehler 260–480 des Forms-Bereichs als Auslöser;
|
||||
`MSGBOX`/`INPUTBOX$` als Sprachelemente.
|
||||
- `crates/tb-runtime`: Fehler 260–480 des Forms-Bereichs als Auslöser,
|
||||
`MSGBOX`/`INPUTBOX$` und die benötigte Zellgrafik.
|
||||
- `crates/tb-frontend`: Signaturen von `MSGBOX` (Anweisung und Funktion)
|
||||
und `INPUTBOX$`; die `Unsupported`-Absenkung von `MSGBOX` entfällt.
|
||||
und `INPUTBOX$`, Projektquellen, `RUN` und die verwendete Grafiksyntax.
|
||||
- `tests/compat`: Formularprogramme mit Sollausgabe; die Programme aus
|
||||
`github.com/cout/vbdos` als Kompatibilitätsnachweis.
|
||||
- `docs/`: `forms-referenz.md` — die offenen Detailfragen (Optik je
|
||||
@@ -19,11 +19,10 @@ Listen-Steuerelement, ein Timer und ein modaler Dialog.
|
||||
|
||||
### Requirement: Kompatibilitätsnachweis an Fremdprogrammen
|
||||
Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL
|
||||
sich ohne Non-Features übersetzen lassen und bedienbar sein. Ein
|
||||
Programm, das an einem Non-Feature scheitert, MUST das Element
|
||||
namentlich nennen; ein Scheitern aus anderem Grund MUST als Befund
|
||||
festgehalten werden.
|
||||
sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar
|
||||
sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder
|
||||
Laufzeitpfad MUST implementiert und als Befund festgehalten werden.
|
||||
|
||||
#### Scenario: Fremdprogramm übersetzt
|
||||
- **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird
|
||||
- **THEN** entstehen keine Diagnosen außer namentlich genannten Non-Features
|
||||
- **THEN** entstehen keine Diagnosen und das Programm ist bedienbar
|
||||
@@ -1,49 +1,49 @@
|
||||
## 1. Festlegungen vorab
|
||||
|
||||
- [ ] 1.1 Optik je Klasse und Zustand (normal, fokussiert, deaktiviert) als Zeichenbild in `docs/forms-referenz.md` festhalten, aus den Original-Screenshots abgeleitet (D3); verifiziert durch je ein Zeichenbild pro Klasse und Zustand
|
||||
- [ ] 1.2 Z-Reihenfolge und Timer-Reihenfolge festlegen und als unsere Festlegung kennzeichnen (D3); verifiziert durch die beiden Abschnitte in der Forms-Referenz
|
||||
- [x] 1.1 Optik je Klasse und Zustand (normal, fokussiert, deaktiviert) als Zeichenbild in `docs/forms-referenz.md` festhalten, aus den Original-Screenshots abgeleitet (D3); verifiziert durch je ein Zeichenbild pro Klasse und Zustand
|
||||
- [x] 1.2 Z-Reihenfolge und Timer-Reihenfolge festlegen und als unsere Festlegung kennzeichnen (D3); verifiziert durch die beiden Abschnitte in der Forms-Referenz
|
||||
|
||||
## 2. Darstellung und Eingabekette
|
||||
|
||||
- [ ] 2.1 Zeichnen je Klasse aus dem Eigenschaftszustand in den Zellenpuffer (D1); verifiziert durch Snapshot-Tests je Klasse, darunter die drei Höhenformen des CommandButton
|
||||
- [ ] 2.2 Eingabekette für Tastatur und Maus (D2); verifiziert durch Unit-Tests, die je Station den Vorrang prüfen
|
||||
- [ ] 2.3 Trefferprüfung über die Z-Reihenfolge; verifiziert durch einen Test mit überlappenden Steuerelementen und einem Klick ins Formular
|
||||
- [x] 2.1 Zeichnen je Klasse aus dem Eigenschaftszustand in den Zellenpuffer (D1); verifiziert durch Snapshot-Tests je Klasse, darunter die drei Höhenformen des CommandButton
|
||||
- [x] 2.2 Eingabekette für Tastatur und Maus (D2); verifiziert durch Unit-Tests, die je Station den Vorrang prüfen
|
||||
- [x] 2.3 Trefferprüfung über die Z-Reihenfolge; verifiziert durch einen Test mit überlappenden Steuerelementen und einem Klick ins Formular
|
||||
|
||||
## 3. Fokus und Tastatur
|
||||
|
||||
- [ ] 3.1 Tabreihenfolge über `TabIndex`/`TabStop` vorwärts und rückwärts, deaktivierte Elemente überspringen; verifiziert durch Unit-Tests je Fall
|
||||
- [ ] 3.2 Access-Keys aus `&`, `Default` für Enter, `Cancel` für Esc; verifiziert durch Unit-Tests je Auslöser
|
||||
- [ ] 3.3 `SETFOCUS`, `GotFocus`/`LostFocus` in der festgelegten Reihenfolge; verifiziert durch einen Test, der die Reihenfolge der beiden Ereignisse prüft
|
||||
- [x] 3.1 Tabreihenfolge über `TabIndex`/`TabStop` vorwärts und rückwärts, deaktivierte Elemente überspringen; verifiziert durch Unit-Tests je Fall
|
||||
- [x] 3.2 Access-Keys aus `&`, `Default` für Enter, `Cancel` für Esc; verifiziert durch Unit-Tests je Auslöser
|
||||
- [x] 3.3 `SETFOCUS`, `GotFocus`/`LostFocus` in der festgelegten Reihenfolge; verifiziert durch einen Test, der die Reihenfolge der beiden Ereignisse prüft
|
||||
|
||||
## 4. Steuerelementklassen
|
||||
|
||||
- [ ] 4.1 CommandButton, Label, Frame, CheckBox, OptionButton (inkl. Gruppierung im Container); verifiziert durch Snapshot- und Verhaltenstests je Klasse
|
||||
- [ ] 4.2 TextBox mit `Text`, `SelStart`/`SelLength`/`SelText`, `MultiLine`, `ScrollBars`, Ereignis `Change`; verifiziert durch Tests für Eingabe, Auswahl und Umbruch
|
||||
- [ ] 4.3 ListBox und ComboBox mit `ADDITEM`/`REMOVEITEM`, `List`/`ListCount`/`ListIndex`, `Sorted`, den drei ComboBox-Stilarten; verifiziert durch Tests für Einfügen, Sortierung und „keine Auswahl"
|
||||
- [ ] 4.4 HScrollBar/VScrollBar mit `Min`/`Max`/`Value`/`SmallChange`/`LargeChange`/`Attached` und Ereignis `Change`; verifiziert durch Tests für Tastatur- und Mausbedienung
|
||||
- [ ] 4.5 PictureBox als Textzeichenfläche (`PRINT`, `CLS`, `CurrentX`/`CurrentY`, `TEXTWIDTH`/`TEXTHEIGHT`) und als Container; verifiziert durch Snapshot-Tests
|
||||
- [ ] 4.6 Timer auf der Zeitquelle der Ereignisschleife, Reihenfolge nach 1.2; verifiziert durch Tests mit virtueller Zeit für Abstand, `Interval = 0` und zwei gleichzeitig fällige Timer
|
||||
- [ ] 4.7 DirListBox, DriveListBox, FileListBox plattformneutral (D6) mit `Path`/`Drive`/`FileName`/`Pattern` und ihren Ereignissen; verifiziert durch Tests auf einem angelegten Verzeichnisbaum
|
||||
- [ ] 4.8 Ziehen und Ablegen: `DragMode`, `DRAG action%`, `DragOver` mit Zustand, `DragDrop` mit Quelle; verifiziert durch Tests für automatisches und manuelles Ziehen sowie Abbruch
|
||||
- [x] 4.1 CommandButton, Label, Frame, CheckBox, OptionButton (inkl. Gruppierung im Container); verifiziert durch Snapshot- und Verhaltenstests je Klasse
|
||||
- [x] 4.2 TextBox mit `Text`, `SelStart`/`SelLength`/`SelText`, `MultiLine`, `ScrollBars`, Ereignis `Change`; verifiziert durch Tests für Eingabe, Auswahl und Umbruch
|
||||
- [x] 4.3 ListBox und ComboBox mit `ADDITEM`/`REMOVEITEM`, `List`/`ListCount`/`ListIndex`, `Sorted`, den drei ComboBox-Stilarten; verifiziert durch Tests für Einfügen, Sortierung und „keine Auswahl"
|
||||
- [x] 4.4 HScrollBar/VScrollBar mit `Min`/`Max`/`Value`/`SmallChange`/`LargeChange`/`Attached` und Ereignis `Change`; verifiziert durch Tests für Tastatur- und Mausbedienung
|
||||
- [x] 4.5 PictureBox als Textzeichenfläche (`PRINT`, `CLS`, `CurrentX`/`CurrentY`, `TEXTWIDTH`/`TEXTHEIGHT`) und als Container; verifiziert durch Snapshot-Tests
|
||||
- [x] 4.6 Timer auf der Zeitquelle der Ereignisschleife, Reihenfolge nach 1.2; verifiziert durch Tests mit virtueller Zeit für Abstand, `Interval = 0` und zwei gleichzeitig fällige Timer
|
||||
- [x] 4.7 DirListBox, DriveListBox, FileListBox plattformneutral (D6) mit `Path`/`Drive`/`FileName`/`Pattern` und ihren Ereignissen; verifiziert durch Tests auf einem angelegten Verzeichnisbaum
|
||||
- [x] 4.8 Ziehen und Ablegen: `DragMode`, `DRAG action%`, `DragOver` mit Zustand, `DragDrop` mit Quelle; verifiziert durch Tests für automatisches und manuelles Ziehen sowie Abbruch
|
||||
|
||||
## 5. Menüs und Dialoge
|
||||
|
||||
- [ ] 5.1 Menüleiste mit bis zu sechs Ebenen, Access-Keys, Shortcuts, `Checked`/`Enabled`/`Visible`/`Separator`, Menü-Arrays; verifiziert durch Snapshot- und Verhaltenstests, darunter die Regelverstöße (Separator mit Shortcut)
|
||||
- [ ] 5.2 Ereigniszustellung ruht im geöffneten Menü über den vorhandenen `STOP`-Zustand (D5); verifiziert durch einen Test mit fälligem Zeit-Trap bei offenem Menü
|
||||
- [ ] 5.3 `MSGBOX` als Anweisung und Funktion mit Schaltflächengruppen, Vorgabeschaltfläche und Rückgabewerten; verifiziert durch Tests je Gruppe und Rückgabewert
|
||||
- [ ] 5.4 `INPUTBOX$` mit fester Größe 46×16, Positionierung in Zeichen, leerem String bei Abbruch; verifiziert durch Snapshot-Test und Abbruchtest
|
||||
- [ ] 5.5 Frontend-Signaturen für `MSGBOX` (Anweisung und Funktion) und `INPUTBOX$`, `Unsupported`-Absenkung entfernen; verifiziert durch `cargo test -p tb-frontend`
|
||||
- [x] 5.1 Menüleiste mit bis zu sechs Ebenen, Access-Keys, Shortcuts, `Checked`/`Enabled`/`Visible`/`Separator`, Menü-Arrays; verifiziert durch Snapshot- und Verhaltenstests, darunter die Regelverstöße (Separator mit Shortcut)
|
||||
- [x] 5.2 Ereigniszustellung ruht im geöffneten Menü über den vorhandenen `STOP`-Zustand (D5); verifiziert durch einen Test mit fälligem Zeit-Trap bei offenem Menü
|
||||
- [x] 5.3 `MSGBOX` als Anweisung und Funktion mit Schaltflächengruppen, Vorgabeschaltfläche und Rückgabewerten; verifiziert durch Tests je Gruppe und Rückgabewert
|
||||
- [x] 5.4 `INPUTBOX$` mit fester Größe 46×16, Positionierung in Zeichen, leerem String bei Abbruch; verifiziert durch Snapshot-Test und Abbruchtest
|
||||
- [x] 5.5 Frontend-Signaturen für `MSGBOX` (Anweisung und Funktion) und `INPUTBOX$`, `Unsupported`-Absenkung entfernen; verifiziert durch `cargo test -p tb-frontend`
|
||||
|
||||
## 6. Korpus und Kompatibilität
|
||||
|
||||
- [ ] 6.1 Ereignisfolge-Direktive im Programmkopf und ihre Einspeisung durch den Harness; verifiziert durch einen Harness-Test mit deklarierten Tasten-, Maus- und Zeitereignissen
|
||||
- [ ] 6.2 Formular-Korpusprogramme für Fokus/Tab, Access-Key, Mausklick, Menüauswahl, Liste, Timer und modalen Dialog; verifiziert durch `cargo test -p tb-cli` gegen die Sollausgaben
|
||||
- [ ] 6.3 Meilenstein: die Formularprogramme aus `github.com/cout/vbdos` übersetzen und bedienen, jeden Befund festhalten; verifiziert durch den Lauf über den Bestand mit Befundliste
|
||||
- [ ] 6.4 Meilenstein: die Beispiel-Formularprogramme des Korpus laufen; verifiziert durch den grünen Korpuslauf
|
||||
- [x] 6.1 Ereignisfolge-Direktive im Programmkopf und ihre Einspeisung durch den Harness; verifiziert durch einen Harness-Test mit deklarierten Tasten-, Maus- und Zeitereignissen
|
||||
- [x] 6.2 Formular-Korpusprogramme für Fokus/Tab, Access-Key, Mausklick, Menüauswahl, Liste, Timer und modalen Dialog; verifiziert durch `cargo test -p tb-cli` gegen die Sollausgaben
|
||||
- [x] 6.3 Meilenstein: die Formularprogramme aus `github.com/cout/vbdos` übersetzen und bedienen, jeden Befund festhalten; verifiziert durch den Lauf über den Bestand mit Befundliste
|
||||
- [x] 6.4 Meilenstein: die Beispiel-Formularprogramme des Korpus laufen; verifiziert durch den grünen Korpuslauf
|
||||
|
||||
## 7. Inventar, Dokumentation, Abschluss
|
||||
|
||||
- [ ] 7.1 `docs/inventar.md`: Einträge dieses Changes auf `implementiert`; verifiziert durch `inventar_stimmt_mit_code_ueberein` mit Abdeckung 0 offen für Phase 4
|
||||
- [ ] 7.2 Abweichungen (Dateisystem-Steuerelemente, festgelegte Optik, Z- und Timer-Reihenfolge) in `docs/sprachreferenz.md` eintragen; verifiziert durch den Abschnitt „Abweichungen"
|
||||
- [ ] 7.3 PLAN.md: die verbleibenden Phase-4-Punkte abhaken und die Befunde der Phase festhalten; verifiziert durch die abgeschlossene Phase-4-Liste
|
||||
- [ ] 7.4 `cargo test` grün und `openspec validate phase-4-steuerelemente --strict` ohne Befund; verifiziert durch beide Kommandos
|
||||
- [x] 7.1 `docs/inventar.md`: Einträge dieses Changes auf `implementiert`; verifiziert durch `inventar_stimmt_mit_code_ueberein` mit Abdeckung 0 offen für Phase 4
|
||||
- [x] 7.2 Abweichungen (Dateisystem-Steuerelemente, festgelegte Optik, Z- und Timer-Reihenfolge) in `docs/sprachreferenz.md` eintragen; verifiziert durch den Abschnitt „Abweichungen"
|
||||
- [x] 7.3 PLAN.md: die verbleibenden Phase-4-Punkte abhaken und die Befunde der Phase festhalten; verifiziert durch die abgeschlossene Phase-4-Liste
|
||||
- [x] 7.4 `cargo test` grün und `openspec validate phase-4-steuerelemente --strict` ohne Befund; verifiziert durch beide Kommandos
|
||||
@@ -0,0 +1,44 @@
|
||||
# Befunde am öffentlichen VBDOS-Bestand
|
||||
|
||||
Geprüft wurde `cout/vbdos` im Commit
|
||||
`1cdd2b32b829fe1721d0b6aecc433abc47a96fb6`. Maßgeblich waren die
|
||||
vorhandenen `.MAK`-Projekte; wo keines existiert, wurde die `.FRM` direkt
|
||||
verwendet. Jeder Eintrag bestand `tbc check` ohne Diagnose. Die Programme
|
||||
wurden anschließend in einem 100×30-Pseudoterminal gestartet und blieben nach
|
||||
dem Ende des Modulrumpfs bedienbar, bis ein Steuerelement das Formular schloss
|
||||
oder der Lauf bewusst abgebrochen wurde.
|
||||
|
||||
| Programm | Check | Bediennachweis |
|
||||
|---|---:|---|
|
||||
| `graphics/graphics.mak` | 0 Diagnosen | Alt+X löst `cmdExit_Click` aus und entlädt das Formular |
|
||||
| `microsoft/check.mak` | 0 Diagnosen | Alt+F öffnet das Menü; X erreicht den Exit-/Speicherdialogpfad |
|
||||
| `microsoft/qlbview.mak` | 0 Diagnosen | Esc löst die Cancel-Schaltfläche aus und entlädt das Formular |
|
||||
| `microsoft/seek.mak` | 0 Diagnosen | Alt+X löst `cmdExit_Click` aus und entlädt das Formular |
|
||||
| `microsoft/spindemo.mak` | 0 Diagnosen | Tab und Pfeiltaste werden dem Text-/Spin-Steuerpfad zugestellt |
|
||||
| `microsoft/notepad.frm` | 0 Diagnosen | Alt+F öffnet das Menü; X löst `mnuFileExit_Click` aus |
|
||||
| `misc/mentors/mentors.frm` | 0 Diagnosen | Alt+F öffnet das Menü; X erreicht den Exit-/Speicherdialogpfad |
|
||||
|
||||
## Geschlossene Befunde
|
||||
|
||||
- `.MAK`-Einträge, `$INCLUDE` und `RUN` werden relativ und
|
||||
DOS-großschreibungsunabhängig aufgelöst; mehrere Module und Formulare
|
||||
werden als gemeinsames Programm übersetzt.
|
||||
- Binärformular-P-Code behält Block- und Anweisungsgrenzen, numerische
|
||||
Suffixe, Record-/UDT-Ausdrücke und Control-Arrays korrekt bei.
|
||||
- Globale Arrays werden vor `Form_Load` initialisiert; `LEN(record)` liefert
|
||||
die feste Satzbreite. `INPUT$` liest auch Binärdateien.
|
||||
- `SCREEN.CONTROLPANEL(index)`, `CLIPBOARD` und `PRINTER` haben
|
||||
Laufzeitverhalten; die von Notepad verwendeten Common-Dialog-Deklarationen
|
||||
arbeiten über die vorhandenen Textdialoge.
|
||||
- Die nativen Controls `VSpin` und `HSpin` werden als bedienbares `Spin`
|
||||
importiert, einschließlich Wertgrenzen, Wiederholung und `Custom`-Ereignis.
|
||||
- `SCREEN` 0–13 sowie die im Grafikbeispiel verwendeten Anweisungen `LINE`,
|
||||
`PAINT` und `VIEW` zeichnen auf den Zellenpuffer.
|
||||
- Modellose Formulare halten nach dem Modulrumpf die VM-Ereignisschleife
|
||||
offen; Formularereignisse laufen auf derselben VM weiter, bis alle Formulare
|
||||
geschlossen sind. Derselbe Pfad funktioniert im Terminal und mit
|
||||
vorab eingelesener Pipe-Eingabe.
|
||||
|
||||
Damit sind alle während dieses Kompatibilitätslaufs gefundenen
|
||||
Implementierungslücken geschlossen; es verbleibt kein dokumentierter oder
|
||||
umgangener Befund.
|
||||
140
openspec/specs/forms-steuerelemente/spec.md
Normal file
140
openspec/specs/forms-steuerelemente/spec.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# forms-steuerelemente Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Die Steuerelemente machen ein Formular bedienbar: sie stellen sich im
|
||||
Textbildschirm dar, nehmen Tastatur und Maus entgegen, führen Fokus und
|
||||
Tabreihenfolge, tragen die Menüleiste und stellen die vordefinierten
|
||||
Dialoge bereit.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Darstellung im Zellenpuffer
|
||||
Steuerelemente SHALL sich im vorhandenen Textbildschirm darstellen; es
|
||||
MUST NOT eine zweite Zeichenschicht neben ihm entstehen. Jede Klasse
|
||||
SHALL die in der Forms-Referenz festgelegte Optik für die Zustände
|
||||
normal, fokussiert und deaktiviert zeigen. Der CommandButton SHALL seine
|
||||
Darstellung nach der Höhe wählen: eine Zeile als `<Text>`, zwei Zeilen
|
||||
mit einzeiligem Rahmen, ab drei Zeilen als Kasten. Überlappen
|
||||
Steuerelemente, SHALL die festgelegte Z-Reihenfolge entscheiden, welches
|
||||
sichtbar ist.
|
||||
|
||||
#### Scenario: Schaltfläche nach Höhe
|
||||
- **WHEN** ein CommandButton mit `Height = 1` und `Caption = "OK"` gezeichnet wird
|
||||
- **THEN** erscheint im Zellenpuffer `<OK>`
|
||||
|
||||
#### Scenario: Deaktivierter Zustand
|
||||
- **WHEN** ein Steuerelement `Enabled = 0` trägt
|
||||
- **THEN** unterscheidet sich seine Darstellung sichtbar vom aktiven Zustand und es nimmt keinen Fokus an
|
||||
|
||||
### Requirement: Fokus, Tabreihenfolge und Access-Keys
|
||||
Der Fokus SHALL mit Tab in aufsteigender `TabIndex`-Folge und mit
|
||||
Umschalt-Tab rückwärts wechseln; Elemente mit `TabStop = 0` oder
|
||||
`Enabled = 0` MUST übersprungen werden. Ein `&` im Text SHALL den
|
||||
folgenden Buchstaben zum Access-Key machen, der mit Alt das Element
|
||||
auslöst oder ihm den Fokus gibt. Enter SHALL die `Default`-Schaltfläche
|
||||
auslösen, Esc die `Cancel`-Schaltfläche. Fokuswechsel MUST `LostFocus`
|
||||
am alten und `GotFocus` am neuen Element auslösen, in dieser Reihenfolge.
|
||||
|
||||
#### Scenario: Tab überspringt
|
||||
- **WHEN** das mittlere von drei Elementen `TabStop = 0` trägt und Tab gedrückt wird
|
||||
- **THEN** erhält das dritte Element den Fokus
|
||||
|
||||
#### Scenario: Access-Key
|
||||
- **WHEN** eine Schaltfläche `Caption = "&OK"` trägt und Alt+O gedrückt wird
|
||||
- **THEN** wird ihr `Click`-Ereignis ausgelöst
|
||||
|
||||
#### Scenario: Reihenfolge der Fokusereignisse
|
||||
- **WHEN** der Fokus von `Text1` auf `Text2` wechselt
|
||||
- **THEN** läuft erst `Text1_LostFocus`, danach `Text2_GotFocus`
|
||||
|
||||
### Requirement: Maussteuerung mit Trefferprüfung
|
||||
Ein Mausereignis SHALL dem obersten Steuerelement an seiner Position
|
||||
zugestellt werden; liegt dort keines, dem Formular. Klick, Doppelklick
|
||||
sowie `MouseDown`, `MouseMove` und `MouseUp` SHALL mit Taste,
|
||||
Umschaltzustand und Position in Zellen zugestellt werden. Bei
|
||||
`DragMode = 1` SHALL das Ziehen automatisch beginnen; `DRAG action%`
|
||||
SHALL es manuell beginnen, ablegen oder abbrechen, mit `DragOver` und
|
||||
`DragDrop` am Ziel.
|
||||
|
||||
#### Scenario: Treffer nach Z-Reihenfolge
|
||||
- **WHEN** zwei Steuerelemente überlappen und in den gemeinsamen Bereich geklickt wird
|
||||
- **THEN** erhält das obere das Ereignis
|
||||
|
||||
#### Scenario: Klick ohne Steuerelement
|
||||
- **WHEN** auf eine freie Stelle des Formulars geklickt wird
|
||||
- **THEN** erhält das Formular das Ereignis
|
||||
|
||||
#### Scenario: Ziehen und Ablegen
|
||||
- **WHEN** ein Element mit `DragMode = 1` auf ein anderes gezogen und dort losgelassen wird
|
||||
- **THEN** läuft am Ziel `DragOver` mit dem Zustand Over und danach `DragDrop` mit der Quelle als Argument
|
||||
|
||||
### Requirement: Steuerelemente mit Listeninhalt
|
||||
ListBox und ComboBox SHALL `ADDITEM` und `REMOVEITEM` unterstützen und
|
||||
`List`, `ListCount`, `ListIndex` und `Text` konsistent führen; bei
|
||||
`Sorted = -1` SHALL die Einfügereihenfolge der Sortierung folgen.
|
||||
`ListIndex = -1` SHALL „keine Auswahl" bedeuten. Die ComboBox SHALL die
|
||||
drei Stilarten (Dropdown, Simple, Dropdown List) darstellen.
|
||||
|
||||
#### Scenario: Element hinzufügen
|
||||
- **WHEN** `List1.ADDITEM "b"` und `List1.ADDITEM "a"` bei `Sorted = -1` ausgeführt werden
|
||||
- **THEN** liefert `List1.List(0)` den Wert `a` und `List1.ListCount` den Wert 2
|
||||
|
||||
#### Scenario: Keine Auswahl
|
||||
- **WHEN** eine ListBox ohne Auswahl gelesen wird
|
||||
- **THEN** liefert `ListIndex` den Wert −1
|
||||
|
||||
### Requirement: Timer-Steuerelement
|
||||
Ein Timer SHALL bei `Enabled = -1` und `Interval > 0` sein
|
||||
`Timer`-Ereignis im eingestellten Abstand auslösen, gestützt auf die
|
||||
Zeitquelle der Ereignissteuerung. `Interval = 0` SHALL ihn abschalten.
|
||||
Sind mehrere Timer gleichzeitig fällig, SHALL die Reihenfolge festgelegt
|
||||
und dokumentiert sein.
|
||||
|
||||
#### Scenario: Timer feuert im Abstand
|
||||
- **WHEN** ein Timer mit `Interval = 100` läuft und die Zeit um 250 ms vorrückt
|
||||
- **THEN** ist sein Ereignis zweimal gelaufen
|
||||
|
||||
#### Scenario: Interval 0 schaltet ab
|
||||
- **WHEN** `Timer1.Interval = 0` gesetzt wird
|
||||
- **THEN** läuft kein weiteres Ereignis
|
||||
|
||||
### Requirement: Menüsystem
|
||||
Ein Formular SHALL eine Menüleiste mit bis zu sechs Ebenen tragen.
|
||||
Menüeinträge SHALL Access-Keys (`&`), Shortcuts, `Checked`, `Enabled`,
|
||||
`Visible` und Separatoren (`-`) unterstützen; ein Separator MUST NOT
|
||||
`Checked`, deaktiviert oder mit Shortcut versehen sein, ein Menütitel
|
||||
MUST NOT einen Shortcut tragen. Solange ein Menü geöffnet ist, MUST die
|
||||
Zustellung von Zeitereignissen und klassischen Traps ruhen und danach
|
||||
fortgesetzt werden.
|
||||
|
||||
#### Scenario: Menüauswahl löst Click aus
|
||||
- **WHEN** ein Menüeintrag über seinen Access-Key gewählt wird
|
||||
- **THEN** läuft seine `Click`-Prozedur
|
||||
|
||||
#### Scenario: Traps ruhen im geöffneten Menü
|
||||
- **WHEN** ein Menü geöffnet ist und ein Zeit-Trap fällig wird
|
||||
- **THEN** läuft sein Handler erst, nachdem das Menü geschlossen wurde
|
||||
|
||||
### Requirement: Vordefinierte Dialoge
|
||||
`MSGBOX text$ [, typ% [, titel$]]` SHALL als Anweisung und als Funktion
|
||||
verfügbar sein; die Funktion SHALL die gedrückte Schaltfläche als
|
||||
INTEGER liefern (1 OK, 2 Cancel/Esc, 3 Abort, 4 Retry, 5 Ignore, 6 Yes,
|
||||
7 No). `typ%` SHALL die Schaltflächengruppe (0–5) und die
|
||||
Vorgabeschaltfläche (0/256/512) tragen. `INPUTBOX$(text$ [, titel$
|
||||
[, vorgabe$ [, x%, y%]]])` SHALL eine Zeichenkette liefern und bei
|
||||
Abbruch den leeren String. Beide Dialoge SHALL modal sein; `INPUTBOX$`
|
||||
SHALL 46×16 Zeichen messen und ohne Positionsangabe zentriert
|
||||
erscheinen.
|
||||
|
||||
#### Scenario: MSGBOX als Funktion
|
||||
- **WHEN** `a% = MSGBOX("Weiter?", 4, "Frage")` ausgeführt und `Yes` gewählt wird
|
||||
- **THEN** liefert der Aufruf 6
|
||||
|
||||
#### Scenario: INPUTBOX$ abgebrochen
|
||||
- **WHEN** ein `INPUTBOX$`-Dialog mit Esc geschlossen wird
|
||||
- **THEN** liefert er den leeren String
|
||||
|
||||
#### Scenario: Dialog ist modal
|
||||
- **WHEN** ein Dialog offen ist
|
||||
- **THEN** wird die Anweisung nach dem Aufruf erst nach dem Schließen ausgeführt
|
||||
@@ -128,3 +128,30 @@ Harness den Lauf abweisen statt auf die Systemuhr auszuweichen.
|
||||
#### Scenario: Fehlende Zeitdeklaration
|
||||
- **WHEN** ein Korpusprogramm zeitgesteuerte Traps nutzt, aber keinen Zeitverlauf deklariert
|
||||
- **THEN** schlägt der Harness mit einer Meldung fehl, die die fehlende Deklaration nennt
|
||||
|
||||
### Requirement: Korpusabdeckung der Formularprogramme
|
||||
Der Testkorpus SHALL Formularprogramme mit byte-genauer
|
||||
Bildschirm-Sollausgabe führen. Ihre Ereignisfolge (Tasten, Maus, Zeit)
|
||||
SHALL im Programmkopf deklariert und vom Harness eingespeist werden,
|
||||
sodass ein Lauf ohne Terminal und ohne Wartezeit auskommt und zweimal
|
||||
dasselbe Ergebnis liefert. Abgedeckt SHALL sein: Fokus- und
|
||||
Tabreihenfolge, Access-Key, Klick über die Maus, Menüauswahl, ein
|
||||
Listen-Steuerelement, ein Timer und ein modaler Dialog.
|
||||
|
||||
#### Scenario: Formularprogramm im Korpus
|
||||
- **WHEN** ein Formular-Korpusprogramm mit deklarierter Ereignisfolge ausgeführt wird
|
||||
- **THEN** entspricht der Bildschirminhalt byte-genau der Sollausgabe
|
||||
|
||||
#### Scenario: Wiederholbarkeit
|
||||
- **WHEN** dasselbe Programm zweimal ausgeführt wird
|
||||
- **THEN** ist die Ausgabe beide Male identisch
|
||||
|
||||
### Requirement: Kompatibilitätsnachweis an Fremdprogrammen
|
||||
Die Formularprogramme aus dem öffentlichen Bestand des Vorbilds SHALL
|
||||
sich ohne Diagnosen oder Non-Features übersetzen lassen und bedienbar
|
||||
sein. Jeder entdeckte fehlende Sprach-, Projekt-, Formular- oder
|
||||
Laufzeitpfad MUST implementiert und als Befund festgehalten werden.
|
||||
|
||||
#### Scenario: Fremdprogramm übersetzt
|
||||
- **WHEN** ein Formularprogramm des öffentlichen Bestands übersetzt wird
|
||||
- **THEN** entstehen keine Diagnosen und das Programm ist bedienbar
|
||||
|
||||
97
tests/compat/formular.frm
Normal file
97
tests/compat/formular.frm
Normal file
@@ -0,0 +1,97 @@
|
||||
VERSION 1.00
|
||||
Begin Form Formular
|
||||
Caption = "Steuerelemente"
|
||||
Height = 16
|
||||
Left = 0
|
||||
Top = 0
|
||||
Width = 46
|
||||
Begin Menu mnuDatei
|
||||
Caption = "&Datei"
|
||||
Begin Menu mnuOpen
|
||||
Caption = "&Open"
|
||||
End
|
||||
End
|
||||
Begin TextBox Text1
|
||||
Height = 3
|
||||
Left = 2
|
||||
TabIndex = 0
|
||||
Text = "Eingabe"
|
||||
Top = 2
|
||||
Width = 18
|
||||
End
|
||||
Begin CommandButton Command1
|
||||
Caption = "&OK"
|
||||
Default = -1
|
||||
Height = 1
|
||||
Left = 23
|
||||
TabIndex = 1
|
||||
Top = 3
|
||||
Width = 10
|
||||
End
|
||||
Begin ListBox List1
|
||||
Height = 5
|
||||
Left = 2
|
||||
Sorted = -1
|
||||
TabIndex = 2
|
||||
Top = 6
|
||||
Width = 18
|
||||
End
|
||||
Begin Label Status
|
||||
Caption = "bereit"
|
||||
Height = 1
|
||||
Left = 23
|
||||
Top = 7
|
||||
Width = 18
|
||||
End
|
||||
Begin Label TimerStatus
|
||||
Caption = "Timer 0"
|
||||
Height = 1
|
||||
Left = 23
|
||||
Top = 9
|
||||
Width = 18
|
||||
End
|
||||
Begin Timer Timer1
|
||||
Enabled = -1
|
||||
Interval = 100
|
||||
End
|
||||
End
|
||||
|
||||
' tb-screen: 80x25
|
||||
' tb-events: key:<TAB> | mouse:down,1,0,4,25 | mouse:up,1,0,4,25 | time:0ms | key:<ENTER> | key:%d | key:o | time:250ms
|
||||
DIM SHARED Klicks%, Ticks%
|
||||
Formular.Show
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
DOEVENTS
|
||||
END
|
||||
|
||||
SUB Form_Load()
|
||||
List1.ADDITEM "zwei"
|
||||
List1.ADDITEM "eins"
|
||||
END SUB
|
||||
|
||||
SUB Command1_Click()
|
||||
SHARED Klicks%
|
||||
Klicks% = Klicks% + 1
|
||||
r% = MSGBOX("Weiter?", 0, "Klick")
|
||||
Status.Caption = "Dialog " + STR$(r%)
|
||||
END SUB
|
||||
|
||||
SUB mnuOpen_Click()
|
||||
Status.Caption = "Menü"
|
||||
END SUB
|
||||
|
||||
SUB Timer1_Timer()
|
||||
SHARED Ticks%
|
||||
Ticks% = Ticks% + 1
|
||||
TimerStatus.Caption = "Timer " + STR$(Ticks%)
|
||||
END SUB
|
||||
33
tests/compat/formular.out
Normal file
33
tests/compat/formular.out
Normal file
@@ -0,0 +1,33 @@
|
||||
┌ Steuerelemente ────────────────────────────┐
|
||||
│ Datei │
|
||||
│ ┌────────────────┐ │
|
||||
│ │Eingabe │ <OK> │
|
||||
│ └────────────────┘ │
|
||||
│ │
|
||||
│ ┌────────────────┐ │
|
||||
│ │ eins │ Menü │
|
||||
│ │ zwei │ │
|
||||
│ │ │ Timer 2 │
|
||||
│ └────────────────┘ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
│ │
|
||||
└────────────────────────────────────────────┘
|
||||
--- Attribute (Vordergrund/Hintergrund je Zelle, hex) ---
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070700707070707070707070770707070707070707070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070700707070707070707070707070707070707077070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
70700707070707070707070707070707070707077070700707070707070707070707070707070707077070707070
|
||||
70700707070707070707070707070707070707077070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
70707070707070707070707070707070707070707070707070707070707070707070707070707070707070707070
|
||||
Reference in New Issue
Block a user