Forms-Zustand und Darstellung korrigieren und Change archivieren
This commit is contained in:
@@ -399,16 +399,31 @@ impl TextScreen {
|
||||
match ch {
|
||||
'\n' => self.newline(),
|
||||
'\r' => self.cur_col = 0,
|
||||
_ => self.zeichen_setzen(ch),
|
||||
_ => self.zeichen_setzen(ch, true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Eine UI-Zeile ohne Umbruch oder Scrollen bis zum Bildschirmrand zeichnen.
|
||||
/// Steuerzeichen werden als Leerzeichen dargestellt; breite Zeichen passen
|
||||
/// nur vollständig. Der Cursor bleibt spätestens in der letzten Spalte.
|
||||
pub fn print_line(&mut self, text: &str) {
|
||||
self.veraendert = true;
|
||||
for ch in text.chars() {
|
||||
let ch = if ch.is_control() { ' ' } else { ch };
|
||||
if self.cur_col + ch.width().unwrap_or(1).max(1) > self.cols {
|
||||
break;
|
||||
}
|
||||
self.zeichen_setzen(ch, false);
|
||||
}
|
||||
self.cur_col = self.cur_col.min(self.cols - 1);
|
||||
}
|
||||
|
||||
/// Ein Zeichen an der Cursorposition setzen und den Cursor um seine
|
||||
/// Darstellungsbreite vorrücken. Ein breites Zeichen, das nicht mehr
|
||||
/// vollständig in die Zeile passt, wandert als Ganzes in die nächste;
|
||||
/// die letzte Spalte bleibt dann leer.
|
||||
fn zeichen_setzen(&mut self, ch: char) {
|
||||
fn zeichen_setzen(&mut self, ch: char, wrap: bool) {
|
||||
// ponytail: Breite 0 (kombinierende Zeichen) wird wie 1 behandelt —
|
||||
// sie bekommen eine eigene Zelle statt sich an die vorige zu hängen.
|
||||
// Ceiling: echte Kombination erst, wenn ein Korpusprogramm sie braucht.
|
||||
@@ -437,7 +452,7 @@ impl TextScreen {
|
||||
let b = &mut self.belegt[self.cur_row];
|
||||
*b = Some(b.map_or(letzte, |c| c.max(letzte)));
|
||||
self.cur_col += breite;
|
||||
if self.cur_col >= self.cols {
|
||||
if wrap && self.cur_col >= self.cols {
|
||||
self.newline();
|
||||
}
|
||||
}
|
||||
@@ -541,6 +556,37 @@ mod tests {
|
||||
assert_eq!((s.csrlin(), s.pos()), (1, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ui_zeile_bleibt_am_rand_ohne_umbruch_oder_scrollen() {
|
||||
let mut s = TextScreen::new();
|
||||
s.print("oben");
|
||||
s.view_print(3, 10).unwrap();
|
||||
s.set_color(2, 4);
|
||||
for row in [10, 25] {
|
||||
s.locate(row, 78).unwrap();
|
||||
s.print_line("中xREST");
|
||||
assert_eq!(s.cell(row, 78).ch, '中');
|
||||
assert!(s.cell(row, 79).fortsetzung);
|
||||
assert_eq!(s.cell(row, 80).ch, 'x');
|
||||
assert_eq!((s.csrlin(), s.pos()), (row, 80));
|
||||
assert_eq!((s.cell(row, 80).fg, s.cell(row, 80).bg), (2, 4));
|
||||
}
|
||||
assert_eq!(s.cell(1, 1).ch, 'o');
|
||||
assert_eq!(s.cell(9, 78).ch, ' ');
|
||||
assert_eq!(s.cell(24, 78).ch, ' ');
|
||||
s.locate(2, 1).unwrap();
|
||||
s.print_line("a\nb\rc");
|
||||
assert_eq!(
|
||||
(1..=5).map(|col| s.cell(2, col).ch).collect::<String>(),
|
||||
"a b c"
|
||||
);
|
||||
s.locate(2, 80).unwrap();
|
||||
s.print_line("中");
|
||||
assert_eq!(s.cell(2, 80).ch, ' ');
|
||||
assert_eq!(s.cell(3, 1).ch, ' ');
|
||||
assert_eq!((s.csrlin(), s.pos()), (2, 80));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unicode_zeichen_belegen_eine_zelle() {
|
||||
let mut s = TextScreen::new();
|
||||
|
||||
@@ -18,6 +18,11 @@ crossterm = { workspace = true, optional = true }
|
||||
signal-hook = { workspace = true, optional = true }
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
unicode-width.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "spike"
|
||||
required-features = ["terminal"]
|
||||
|
||||
@@ -10,6 +10,7 @@ use tb_frontend::forms::{self, FormObject, ObjectClass, PropertyDefault, Propert
|
||||
use tb_runtime::errors::RuntimeError;
|
||||
use tb_runtime::host::{taste, umschalt, Ereignis, Host, MausArt, MausEreignis};
|
||||
use tb_runtime::screen::TextScreen;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
type ObjectKey = (u16, Option<i32>);
|
||||
|
||||
@@ -195,21 +196,7 @@ impl FormsModel {
|
||||
| ObjectClass::FileListBox
|
||||
)
|
||||
{
|
||||
let selected = forms::property(obj.description.class, "LISTINDEX")
|
||||
.and_then(|(id, _)| match obj.properties.get(id as usize) {
|
||||
Some(PropertyValue::Integer(v)) => Some(*v),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or(-1);
|
||||
if selected >= 0 {
|
||||
if let Some(text) = self
|
||||
.lists
|
||||
.get(&key)
|
||||
.and_then(|items| items.get(selected as usize))
|
||||
{
|
||||
return Ok(PropertyValue::String(text.clone()));
|
||||
}
|
||||
}
|
||||
return Ok(PropertyValue::String(self.list_text(key)));
|
||||
}
|
||||
if spec.name == "SELTEXT"
|
||||
&& matches!(
|
||||
@@ -403,7 +390,7 @@ impl FormsModel {
|
||||
let PropertyValue::Integer(v) = value else {
|
||||
unreachable!()
|
||||
};
|
||||
if v < -1 || v as usize >= self.lists.get(&key).map_or(0, Vec::len) {
|
||||
if v < -1 || (v >= 0 && v as usize >= self.lists.get(&key).map_or(0, Vec::len)) {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
self.instance_mut(key)?.properties[property as usize] = PropertyValue::Integer(v);
|
||||
@@ -438,6 +425,9 @@ impl FormsModel {
|
||||
self.replace_selection(key, &replacement)?;
|
||||
return Ok(());
|
||||
}
|
||||
let timer_reset = class == ObjectClass::Timer
|
||||
&& (spec.name == "INTERVAL"
|
||||
|| (spec.name == "ENABLED" && self.value(key, "ENABLED") != Some(&value)));
|
||||
{
|
||||
let obj = self.instance_mut(key)?;
|
||||
obj.properties[property as usize] = value;
|
||||
@@ -448,8 +438,14 @@ impl FormsModel {
|
||||
);
|
||||
}
|
||||
}
|
||||
if matches!(spec.name, "INTERVAL" | "ENABLED") && class == ObjectClass::Timer {
|
||||
self.timer_last.remove(&key);
|
||||
if spec.name == "SORTED" && self.boolean(key, "SORTED") {
|
||||
self.sort_list(key)?;
|
||||
}
|
||||
if timer_reset {
|
||||
self.reset_timer(key);
|
||||
}
|
||||
if class == ObjectClass::Form && spec.name == "VISIBLE" && !self.is_visible(object) {
|
||||
self.reset_form_timers(object);
|
||||
}
|
||||
if spec.name == "TEXT" && matches!(class, ObjectClass::TextBox | ObjectClass::ComboBox) {
|
||||
self.queue_named(key, "CHANGE", vec![]);
|
||||
@@ -505,6 +501,11 @@ impl FormsModel {
|
||||
obj.visible = matches!(value, PropertyValue::Boolean(true));
|
||||
}
|
||||
obj.properties[property as usize] = value;
|
||||
if forms::properties(obj.description.class)[property as usize].name == "SORTED"
|
||||
&& self.boolean(key, "SORTED")
|
||||
{
|
||||
self.sort_list(key)?;
|
||||
}
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
@@ -597,6 +598,7 @@ impl FormsModel {
|
||||
self.active_form = self.visible_forms.last().copied();
|
||||
}
|
||||
self.clear_active_control_for_form(object);
|
||||
self.reset_form_timers(object);
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
@@ -635,6 +637,7 @@ impl FormsModel {
|
||||
self.active_form = self.visible_forms.last().copied();
|
||||
}
|
||||
self.clear_active_control_for_form(object);
|
||||
self.reset_form_timers(object);
|
||||
self.dirty = true;
|
||||
Ok(true)
|
||||
}
|
||||
@@ -694,6 +697,10 @@ impl FormsModel {
|
||||
.remove(&(base, index))
|
||||
.map(|_| ())
|
||||
.ok_or(RuntimeError(340));
|
||||
if removed.is_ok() {
|
||||
self.reset_timer((base, Some(index)));
|
||||
self.lists.remove(&(base, Some(index)));
|
||||
}
|
||||
self.dirty = true;
|
||||
removed
|
||||
}
|
||||
@@ -1013,6 +1020,27 @@ impl FormsModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn sort_list(&mut self, key: ObjectKey) -> Result<(), RuntimeError> {
|
||||
let selected = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
let Some(items) = self.lists.get_mut(&key) else {
|
||||
return Ok(());
|
||||
};
|
||||
// Ursprüngliche Indizes erhalten die Auswahl auch bei gleichen Texten.
|
||||
let mut indexed: Vec<_> = items.drain(..).enumerate().collect();
|
||||
indexed.sort_by_cached_key(|(_, text)| text.to_uppercase());
|
||||
let selected = indexed
|
||||
.iter()
|
||||
.position(|(index, _)| *index as i32 == selected)
|
||||
.map_or(-1, |index| index as i32);
|
||||
items.extend(indexed.into_iter().map(|(_, text)| text));
|
||||
let obj = self.instance_mut(key)?;
|
||||
let property = forms::property(obj.description.class, "LISTINDEX")
|
||||
.unwrap()
|
||||
.0;
|
||||
obj.properties[property as usize] = PropertyValue::Integer(selected);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn add_item(
|
||||
&mut self,
|
||||
key: ObjectKey,
|
||||
@@ -1023,46 +1051,70 @@ impl FormsModel {
|
||||
if !matches!(class, ObjectClass::ListBox | ObjectClass::ComboBox) {
|
||||
return Err(RuntimeError(421));
|
||||
}
|
||||
let selected = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
let sorted = self.boolean(key, "SORTED");
|
||||
let items = self.lists.entry(key).or_default();
|
||||
if sorted {
|
||||
let pos = items.partition_point(|item| item.to_uppercase() <= text.to_uppercase());
|
||||
items.insert(pos, text);
|
||||
} else if let Some(at) = at {
|
||||
let pos = if sorted {
|
||||
items.partition_point(|item| item.to_uppercase() <= text.to_uppercase())
|
||||
} else {
|
||||
let at = at.unwrap_or(items.len() as i32);
|
||||
if at < 0 || at as usize > items.len() {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
items.insert(at as usize, text);
|
||||
} else {
|
||||
items.push(text);
|
||||
at as usize
|
||||
};
|
||||
items.insert(pos, text);
|
||||
if selected >= pos as i32 {
|
||||
let property = forms::property(class, "LISTINDEX").unwrap().0;
|
||||
self.set_at(key.0, key.1, property, PropertyValue::Integer(selected + 1))?;
|
||||
}
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn remove_item(&mut self, key: ObjectKey, at: i32) -> Result<(), RuntimeError> {
|
||||
let len = {
|
||||
let items = self
|
||||
.lists
|
||||
.get_mut(&key)
|
||||
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
if at < 0 || at as usize >= items.len() {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
items.remove(at as usize);
|
||||
items.len()
|
||||
};
|
||||
let list_index = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
if list_index >= len as i32 {
|
||||
let id = forms::property(self.instance(key)?.description.class, "LISTINDEX")
|
||||
let items = self
|
||||
.lists
|
||||
.get_mut(&key)
|
||||
.ok_or(RuntimeError::ILLEGAL_FUNCTION_CALL)?;
|
||||
if at < 0 || at as usize >= items.len() {
|
||||
return Err(RuntimeError::ILLEGAL_FUNCTION_CALL);
|
||||
}
|
||||
items.remove(at as usize);
|
||||
let selected = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
if selected >= at {
|
||||
let property = forms::property(self.instance(key)?.description.class, "LISTINDEX")
|
||||
.unwrap()
|
||||
.0 as usize;
|
||||
self.instance_mut(key)?.properties[id] = PropertyValue::Integer(-1);
|
||||
.0;
|
||||
let selected = if selected == at { -1 } else { selected - 1 };
|
||||
self.set_at(key.0, key.1, property, PropertyValue::Integer(selected))?;
|
||||
}
|
||||
self.dirty = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn list_text(&self, key: ObjectKey) -> String {
|
||||
let selected = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
if selected >= 0 {
|
||||
if let Some(text) = self
|
||||
.lists
|
||||
.get(&key)
|
||||
.and_then(|items| items.get(selected as usize))
|
||||
{
|
||||
return text.clone();
|
||||
}
|
||||
}
|
||||
if self
|
||||
.instance(key)
|
||||
.is_ok_and(|obj| obj.description.class == ObjectClass::ComboBox)
|
||||
&& self.integer(key, "STYLE").unwrap_or(0) != 2
|
||||
{
|
||||
self.string(key, "TEXT")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
fn wildcard(pattern: &str, name: &str) -> bool {
|
||||
fn matches(p: &[char], n: &[char]) -> bool {
|
||||
match p.split_first() {
|
||||
@@ -1777,6 +1829,42 @@ impl FormsModel {
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_timer(&mut self, key: ObjectKey) {
|
||||
self.timer_last.remove(&key);
|
||||
self.events.retain(|event| {
|
||||
event.name != "TIMER"
|
||||
|| (event.object, event.array_index.filter(|index| *index != 0)) != key
|
||||
});
|
||||
}
|
||||
|
||||
fn reset_form_timers(&mut self, form: u16) {
|
||||
for key in self.keys() {
|
||||
if self
|
||||
.instance(key)
|
||||
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
|
||||
&& self.root_form(key) == Some(form)
|
||||
{
|
||||
self.reset_timer(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Nach Zustandsänderungen aktive Timer an der Hostzeit beginnen lassen.
|
||||
/// Die Uhr wird nur bei einer neuen aktiven Phase abgefragt, auch im Menü.
|
||||
pub fn sync_timers(&mut self, now: impl FnOnce() -> u64) {
|
||||
let starts: Vec<_> = self
|
||||
.keys()
|
||||
.into_iter()
|
||||
.filter(|key| self.timer_enabled(*key) && !self.timer_last.contains_key(key))
|
||||
.collect();
|
||||
if !starts.is_empty() {
|
||||
let now_ms = now();
|
||||
for key in starts {
|
||||
self.timer_last.insert(key, now_ms);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn timer_enabled(&self, key: ObjectKey) -> bool {
|
||||
self.instance(key)
|
||||
.is_ok_and(|obj| obj.description.class == ObjectClass::Timer)
|
||||
@@ -1821,6 +1909,7 @@ impl FormsModel {
|
||||
}
|
||||
|
||||
pub fn timers(&mut self, now_ms: u64) {
|
||||
self.sync_timers(|| now_ms);
|
||||
if !self.menu_path.is_empty() {
|
||||
return;
|
||||
}
|
||||
@@ -1830,15 +1919,18 @@ impl FormsModel {
|
||||
.filter(|key| self.timer_enabled(*key))
|
||||
.collect();
|
||||
due.sort_by_key(|key| {
|
||||
self.instance(*key)
|
||||
.map(|obj| obj.description.name.to_uppercase())
|
||||
.unwrap_or_default()
|
||||
(
|
||||
self.instance(*key)
|
||||
.map(|obj| obj.description.name.to_uppercase())
|
||||
.unwrap_or_default(),
|
||||
key.1.unwrap_or(0),
|
||||
)
|
||||
});
|
||||
for key in due {
|
||||
let interval = self.integer(key, "INTERVAL").unwrap() as u64;
|
||||
let mut count = 0;
|
||||
{
|
||||
let last = self.timer_last.entry(key).or_insert(0);
|
||||
let last = self.timer_last.get_mut(&key).unwrap();
|
||||
while now_ms.saturating_sub(*last) >= interval {
|
||||
*last += interval;
|
||||
count += 1;
|
||||
@@ -1901,11 +1993,19 @@ impl FormsModel {
|
||||
}
|
||||
|
||||
fn fit(text: &str, width: usize) -> String {
|
||||
let mut out: String = text.chars().take(width).collect();
|
||||
out.extend(std::iter::repeat_n(
|
||||
' ',
|
||||
width.saturating_sub(out.chars().count()),
|
||||
));
|
||||
let mut out = String::new();
|
||||
let mut remaining = width;
|
||||
for ch in text.chars() {
|
||||
let ch = if ch.is_control() { ' ' } else { ch };
|
||||
// Dieselbe Zellenbreite wie TextScreen, auch für kombinierende Zeichen.
|
||||
let cells = ch.width().unwrap_or(1).max(1);
|
||||
if cells > remaining {
|
||||
break;
|
||||
}
|
||||
out.push(ch);
|
||||
remaining -= cells;
|
||||
}
|
||||
out.extend(std::iter::repeat_n(' ', remaining));
|
||||
out
|
||||
}
|
||||
|
||||
@@ -1921,10 +2021,9 @@ impl FormsModel {
|
||||
if row == 0 || col == 0 || row > screen.rows() || col > screen.cols() {
|
||||
return;
|
||||
}
|
||||
let clipped: String = text.chars().take(screen.cols() - col + 1).collect();
|
||||
screen.set_color(fg, bg);
|
||||
let _ = screen.locate(row, col);
|
||||
screen.print(&clipped);
|
||||
screen.print_line(text);
|
||||
}
|
||||
|
||||
fn box_lines(width: usize, height: usize, caption: &str, double: bool) -> Vec<String> {
|
||||
@@ -1952,6 +2051,30 @@ impl FormsModel {
|
||||
lines
|
||||
}
|
||||
|
||||
fn list_lines(&self, key: ObjectKey, width: usize, height: usize) -> Vec<String> {
|
||||
let mut lines: Vec<_> = Self::box_lines(width, height, "", false)
|
||||
.into_iter()
|
||||
.take(height)
|
||||
.map(|line| Self::fit(&line, width))
|
||||
.collect();
|
||||
if width < 3 {
|
||||
return lines;
|
||||
}
|
||||
let selected = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
for (row, item) in self
|
||||
.lists
|
||||
.get(&key)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.take(height.saturating_sub(2))
|
||||
.enumerate()
|
||||
{
|
||||
let marker = if selected == row as i32 { '>' } else { ' ' };
|
||||
lines[row + 1] = format!("│{marker}{}│", Self::fit(item, width.saturating_sub(3)));
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn control_lines(&self, key: ObjectKey, width: usize, height: usize) -> Vec<String> {
|
||||
let Ok(class) = self.instance(key).map(|obj| obj.description.class) else {
|
||||
return vec![];
|
||||
@@ -2086,21 +2209,7 @@ impl FormsModel {
|
||||
lines
|
||||
}
|
||||
ObjectClass::ListBox | ObjectClass::DirListBox | ObjectClass::FileListBox => {
|
||||
let mut lines = Self::box_lines(width, height.max(2), "", false);
|
||||
let selected = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
for (row, item) in self
|
||||
.lists
|
||||
.get(&key)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.take(height.saturating_sub(2))
|
||||
.enumerate()
|
||||
{
|
||||
let marker = if selected == row as i32 { '>' } else { ' ' };
|
||||
lines[row + 1] =
|
||||
format!("│{marker}{}│", Self::fit(item, width.saturating_sub(3)));
|
||||
}
|
||||
lines
|
||||
self.list_lines(key, width, height)
|
||||
}
|
||||
ObjectClass::ComboBox | ObjectClass::DriveListBox => {
|
||||
let text = if class == ObjectClass::DriveListBox {
|
||||
@@ -2110,12 +2219,7 @@ impl FormsModel {
|
||||
.cloned()
|
||||
.unwrap_or_else(|| std::path::MAIN_SEPARATOR.to_string())
|
||||
} else {
|
||||
let selected = self.integer(key, "LISTINDEX").unwrap_or(-1);
|
||||
self.lists
|
||||
.get(&key)
|
||||
.and_then(|items| items.get(selected.max(0) as usize))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| self.string(key, "TEXT"))
|
||||
self.list_text(key)
|
||||
};
|
||||
let style = self.integer(key, "STYLE").unwrap_or(0);
|
||||
let first = if class == ObjectClass::ComboBox && style == 2 {
|
||||
@@ -2125,12 +2229,7 @@ impl FormsModel {
|
||||
};
|
||||
let mut lines = vec![Self::fit(&first, width)];
|
||||
if class == ObjectClass::ComboBox && style == 1 {
|
||||
lines.extend(Self::box_lines(
|
||||
width,
|
||||
height.saturating_sub(1).max(2),
|
||||
"",
|
||||
false,
|
||||
));
|
||||
lines.extend(self.list_lines(key, width, height.saturating_sub(1)));
|
||||
}
|
||||
lines.truncate(height);
|
||||
lines
|
||||
@@ -2919,6 +3018,10 @@ mod tests {
|
||||
let mut m = controls_model();
|
||||
m.add_item((7, None), "eins".into(), None).unwrap();
|
||||
m.add_item((8, None), "wahl".into(), None).unwrap();
|
||||
let selected = forms::property(ObjectClass::ComboBox, "LISTINDEX")
|
||||
.unwrap()
|
||||
.0;
|
||||
m.set(8, selected, PropertyValue::Integer(0)).unwrap();
|
||||
m.object_method(11, "PRINT", vec![PropertyValue::String("Bild".into())])
|
||||
.unwrap();
|
||||
let snapshots = [
|
||||
@@ -3060,6 +3163,7 @@ mod tests {
|
||||
assert!(m.menu_is_open());
|
||||
let interval = forms::property(ObjectClass::Timer, "INTERVAL").unwrap().0;
|
||||
m.set(12, interval, PropertyValue::Integer(100)).unwrap();
|
||||
m.sync_timers(|| 0);
|
||||
m.timers(250);
|
||||
assert!(!m.events.iter().any(|event| event.name == "TIMER"));
|
||||
m.handle_key("o", 0);
|
||||
@@ -3129,6 +3233,462 @@ mod tests {
|
||||
assert_ne!(m.active_control, Some((1, None)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listenauswahl_bleibt_beim_einfuegen_und_entfernen_konsistent() {
|
||||
for (id, class, style) in [
|
||||
(7, ObjectClass::ListBox, 0),
|
||||
(8, ObjectClass::ComboBox, 0),
|
||||
(8, ObjectClass::ComboBox, 1),
|
||||
(8, ObjectClass::ComboBox, 2),
|
||||
] {
|
||||
for sorted in [false, true] {
|
||||
let mut m = controls_model();
|
||||
let prop = |name| forms::property(class, name).unwrap().0;
|
||||
let key = (id, None);
|
||||
if class == ObjectClass::ComboBox {
|
||||
m.set_initial(id, prop("STYLE"), PropertyValue::Integer(style))
|
||||
.unwrap();
|
||||
}
|
||||
m.set_initial(id, prop("TEXT"), PropertyValue::String("frei".into()))
|
||||
.unwrap();
|
||||
m.set(id, prop("SORTED"), PropertyValue::Boolean(sorted))
|
||||
.unwrap();
|
||||
m.set(id, prop("LISTINDEX"), PropertyValue::Integer(-1))
|
||||
.unwrap();
|
||||
for invalid in [-2, 0] {
|
||||
assert_eq!(
|
||||
m.set(id, prop("LISTINDEX"), PropertyValue::Integer(invalid)),
|
||||
Err(RuntimeError(5))
|
||||
);
|
||||
}
|
||||
m.add_item(key, "b".into(), None).unwrap();
|
||||
m.add_item(key, "c".into(), None).unwrap();
|
||||
m.set(id, prop("LISTINDEX"), PropertyValue::Integer(1))
|
||||
.unwrap();
|
||||
m.add_item(key, "a".into(), Some(0)).unwrap();
|
||||
assert_eq!(
|
||||
m.get(id, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(2)
|
||||
);
|
||||
assert_eq!(
|
||||
m.get(id, prop("TEXT")).unwrap(),
|
||||
PropertyValue::String("c".into())
|
||||
);
|
||||
assert_eq!(
|
||||
m.get_indexed(id, prop("LIST"), 2).unwrap(),
|
||||
PropertyValue::String("c".into())
|
||||
);
|
||||
m.add_item(key, "z".into(), None).unwrap();
|
||||
m.remove_item(key, 0).unwrap();
|
||||
assert_eq!(
|
||||
m.get(id, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(1)
|
||||
);
|
||||
assert_eq!(
|
||||
m.get(id, prop("TEXT")).unwrap(),
|
||||
PropertyValue::String("c".into())
|
||||
);
|
||||
m.remove_item(key, 1).unwrap();
|
||||
assert_eq!(
|
||||
m.get(id, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(-1)
|
||||
);
|
||||
assert_eq!(
|
||||
m.get(id, prop("LISTCOUNT")).unwrap(),
|
||||
PropertyValue::Integer(2)
|
||||
);
|
||||
m.set(id, prop("LISTINDEX"), PropertyValue::Integer(0))
|
||||
.unwrap();
|
||||
m.set(id, prop("LISTINDEX"), PropertyValue::Integer(-1))
|
||||
.unwrap();
|
||||
let expected = if class == ObjectClass::ComboBox && style != 2 {
|
||||
"frei"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
assert_eq!(
|
||||
m.get(id, prop("TEXT")).unwrap(),
|
||||
PropertyValue::String(expected.into())
|
||||
);
|
||||
if class == ObjectClass::ComboBox {
|
||||
assert_eq!(m.control_lines(key, 12, 4)[0].contains("frei"), style != 2);
|
||||
assert!(!m.control_lines(key, 12, 4)[0].contains('b'));
|
||||
}
|
||||
m.remove_item(key, 1).unwrap();
|
||||
m.remove_item(key, 0).unwrap();
|
||||
assert_eq!(
|
||||
m.get(id, prop("LISTCOUNT")).unwrap(),
|
||||
PropertyValue::Integer(0)
|
||||
);
|
||||
assert_eq!(
|
||||
m.get(id, prop("TEXT")).unwrap(),
|
||||
PropertyValue::String(expected.into())
|
||||
);
|
||||
assert_eq!(m.remove_item(key, 0), Err(RuntimeError(5)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sorted_wechsel_erhaelt_auch_die_auswahl_zwischen_duplikaten() {
|
||||
for (id, class, style) in [
|
||||
(7, ObjectClass::ListBox, 0),
|
||||
(8, ObjectClass::ComboBox, 0),
|
||||
(8, ObjectClass::ComboBox, 1),
|
||||
(8, ObjectClass::ComboBox, 2),
|
||||
] {
|
||||
for index in [None, Some(0), Some(1)] {
|
||||
let mut m = controls_model();
|
||||
let prop = |name| forms::property(class, name).unwrap().0;
|
||||
if index.is_some() {
|
||||
m.objects[id as usize].description.array = true;
|
||||
}
|
||||
if index == Some(1) {
|
||||
m.load_array(id, 1).unwrap();
|
||||
}
|
||||
if class == ObjectClass::ComboBox {
|
||||
m.set_initial_at(id, index, prop("STYLE"), PropertyValue::Integer(style))
|
||||
.unwrap();
|
||||
}
|
||||
m.set_initial_at(
|
||||
id,
|
||||
index,
|
||||
prop("TEXT"),
|
||||
PropertyValue::String("frei".into()),
|
||||
)
|
||||
.unwrap();
|
||||
m.set_at(id, index, prop("SORTED"), PropertyValue::Boolean(true))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m.get_at(id, index, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(-1)
|
||||
);
|
||||
m.set_at(id, index, prop("SORTED"), PropertyValue::Boolean(false))
|
||||
.unwrap();
|
||||
for text in ["b", "a", "b", "A"] {
|
||||
m.object_method_at(
|
||||
id,
|
||||
index,
|
||||
"ADDITEM",
|
||||
vec![PropertyValue::String(text.into())],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
m.set_at(id, index, prop("LISTINDEX"), PropertyValue::Integer(2))
|
||||
.unwrap();
|
||||
for initial in [false, true] {
|
||||
if initial {
|
||||
m.set_initial_at(id, index, prop("SORTED"), PropertyValue::Boolean(true))
|
||||
.unwrap();
|
||||
} else {
|
||||
m.set_at(id, index, prop("SORTED"), PropertyValue::Boolean(true))
|
||||
.unwrap();
|
||||
}
|
||||
let values: Vec<_> = (0..4)
|
||||
.map(|i| m.get_indexed_at(id, index, prop("LIST"), i).unwrap())
|
||||
.collect();
|
||||
assert_eq!(
|
||||
values,
|
||||
["a", "A", "b", "b"].map(|s| PropertyValue::String(s.into()))
|
||||
);
|
||||
assert_eq!(
|
||||
m.get_at(id, index, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(3)
|
||||
);
|
||||
assert_eq!(
|
||||
m.get_at(id, index, prop("TEXT")).unwrap(),
|
||||
PropertyValue::String("b".into())
|
||||
);
|
||||
}
|
||||
m.object_method_at(
|
||||
id,
|
||||
index,
|
||||
"ADDITEM",
|
||||
vec![PropertyValue::String("B".into())],
|
||||
)
|
||||
.unwrap();
|
||||
m.object_method_at(
|
||||
id,
|
||||
index,
|
||||
"ADDITEM",
|
||||
vec![PropertyValue::String("0".into())],
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m.get_at(id, index, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(4)
|
||||
);
|
||||
m.object_method_at(id, index, "REMOVEITEM", vec![PropertyValue::Integer(0)])
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m.get_at(id, index, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(3)
|
||||
);
|
||||
m.object_method_at(id, index, "REMOVEITEM", vec![PropertyValue::Integer(3)])
|
||||
.unwrap();
|
||||
m.set_at(id, index, prop("SORTED"), PropertyValue::Boolean(true))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
m.get_at(id, index, prop("LISTINDEX")).unwrap(),
|
||||
PropertyValue::Integer(-1)
|
||||
);
|
||||
let expected = if class == ObjectClass::ComboBox && style != 2 {
|
||||
"frei"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
assert_eq!(
|
||||
m.get_at(id, index, prop("TEXT")).unwrap(),
|
||||
PropertyValue::String(expected.into())
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_combo_zeichnet_listeninhalt_auswahl_und_begrenzung() {
|
||||
let mut m = controls_model();
|
||||
let prop = |name| forms::property(ObjectClass::ComboBox, name).unwrap().0;
|
||||
for (name, value) in [
|
||||
("STYLE", PropertyValue::Integer(1)),
|
||||
("WIDTH", PropertyValue::Integer(15)),
|
||||
("HEIGHT", PropertyValue::Integer(5)),
|
||||
("LEFT", PropertyValue::Integer(2)),
|
||||
("TOP", PropertyValue::Integer(2)),
|
||||
("TEXT", PropertyValue::String("frei".into())),
|
||||
] {
|
||||
m.set_initial(8, prop(name), value).unwrap();
|
||||
}
|
||||
// Andere Controls aus dem Zeichenbereich nehmen.
|
||||
for id in 1..m.objects.len() {
|
||||
if id == 8 {
|
||||
continue;
|
||||
}
|
||||
let class = m.objects[id].description.class;
|
||||
if let Some((visible, _)) = forms::property(class, "VISIBLE") {
|
||||
m.set_initial(id as u16, visible, PropertyValue::Boolean(false))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
for text in ["apfel", "birne", "unsichtbar"] {
|
||||
m.add_item((8, None), text.into(), None).unwrap();
|
||||
}
|
||||
m.show(0, false).unwrap();
|
||||
let mut screen = TextScreen::new();
|
||||
m.render(&mut screen);
|
||||
let text = tb_runtime::snapshot::text(&screen);
|
||||
assert!(text.contains("frei"));
|
||||
assert!(text.contains("apfel") && text.contains("birne"));
|
||||
assert!(!text.contains("unsichtbar"));
|
||||
m.set(8, prop("LISTINDEX"), PropertyValue::Integer(1))
|
||||
.unwrap();
|
||||
m.render(&mut screen);
|
||||
assert!(tb_runtime::snapshot::text(&screen).contains("│>birne"));
|
||||
m.remove_item((8, None), 1).unwrap();
|
||||
m.render(&mut screen);
|
||||
let text = tb_runtime::snapshot::text(&screen);
|
||||
assert!(text.contains("frei") && text.contains("unsichtbar"));
|
||||
assert!(!text.contains("birne") && !text.contains("│>"));
|
||||
let lines = m.control_lines((8, None), 6, 4);
|
||||
assert_eq!(lines.len(), 4);
|
||||
assert!(lines.iter().all(|line| line.chars().count() == 6));
|
||||
assert_eq!(lines[2], "│ apf│");
|
||||
for height in 1..=3 {
|
||||
assert_eq!(m.control_lines((8, None), 6, height).len(), height);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn listenrenderer_haelt_zellgrenzen_auch_an_bildschirmraendern_ein() {
|
||||
for (class, style) in [
|
||||
(ObjectClass::ListBox, 0),
|
||||
(ObjectClass::ComboBox, 0),
|
||||
(ObjectClass::ComboBox, 1),
|
||||
(ObjectClass::ComboBox, 2),
|
||||
(ObjectClass::DirListBox, 0),
|
||||
(ObjectClass::FileListBox, 0),
|
||||
(ObjectClass::DriveListBox, 0),
|
||||
] {
|
||||
for index in [None, Some(1)] {
|
||||
for width in [1, 2, 3, 6, 15, 254] {
|
||||
for height in [1, 2, 3, 5, 254] {
|
||||
for (left, top) in [(2, 2), (78, 23), (79, 24), (254, 254)] {
|
||||
let mut catalog = forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("List1", class, Some("Form1"), index.is_some());
|
||||
let mut m = FormsModel::new(catalog.objects, 80, 25);
|
||||
for (name, value) in [("WIDTH", 40), ("HEIGHT", 12)] {
|
||||
let prop = forms::property(ObjectClass::Form, name).unwrap().0;
|
||||
m.set_initial(0, prop, PropertyValue::Integer(value))
|
||||
.unwrap();
|
||||
}
|
||||
let prop = |name| forms::property(class, name).unwrap().0;
|
||||
m.set_initial(1, prop("VISIBLE"), PropertyValue::Boolean(false))
|
||||
.unwrap();
|
||||
if let Some(index) = index {
|
||||
m.load_array(1, index).unwrap();
|
||||
}
|
||||
for (name, value) in [
|
||||
("WIDTH", width),
|
||||
("HEIGHT", height),
|
||||
("LEFT", left),
|
||||
("TOP", top),
|
||||
] {
|
||||
m.set_initial_at(
|
||||
1,
|
||||
index,
|
||||
prop(name),
|
||||
PropertyValue::Integer(value),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
if class == ObjectClass::ComboBox {
|
||||
m.set_initial_at(
|
||||
1,
|
||||
index,
|
||||
prop("STYLE"),
|
||||
PropertyValue::Integer(style),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let key = (1, index);
|
||||
m.show(0, false).unwrap();
|
||||
let mut screen = TextScreen::new();
|
||||
m.render(&mut screen);
|
||||
let before = screen.clone();
|
||||
for items in [
|
||||
vec![],
|
||||
vec!["中🙂ab\ncd\ref".into(), "ein sehr langer Eintrag".into()],
|
||||
] {
|
||||
// Auch die Dateilisten erhalten kontrollierte Inhalte ohne Dateisystemzugriff.
|
||||
m.lists.insert(key, items);
|
||||
m.set_at(1, index, prop("VISIBLE"), PropertyValue::Boolean(true))
|
||||
.unwrap();
|
||||
if class == ObjectClass::ComboBox {
|
||||
m.set_at(
|
||||
1,
|
||||
index,
|
||||
prop("TEXT"),
|
||||
PropertyValue::String("中🙂ab\ncd\ref".into()),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
m.render(&mut screen);
|
||||
let mut changed = false;
|
||||
for row in 1..=screen.rows() {
|
||||
for col in 1..=screen.cols() {
|
||||
if row > top as usize
|
||||
&& row <= (top + height) as usize
|
||||
&& col > left as usize
|
||||
&& col <= (left + width) as usize
|
||||
{
|
||||
changed |=
|
||||
screen.cell(row, col) != before.cell(row, col);
|
||||
} else {
|
||||
assert_eq!(screen.cell(row, col), before.cell(row, col),
|
||||
"{class:?} style={style} index={index:?} {width}x{height} at {left},{top}: outside cell {col},{row}");
|
||||
}
|
||||
}
|
||||
}
|
||||
// Am rechten Rand kann ein breites erstes Zeichen ganz entfallen.
|
||||
if left < 78 && top < 25 {
|
||||
assert!(
|
||||
changed,
|
||||
"sichtbares Control muss tatsächlich gezeichnet werden"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timer_neustart_verwirft_alte_fristen_und_ereignisse() {
|
||||
let mut m = controls_model();
|
||||
m.show(0, false).unwrap();
|
||||
m.events.clear();
|
||||
let interval = forms::property(ObjectClass::Timer, "INTERVAL").unwrap().0;
|
||||
let enabled = forms::property(ObjectClass::Timer, "ENABLED").unwrap().0;
|
||||
m.set(12, interval, PropertyValue::Integer(100)).unwrap();
|
||||
m.sync_timers(|| 1000);
|
||||
assert_eq!(m.next_deadline(), Some(1100));
|
||||
m.timers(1099);
|
||||
assert!(m.events.is_empty());
|
||||
m.timers(1100);
|
||||
assert_eq!(m.events.drain(..).count(), 1);
|
||||
m.timers(1350);
|
||||
assert_eq!(m.events.len(), 2);
|
||||
m.set(12, interval, PropertyValue::Integer(200)).unwrap();
|
||||
m.sync_timers(|| 1350);
|
||||
assert!(m.events.is_empty());
|
||||
m.timers(1549);
|
||||
assert!(m.events.is_empty());
|
||||
m.timers(1550);
|
||||
assert_eq!(m.events.len(), 1);
|
||||
m.set(12, enabled, PropertyValue::Boolean(false)).unwrap();
|
||||
assert!(m.events.is_empty());
|
||||
assert_eq!(m.next_deadline(), None);
|
||||
m.set(12, enabled, PropertyValue::Boolean(true)).unwrap();
|
||||
m.sync_timers(|| 2000);
|
||||
m.set(12, enabled, PropertyValue::Boolean(true)).unwrap();
|
||||
m.sync_timers(|| panic!("Unverändertes Enabled startet keine neue Phase"));
|
||||
assert_eq!(m.next_deadline(), Some(2200));
|
||||
m.timers(2200);
|
||||
assert_eq!(m.events.len(), 1);
|
||||
m.set(12, interval, PropertyValue::Integer(0)).unwrap();
|
||||
assert!(m.events.is_empty());
|
||||
assert_eq!(m.next_deadline(), None);
|
||||
m.sync_timers(|| panic!("Inaktiver Timer braucht keine Uhr"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timerarrays_holen_nur_aktive_zeit_nach_und_sortieren_nach_name_und_index() {
|
||||
for _ in 0..2 {
|
||||
let mut catalog = forms::FormCatalog::default();
|
||||
catalog.add("Form1", ObjectClass::Form, None, false);
|
||||
catalog.add("TimerZ", ObjectClass::Timer, Some("Form1"), false);
|
||||
catalog.add("TimerA", ObjectClass::Timer, Some("Form1"), true);
|
||||
let mut m = FormsModel::new(catalog.objects, 80, 25);
|
||||
let interval = forms::property(ObjectClass::Timer, "INTERVAL").unwrap().0;
|
||||
m.set(1, interval, PropertyValue::Integer(100)).unwrap();
|
||||
m.set(2, interval, PropertyValue::Integer(100)).unwrap();
|
||||
m.load_array(2, 2).unwrap();
|
||||
m.load_array(2, 1).unwrap();
|
||||
m.show(0, false).unwrap();
|
||||
m.sync_timers(|| 1000);
|
||||
m.events.clear();
|
||||
m.timers(1250);
|
||||
let order: Vec<_> = m.events.iter().map(|e| (e.object, e.array_index)).collect();
|
||||
assert_eq!(
|
||||
order,
|
||||
[
|
||||
(2, Some(0)),
|
||||
(2, Some(0)),
|
||||
(2, Some(1)),
|
||||
(2, Some(1)),
|
||||
(2, Some(2)),
|
||||
(2, Some(2)),
|
||||
(1, None),
|
||||
(1, None)
|
||||
]
|
||||
);
|
||||
m.set_at(2, Some(0), interval, PropertyValue::Integer(0))
|
||||
.unwrap();
|
||||
assert!(!m.events.iter().any(|e| e.array_index == Some(0)));
|
||||
assert_eq!(m.events.len(), 6);
|
||||
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());
|
||||
m.show(0, false).unwrap();
|
||||
m.sync_timers(|| 2000);
|
||||
assert_eq!(m.next_deadline(), Some(2100));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_liste_option_scroll_picture_und_dateisystem() {
|
||||
let mut m = controls_model();
|
||||
@@ -3470,6 +4030,7 @@ mod tests {
|
||||
|
||||
let interval = forms::property(ObjectClass::Timer, "INTERVAL").unwrap().0;
|
||||
m.set(12, interval, PropertyValue::Integer(100)).unwrap();
|
||||
m.sync_timers(|| 0);
|
||||
m.timers(250);
|
||||
assert_eq!(
|
||||
m.events
|
||||
@@ -3536,6 +4097,7 @@ mod tests {
|
||||
m.set(1, interval, PropertyValue::Integer(100)).unwrap();
|
||||
m.set(2, interval, PropertyValue::Integer(100)).unwrap();
|
||||
m.show(0, false).unwrap();
|
||||
m.sync_timers(|| 0);
|
||||
m.events.clear();
|
||||
m.timers(250);
|
||||
let names: Vec<_> = m
|
||||
|
||||
@@ -903,6 +903,7 @@ impl Vm {
|
||||
/// Formular schließen, ein anderes Programm starten oder einen Fehler
|
||||
/// auslösen.
|
||||
pub fn run_visible_forms(&mut self, host: &mut dyn Host) -> RunEvent {
|
||||
self.forms.sync_timers(|| host.jetzt_ms());
|
||||
while self.forms.has_visible_forms() {
|
||||
self.tick(host);
|
||||
let dispatched = self.zustellen(host, Zustellpunkt::Kooperativ);
|
||||
@@ -928,6 +929,7 @@ impl Vm {
|
||||
}
|
||||
|
||||
pub fn run(&mut self, host: &mut dyn Host) -> RunEvent {
|
||||
self.forms.sync_timers(|| host.jetzt_ms());
|
||||
loop {
|
||||
if let Some(form) = self.frames.last().and_then(|f| f.waiting_form) {
|
||||
if !self.forms.is_visible(form) {
|
||||
@@ -1381,11 +1383,13 @@ impl Vm {
|
||||
let value = self.pop()?;
|
||||
let value = self.property_value(object, property, value)?;
|
||||
self.forms.set_at(object, index, property, value)?;
|
||||
self.forms.sync_timers(|| host.jetzt_ms());
|
||||
return Ok(Flow::Normal);
|
||||
}
|
||||
}
|
||||
let value = self.property_value(object, property, value)?;
|
||||
self.forms.set_at(object, index, property, value)?;
|
||||
self.forms.sync_timers(|| host.jetzt_ms());
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::PushObject(object, has_index) => {
|
||||
@@ -1442,6 +1446,7 @@ impl Vm {
|
||||
.ok_or(RuntimeError(422))?;
|
||||
let value = self.property_value(object, property, value)?;
|
||||
self.forms.set_at(object, index, property, value)?;
|
||||
self.forms.sync_timers(|| host.jetzt_ms());
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::LoadObjectIndexedProperty(object, property) => {
|
||||
@@ -1585,6 +1590,7 @@ impl Vm {
|
||||
self.forms.object_method_at(object, index, name, args)?;
|
||||
}
|
||||
}
|
||||
self.forms.sync_timers(|| host.jetzt_ms());
|
||||
self.dispatch_next_form_event(true);
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
@@ -1602,6 +1608,7 @@ impl Vm {
|
||||
self.forms.ensure_loaded(object)?;
|
||||
self.dispatch_next_form_event(true);
|
||||
}
|
||||
self.forms.sync_timers(|| host.jetzt_ms());
|
||||
Ok(Flow::Normal)
|
||||
}
|
||||
I::Dup => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! Regressionen zum Change ereigniszustellung-und-hostgrenzen.
|
||||
//! Regressionen zu Ereigniszustellung, Hostgrenzen und Forms-Zustand.
|
||||
use tb_frontend::forms::{FormCatalog, ObjectClass};
|
||||
use tb_runtime::host::{CaptureHost, Ereignis, Host, MausArt, MausEreignis};
|
||||
use tb_runtime::screen::TextScreen;
|
||||
@@ -582,3 +582,47 @@ fn return_label_verwirft_abgebrochene_eingabe_und_ausdruck() {
|
||||
assert_eq!(string(&vm, "t"), "b");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spaete_timeraktivierung_nutzt_hostzeit_auch_ueber_control_parameter() {
|
||||
for (setup, start) in [
|
||||
("", "Timer1.Interval = 100"),
|
||||
("", "CALL Starten(Timer1)"),
|
||||
(
|
||||
"Timer1.Enabled = 0\nTimer1.Interval = 100",
|
||||
"Timer1.Enabled = -1",
|
||||
),
|
||||
] {
|
||||
let mut vm = vm(&format!("DIM SHARED n%\nForm1.Show\n{setup}\nSTOP\n{start}\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nEND\nSUB Starten(t AS CONTROL)\nt.Interval = 100\nEND SUB\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nEND SUB"));
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host);
|
||||
host.uhr_vorruecken(1000);
|
||||
stop(&mut vm, &mut host);
|
||||
assert_eq!(vm.forms.next_deadline(), Some(1100));
|
||||
host.uhr_vorruecken(99);
|
||||
stop(&mut vm, &mut host);
|
||||
assert_eq!(integer(&vm, "n"), 0);
|
||||
host.uhr_vorruecken(1);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intervallaenderung_im_handler_verwirft_bereits_faellige_altintervalle() {
|
||||
let mut vm = vm("DIM SHARED n%\nForm1.Show\nTimer1.Interval = 100\nSLEEP 60\nSTOP\nDOEVENTS\nSTOP\nDOEVENTS\nEND\nSUB Timer1_Timer()\nSHARED n%\nn% = n% + 1\nIF n% = 1 THEN\nSTOP\nTimer1.Interval = 200\nELSE\nTimer1.Interval = 0\nEND IF\nEND SUB");
|
||||
let mut host = CaptureHost::default();
|
||||
stop(&mut vm, &mut host); // Im ersten Handler bei 100 ms.
|
||||
assert_eq!(host.jetzt_ms(), 100);
|
||||
host.uhr_vorruecken(350);
|
||||
stop(&mut vm, &mut host); // Alter Rückstau darf nach dem Handler nicht laufen.
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
assert_eq!(vm.forms.next_deadline(), Some(650));
|
||||
host.uhr_vorruecken(199);
|
||||
stop(&mut vm, &mut host);
|
||||
assert_eq!(integer(&vm, "n"), 1);
|
||||
host.uhr_vorruecken(1);
|
||||
assert_eq!(vm.run(&mut host), RunEvent::Ended);
|
||||
assert_eq!(integer(&vm, "n"), 2);
|
||||
assert_eq!(vm.forms.next_deadline(), None);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user