Phase 4: Steuerelemente implementieren
This commit is contained in:
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);
|
||||
|
||||
Reference in New Issue
Block a user