Phase 4: Steuerelemente implementieren

This commit is contained in:
2026-09-05 12:49:46 +02:00
parent 1cb6ae8fd9
commit 19804e0e2d
36 changed files with 6472 additions and 689 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -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)

View File

@@ -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);