Spezifikationsabgleich und Regressionsnachweise abschließen
This commit is contained in:
@@ -75,6 +75,7 @@ pub struct FormsModel {
|
||||
visible_forms: Vec<u16>,
|
||||
active_form: Option<u16>,
|
||||
active_control: Option<(u16, Option<i32>)>,
|
||||
dropdown: Option<ObjectKey>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
screen_visible: bool,
|
||||
@@ -103,16 +104,35 @@ impl FormsModel {
|
||||
};
|
||||
if self.root_form(control) == Some(form) {
|
||||
self.active_control = None;
|
||||
self.dropdown = None;
|
||||
}
|
||||
}
|
||||
fn activate_form(&mut self, form: Option<u16>) {
|
||||
if self.active_form == form {
|
||||
return;
|
||||
}
|
||||
if let Some(old) = self.active_form {
|
||||
self.queue_named((old, None), "LOSTFOCUS", vec![]);
|
||||
}
|
||||
self.active_form = form;
|
||||
self.dropdown = None;
|
||||
if let Some(new) = form {
|
||||
self.queue_named((new, None), "GOTFOCUS", vec![]);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(objects: Vec<FormObject>, width: usize, height: usize) -> Self {
|
||||
let objects = objects
|
||||
.into_iter()
|
||||
.map(|description| {
|
||||
let properties = forms::properties(description.class)
|
||||
let mut properties: Vec<_> = forms::properties(description.class)
|
||||
.into_iter()
|
||||
.map(|p| PropertyValue::from_default(p.default, p.ty))
|
||||
.collect();
|
||||
if let Some((id, _)) = forms::property(description.class, "PARENT") {
|
||||
properties[id as usize] =
|
||||
PropertyValue::Object(description.parent.map(|id| (id, None)));
|
||||
}
|
||||
let loaded = description.class == ObjectClass::Screen;
|
||||
ObjectInstance {
|
||||
description,
|
||||
@@ -132,6 +152,7 @@ impl FormsModel {
|
||||
visible_forms: Vec::new(),
|
||||
active_form: None,
|
||||
active_control: None,
|
||||
dropdown: None,
|
||||
width,
|
||||
height,
|
||||
screen_visible: true,
|
||||
@@ -251,10 +272,6 @@ impl FormsModel {
|
||||
.unwrap_or(1);
|
||||
return Ok(PropertyValue::Integer(value.saturating_sub(2)));
|
||||
}
|
||||
if spec.name == "PARENT" {
|
||||
let parent = obj.description.parent.map(|id| (id, None));
|
||||
return Ok(PropertyValue::Object(parent));
|
||||
}
|
||||
Ok(obj.properties[property as usize].clone())
|
||||
}
|
||||
|
||||
@@ -420,6 +437,7 @@ impl FormsModel {
|
||||
self.replace_selection(key, &replacement)?;
|
||||
return Ok(());
|
||||
}
|
||||
let changed = self.value(key, spec.name) != Some(&value);
|
||||
let timer_reset = class == ObjectClass::Timer
|
||||
&& (spec.name == "INTERVAL"
|
||||
|| (spec.name == "ENABLED" && self.value(key, "ENABLED") != Some(&value)));
|
||||
@@ -433,6 +451,12 @@ impl FormsModel {
|
||||
);
|
||||
}
|
||||
}
|
||||
if changed && class == ObjectClass::Form && matches!(spec.name, "WIDTH" | "HEIGHT") {
|
||||
self.queue_named(key, "RESIZE", vec![]);
|
||||
}
|
||||
if changed && class == ObjectClass::Label && spec.name == "CAPTION" {
|
||||
self.queue_named(key, "CHANGE", vec![]);
|
||||
}
|
||||
if spec.name == "SORTED" && self.boolean(key, "SORTED") {
|
||||
self.sort_list(key)?;
|
||||
}
|
||||
@@ -464,6 +488,17 @@ impl FormsModel {
|
||||
) && matches!(spec.name, "PATH" | "DRIVE" | "PATTERN")
|
||||
{
|
||||
self.refresh_filesystem(key)?;
|
||||
if changed {
|
||||
let event = match (class, spec.name) {
|
||||
(ObjectClass::FileListBox, "PATTERN") => "PATTERNCHANGE",
|
||||
(ObjectClass::FileListBox | ObjectClass::DirListBox, "PATH") => "PATHCHANGE",
|
||||
_ => "CHANGE",
|
||||
};
|
||||
self.queue_named(key, event, vec![]);
|
||||
if class == ObjectClass::DirListBox {
|
||||
self.queue_named(key, "CHANGE", vec![]);
|
||||
}
|
||||
}
|
||||
}
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
@@ -566,7 +601,8 @@ impl FormsModel {
|
||||
Self::set_visible_property(obj, true);
|
||||
self.visible_forms.retain(|id| *id != object);
|
||||
self.visible_forms.push(object);
|
||||
self.active_form = Some(object);
|
||||
self.activate_form(Some(object));
|
||||
self.queue_named((object, None), "PAINT", vec![]);
|
||||
self.dirty = true;
|
||||
Ok(if modal {
|
||||
ShowResult::ModalWait
|
||||
@@ -590,7 +626,7 @@ impl FormsModel {
|
||||
self.modal.pop();
|
||||
}
|
||||
if self.active_form == Some(object) {
|
||||
self.active_form = self.visible_forms.last().copied();
|
||||
self.activate_form(self.visible_forms.last().copied());
|
||||
}
|
||||
self.clear_active_control_for_form(object);
|
||||
self.reset_form_timers(object);
|
||||
@@ -629,7 +665,7 @@ impl FormsModel {
|
||||
self.modal.pop();
|
||||
}
|
||||
if self.active_form == Some(object) {
|
||||
self.active_form = self.visible_forms.last().copied();
|
||||
self.activate_form(self.visible_forms.last().copied());
|
||||
}
|
||||
self.clear_active_control_for_form(object);
|
||||
self.reset_form_timers(object);
|
||||
@@ -693,6 +729,9 @@ impl FormsModel {
|
||||
.map(|_| ())
|
||||
.ok_or(RuntimeError(340));
|
||||
if removed.is_ok() {
|
||||
if self.dropdown == Some((base, Some(index))) {
|
||||
self.dropdown = None;
|
||||
}
|
||||
self.reset_timer((base, Some(index)));
|
||||
self.lists.remove(&(base, Some(index)));
|
||||
}
|
||||
@@ -745,10 +784,21 @@ impl FormsModel {
|
||||
.ok_or(RuntimeError(420))?
|
||||
.description
|
||||
};
|
||||
if matches!(description.class, ObjectClass::Form | ObjectClass::Screen) {
|
||||
if matches!(
|
||||
description.class,
|
||||
ObjectClass::Form
|
||||
| ObjectClass::Screen
|
||||
| ObjectClass::Frame
|
||||
| ObjectClass::Label
|
||||
| ObjectClass::Menu
|
||||
| ObjectClass::Timer
|
||||
) {
|
||||
return Err(RuntimeError(421));
|
||||
}
|
||||
self.active_form = self.root_form(key);
|
||||
self.activate_form(self.root_form(key));
|
||||
if self.active_control != Some(key) {
|
||||
self.dropdown = None;
|
||||
}
|
||||
self.active_control = Some(key);
|
||||
Ok(())
|
||||
}
|
||||
@@ -822,14 +872,28 @@ impl FormsModel {
|
||||
});
|
||||
}
|
||||
|
||||
fn root_form(&self, key: ObjectKey) -> Option<u16> {
|
||||
let mut current = key.0;
|
||||
for _ in 0..self.objects.len() {
|
||||
let obj = self.objects.get(current as usize)?;
|
||||
if obj.description.class == ObjectClass::Form {
|
||||
return Some(current);
|
||||
fn parent_key(&self, key: ObjectKey) -> Option<ObjectKey> {
|
||||
match self.value(key, "PARENT") {
|
||||
Some(PropertyValue::Object(parent)) => {
|
||||
parent.map(|(id, index)| (id, index.filter(|i| *i != 0)))
|
||||
}
|
||||
current = obj.description.parent?;
|
||||
_ => self
|
||||
.instance(key)
|
||||
.ok()?
|
||||
.description
|
||||
.parent
|
||||
.map(|id| (id, None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn root_form(&self, key: ObjectKey) -> Option<u16> {
|
||||
let mut current = key;
|
||||
for _ in 0..self.objects.len() + self.dynamic.len() {
|
||||
let obj = self.instance(current).ok()?;
|
||||
if obj.description.class == ObjectClass::Form {
|
||||
return Some(current.0);
|
||||
}
|
||||
current = self.parent_key(current)?;
|
||||
}
|
||||
None
|
||||
}
|
||||
@@ -872,7 +936,10 @@ impl FormsModel {
|
||||
if let Some(old) = self.active_control {
|
||||
self.queue_named(old, "LOSTFOCUS", vec![]);
|
||||
}
|
||||
self.active_form = self.root_form(key);
|
||||
self.activate_form(self.root_form(key));
|
||||
if self.active_control != Some(key) {
|
||||
self.dropdown = None;
|
||||
}
|
||||
self.active_control = Some(key);
|
||||
self.queue_named(key, "GOTFOCUS", vec![]);
|
||||
self.dirty = true;
|
||||
@@ -949,7 +1016,7 @@ impl FormsModel {
|
||||
}
|
||||
|
||||
fn select_option(&mut self, key: ObjectKey) -> Result<(), RuntimeError> {
|
||||
let parent = self.instance(key)?.description.parent;
|
||||
let parent = self.parent_key(key);
|
||||
let value_id = forms::property(ObjectClass::OptionButton, "VALUE")
|
||||
.unwrap()
|
||||
.0 as usize;
|
||||
@@ -959,7 +1026,7 @@ impl FormsModel {
|
||||
}
|
||||
let same_group = self.instance(other).is_ok_and(|obj| {
|
||||
obj.description.class == ObjectClass::OptionButton
|
||||
&& obj.description.parent == parent
|
||||
&& self.parent_key(other) == parent
|
||||
});
|
||||
if same_group {
|
||||
self.instance_mut(other)?.properties[value_id] = PropertyValue::Integer(0);
|
||||
@@ -994,10 +1061,8 @@ impl FormsModel {
|
||||
let title_shortcut = name == "SHORTCUT"
|
||||
&& matches!(value, PropertyValue::String(text) if !text.is_empty())
|
||||
&& self
|
||||
.instance(key)
|
||||
.ok()
|
||||
.and_then(|obj| obj.description.parent)
|
||||
.and_then(|parent| self.objects.get(parent as usize))
|
||||
.parent_key(key)
|
||||
.and_then(|parent| self.instance(parent).ok())
|
||||
.is_some_and(|parent| parent.description.class == ObjectClass::Form);
|
||||
if invalid || title_shortcut {
|
||||
Err(RuntimeError::ILLEGAL_FUNCTION_CALL)
|
||||
@@ -1163,12 +1228,6 @@ impl FormsModel {
|
||||
}
|
||||
entries.sort_by_key(|entry| entry.to_uppercase());
|
||||
self.lists.insert(key, entries);
|
||||
let event = match class {
|
||||
ObjectClass::DirListBox => "PATHCHANGE",
|
||||
ObjectClass::FileListBox if !self.string(key, "PATTERN").is_empty() => "PATTERNCHANGE",
|
||||
_ => "CHANGE",
|
||||
};
|
||||
self.queue_named(key, event, vec![]);
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1204,7 +1263,12 @@ impl FormsModel {
|
||||
let class = self.instance(key)?.description.class;
|
||||
match name {
|
||||
"SETFOCUS" => self.focus(key)?,
|
||||
"REFRESH" => self.dirty = true,
|
||||
"REFRESH" => {
|
||||
if matches!(class, ObjectClass::Form | ObjectClass::PictureBox) {
|
||||
self.queue_named(key, "PAINT", vec![]);
|
||||
}
|
||||
self.dirty = true;
|
||||
}
|
||||
"ADDITEM" => self.add_item(
|
||||
key,
|
||||
args.first().map(Self::text_of).unwrap_or_default(),
|
||||
@@ -1304,10 +1368,27 @@ impl FormsModel {
|
||||
}
|
||||
|
||||
fn form_controls(&self, form: u16) -> Vec<ObjectKey> {
|
||||
self.keys()
|
||||
let mut controls = Vec::new();
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
for key in self
|
||||
.keys()
|
||||
.into_iter()
|
||||
.filter(|key| self.root_form(*key) == Some(form) && key.0 != form)
|
||||
.collect()
|
||||
{
|
||||
// Auch ein nachträglich angelegtes Array-Containerobjekt muss unter
|
||||
// seinen Kindern liegen. Übrige Erzeugungsreihenfolge beibehalten.
|
||||
let mut ancestors = Vec::new();
|
||||
let mut current = Some(key);
|
||||
while let Some(parent) = current.filter(|p| p.0 != form && !seen.contains(p)) {
|
||||
ancestors.push(parent);
|
||||
current = self.parent_key(parent);
|
||||
}
|
||||
for key in ancestors.into_iter().rev() {
|
||||
seen.insert(key);
|
||||
controls.push(key);
|
||||
}
|
||||
}
|
||||
controls
|
||||
}
|
||||
|
||||
fn menu_children(&self, parent: ObjectKey) -> Vec<ObjectKey> {
|
||||
@@ -1316,7 +1397,7 @@ impl FormsModel {
|
||||
.filter(|key| {
|
||||
self.instance(*key).is_ok_and(|object| {
|
||||
object.description.class == ObjectClass::Menu
|
||||
&& object.description.parent == Some(parent.0)
|
||||
&& self.parent_key(*key) == Some(parent)
|
||||
}) && self.boolean(*key, "VISIBLE")
|
||||
})
|
||||
.collect()
|
||||
@@ -1370,7 +1451,7 @@ impl FormsModel {
|
||||
&& self.boolean(*candidate, "VISIBLE")
|
||||
&& self.instance(*candidate).is_ok_and(|obj| {
|
||||
obj.description.class != ObjectClass::Menu
|
||||
|| obj.description.parent == Some(form)
|
||||
|| self.parent_key(*candidate) == Some((form, None))
|
||||
})
|
||||
}) {
|
||||
if self
|
||||
@@ -1410,7 +1491,29 @@ impl FormsModel {
|
||||
PropertyValue::Integer(shift as i32),
|
||||
],
|
||||
);
|
||||
let handled = self.control_key(active, key);
|
||||
let dropdown_key = self
|
||||
.instance(active)
|
||||
.is_ok_and(|o| o.description.class == ObjectClass::ComboBox)
|
||||
&& shift & umschalt::ALT != 0
|
||||
&& key == taste::sonder(80);
|
||||
let handled = if dropdown_key {
|
||||
self.dropdown = if self.dropdown == Some(active) {
|
||||
None
|
||||
} else {
|
||||
Some(active)
|
||||
};
|
||||
if self.dropdown.is_some() {
|
||||
self.queue_named(active, "DROPDOWN", vec![]);
|
||||
}
|
||||
self.dirty = true;
|
||||
true
|
||||
} else if self.dropdown == Some(active) && matches!(key, taste::ESC | taste::ENTER) {
|
||||
self.dropdown = None;
|
||||
self.dirty = true;
|
||||
true
|
||||
} else {
|
||||
self.control_key(active, key)
|
||||
};
|
||||
if key.chars().count() == 1 {
|
||||
self.queue_named(active, "KEYPRESS", vec![PropertyValue::Integer(code)]);
|
||||
}
|
||||
@@ -1453,6 +1556,17 @@ impl FormsModel {
|
||||
PropertyValue::Integer(shift as i32),
|
||||
],
|
||||
);
|
||||
if key.chars().count() == 1 {
|
||||
self.queue_named((form, None), "KEYPRESS", vec![PropertyValue::Integer(code)]);
|
||||
}
|
||||
self.queue_named(
|
||||
(form, None),
|
||||
"KEYUP",
|
||||
vec![
|
||||
PropertyValue::Integer(code),
|
||||
PropertyValue::Integer(shift as i32),
|
||||
],
|
||||
);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -1546,19 +1660,18 @@ impl FormsModel {
|
||||
}
|
||||
|
||||
fn rect(&self, key: ObjectKey) -> Option<(usize, usize, usize, usize)> {
|
||||
let obj = self.instance(key).ok()?;
|
||||
self.instance(key).ok()?;
|
||||
let mut left = self.integer(key, "LEFT").unwrap_or(0).max(0) as usize + 1;
|
||||
let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1;
|
||||
let width = self.integer(key, "WIDTH").unwrap_or(1).max(1) as usize;
|
||||
let height = self.integer(key, "HEIGHT").unwrap_or(1).max(1) as usize;
|
||||
let mut parent = obj.description.parent;
|
||||
for _ in 0..self.objects.len() {
|
||||
let Some(id) = parent else { break };
|
||||
let parent_obj = self.objects.get(id as usize)?;
|
||||
let parent_key = (id, None);
|
||||
let mut parent = self.parent_key(key);
|
||||
for _ in 0..self.objects.len() + self.dynamic.len() {
|
||||
let Some(parent_key) = parent else { break };
|
||||
self.instance(parent_key).ok()?;
|
||||
left += self.integer(parent_key, "LEFT").unwrap_or(0).max(0) as usize;
|
||||
top += self.integer(parent_key, "TOP").unwrap_or(0).max(0) as usize;
|
||||
parent = parent_obj.description.parent;
|
||||
parent = self.parent_key(parent_key);
|
||||
}
|
||||
Some((left, top, width, height))
|
||||
}
|
||||
@@ -2192,7 +2305,7 @@ impl FormsModel {
|
||||
format!("{text} ▼")
|
||||
};
|
||||
let mut lines = vec![Self::fit(&first, width)];
|
||||
if class == ObjectClass::ComboBox && style == 1 {
|
||||
if class == ObjectClass::ComboBox && (style == 1 || self.dropdown == Some(key)) {
|
||||
lines.extend(self.list_lines(key, width, height.saturating_sub(1)));
|
||||
}
|
||||
lines.truncate(height);
|
||||
@@ -2317,12 +2430,7 @@ impl FormsModel {
|
||||
let Ok(class) = self.instance(key).map(|obj| obj.description.class) else {
|
||||
continue;
|
||||
};
|
||||
if class == ObjectClass::Menu
|
||||
&& self
|
||||
.instance(key)
|
||||
.ok()
|
||||
.is_some_and(|obj| obj.description.parent == Some(form))
|
||||
{
|
||||
if class == ObjectClass::Menu && self.parent_key(key) == Some((form, None)) {
|
||||
if self.boolean(key, "VISIBLE") {
|
||||
let caption = format!(" {} ", self.caption(key));
|
||||
let focused = self.menu_path.first() == Some(&key);
|
||||
@@ -2381,7 +2489,7 @@ impl FormsModel {
|
||||
for root in self.form_controls(form).into_iter().filter(|key| {
|
||||
self.instance(*key).is_ok_and(|object| {
|
||||
object.description.class == ObjectClass::Menu
|
||||
&& object.description.parent == Some(form)
|
||||
&& self.parent_key(*key) == Some((form, None))
|
||||
}) && self.boolean(*key, "VISIBLE")
|
||||
}) {
|
||||
if Some(&root) == self.menu_path.first() {
|
||||
@@ -2749,6 +2857,90 @@ mod tests {
|
||||
FormsModel::new(c.objects, 80, 25)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combobox_dropdown_closes_on_focus_change_and_hide() {
|
||||
let mut catalog = forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("Combo", ObjectClass::ComboBox, Some("Form1"), false);
|
||||
catalog.add("Text", ObjectClass::TextBox, Some("Form1"), false);
|
||||
let mut m = FormsModel::new(catalog.objects, 80, 25);
|
||||
m.show(0, false).unwrap();
|
||||
for hide in [false, true] {
|
||||
m.focus((1, None)).unwrap();
|
||||
assert!(m.handle_key(&taste::sonder(80), umschalt::ALT));
|
||||
assert_eq!(m.dropdown, Some((1, None)));
|
||||
if hide {
|
||||
m.hide(0).unwrap();
|
||||
} else {
|
||||
m.focus((2, None)).unwrap();
|
||||
}
|
||||
assert_eq!(m.dropdown, None);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_containers_preserve_parent_geometry_and_option_groups() {
|
||||
let form = crate::frm::read_text(
|
||||
"parent.frm",
|
||||
r#"VERSION 1.00
|
||||
Begin Form Form1
|
||||
Width = 80
|
||||
Height = 25
|
||||
Begin Frame Group
|
||||
Index = 0
|
||||
Left = 2
|
||||
Begin OptionButton Choice
|
||||
Index = 0
|
||||
End
|
||||
End
|
||||
Begin Frame Group
|
||||
Index = 1
|
||||
Width = 25
|
||||
Height = 12
|
||||
Left = 30
|
||||
Top = 4
|
||||
Begin OptionButton Choice
|
||||
Index = 1
|
||||
Left = 2
|
||||
Top = 2
|
||||
End
|
||||
Begin OptionButton Other
|
||||
Caption = "Second"
|
||||
Width = 12
|
||||
Left = 2
|
||||
Top = 4
|
||||
End
|
||||
End
|
||||
End
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let catalog = form.catalog();
|
||||
let id = |name| catalog.find(name).unwrap().0;
|
||||
let (root, group, choice, other) = (id("Form1"), id("Group"), id("Choice"), id("Other"));
|
||||
let mut m = FormsModel::new(catalog.objects, 80, 25);
|
||||
form.apply(&mut m).unwrap();
|
||||
m.show(root, false).unwrap();
|
||||
assert_eq!(m.parent_key((choice, Some(1))), Some((group, Some(1))));
|
||||
assert_eq!(m.root_form((choice, Some(1))), Some(root));
|
||||
assert_eq!(
|
||||
m.rect((choice, Some(1))).map(|(x, y, _, _)| (x, y)),
|
||||
Some((33, 7))
|
||||
);
|
||||
assert_eq!(m.hit_test(root, 7, 33), (choice, Some(1)));
|
||||
m.activate((choice, None)).unwrap();
|
||||
m.activate((choice, Some(1))).unwrap();
|
||||
assert_eq!(m.integer((choice, None), "VALUE"), Some(-1));
|
||||
assert_eq!(m.integer((choice, Some(1)), "VALUE"), Some(-1));
|
||||
m.activate((other, None)).unwrap();
|
||||
assert_eq!(m.integer((choice, None), "VALUE"), Some(-1));
|
||||
assert_eq!(m.integer((choice, Some(1)), "VALUE"), Some(0));
|
||||
assert_eq!(m.hit_test(root, 9, 33), (other, None));
|
||||
let mut screen = TextScreen::new();
|
||||
m.render(&mut screen);
|
||||
assert!(tb_runtime::snapshot::text(&screen).contains("Second"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_bereich_und_implizites_laden() {
|
||||
let mut m = model();
|
||||
@@ -3633,7 +3825,8 @@ mod tests {
|
||||
m.unload_array(2, 1).unwrap();
|
||||
assert!(!m.events.iter().any(|e| e.array_index == Some(1)));
|
||||
m.hide(0).unwrap();
|
||||
assert!(m.events.is_empty());
|
||||
assert_eq!(m.events.len(), 1);
|
||||
assert_eq!(m.events.pop_front().unwrap().name, "LOSTFOCUS");
|
||||
m.show(0, false).unwrap();
|
||||
m.sync_timers(|| 2000);
|
||||
assert_eq!(m.next_deadline(), Some(2100));
|
||||
|
||||
@@ -84,8 +84,9 @@ impl FormFile {
|
||||
) -> Result<Vec<FormInitial>, tb_runtime::errors::RuntimeError> {
|
||||
fn collect(
|
||||
node: &FormNode,
|
||||
parent: Option<u16>,
|
||||
parent: Option<(u16, Option<i32>)>,
|
||||
catalog: &FormCatalog,
|
||||
form: &str,
|
||||
out: &mut Vec<FormInitial>,
|
||||
depth: usize,
|
||||
) -> Result<(), tb_runtime::errors::RuntimeError> {
|
||||
@@ -98,7 +99,8 @@ impl FormFile {
|
||||
.iter()
|
||||
.position(|o| {
|
||||
o.name.eq_ignore_ascii_case(&node.name)
|
||||
&& o.parent == parent
|
||||
&& ((o.array && catalog.belongs_to(o, form))
|
||||
|| o.parent == parent.map(|p| p.0))
|
||||
&& o.class == node.class
|
||||
})
|
||||
.ok_or(tb_runtime::errors::RuntimeError(420))? as u16;
|
||||
@@ -115,18 +117,29 @@ impl FormFile {
|
||||
if out.iter().any(|v| v.object == object && v.index == index) {
|
||||
return Err(tb_runtime::errors::RuntimeError(5));
|
||||
}
|
||||
let mut properties = node.properties.clone();
|
||||
if let Some((id, _)) = forms::property(node.class, "PARENT") {
|
||||
properties.insert(id, PropertyValue::Object(parent));
|
||||
}
|
||||
out.push(FormInitial {
|
||||
object,
|
||||
index,
|
||||
properties: node.properties.clone(),
|
||||
properties,
|
||||
});
|
||||
for child in &node.children {
|
||||
collect(child, Some(object), catalog, out, depth)?;
|
||||
collect(
|
||||
child,
|
||||
Some((object, (index != 0).then_some(index))),
|
||||
catalog,
|
||||
form,
|
||||
out,
|
||||
depth,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
let mut values = Vec::new();
|
||||
collect(&self.root, None, catalog, &mut values, 0)?;
|
||||
collect(&self.root, None, catalog, &self.root.name, &mut values, 0)?;
|
||||
Ok(values)
|
||||
}
|
||||
|
||||
@@ -941,7 +954,7 @@ fn decode_object(symbol: &BinarySymbol, record: &BinaryRecord, bytes: &[u8]) ->
|
||||
set_property(&mut node, name, PropertyValue::Integer(value as i32));
|
||||
}
|
||||
}
|
||||
if let Some(value) = record_u16(bytes, start, end, 4) {
|
||||
if let Some(value) = record_u16(bytes, start, end, 4).filter(|_| symbol.is_array) {
|
||||
set_property(
|
||||
&mut node,
|
||||
"INDEX",
|
||||
@@ -1314,7 +1327,7 @@ fn write_node(node: &FormNode, depth: usize, out: &mut String) {
|
||||
let Some(value) = node.properties.get(&(id as u16)) else {
|
||||
continue;
|
||||
};
|
||||
if *value == default_value(spec) {
|
||||
if spec.name != "INDEX" && *value == default_value(spec) {
|
||||
continue;
|
||||
}
|
||||
out.push_str(&" ".repeat(depth + 1));
|
||||
@@ -1437,6 +1450,54 @@ mod tests {
|
||||
|
||||
const EXAMPLE: &str = "VERSION 1.00\nBegin Form Form1\n Caption = \"Beispiel\"\n Height = 15\n Begin CommandButton cmdOK\n Caption = \"&OK\"\n End\nEnd\n\nSUB cmdOK_Click ()\n UNLOAD Form1\nEND SUB\n";
|
||||
|
||||
#[test]
|
||||
fn explicit_default_is_preserved_until_canonical_write() {
|
||||
let original = "VERSION 1.00\r\nBegin Form Form1\r\n Begin CommandButton Ok\r\n Enabled = -1\r\n End\r\nEnd\r\n";
|
||||
let mut form = read_text("default.frm", original).unwrap();
|
||||
assert_eq!(write_text(&form), original);
|
||||
form.root.name = "Changed".into();
|
||||
let canonical = write_text(&form);
|
||||
assert_eq!(
|
||||
canonical,
|
||||
"VERSION 1.00\nBegin Form Changed\n Begin CommandButton Ok\n End\nEnd\n"
|
||||
);
|
||||
assert_eq!(
|
||||
write_text(&read_text("canonical.frm", &canonical).unwrap()),
|
||||
canonical
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonical_index_zero_remains_an_array_and_array_parents_are_preserved() {
|
||||
let mut form=read_text("array.frm", "VERSION 1.00\nBegin Form Form1\n Begin Frame A\n Begin Label Item\n Index = 0\n End\n End\n Begin Frame B\n Begin Label Item\n Index = 1\n End\n End\nEnd\n").unwrap();
|
||||
form.code = "END\n".into();
|
||||
let text = write_text(&form);
|
||||
assert!(text.contains("Index = 0"));
|
||||
let roundtrip = read_text("roundtrip.frm", &text).unwrap();
|
||||
let catalog = roundtrip.catalog();
|
||||
let item = catalog.find("Item").unwrap().0;
|
||||
assert!(catalog.objects[item as usize].array);
|
||||
let a = catalog.find("A").unwrap().0;
|
||||
let b = catalog.find("B").unwrap().0;
|
||||
let mut model = FormsModel::new(catalog.objects, 80, 25);
|
||||
roundtrip.apply(&mut model).unwrap();
|
||||
let parent = forms::property(ObjectClass::Label, "PARENT").unwrap().0;
|
||||
assert_eq!(
|
||||
model.get_at(item, Some(0), parent).unwrap(),
|
||||
PropertyValue::Object(Some((a, None)))
|
||||
);
|
||||
assert_eq!(
|
||||
model.get_at(item, Some(1), parent).unwrap(),
|
||||
PropertyValue::Object(Some((b, None)))
|
||||
);
|
||||
form.root.children.pop();
|
||||
let single = read_text("single.frm", &write_text(&form)).unwrap();
|
||||
assert!(
|
||||
single.catalog().objects[item as usize].array,
|
||||
"Einzelelement mit Index 0 bleibt Array"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_nested_form_and_preserves_source_exactly() {
|
||||
let form = read_text("test.frm", EXAMPLE).unwrap();
|
||||
|
||||
@@ -2365,7 +2365,15 @@ impl<'a> Decoder<'a> {
|
||||
continue;
|
||||
}
|
||||
if matches!(kind, Some(1 | 2)) {
|
||||
let line = self.pending[..marker].trim_end().to_owned();
|
||||
// Binär-P-Code enthält den Include-Inhalt bereits. Die
|
||||
// Herkunft bleibt Kommentar, keine erneut aktive Direktive.
|
||||
let line = self.pending[..marker].trim_end();
|
||||
let directive = line.trim_start().trim_start_matches('\'').trim_start();
|
||||
let line = if directive.to_ascii_uppercase().starts_with("$INCLUDE:") {
|
||||
format!("' Expanded INCLUDE:{}", &directive[9..])
|
||||
} else {
|
||||
line.to_owned()
|
||||
};
|
||||
self.lines.push(line);
|
||||
self.pending.drain(..marker + 2);
|
||||
continue;
|
||||
@@ -2604,6 +2612,21 @@ mod tests {
|
||||
(table, offsets)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn embedded_include_is_a_comment_but_literals_remain_unchanged() {
|
||||
let mut decoder = Decoder::new("test.frm", &[], &[], 0);
|
||||
decoder.pending=" '$INCLUDE: 'embedded.bi'\r\u{1}DECLARE SUB Test()\r\u{2}PRINT \"$INCLUDE: 'literal.bi'\"\r\u{1}".into();
|
||||
decoder.postprocess(true);
|
||||
assert_eq!(
|
||||
decoder.lines,
|
||||
vec![
|
||||
"' Expanded INCLUDE: 'embedded.bi'",
|
||||
"DECLARE SUB Test()",
|
||||
"PRINT \"$INCLUDE: 'literal.bi'\""
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_vbdos_declarations_without_losing_type_or_bounds() {
|
||||
let (sym, ids) = symbols(&["PrepVal", "StringToPrep", "Oldcontents"]);
|
||||
|
||||
Reference in New Issue
Block a user