Phase 4: Formularmodell und Objektsprache
This commit is contained in:
@@ -8,6 +8,7 @@ authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
tb-runtime.workspace = true
|
||||
tb-frontend.workspace = true
|
||||
ratatui.workspace = true
|
||||
crossterm.workspace = true
|
||||
signal-hook.workspace = true
|
||||
|
||||
@@ -3,4 +3,592 @@
|
||||
//! Ziel ist volle Kompatibilität zum Forms-Modell des Vorbilds inklusive
|
||||
//! des textbasierten Formular-Dateiformats (`.FRM`).
|
||||
|
||||
// Platzhalter — wird in Phase 4 ausgearbeitet (siehe PLAN.md)
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
|
||||
use tb_frontend::forms::{self, FormObject, ObjectClass, PropertyDefault, PropertyType};
|
||||
use tb_runtime::errors::RuntimeError;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum PropertyValue {
|
||||
Integer(i32),
|
||||
Single(f32),
|
||||
String(String),
|
||||
Boolean(bool),
|
||||
Object(Option<(u16, Option<i32>)>),
|
||||
IntegerArray(Vec<i32>),
|
||||
}
|
||||
|
||||
impl PropertyValue {
|
||||
fn from_default(default: PropertyDefault, ty: PropertyType) -> Self {
|
||||
match default {
|
||||
PropertyDefault::Integer(v) if ty == PropertyType::IntegerArray => {
|
||||
Self::IntegerArray(vec![v; 18])
|
||||
}
|
||||
PropertyDefault::Integer(v) => Self::Integer(v),
|
||||
PropertyDefault::Single(v) => Self::Single(v),
|
||||
PropertyDefault::String(v) => Self::String(v.into()),
|
||||
PropertyDefault::Boolean(v) => Self::Boolean(v),
|
||||
PropertyDefault::Empty if ty == PropertyType::Object => Self::Object(None),
|
||||
PropertyDefault::Empty if ty == PropertyType::String => Self::String(String::new()),
|
||||
PropertyDefault::Empty => Self::Integer(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjectInstance {
|
||||
pub description: FormObject,
|
||||
properties: Vec<PropertyValue>,
|
||||
pub loaded: bool,
|
||||
pub visible: bool,
|
||||
pub design_time: bool,
|
||||
pub array_index: Option<i32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct FormEvent {
|
||||
pub object: u16,
|
||||
pub array_index: Option<i32>,
|
||||
pub name: String,
|
||||
pub args: Vec<PropertyValue>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ShowResult {
|
||||
Modeless,
|
||||
ModalWait,
|
||||
}
|
||||
|
||||
/// Reines Zustandsmodell. Zeichnen und Bedienung kommen im Folge-Change;
|
||||
/// diese Schicht hält Objekte, Lebenszyklus, Modalität und Ereignisqueue.
|
||||
pub struct FormsModel {
|
||||
pub objects: Vec<ObjectInstance>,
|
||||
dynamic: BTreeMap<(u16, i32), ObjectInstance>,
|
||||
pub events: VecDeque<FormEvent>,
|
||||
modal: Vec<u16>,
|
||||
visible_forms: Vec<u16>,
|
||||
active_form: Option<u16>,
|
||||
active_control: Option<(u16, Option<i32>)>,
|
||||
width: usize,
|
||||
height: usize,
|
||||
screen_visible: bool,
|
||||
}
|
||||
|
||||
impl FormsModel {
|
||||
fn set_visible_property(obj: &mut ObjectInstance, visible: bool) {
|
||||
if let Some((property, _)) = forms::property(obj.description.class, "VISIBLE") {
|
||||
obj.properties[property as usize] = PropertyValue::Boolean(visible);
|
||||
}
|
||||
}
|
||||
fn clear_active_control_for_form(&mut self, form: u16) {
|
||||
let Some((control, _)) = self.active_control else {
|
||||
return;
|
||||
};
|
||||
let Some(form_name) = self
|
||||
.objects
|
||||
.get(form as usize)
|
||||
.map(|o| o.description.name.as_str())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
if self
|
||||
.objects
|
||||
.get(control as usize)
|
||||
.and_then(|o| o.description.parent_form.as_deref())
|
||||
.is_some_and(|parent| parent.eq_ignore_ascii_case(form_name))
|
||||
{
|
||||
self.active_control = None;
|
||||
}
|
||||
}
|
||||
pub fn new(objects: Vec<FormObject>, width: usize, height: usize) -> Self {
|
||||
let objects = objects
|
||||
.into_iter()
|
||||
.map(|description| {
|
||||
let properties = forms::properties(description.class)
|
||||
.into_iter()
|
||||
.map(|p| PropertyValue::from_default(p.default, p.ty))
|
||||
.collect();
|
||||
let loaded = description.class == ObjectClass::Screen;
|
||||
ObjectInstance {
|
||||
description,
|
||||
properties,
|
||||
loaded,
|
||||
visible: false,
|
||||
design_time: true,
|
||||
array_index: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Self {
|
||||
objects,
|
||||
dynamic: BTreeMap::new(),
|
||||
events: VecDeque::new(),
|
||||
modal: Vec::new(),
|
||||
visible_forms: Vec::new(),
|
||||
active_form: None,
|
||||
active_control: None,
|
||||
width,
|
||||
height,
|
||||
screen_visible: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: usize, height: usize) {
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
}
|
||||
|
||||
pub fn get(&mut self, object: u16, property: u16) -> Result<PropertyValue, RuntimeError> {
|
||||
self.get_at(object, None, property)
|
||||
}
|
||||
|
||||
pub fn get_at(
|
||||
&mut self,
|
||||
object: u16,
|
||||
index: Option<i32>,
|
||||
property: u16,
|
||||
) -> Result<PropertyValue, RuntimeError> {
|
||||
self.ensure_loaded_at(object, index)?;
|
||||
let obj = if index.is_some_and(|i| i != 0) {
|
||||
self.dynamic
|
||||
.get(&(object, index.unwrap()))
|
||||
.ok_or(RuntimeError(340))?
|
||||
} else {
|
||||
self.objects.get(object as usize).ok_or(RuntimeError(420))?
|
||||
};
|
||||
let spec = forms::properties(obj.description.class)
|
||||
.get(property as usize)
|
||||
.copied()
|
||||
.ok_or(RuntimeError(422))?;
|
||||
if obj.description.class == ObjectClass::Screen {
|
||||
return Ok(match spec.name {
|
||||
"WIDTH" => PropertyValue::Integer(self.width as i32),
|
||||
"HEIGHT" => PropertyValue::Integer(self.height as i32),
|
||||
"ACTIVEFORM" => PropertyValue::Object(self.active_form.map(|id| (id, None))),
|
||||
"ACTIVECONTROL" => PropertyValue::Object(self.active_control),
|
||||
_ => obj.properties[property as usize].clone(),
|
||||
});
|
||||
}
|
||||
Ok(obj.properties[property as usize].clone())
|
||||
}
|
||||
|
||||
pub fn set(
|
||||
&mut self,
|
||||
object: u16,
|
||||
property: u16,
|
||||
value: PropertyValue,
|
||||
) -> Result<(), RuntimeError> {
|
||||
self.set_at(object, None, property, value)
|
||||
}
|
||||
|
||||
pub fn set_at(
|
||||
&mut self,
|
||||
object: u16,
|
||||
index: Option<i32>,
|
||||
property: u16,
|
||||
value: PropertyValue,
|
||||
) -> Result<(), RuntimeError> {
|
||||
self.ensure_loaded_at(object, index)?;
|
||||
let obj = if index.is_some_and(|i| i != 0) {
|
||||
self.dynamic
|
||||
.get_mut(&(object, index.unwrap()))
|
||||
.ok_or(RuntimeError(340))?
|
||||
} else {
|
||||
self.objects
|
||||
.get_mut(object as usize)
|
||||
.ok_or(RuntimeError(420))?
|
||||
};
|
||||
let spec = forms::properties(obj.description.class)
|
||||
.get(property as usize)
|
||||
.copied()
|
||||
.ok_or(RuntimeError(422))?;
|
||||
if !spec.writable {
|
||||
return Err(RuntimeError(383));
|
||||
}
|
||||
let type_ok = matches!(
|
||||
(&value, spec.ty),
|
||||
(PropertyValue::Integer(_), PropertyType::Integer)
|
||||
| (PropertyValue::Single(_), PropertyType::Single)
|
||||
| (PropertyValue::String(_), PropertyType::String)
|
||||
| (PropertyValue::Boolean(_), PropertyType::Boolean)
|
||||
| (PropertyValue::Object(_), PropertyType::Object)
|
||||
| (PropertyValue::IntegerArray(_), PropertyType::IntegerArray)
|
||||
);
|
||||
if !type_ok {
|
||||
return Err(RuntimeError::TYPE_MISMATCH);
|
||||
}
|
||||
if let PropertyValue::Integer(v) = value {
|
||||
if spec.min.is_some_and(|min| v < min) || spec.max.is_some_and(|max| v > max) {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
obj.properties[property as usize] = PropertyValue::Integer(v);
|
||||
} else {
|
||||
obj.properties[property as usize] = value;
|
||||
}
|
||||
if spec.name == "VISIBLE" {
|
||||
obj.visible = matches!(
|
||||
obj.properties[property as usize],
|
||||
PropertyValue::Boolean(true)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_loaded(&mut self, object: u16) -> Result<(), RuntimeError> {
|
||||
let obj = self
|
||||
.objects
|
||||
.get_mut(object as usize)
|
||||
.ok_or(RuntimeError(420))?;
|
||||
if obj.loaded {
|
||||
return Ok(());
|
||||
}
|
||||
obj.loaded = true;
|
||||
if obj.description.class == ObjectClass::Form {
|
||||
self.events.push_back(FormEvent {
|
||||
object,
|
||||
array_index: None,
|
||||
name: "LOAD".into(),
|
||||
args: Vec::new(),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn ensure_loaded_at(
|
||||
&mut self,
|
||||
object: u16,
|
||||
index: Option<i32>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
if index.is_some_and(|i| i != 0) {
|
||||
self.dynamic
|
||||
.get(&(object, index.unwrap()))
|
||||
.map(|_| ())
|
||||
.ok_or(RuntimeError(340))
|
||||
} else {
|
||||
self.ensure_loaded(object)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(&mut self, object: u16, modal: bool) -> Result<ShowResult, RuntimeError> {
|
||||
self.ensure_loaded(object)?;
|
||||
let obj = self
|
||||
.objects
|
||||
.get_mut(object as usize)
|
||||
.ok_or(RuntimeError(420))?;
|
||||
if obj.description.class != ObjectClass::Form {
|
||||
return Err(RuntimeError(421));
|
||||
}
|
||||
if modal {
|
||||
if obj.visible {
|
||||
return Err(RuntimeError(400));
|
||||
}
|
||||
let form_type = forms::property(ObjectClass::Form, "FORMTYPE").unwrap().0 as usize;
|
||||
if obj.properties.get(form_type) == Some(&PropertyValue::Integer(1)) {
|
||||
return Err(RuntimeError(403));
|
||||
}
|
||||
self.modal.push(object);
|
||||
} else if !self.modal.is_empty() {
|
||||
return Err(RuntimeError(401));
|
||||
}
|
||||
obj.visible = true;
|
||||
Self::set_visible_property(obj, true);
|
||||
self.visible_forms.retain(|id| *id != object);
|
||||
self.visible_forms.push(object);
|
||||
self.active_form = Some(object);
|
||||
Ok(if modal {
|
||||
ShowResult::ModalWait
|
||||
} else {
|
||||
ShowResult::Modeless
|
||||
})
|
||||
}
|
||||
|
||||
pub fn hide(&mut self, object: u16) -> Result<(), RuntimeError> {
|
||||
if self.modal.contains(&object) && self.modal.last().copied() != Some(object) {
|
||||
return Err(RuntimeError(402));
|
||||
}
|
||||
let obj = self
|
||||
.objects
|
||||
.get_mut(object as usize)
|
||||
.ok_or(RuntimeError(420))?;
|
||||
obj.visible = false;
|
||||
Self::set_visible_property(obj, false);
|
||||
self.visible_forms.retain(|id| *id != object);
|
||||
if self.modal.last().copied() == Some(object) {
|
||||
self.modal.pop();
|
||||
}
|
||||
if self.active_form == Some(object) {
|
||||
self.active_form = self.visible_forms.last().copied();
|
||||
}
|
||||
self.clear_active_control_for_form(object);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unload_with(
|
||||
&mut self,
|
||||
object: u16,
|
||||
mut handler: impl FnMut(&mut i16),
|
||||
) -> Result<bool, RuntimeError> {
|
||||
if self.modal.contains(&object) && self.modal.last().copied() != Some(object) {
|
||||
return Err(RuntimeError(402));
|
||||
}
|
||||
let class = self
|
||||
.objects
|
||||
.get(object as usize)
|
||||
.ok_or(RuntimeError(420))?
|
||||
.description
|
||||
.class;
|
||||
if class != ObjectClass::Form {
|
||||
return Err(RuntimeError(361));
|
||||
}
|
||||
let mut cancel = 0i16;
|
||||
handler(&mut cancel);
|
||||
if cancel != 0 {
|
||||
return Ok(false);
|
||||
}
|
||||
let obj = &mut self.objects[object as usize];
|
||||
obj.visible = false;
|
||||
Self::set_visible_property(obj, false);
|
||||
obj.loaded = false;
|
||||
self.visible_forms.retain(|id| *id != object);
|
||||
if self.modal.last().copied() == Some(object) {
|
||||
self.modal.pop();
|
||||
}
|
||||
if self.active_form == Some(object) {
|
||||
self.active_form = self.visible_forms.last().copied();
|
||||
}
|
||||
self.clear_active_control_for_form(object);
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub fn load_array(&mut self, base: u16, index: i32) -> Result<(), RuntimeError> {
|
||||
let template = self.objects.get(base as usize).ok_or(RuntimeError(420))?;
|
||||
if !template.description.array {
|
||||
return Err(RuntimeError(343));
|
||||
}
|
||||
if index == 0 {
|
||||
return Err(RuntimeError(360));
|
||||
}
|
||||
let key = (base, index);
|
||||
if self.dynamic.contains_key(&key) {
|
||||
return Err(RuntimeError(360));
|
||||
}
|
||||
let mut obj = template.clone();
|
||||
obj.loaded = true;
|
||||
obj.visible = false;
|
||||
obj.design_time = false;
|
||||
obj.array_index = Some(index);
|
||||
self.dynamic.insert(key, obj);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn unload_array(&mut self, base: u16, index: i32) -> Result<(), RuntimeError> {
|
||||
let template = self.objects.get(base as usize).ok_or(RuntimeError(420))?;
|
||||
if !template.description.array {
|
||||
return Err(RuntimeError(343));
|
||||
}
|
||||
if template.array_index == Some(index) || (template.array_index.is_none() && index == 0) {
|
||||
return Err(RuntimeError(362));
|
||||
}
|
||||
self.dynamic
|
||||
.remove(&(base, index))
|
||||
.map(|_| ())
|
||||
.ok_or(RuntimeError(340))
|
||||
}
|
||||
|
||||
pub fn queue(&mut self, event: FormEvent) {
|
||||
self.events.push_back(event);
|
||||
}
|
||||
pub fn next_event(&mut self) -> Option<FormEvent> {
|
||||
self.events.pop_front()
|
||||
}
|
||||
pub fn is_visible(&self, object: u16) -> bool {
|
||||
self.objects.get(object as usize).is_some_and(|o| o.visible)
|
||||
}
|
||||
pub fn is_loaded(&self, object: u16) -> bool {
|
||||
self.objects.get(object as usize).is_some_and(|o| o.loaded)
|
||||
}
|
||||
pub fn is_loaded_at(&self, object: u16, index: Option<i32>) -> bool {
|
||||
if index.is_some_and(|i| i != 0) {
|
||||
self.dynamic.contains_key(&(object, index.unwrap()))
|
||||
} else {
|
||||
self.is_loaded(object)
|
||||
}
|
||||
}
|
||||
pub fn active_form(&self) -> Option<u16> {
|
||||
self.active_form
|
||||
}
|
||||
pub fn set_active_control(
|
||||
&mut self,
|
||||
object: u16,
|
||||
index: Option<i32>,
|
||||
) -> Result<(), RuntimeError> {
|
||||
self.ensure_loaded_at(object, index)?;
|
||||
let description = if index.is_some_and(|i| i != 0) {
|
||||
&self
|
||||
.dynamic
|
||||
.get(&(object, index.unwrap()))
|
||||
.unwrap()
|
||||
.description
|
||||
} else {
|
||||
&self
|
||||
.objects
|
||||
.get(object as usize)
|
||||
.ok_or(RuntimeError(420))?
|
||||
.description
|
||||
};
|
||||
if matches!(description.class, ObjectClass::Form | ObjectClass::Screen) {
|
||||
return Err(RuntimeError(421));
|
||||
}
|
||||
let parent = description
|
||||
.parent_form
|
||||
.as_deref()
|
||||
.ok_or(RuntimeError(421))?;
|
||||
self.active_form = self
|
||||
.objects
|
||||
.iter()
|
||||
.position(|o| {
|
||||
o.description.class == ObjectClass::Form
|
||||
&& o.description.name.eq_ignore_ascii_case(parent)
|
||||
})
|
||||
.map(|id| id as u16);
|
||||
self.active_control = Some((object, index));
|
||||
Ok(())
|
||||
}
|
||||
pub fn modal_top(&self) -> Option<u16> {
|
||||
self.modal.last().copied()
|
||||
}
|
||||
pub fn screen_show(&mut self, visible: bool) {
|
||||
self.screen_visible = visible;
|
||||
}
|
||||
pub fn screen_visible(&self) -> bool {
|
||||
self.screen_visible
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn model() -> FormsModel {
|
||||
let mut c = forms::FormCatalog::default();
|
||||
c.add("Form1", ObjectClass::Form, None, false);
|
||||
c.add("Check1", ObjectClass::CheckBox, Some("Form1"), false);
|
||||
c.add("Command1", ObjectClass::CommandButton, Some("Form1"), true);
|
||||
c.add("SCREEN", ObjectClass::Screen, None, false);
|
||||
FormsModel::new(c.objects, 80, 25)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_bereich_und_implizites_laden() {
|
||||
let mut m = model();
|
||||
let value = forms::property(ObjectClass::CheckBox, "VALUE").unwrap().0;
|
||||
assert_eq!(m.get(1, value).unwrap(), PropertyValue::Integer(0));
|
||||
assert!(m.objects[1].loaded);
|
||||
assert_eq!(
|
||||
m.set(1, value, PropertyValue::Integer(3)),
|
||||
Err(RuntimeError(5))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hide_entlaedt_nicht_und_unload_ist_abbrechbar() {
|
||||
let mut m = model();
|
||||
m.show(0, false).unwrap();
|
||||
m.hide(0).unwrap();
|
||||
assert!(m.objects[0].loaded);
|
||||
assert!(!m.unload_with(0, |cancel| *cancel = 1).unwrap());
|
||||
assert!(m.objects[0].loaded);
|
||||
assert!(m.unload_with(0, |_| {}).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrays_und_designzeitfehler() {
|
||||
let mut m = model();
|
||||
let caption = forms::property(ObjectClass::CommandButton, "CAPTION")
|
||||
.unwrap()
|
||||
.0;
|
||||
m.set(2, caption, PropertyValue::String("Vorlage".into()))
|
||||
.unwrap();
|
||||
assert_eq!(m.load_array(2, 0), Err(RuntimeError(360)));
|
||||
m.load_array(2, 3).unwrap();
|
||||
assert_eq!(
|
||||
m.get_at(2, Some(3), caption).unwrap(),
|
||||
PropertyValue::String("Vorlage".into())
|
||||
);
|
||||
m.set_at(2, Some(3), caption, PropertyValue::String("Klon".into()))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m.get(2, caption).unwrap(),
|
||||
PropertyValue::String("Vorlage".into())
|
||||
);
|
||||
assert_eq!(m.unload_array(2, 0), Err(RuntimeError(362)));
|
||||
m.unload_array(2, 3).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_meldet_oberstes_formular_und_aktives_control() {
|
||||
let mut c = forms::FormCatalog::default();
|
||||
c.add("A", ObjectClass::Form, None, false);
|
||||
c.add("B", ObjectClass::Form, None, false);
|
||||
c.add("Text1", ObjectClass::TextBox, Some("B"), false);
|
||||
c.add("SCREEN", ObjectClass::Screen, None, false);
|
||||
let mut m = FormsModel::new(c.objects, 80, 25);
|
||||
m.show(0, false).unwrap();
|
||||
m.show(1, false).unwrap();
|
||||
m.set_active_control(2, None).unwrap();
|
||||
let active_form = forms::property(ObjectClass::Screen, "ACTIVEFORM")
|
||||
.unwrap()
|
||||
.0;
|
||||
let active_control = forms::property(ObjectClass::Screen, "ACTIVECONTROL")
|
||||
.unwrap()
|
||||
.0;
|
||||
assert_eq!(
|
||||
m.get(3, active_form).unwrap(),
|
||||
PropertyValue::Object(Some((1, None)))
|
||||
);
|
||||
assert_eq!(
|
||||
m.get(3, active_control).unwrap(),
|
||||
PropertyValue::Object(Some((2, None)))
|
||||
);
|
||||
m.hide(1).unwrap();
|
||||
assert_eq!(
|
||||
m.get(3, active_form).unwrap(),
|
||||
PropertyValue::Object(Some((0, None)))
|
||||
);
|
||||
assert_eq!(
|
||||
m.get(3, active_control).unwrap(),
|
||||
PropertyValue::Object(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_folgt_resize() {
|
||||
let mut m = model();
|
||||
m.resize(120, 40);
|
||||
let width = forms::property(ObjectClass::Screen, "WIDTH").unwrap().0;
|
||||
let height = forms::property(ObjectClass::Screen, "HEIGHT").unwrap().0;
|
||||
assert_eq!(m.get(3, width).unwrap(), PropertyValue::Integer(120));
|
||||
assert_eq!(m.get(3, height).unwrap(), PropertyValue::Integer(40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modalfehler_400_bis_403() {
|
||||
let mut m = model();
|
||||
m.show(0, false).unwrap();
|
||||
assert_eq!(m.show(0, true), Err(RuntimeError(400)));
|
||||
m.hide(0).unwrap();
|
||||
m.show(0, true).unwrap();
|
||||
assert_eq!(m.show(0, false), Err(RuntimeError(401)));
|
||||
let mut c = forms::FormCatalog::default();
|
||||
c.add("A", ObjectClass::Form, None, false);
|
||||
c.add("B", ObjectClass::Form, None, false);
|
||||
let mut stacked = FormsModel::new(c.objects, 80, 25);
|
||||
stacked.show(0, true).unwrap();
|
||||
stacked.show(1, true).unwrap();
|
||||
assert_eq!(stacked.hide(0), Err(RuntimeError(402)));
|
||||
let ft = forms::property(ObjectClass::Form, "FORMTYPE").unwrap().0;
|
||||
let mut mdi = model();
|
||||
mdi.set(0, ft, PropertyValue::Integer(1)).unwrap();
|
||||
assert_eq!(mdi.show(0, true), Err(RuntimeError(403)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user