//! Designeraktionen auf dem gemeinsamen FRM-Dokument, ohne VM oder Ereignisschleife. use crate::{ app::{App, Dialog, DialogKind, Field, Hit, Mode, WindowKind}, commands::Command, documents::DocumentId, }; use anyhow::{anyhow, bail, ensure, Context, Result}; use crossterm::event::{ KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton, MouseEvent, MouseEventKind, }; use ratatui::{ layout::Rect, style::{Modifier, Style}, widgets::Paragraph, Frame, }; use std::collections::{BTreeMap, BTreeSet}; use tb_frontend::{ forms::{self, ObjectClass as Class, PropertyType}, lexer::{self, TokenKind}, }; use tb_runtime::screen::TextScreen; use tb_ui::{ forms::{FormsModel, PropertyValue as Value}, frm::{self, FormFile, FormNode}, }; use tb_vm::project_io::Content; pub type DesignId = u64; type Key = (String, Option); type Drag = (u16, u16, Option<(i8, i8)>); #[derive(Default)] pub struct Designer { pub document: Option, pub selections: BTreeMap>, pub grid: bool, pub tool: Option<(Class, i32)>, pub property: usize, pub value: Option, clipboard: Vec, pub viewport: Rect, pub objects: Vec<(DesignId, Rect)>, drag: Option, paint: Option<(&'static str, u8)>, last_tool_click: Option<(usize, std::time::Instant)>, } pub fn node_key(n: &FormNode) -> Key { ( n.name.to_uppercase(), forms::property(n.class, "INDEX") .and_then(|(p, _)| n.properties.get(&p)) .and_then(|v| { if let Value::Integer(n) = v { Some(*n) } else { None } }), ) } pub fn nodes(root: &FormNode) -> Vec<&FormNode> { let mut out = vec![root]; for n in &root.children { out.extend(nodes(n)); } out } fn find_mut<'a>(n: &'a mut FormNode, key: &Key) -> Option<&'a mut FormNode> { if node_key(n) == *key { return Some(n); } n.children.iter_mut().find_map(|n| find_mut(n, key)) } pub fn value(n: &FormNode, name: &str) -> Option { let (p, s) = forms::property(n.class, name)?; Some( n.properties .get(&p) .cloned() .unwrap_or_else(|| Value::from_default(s.default, s.ty)), ) } fn integer(n: &FormNode, name: &str) -> i32 { match value(n, name) { Some(Value::Integer(n)) => n, _ => 0, } } fn set(n: &mut FormNode, name: &str, v: Value) -> Result<()> { let (p, _) = forms::property(n.class, name).ok_or_else(|| anyhow!("{}.{} unbekannt", n.name, name))?; n.properties.insert(p, v); Ok(()) } fn text(v: &Value) -> String { match v { Value::Integer(v) => v.to_string(), Value::Single(v) => v.to_string(), Value::String(v) => v.clone(), Value::Boolean(v) => v.to_string(), _ => format!("{v:?}"), } } fn container(c: Class) -> bool { matches!(c, Class::Form | Class::Frame | Class::PictureBox) } /// Beide Spin-Stile sind Varianten derselben Metadatenklasse. pub fn tools() -> Vec<(&'static str, Class, i32)> { let mut out: Vec<_> = Class::ALL .into_iter() .filter(|c| !matches!(c, Class::Form | Class::Screen | Class::Menu | Class::Spin)) .map(|c| (c.display_name(), c, 0)) .collect(); out.extend([("VSpin", Class::Spin, 0), ("HSpin", Class::Spin, 1)]); out } pub fn preview(form: &FormFile, size: (u16, u16)) -> Result<(FormsModel, TextScreen)> { let mut model = FormsModel::new(form.catalog().objects, size.0 as usize, size.1 as usize); form.apply(&mut model)?; // Timer bleiben ausschließlich im Dokument konfiguriert, nie in der Vorschau aktiv. let enabled = forms::property(Class::Timer, "ENABLED").unwrap().0; let catalog = form.catalog(); for node in nodes(&form.root) .into_iter() .filter(|n| n.class == Class::Timer) { let object = catalog.find(&node.name).unwrap().0; model.set_initial_at(object, node_key(node).1, enabled, Value::Boolean(false))?; } model.show(0, false)?; // Es gibt weder eine VM noch einen Timer-/Event-Pump im Designer. model.events.clear(); let mut screen = TextScreen::with_size(size.0 as usize, size.1 as usize); model.render(&mut screen); Ok((model, screen)) } fn validate(form: &FormFile) -> Result<()> { let parsed = frm::read_text("Designer", &frm::write_text(form))?; let catalog = parsed.catalog(); parsed.initial_values(&catalog)?; fn visit(n: &FormNode, parent: Option<&FormNode>, depth: usize) -> Result<()> { if let Some(p) = parent { ensure!( if n.class == Class::Menu { matches!(p.class, Class::Menu | Class::Form) } else { container(p.class) }, "{}: ungültiger Container {}", n.name, p.name ); if !matches!(n.class, Class::Timer | Class::Menu) { ensure!( integer(n, "LEFT") + integer(n, "WIDTH") <= integer(p, "WIDTH") && integer(n, "TOP") + integer(n, "HEIGHT") <= integer(p, "HEIGHT"), "{}: Geometrie außerhalb von {}", n.name, p.name ); } } if n.class == Class::Menu { ensure!(depth <= 6, "{}: höchstens sechs Menüebenen", n.name); let shortcut = text(&value(n, "SHORTCUT").unwrap()); ensure!( shortcut.is_empty() || forms::menu_shortcuts() .iter() .any(|s| s.eq_ignore_ascii_case(&shortcut.replace(' ', ""))), "{}.Shortcut: nicht unterstützt", n.name ); ensure!( shortcut.is_empty() || parent.is_some_and(|p| p.class == Class::Menu), "{}.Shortcut: kein Shortcut auf Menütitel", n.name ); if value(n, "SEPARATOR") == Some(Value::Boolean(true)) || value(n, "CAPTION") == Some(Value::String("-".into())) { ensure!( n.children.is_empty() && shortcut.is_empty() && value(n, "CHECKED") == Some(Value::Boolean(false)) && value(n, "ENABLED") == Some(Value::Boolean(true)), "{}: ungültiger Separator", n.name ); } } for child in &n.children { visit( child, Some(n), depth + usize::from(child.class == Class::Menu), )?; } Ok(()) } visit(&parsed.root, None, 0) } impl App { pub fn design_document(&self) -> Result { self.active_window() .and_then(|w| { if let WindowKind::Code(v) = w.kind { self.project.view(v).ok().map(|v| v.document()) } else { None } }) .filter(|id| { self.project .document(*id) .is_ok_and(|d| matches!(d.content(), Content::Form(_))) }) .or(self.designer.document) .ok_or_else(|| anyhow!("Kein Formular ausgewählt")) } pub fn design_form(&self) -> Result<&FormFile> { let Content::Form(f) = self.project.document(self.design_document()?)?.content() else { bail!("Kein Formular") }; Ok(f) } pub fn design_enter(&mut self, id: DocumentId) -> Result<()> { self.designer.document = Some(id); let ids = self.project.design_ids(id)?.clone(); let root = ids[&node_key(&self.design_form()?.root)]; let selection = self.designer.selections.entry(id).or_default(); selection.retain(|i| ids.values().any(|v| i == v)); if selection.is_empty() { selection.push(root); } self.designer.value = None; Ok(()) } pub fn design_objects(&mut self) -> Result> { let id = self.design_document()?; let ids = self.project.design_ids(id)?.clone(); Ok(nodes(&self.design_form()?.root) .into_iter() .map(|n| (ids[&node_key(n)], n.clone())) .collect()) } pub fn design_select(&mut self, id: DesignId, multiple: bool) -> Result<()> { ensure!( self.design_objects()?.iter().any(|(i, _)| *i == id), "Unbekanntes Designobjekt" ); let doc = self.design_document()?; let selection = self.designer.selections.entry(doc).or_default(); if !multiple { selection.clear(); } if multiple && selection.contains(&id) { selection.retain(|i| *i != id); } else { selection.push(id); } self.designer.property = 0; self.designer.value = None; self.value_focus = false; Ok(()) } fn selected_keys(&mut self) -> Result> { let doc = self.design_document()?; let selected = self .designer .selections .get(&doc) .cloned() .unwrap_or_default(); Ok(self .design_objects()? .into_iter() .filter(|(id, _)| selected.contains(id)) .map(|(_, n)| node_key(&n)) .collect()) } fn selected_node(&mut self) -> Result { let key = self .selected_keys()? .last() .cloned() .ok_or_else(|| anyhow!("Kein Control ausgewählt"))?; Ok(nodes(&self.design_form()?.root) .into_iter() .find(|n| node_key(n) == key) .unwrap() .clone()) } fn design_edit(&mut self, edit: impl FnOnce(&mut FormFile) -> Result<()>) -> Result<()> { let id = self.design_document()?; self.project.edit_form(id, |f| { edit(f)?; validate(f) })?; self.designer.value = None; Ok(()) } pub fn design_place( &mut self, class: Class, style: i32, parent: DesignId, rect: Rect, ) -> Result { ensure!( !matches!(class, Class::Form | Class::Screen), "Keine platzierbare Klasse" ); let objects = self.design_objects()?; let p = objects .iter() .find(|(id, _)| *id == parent) .ok_or_else(|| anyhow!("Container fehlt"))? .1 .clone(); ensure!( if class == Class::Menu { matches!(p.class, Class::Menu | Class::Form) } else { container(p.class) }, "{} ist kein passender Container", p.name ); let names: BTreeSet<_> = objects.iter().map(|(_, n)| n.name.to_uppercase()).collect(); let name = (1..) .map(|n| format!("{}{n}", class.display_name())) .find(|n| !names.contains(&n.to_uppercase())) .unwrap(); let mut node = FormNode { class, name: name.clone(), properties: BTreeMap::new(), children: vec![], }; if !matches!(class, Class::Timer | Class::Menu) { for (name, n) in [ ("LEFT", rect.x), ("TOP", rect.y), ("WIDTH", rect.width), ("HEIGHT", rect.height), ] { set(&mut node, name, Value::Integer(n as i32))?; } } if class == Class::Spin { ensure!((0..=1).contains(&style), "Spin.Style ungültig"); set(&mut node, "STYLE", Value::Integer(style))?; set( &mut node, "WIDTH", Value::Integer(if style == 1 { 2 } else { 1 }), )?; set( &mut node, "HEIGHT", Value::Integer(if style == 1 { 1 } else { 2 }), )?; } if forms::property(class, "CAPTION").is_some() { set(&mut node, "CAPTION", Value::String(name.clone()))?; } if forms::property(class, "TABINDEX").is_some() { set( &mut node, "TABINDEX", Value::Integer( objects .iter() .map(|(_, n)| integer(n, "TABINDEX")) .max() .unwrap_or(0) + 1, ), )?; } let key = node_key(&node); self.design_edit(|f| { find_mut(&mut f.root, &node_key(&p)) .unwrap() .children .push(node); Ok(()) })?; let doc = self.design_document()?; let id = self.project.design_ids(doc)?[&key]; self.design_select(id, false)?; Ok(id) } pub fn design_geometry(&mut self, dx: i32, dy: i32, sizing: bool) -> Result<()> { let keys = self.selected_keys()?; self.design_edit(|f| { // Ausgewählte Kinder eines ausgewählten Containers nicht doppelt verschieben. fn edit(n: &mut FormNode, keys: &[Key], dx: i32, dy: i32, sizing: bool) -> Result<()> { if keys.contains(&node_key(n)) && !matches!(n.class, Class::Timer | Class::Menu) { for (name, delta) in [ (if sizing { "WIDTH" } else { "LEFT" }, dx), (if sizing { "HEIGHT" } else { "TOP" }, dy), ] { if delta == 0 { continue; } let (_, spec) = forms::property(n.class, name).unwrap(); ensure!(spec.writable, "{}.{} ist schreibgeschützt", n.name, name); set(n, name, Value::Integer(integer(n, name) + delta))?; } if !sizing { return Ok(()); } } for c in &mut n.children { edit(c, keys, dx, dy, sizing)?; } Ok(()) } edit(&mut f.root, &keys, dx, dy, sizing) }) } fn design_resize(&mut self, dx: i32, dy: i32, edges: (i8, i8)) -> Result<()> { let keys = self.selected_keys()?; self.design_edit(|f| { for key in keys { let n = find_mut(&mut f.root, &key).ok_or_else(|| anyhow!("Control fehlt"))?; if matches!(n.class, Class::Timer | Class::Menu) { continue; } for (position, size, edge, delta) in [ ("LEFT", "WIDTH", edges.0, dx), ("TOP", "HEIGHT", edges.1, dy), ] { if edge == 0 || delta == 0 { continue; } ensure!( forms::property(n.class, size).unwrap().1.writable, "{}.{} ist schreibgeschützt", n.name, size ); if edge < 0 { set(n, position, Value::Integer(integer(n, position) + delta))?; } set( n, size, Value::Integer(integer(n, size) + delta * i32::from(edge)), )?; } } Ok(()) }) } pub fn design_property(&mut self, name: &str, input: &str) -> Result<()> { let n = self.selected_node()?; if name.eq_ignore_ascii_case("CTLNAME") || name.eq_ignore_ascii_case("FORMNAME") { return self.design_rename(input); } let (p, s) = forms::property(n.class, name) .ok_or_else(|| anyhow!("{}.{} unbekannt", n.name, name))?; let edit = || -> Result { ensure!(s.writable, "schreibgeschützt"); Ok(match s.ty { PropertyType::Integer => { let v = input.parse::()?; ensure!( s.min.is_none_or(|min| v >= min) && s.max.is_none_or(|max| v <= max), "außerhalb des Wertebereichs" ); Value::Integer(v) } PropertyType::Single => { let v = input.parse::()?; ensure!(v.is_finite(), "nicht endlich"); Value::Single(v) } PropertyType::Boolean => { Value::Boolean(match input.to_ascii_lowercase().as_str() { "true" | "-1" | "1" => true, "false" | "0" => false, _ => bail!("Boolean erwartet: true/false"), }) } PropertyType::String => Value::String(input.into()), _ => bail!("nicht editierbarer Propertytyp"), }) }; let v = edit().with_context(|| format!("{}.{}", n.name, s.name))?; let (mut preview_model, _) = preview(self.design_form()?, self.size)?; let object = self.design_form()?.catalog().find(&n.name).unwrap().0; preview_model .set_at(object, node_key(&n).1, p, v.clone()) .with_context(|| format!("{}.{}", n.name, s.name))?; let old = node_key(&n); let doc = self.design_document()?; let stable = self.project.design_ids(doc)?[&old]; let mut changed = n.clone(); changed.properties.insert(p, v.clone()); if n.class == Class::Spin && s.name == "STYLE" { set( &mut changed, "WIDTH", Value::Integer(if input == "1" { 2 } else { 1 }), )?; set( &mut changed, "HEIGHT", Value::Integer(if input == "1" { 1 } else { 2 }), )?; } let new = node_key(&changed); self.design_edit(|f| { *find_mut(&mut f.root, &old).unwrap() = changed; Ok(()) }) .with_context(|| format!("{}.{}", n.name, s.name))?; if old != new { self.project.rekey_design_id(doc, &old, new, stable); } Ok(()) } pub fn design_copy(&mut self, cut: bool) -> Result<()> { let keys = self.selected_keys()?; let mut copied = Vec::new(); fn collect(n: &FormNode, keys: &[Key], out: &mut Vec) { if n.class != Class::Form && keys.contains(&node_key(n)) { out.push(n.clone()); } else { for c in &n.children { collect(c, keys, out); } } } collect(&self.design_form()?.root, &keys, &mut copied); ensure!(!copied.is_empty(), "Keine Controls ausgewählt"); if cut { self.design_delete()?; } self.designer.clipboard = copied; Ok(()) } pub fn design_delete(&mut self) -> Result<()> { let keys = self.selected_keys()?; ensure!( !keys.contains(&node_key(&self.design_form()?.root)), "Formular über Remove File entfernen" ); let names: Vec<_> = nodes(&self.design_form()?.root) .into_iter() .filter(|n| keys.contains(&node_key(n))) .flat_map(nodes) .map(|n| n.name.clone()) .collect(); self.design_edit(|f| { fn remove(n: &mut FormNode, keys: &[Key]) { n.children.retain(|c| !keys.contains(&node_key(c))); for c in &mut n.children { remove(c, keys); } } remove(&mut f.root, &keys); Ok(()) })?; let id = self.design_document()?; self.design_enter(id)?; self.message=format!("Entfernt: {}. Vorhandener Code bleibt erhalten; zugehörige Handler sind jetzt ungebunden.",names.join(", ")); self.open_dialog( "Löschfolgen", DialogKind::Message, vec![Field::text("Hinweis", self.message.clone())], ); Ok(()) } pub fn design_paste(&mut self) -> Result<()> { ensure!( !self.designer.clipboard.is_empty(), "Control-Zwischenablage leer" ); let parent = self.selected_node()?; ensure!( container(parent.class) || parent.class == Class::Menu, "Zum Einfügen einen Container auswählen" ); let mut copies = self.designer.clipboard.clone(); let mut names: BTreeSet<_> = nodes(&self.design_form()?.root) .iter() .map(|n| n.name.to_uppercase()) .collect(); let mut renamed = BTreeMap::new(); fn rename( n: &mut FormNode, names: &mut BTreeSet, renamed: &mut BTreeMap, ) -> Result<()> { let old = n.name.to_uppercase(); let name = renamed .entry(old.clone()) .or_insert_with(|| { let chosen = (1..) .map(|i| format!("{old}Copy{i}")) .find(|n| !names.contains(n)) .unwrap(); names.insert(chosen.clone()); chosen }) .clone(); n.name = name.clone(); if let Some((p, _)) = forms::property(n.class, "CTLNAME") { if n.properties.contains_key(&p) { n.properties.insert(p, Value::String(name)); } } for c in &mut n.children { rename(c, names, renamed)?; } Ok(()) } for n in &mut copies { rename(n, &mut names, &mut renamed)?; } let pasted: Vec<_> = copies.iter().map(node_key).collect(); self.design_edit(|f| { find_mut(&mut f.root, &node_key(&parent)) .unwrap() .children .extend(copies); Ok(()) })?; let doc = self.design_document()?; let ids = self.project.design_ids(doc)?; self.designer .selections .insert(doc, pasted.iter().map(|k| ids[k]).collect()); Ok(()) } pub fn design_menu_move(&mut self, direction: &str) -> Result<()> { let key = node_key(&self.selected_node()?); self.design_edit(|f| { fn change(n: &mut FormNode, key: &Key, d: &str) -> Result { if let Some(i) = n.children.iter().position(|c| node_key(c) == *key) { ensure!(n.children[i].class == Class::Menu, "Kein Menüelement"); match d { "up" => { ensure!( i > 0 && n.children[i - 1].class == Class::Menu, "Kein Vorgänger" ); n.children.swap(i, i - 1); } "down" => { ensure!( i + 1 < n.children.len() && n.children[i + 1].class == Class::Menu, "Kein Nachfolger" ); n.children.swap(i, i + 1); } "in" => { ensure!( i > 0 && n.children[i - 1].class == Class::Menu, "Kein Menüvorgänger" ); let c = n.children.remove(i); n.children[i - 1].children.push(c); } _ => bail!("Keine äußere Menüebene"), } return Ok(true); } for i in 0..n.children.len() { if d == "out" { if let Some(j) = n.children[i] .children .iter() .position(|c| node_key(c) == *key) { let c = n.children[i].children.remove(j); n.children.insert(i + 1, c); return Ok(true); } } if change(&mut n.children[i], key, d)? { return Ok(true); } } Ok(false) } ensure!(change(&mut f.root, &key, direction)?, "Menüelement fehlt"); Ok(()) }) } pub fn design_rename(&mut self, new: &str) -> Result<()> { self.design_rename_with(new, None) } fn design_rename_with(&mut self, new: &str, extra: Option) -> Result<()> { let selected = self.selected_node()?; let old = selected.name.to_uppercase(); if old == new.to_uppercase() { return Ok(()); } // Benutzt dieselbe Namensprüfung wie die Prozedurerzeugung. tb_frontend::editing::new_procedure("", 0, new, false).map_err(|e| anyhow!(e))?; let doc = self.design_document()?; let ids = self.project.design_ids(doc)?.clone(); let input = self.project.sources()?; let mut compiler = tb_vm::project::ProjectCompiler::default(); let compiled = input.compile(&mut compiler, "RENAME").map_err(|d| { anyhow!( "Umbenennen erfordert eindeutig auflösbare Quellen: {}", d.iter() .map(ToString::to_string) .collect::>() .join("; ") ) })?; let mut catalog = forms::FormCatalog::default(); for f in &input.forms { catalog.append(&f.catalog()); } let form_name = self.design_form()?.root.name.clone(); ensure!( catalog.objects.iter().filter(|o| o.name.eq_ignore_ascii_case(new)).all(|o| { node_key(&selected).1.is_some() && o.array && o.class == selected.class && o.parent_form.as_deref().is_some_and(|name| name.eq_ignore_ascii_case(&form_name)) }), "Name {new} bereits vorhanden; nur indizierte Controls derselben Klasse und Form können ein Array bilden" ); let target = if selected.class == Class::Form { catalog.find(&old) } else { catalog.find_in(&old, &form_name) } .ok_or_else(|| anyhow!("Objektbindung fehlt"))? .0; let mut changes: BTreeMap = BTreeMap::new(); let mut edits: BTreeMap> = BTreeMap::new(); let mut handlers = BTreeMap::new(); let mut references = Vec::new(); let mut procedures = Vec::new(); let source_files = compiled.sources; let bound = compiler.bound_form_references().map_err(|d| { anyhow!( "Unklare Bindungen: {}", d.iter() .map(ToString::to_string) .collect::>() .join("; ") ) })?; procedures.extend(bound.procedures); for (pos, name, object) in &bound.objects { if *object != target { continue; } if *name == old { references.push((*pos, name.clone(), new.to_owned())); } else if let Some(event) = name.strip_prefix(&format!("{old}_")) { let replacement = format!("{new}_{event}"); handlers.insert(name.clone(), replacement.clone()); references.push((*pos, name.clone(), replacement)); } } for (pos, name) in procedures { if let Some(replacement) = handlers.get(&name) { references.push((pos, name, replacement.clone())); } } for (pos, name, replacement) in references { let path = &source_files[pos.source as usize].path; let id = self .project .find_document(std::path::Path::new(path)) .ok_or_else(|| anyhow!("Betroffene Include-Datei nicht geöffnet: {path}"))?; let d = self.project.document(id)?; let first = match d.content() { Content::Form(f) => frm::read_text(path, &frm::write_text(f))?.code_line(), _ => 1, }; let line = pos .line .checked_sub(first) .ok_or_else(|| anyhow!("Referenz außerhalb des Codebereichs"))? + 1; let lexed = lexer::lex(d.code()); let t = lexed .tokens .iter() .find(|t| { t.pos.line == line && t.pos.column >= pos.column && matches!(&t.kind,TokenKind::Ident{name:n,..} if n.split(['.','!']).any(|part|part==name)) }) .ok_or_else(|| anyhow!("{path}:{line}: {name} nicht sicher lokalisierbar"))?; let token_start = token_offset(d.code(), t.pos.line, t.pos.column)?; let TokenKind::Ident { name: path_name, .. } = &t.kind else { unreachable!() }; let mut component = 0; for part in path_name.split(['.', '!']) { if part == name { break; } component += part.len() + 1; } let start = token_start + component; ensure!( d.code() .get(start..start + name.len()) .is_some_and(|s| s.eq_ignore_ascii_case(&name)), "Referenz nicht bytegenau auflösbar" ); edits .entry(id) .or_default() .insert(start, (start + name.len(), replacement)); } for (id, replacements) in edits { let mut content = self.project.document(id)?.content().clone(); for (start, (end, replacement)) in replacements.into_iter().rev() { content.code_mut().replace_range(start..end, &replacement); } changes.insert(id, content); } let content = changes .entry(doc) .or_insert_with(|| self.project.document(doc).unwrap().content().clone()); let Content::Form(f) = content else { unreachable!() }; fn rename(n: &mut FormNode, old: &str, new: &str) { if n.name.eq_ignore_ascii_case(old) { n.name = new.into(); let property = if n.class == Class::Form { "FORMNAME" } else { "CTLNAME" }; if let Some((p, _)) = forms::property(n.class, property) { if n.properties.contains_key(&p) { n.properties.insert(p, Value::String(new.into())); } } } for c in &mut n.children { rename(c, old, new); } } if let Some(extra) = extra { f.root = extra.root; } rename(&mut f.root, &old, new); validate(f)?; let mut loader = self.project.loader()?; for (id, c) in &changes { loader .insert(self.project.document(*id)?.source_path(), c.clone()) .map_err(|e| anyhow!(e))?; } let candidate = loader .load_manifest(self.project.manifest().clone()) .map_err(|e| anyhow!(e))?; let mut candidate_compiler = tb_vm::project::ProjectCompiler::default(); let candidate_compiled = candidate .compile(&mut candidate_compiler, "RENAME") .map_err(|d| { anyhow!( "Namenskonflikt; nichts geändert: {}", d.iter() .map(ToString::to_string) .collect::>() .join("; ") ) })?; // Dieselben vollständig aufgelösten Importkontexte vor und nach der Änderung. let candidate_bound = candidate_compiler.bound_form_references().map_err(|d| { anyhow!( "Unklare neue Bindungen: {}", d.iter() .map(ToString::to_string) .collect::>() .join("; ") ) })?; let mut candidate_catalog = forms::FormCatalog::default(); for form in &candidate.forms { candidate_catalog.append(&form.catalog()); } let bindings = |references: &[(tb_frontend::SourcePos, String, u16)], files: &[tb_frontend::source::SourceFile], objects: &forms::FormCatalog, rename_old: bool| { let mut result = Vec::new(); for (p, n, o) in references { let mut n = n.clone(); let object = &objects.objects[*o as usize]; let mut name = object.name.clone(); let mut owner = object.parent_form.clone(); if rename_old && *o == target { name = new.to_uppercase(); if n == old { n = new.to_uppercase(); } else if let Some(r) = handlers.get(&n) { n = r.to_uppercase(); } } if rename_old && selected.class == Class::Form && owner.as_deref() == Some(&old) { owner = Some(new.to_uppercase()); } // Merging arrays changes catalog indices. Compare semantic object identities. result.push(( files[p.source as usize].path.clone(), n, name, owner, object.class.name(), object.array, )); } result.sort(); result }; ensure!( bindings(&bound.objects, &source_files, &catalog, true) == bindings( &candidate_bound.objects, &candidate_compiled.sources, &candidate_catalog, false ), "Unklare oder verdeckte Objektverweise; nichts geändert" ); self.project .commit_transaction(changes.into_iter().collect())?; self.project.rename_design_ids(doc, &old, new, &ids); self.designer.value = None; Ok(()) } pub fn design_event(&mut self, object: DesignId, event: &str) -> Result<()> { let doc = self.design_document()?; let n = self .design_objects()? .into_iter() .find(|(id, _)| *id == object) .ok_or_else(|| anyhow!("Objekt fehlt"))? .1; let event = event.to_uppercase(); ensure!( forms::events(n.class).contains(&event.as_str()), "{}.{} ist kein Ereignis", n.name, event ); let prefix = if n.class == Class::Form { "Form" } else { &n.name }; let name = format!("{prefix}_{event}"); let input = self.project.sources()?; let mut catalog = forms::FormCatalog::default(); for f in &input.forms { catalog.append(&f.catalog()); } let target_object = if n.class == Class::Form { catalog.find(&n.name) } else { catalog.find_in(&n.name, &self.design_form()?.root.name) } .ok_or_else(|| anyhow!("Objektbindung fehlt"))? .0; let mut found = None; for unit in &input.units { let mut files = Vec::new(); let (module, diags) = unit.parse(0, &mut files); ensure!( diags.is_empty(), "Unvollständiger Quelltext vor Ereigniserzeugung" ); let bound = tb_frontend::sema::bound_form_references(&module, &catalog); let exports = tb_frontend::sema::export_declarations(&module, &[]); for p in module.procs.iter().filter(|p| { p.sig.name.rsplit_once('_').is_some_and(|(_, e)| e == event) && bound.objects.iter().any(|(pos, name, object)| { *pos == p.pos && *name == p.sig.name && *object == target_object }) }) { let sig = exports .iter() .find_map(|s| match s { tb_frontend::ast::Stmt::Declare { sig, .. } if sig.name == p.sig.name => { Some(sig) } _ => None, }) .unwrap(); ensure!(found.is_none(), "Mehrdeutige Ereignisprozedur {name}"); let target = self .project .find_document(std::path::Path::new(&files[p.pos.source as usize].path)) .ok_or_else(|| anyhow!("Ereignisdatei fehlt"))?; let params = event_parameters(&n, &event); ensure!( sig.kind == tb_frontend::ast::ProcKind::Sub && sig.params.len() == params.len() && sig .params .iter() .zip(¶ms) .all( |(actual, (name, ty))| actual.name.eq_ignore_ascii_case(name) && actual .as_type .as_ref() .map(tb_frontend::editing::type_text) .or_else(|| actual.suffix.map(|s| match s { lexer::Suffix::Integer => "INTEGER", lexer::Suffix::Single => "SINGLE", _ => "", } .into())) .is_some_and(|t| t == *ty) ), "{name}: vorhandene Ereignissignatur stimmt nicht" ); let first = match self.project.document(target)?.content() { Content::Form(f) => frm::read_text("Event", &frm::write_text(f))?.code_line(), _ => 1, }; found = Some(( target, crate::editor::line_offset( self.project.document(target)?.code(), p.pos.line - first + 1, ), )); } } let (target, at) = if let Some(found) = found { found } else { let code = self.project.document(doc)?.code(); let mut addition = self.new_procedure_text(doc, code.len(), &name, false, true)?; let params = event_parameters(&n, &event) .iter() .map(|(name, ty)| format!("{name} AS {ty}")) .collect::>() .join(", "); // BASIC-Parameter sind BYREF; derselbe Prozedurrumpf wie New Sub. addition = addition.replacen(" ()", &format!(" ({params})"), 1); let at = code.len(); self.project.replace_text(doc, at..at, &addition)?; (doc, at + 1) }; self.designer.document = Some(doc); self.designer.selections.insert(doc, vec![object]); self.show_document(target, false)?; let v = self.editor_view()?; self.project.view_mut(v)?.cursor = at; self.editor_scroll()?; Ok(()) } pub(crate) fn designer_command(&mut self, command: Command) -> Result { use Command::*; match command { Grid => self.designer.grid = !self.designer.grid, Toolbox | Palette | MenuDesign => { self.design_document()?; self.show_tool(match command { Toolbox => WindowKind::Toolbox, Palette => WindowKind::Palette, _ => WindowKind::MenuDesign, }); } Tool(name) => { let (_, class, style) = tools() .into_iter() .find(|(n, _, _)| n.eq_ignore_ascii_case(name)) .ok_or_else(|| anyhow!("Unbekanntes Werkzeug"))?; let doc = self.design_document()?; self.show_document(doc, true)?; self.designer.tool = Some((class, style)); self.value_focus = false; } Events => { let objects = self.design_objects()?; let selected = self.selected_keys()?.last().cloned(); let at = objects .iter() .position(|(_, n)| Some(node_key(n)) == selected) .unwrap_or(0); self.open_dialog( "Event Procedures · Objekt", DialogKind::DesignObject(objects.iter().map(|(id, _)| *id).collect()), vec![Field::choice( "Objekt", objects .iter() .map(|(_, n)| format!("{} {:?}", n.name, node_key(n).1)) .collect(), at, )], ); } Cut | Copy | Paste | Clear if self.mode == Mode::Designer => match command { Cut => self.design_copy(true)?, Copy => self.design_copy(false)?, Paste => self.design_paste()?, _ => self.design_delete()?, }, Undo if self.mode == Mode::Designer => { let doc = self.design_document()?; self.project.undo(doc)?; self.design_enter(doc)?; } Form if self.designer.document.is_some() && self.mode == Mode::Environment => { self.show_document(self.designer.document.unwrap(), true)?; } Code if self.mode == Mode::Designer => { let doc = self.design_document()?; self.show_document(doc, false)?; } _ => return Ok(false), } Ok(true) } pub(crate) fn design_help_context(&mut self) -> Result<(String, String)> { let node = self.selected_node()?; let specs = forms::properties(node.class); let property = if self.properties || self.value_focus { specs.get(self.designer.property).map_or("", |p| p.name) } else { "" }; Ok((node.class.name().to_owned(), property.into())) } pub(crate) fn design_value_focus(&mut self) -> Result<()> { let node = self.selected_node()?; let specs = forms::properties(node.class); self.designer.property = self.designer.property.min(specs.len() - 1); let name = specs[self.designer.property].name; let v = if matches!(name, "CTLNAME" | "FORMNAME") { node.name.clone() } else { text(&value(&node, name).unwrap()) }; self.designer.value = Some(Field::text(name, v)); self.properties = true; self.value_focus = true; Ok(()) } pub(crate) fn design_action(&mut self, action: &str) -> Result<()> { match action { "property" => { let n = self.selected_node()?; let specs = forms::properties(n.class); self.open_dialog( "Properties", DialogKind::DesignProperty, vec![Field::choice( "Property", specs.iter().map(|p| p.name.into()).collect(), self.designer.property.min(specs.len() - 1), )], ); } "value" => self.design_value_focus()?, "palette" => self.open_dialog( "Color Palette", DialogKind::DesignPalette, vec![ Field::choice("ForeColor", (0..16).map(|n| n.to_string()).collect(), 0), Field::choice("BackColor", (0..16).map(|n| n.to_string()).collect(), 7), Field::toggle("Auf Formular auftragen", false), ], ), "insert" => { let objects = self.design_objects()?; let selected = self.selected_node()?; let parent = if selected.class == Class::Menu { objects .iter() .find(|(_, n)| node_key(n) == node_key(&selected)) .unwrap() .0 } else { objects[0].0 }; self.design_place(Class::Menu, 0, parent, Rect::default())?; } "delete" => self.design_delete()?, "up" | "down" | "in" | "out" => self.design_menu_move(action)?, "menu" => { let n = self.selected_node()?; ensure!(n.class == Class::Menu, "Menüelement auswählen"); let shortcuts = forms::menu_shortcuts(); let current = text(&value(&n, "SHORTCUT").unwrap()); let at = shortcuts .iter() .position(|s| s.eq_ignore_ascii_case(¤t.replace(' ', ""))) .unwrap_or(0); self.open_dialog( "Menu Design", DialogKind::DesignMenu(node_key(&n)), vec![ Field::text("CtlName", n.name.clone()), Field::text("Caption", text(&value(&n, "CAPTION").unwrap())), Field::text("Tag", text(&value(&n, "TAG").unwrap())), Field::text( "Index (leer: skalar)", node_key(&n).1.map(|i| i.to_string()).unwrap_or_default(), ), Field::toggle( "Checked", value(&n, "CHECKED") == Some(Value::Boolean(true)), ), Field::toggle( "Enabled", value(&n, "ENABLED") == Some(Value::Boolean(true)), ), Field::toggle( "Visible", value(&n, "VISIBLE") == Some(Value::Boolean(true)), ), Field::toggle( "Separator", value(&n, "SEPARATOR") == Some(Value::Boolean(true)), ), Field::choice("Shortcut", shortcuts, at), ], ); } _ => bail!("Unbekannte Designeraktion"), } Ok(()) } pub(crate) fn design_submit(&mut self, d: &mut Dialog) -> Result { match &d.kind { DialogKind::DesignProperty => { self.designer.property = d.fields[0].index(); self.design_value_focus()?; } DialogKind::DesignObject(ids) => { let id = ids[d.fields[0].index()]; let n = self .design_objects()? .into_iter() .find(|(i, _)| *i == id) .unwrap() .1; self.open_dialog( "Event Procedures · Ereignis", DialogKind::DesignEvent(id), vec![Field::choice( "Ereignis", forms::events(n.class) .iter() .map(|s| s.to_string()) .collect(), 0, )], ); } DialogKind::DesignEvent(id) => self.design_event(*id, &d.fields[0].string())?, DialogKind::DesignPalette => { let keys = if d.fields[2].flag() { vec![node_key(&self.design_form()?.root)] } else { self.selected_keys()? }; let fg = d.fields[0].index() as i32; let bg = d.fields[1].index() as i32; self.design_edit(|f| { for key in keys { let n = find_mut(&mut f.root, &key).unwrap(); for (name, v) in [("FORECOLOR", fg), ("BACKCOLOR", bg)] { if forms::property(n.class, name).is_some() { set(n, name, Value::Integer(v))?; } } } Ok(()) })?; } DialogKind::DesignMenu(key) => { let key = key.clone(); let new = d.fields[0].string(); let mut candidate = self.design_form()?.clone(); let n = find_mut(&mut candidate.root, &key) .ok_or_else(|| anyhow!("Menüelement fehlt"))?; for (name, field) in [("CAPTION", 1), ("TAG", 2), ("SHORTCUT", 8)] { set(n, name, Value::String(d.fields[field].string()))?; } for (name, field) in [ ("CHECKED", 4), ("ENABLED", 5), ("VISIBLE", 6), ("SEPARATOR", 7), ] { set(n, name, Value::Boolean(d.fields[field].flag()))?; } let p = forms::property(Class::Menu, "INDEX").unwrap().0; if d.fields[3].string().is_empty() { n.properties.remove(&p); } else { n.properties.insert( p, Value::Integer( d.fields[3] .string() .parse() .context("Menu.Index: Integer erwartet")?, ), ); } let final_key = node_key(n); let updated = n.clone(); validate(&candidate)?; // Eine Namensänderung und die übrigen Felder werden zusammen validiert. // Die Rename-Transaktion muss vor jedem Commit alle Kandidaten kennen. self.design_menu_commit(&key, &new, updated, final_key)?; } _ => return Ok(false), } Ok(true) } pub(crate) fn design_key(&mut self, key: KeyEvent) -> Result { if self.mode != Mode::Designer { return Ok(false); } if self.value_focus { if key.code == K::Esc { self.value_focus = false; self.designer.value = None; return Ok(true); } if key.code == K::Enter { let field = self .designer .value .clone() .ok_or_else(|| anyhow!("Keine Property ausgewählt"))?; self.design_property(&field.label, &field.string())?; self.value_focus = false; return Ok(true); } if let Some(v) = &mut self.designer.value { v.key(key); } return Ok(true); } match self.active_window().map(|w| w.kind) { Some(WindowKind::Palette) if key.code == K::Enter => { self.design_action("palette")?; return Ok(true); } Some(WindowKind::Toolbox) => { let list = tools(); let at = self .designer .tool .and_then(|t| list.iter().position(|(_, c, s)| (*c, *s) == t)) .unwrap_or(0); match key.code { K::Up | K::Down => { let i = (at + list.len() + if key.code == K::Down { 1 } else { list.len() - 1 }) % list.len(); self.designer.tool = Some((list[i].1, list[i].2)); } K::Enter => { self.design_tool_double(at)?; } _ => return Ok(false), } return Ok(true); } Some(WindowKind::MenuDesign) => { let control = key.modifiers.contains(M::CONTROL); match key.code { K::Insert => self.design_action("insert")?, K::Delete => self.design_action("delete")?, K::Enter => self.design_action("menu")?, K::Left if control => self.design_action("out")?, K::Right if control => self.design_action("in")?, K::Up if control => self.design_action("up")?, K::Down if control => self.design_action("down")?, K::Up | K::Down => self.design_tab(key.code == K::Down, true)?, _ => return Ok(false), } return Ok(true); } _ => {} } match key.code { K::Tab | K::BackTab => self.design_tab( key.code == K::Tab && !key.modifiers.contains(M::SHIFT), false, )?, K::PageUp | K::PageDown => { let n = forms::properties(self.selected_node()?.class).len(); self.designer.property = (self.designer.property + n + if key.code == K::PageDown { 1 } else { n - 1 }) % n; } K::Left | K::Right | K::Up | K::Down => { let step = if key.modifiers.contains(M::CONTROL) { 5 } else { 1 }; let (dx, dy) = match key.code { K::Left => (-step, 0), K::Right => (step, 0), K::Up => (0, -step), _ => (0, step), }; self.design_geometry(dx, dy, key.modifiers.contains(M::SHIFT))?; } K::Delete => self.design_delete()?, K::Esc => self.designer.tool = None, _ => return Ok(false), } Ok(true) } fn design_tab(&mut self, forward: bool, menus: bool) -> Result<()> { let mut objects = self.design_objects()?; objects.retain(|(_, n)| { if menus { n.class == Class::Menu } else { !matches!(n.class, Class::Form | Class::Menu) } }); if objects.is_empty() { return Ok(()); } if !menus { objects.sort_by_key(|(_, n)| integer(n, "TABINDEX")); } let selected = self.selected_keys()?.last().cloned(); let i = objects .iter() .position(|(_, n)| Some(node_key(n)) == selected); let at = i.map_or(if forward { 0 } else { objects.len() - 1 }, |i| { (i + objects.len() + if forward { 1 } else { objects.len() - 1 }) % objects.len() }); self.design_select(objects[at].0, false) } fn design_tool_double(&mut self, index: usize) -> Result<()> { let (_, class, style) = tools()[index]; let objects = self.design_objects()?; let selected = self.selected_node()?; let parent = objects .iter() .find(|(_, n)| node_key(n) == node_key(&selected) && container(n.class)) .unwrap_or(&objects[0]) .0; self.design_place( class, style, parent, Rect::new( 1, 1, if class == Class::VScrollBar { 1 } else { 8 }, if class == Class::VScrollBar { 5 } else { 1 }, ), )?; self.designer.tool = None; Ok(()) } pub(crate) fn design_mouse(&mut self, m: MouseEvent) -> Result { if self.mode != Mode::Designer || self.dialog.is_some() || self.menu.is_some() { return Ok(false); } if let Some((property, color)) = self.designer.paint { if m.kind == MouseEventKind::Up(MouseButton::Left) { self.designer.paint = None; let id = self .designer .objects .iter() .rev() .find(|(_, r)| r.contains((m.column, m.row).into())) .map(|(id, _)| *id) .ok_or_else(|| anyhow!("Farbe auf die Designfläche ziehen"))?; let n = self .design_objects()? .into_iter() .find(|(i, _)| *i == id) .unwrap() .1; ensure!( forms::property(n.class, property).is_some(), "{}.{} nicht vorhanden", n.name, property ); self.design_edit(|f| { set( find_mut(&mut f.root, &node_key(&n)).unwrap(), property, Value::Integer(color as i32), ) })?; } return Ok(true); } if m.kind == MouseEventKind::Down(MouseButton::Left) { if let Some((_, Hit::DesignPaint(property, color))) = self .hits .iter() .rev() .find(|(r, _)| r.contains((m.column, m.row).into())) { self.designer.paint = Some((*property, *color)); return Ok(true); } } if m.kind == MouseEventKind::Down(MouseButton::Left) { if let Some((_, Hit::DesignTool(index))) = self .hits .iter() .rev() .find(|(r, _)| r.contains((m.column, m.row).into())) { let index = *index; let double = self .designer .last_tool_click .is_some_and(|(i, t)| i == index && t.elapsed().as_millis() < 500); self.designer.last_tool_click = Some((index, std::time::Instant::now())); let (_, class, style) = tools()[index]; self.designer.tool = Some((class, style)); if double { self.designer.last_tool_click = None; self.design_tool_double(index)?; } return Ok(true); } } if self.designer.drag.is_none() { if let Some((_, hit)) = self .hits .iter() .rev() .find(|(r, _)| r.contains((m.column, m.row).into())) { match hit { Hit::Window(id) if self .windows .iter() .any(|w| w.id == *id && matches!(w.kind, WindowKind::Code(_))) => {} _ => return Ok(false), } } } let inside = self.designer.viewport.contains((m.column, m.row).into()); if !inside && self.designer.drag.is_none() { return Ok(false); } match m.kind { MouseEventKind::Down(MouseButton::Left) => { if let Some(doc) = self.designer.document { if let Some(w)=self.windows.iter().find(|w|matches!(w.kind,WindowKind::Code(v) if self.project.view(v).is_ok_and(|v|v.document()==doc))) {self.active=w.id;} } let hit = self .designer .objects .iter() .rev() .find(|(_, r)| r.contains((m.column, m.row).into())) .copied(); if let Some((id, r)) = hit { let selected = self .designer .selections .get(&self.design_document()?) .is_some_and(|s| s.contains(&id)); let resize = if selected && self.designer.tool.is_none() { handle_at(r, m.column, m.row) } else { None }; if !selected || m.modifiers.contains(M::CONTROL) { self.design_select(id, m.modifiers.contains(M::CONTROL))?; } self.designer.drag = Some((m.column, m.row, resize)); } else if self.designer.tool.is_some() { self.designer.drag = Some((m.column, m.row, None)); } self.value_focus = false; } MouseEventKind::Up(MouseButton::Left) => { if let Some((x, y, handle)) = self.designer.drag.take() { if let Some((class, style)) = self.designer.tool.take() { let objects = self.design_objects()?; let parent = self .designer .objects .iter() .rev() .find(|(id, r)| { r.contains((x, y).into()) && objects.iter().any(|(i, n)| i == id && container(n.class)) }) .copied() .ok_or_else(|| anyhow!("Innerhalb eines Containers aufziehen"))?; let left = x .min(m.column) .checked_sub(parent.1.x) .ok_or_else(|| anyhow!("Außerhalb des Containers"))?; let top = y .min(m.row) .checked_sub(parent.1.y) .ok_or_else(|| anyhow!("Außerhalb des Containers"))?; self.design_place( class, style, parent.0, Rect::new(left, top, x.abs_diff(m.column) + 1, y.abs_diff(m.row) + 1), )?; } else { let dx = m.column as i32 - x as i32; let dy = m.row as i32 - y as i32; if dx != 0 || dy != 0 { if let Some(edges) = handle { self.design_resize(dx, dy, edges)?; } else { self.design_geometry(dx, dy, false)?; } } } } } MouseEventKind::Drag(MouseButton::Left) => {} _ => return Ok(false), } Ok(true) } fn design_menu_commit( &mut self, key: &Key, new: &str, updated: FormNode, final_key: Key, ) -> Result<()> { let doc = self.design_document()?; let stable = self.project.design_ids(doc)?[key]; let mut candidate = self.design_form()?.clone(); *find_mut(&mut candidate.root, key).unwrap() = updated; if !key.0.eq_ignore_ascii_case(new) { self.design_rename_with(new, Some(candidate))?; } else { self.design_edit(|f| { f.root = candidate.root; Ok(()) })?; } self.project .rekey_design_id(doc, key, (new.to_uppercase(), final_key.1), stable); Ok(()) } pub(crate) fn design_render(&mut self, f: &mut Frame, area: Rect) { self.designer.viewport = area; self.designer.objects.clear(); let render = || -> Result<_> { let form = self.design_form()?.clone(); let (model, screen) = preview(&form, (area.width, area.height))?; Ok((form, model, screen)) }; let Ok((form, model, screen)) = render() else { return; }; f.render_widget(tb_ui::screen::ScreenWidget(&screen), area); let Ok(objects) = self.design_objects() else { return; }; let catalog = form.catalog(); let mut timers = 0; for (id, n) in objects { if n.class == Class::Menu { continue; } let object = catalog.find(&n.name).unwrap().0; let Some((x, y, w, h)) = model.rect((object, node_key(&n).1)) else { continue; }; let r = if n.class == Class::Timer { let r = Rect::new(area.x + timers * 4, area.bottom().saturating_sub(1), 3, 1); timers += 1; f.render_widget( Paragraph::new("[T]").style(crate::render::pair(0, 14)), r.intersection(area), ); r } else { Rect::new( area.x + x.saturating_sub(1) as u16, area.y + y.saturating_sub(1) as u16, w as u16, h as u16, ) }; let r = r.intersection(area); if r.is_empty() { continue; } if self.designer.grid && n.class == Class::Form { for y in r.y..r.bottom() { for x in r.x..r.right() { let cell = &mut f.buffer_mut()[(x, y)]; if cell.symbol() == " " { cell.set_symbol("·"); } } } } self.designer.objects.push((id, r)); } let selection = self .design_document() .ok() .and_then(|id| self.designer.selections.get(&id)) .cloned() .unwrap_or_default(); for (id, r) in &self.designer.objects { if selection.contains(id) { for (x, y) in [ (r.x, r.y), (r.right() - 1, r.y), (r.x, r.bottom() - 1), (r.right() - 1, r.bottom() - 1), (r.x + r.width / 2, r.y), (r.x + r.width / 2, r.bottom() - 1), (r.x, r.y + r.height / 2), (r.right() - 1, r.y + r.height / 2), ] { f.render_widget( Paragraph::new("■").style(crate::render::pair(0, 14)), Rect::new(x, y, 1, 1), ); } } } } pub(crate) fn design_tool_render(&mut self, f: &mut Frame, kind: WindowKind, area: Rect) { match kind { WindowKind::Toolbox => { for (i, (name, class, style)) in tools().iter().enumerate().take(area.height as usize) { let r = Rect::new(area.x, area.y + i as u16, area.width, 1); f.render_widget( Paragraph::new(format!( "{} {name}", if self.designer.tool == Some((*class, *style)) { ">" } else { " " } )), r, ); self.hits.push((r, Hit::DesignTool(i))); } } WindowKind::Palette => { for (row, property) in [(0, "FORECOLOR"), (2, "BACKCOLOR")] { f.render_widget( Paragraph::new(property), Rect::new(area.x, area.y + row, area.width, 1).intersection(area), ); for n in 0..16u16 { let r = Rect::new(area.x + n * 2, area.y + row + 1, 2, 1).intersection(area); if !r.is_empty() { f.render_widget( Paragraph::new(format!("{n:X} ")).style( Style::default().bg(crate::render::dos(n as u8)).fg( crate::render::dos( if matches!(n, 0 | 1 | 4 | 5 | 6 | 8 | 9) { 15 } else { 0 }, ), ), ), r, ); self.hits.push((r, Hit::DesignPaint(property, n as u8))); } } } let r = Rect::new(area.x, area.y + 4, area.width, 1).intersection(area); f.render_widget(Paragraph::new("Enter: Fore/Back auftragen"), r); self.hits.push((r, Hit::DesignAction("palette"))); } WindowKind::MenuDesign => { let labels = [ ("+", "insert"), ("Edit", "menu"), ("Del", "delete"), ("↑", "up"), ("↓", "down"), ("→", "in"), ("←", "out"), ]; let mut x = area.x; for (label, action) in labels { let r = Rect::new(x, area.y, label.len() as u16 + 1, 1).intersection(area); f.render_widget(Paragraph::new(label), r); self.hits.push((r, Hit::DesignAction(action))); x += r.width; } if let Ok(objects) = self.design_objects() { let selected = self .design_document() .ok() .and_then(|d| self.designer.selections.get(&d)) .cloned() .unwrap_or_default(); let menus: Vec<_> = objects .into_iter() .filter(|(_, n)| n.class == Class::Menu) .collect(); let current = menus .iter() .position(|(id, _)| selected.contains(id)) .unwrap_or(0); let start = current.saturating_sub(area.height.saturating_sub(3) as usize); for (row, (id, n)) in menus .iter() .skip(start) .take(area.height.saturating_sub(1) as usize) .enumerate() { fn depth(n: &FormNode, key: &Key) -> Option { if node_key(n) == *key { Some(0) } else { n.children.iter().find_map(|c| depth(c, key).map(|d| d + 1)) } } let depth = self .design_form() .ok() .and_then(|f| depth(&f.root, &node_key(n))) .unwrap_or(1); let r = Rect::new(area.x, area.y + 1 + row as u16, area.width, 1); f.render_widget( Paragraph::new(format!( "{}{} {}{}", " ".repeat(depth.saturating_sub(1) * 2), text(&value(n, "CAPTION").unwrap()), n.name, node_key(n).1.map(|i| format!("({i})")).unwrap_or_default() )) .style(if selected.contains(id) { Style::default().add_modifier(Modifier::REVERSED) } else { Style::default() }), r, ); self.hits.push((r, Hit::DesignObject(*id))); } } } _ => {} } } pub(crate) fn design_bar(&mut self) -> (String, String) { let Ok(n) = self.selected_node() else { return ("Properties: kein Control ausgewählt".into(), String::new()); }; let specs = forms::properties(n.class); self.designer.property = self.designer.property.min(specs.len() - 1); let name = specs[self.designer.property].name; let v = self .designer .value .as_ref() .filter(|_| self.value_focus) .map(Field::string) .unwrap_or_else(|| { if matches!(name, "CTLNAME" | "FORMNAME") { n.name.clone() } else { text(&value(&n, name).unwrap()) } }); ( format!( "Property: {} [{}]↓ Value: [{}]{}", n.name, name, v, if self.value_focus { " *" } else { "" } ), format!( "{},{} │ {}×{}", integer(&n, "LEFT"), integer(&n, "TOP"), integer(&n, "WIDTH"), integer(&n, "HEIGHT") ), ) } } fn token_offset(code: &str, line: u32, col: u32) -> Result { let start = crate::editor::line_offset(code, line); let offset = code[start..] .char_indices() .nth(col.saturating_sub(1) as usize) .map(|(i, _)| i) .ok_or_else(|| anyhow!("Ungültige Quellposition"))?; Ok(start + offset) } fn event_parameters(n: &FormNode, event: &str) -> Vec<(String, String)> { let mut params = Vec::new(); if node_key(n).1.is_some() { params.push(("Index".into(), "INTEGER".into())); } for (name, ty) in forms::event_params(event).unwrap_or_default() { params.push(( (*name).into(), match ty { forms::EventParamType::Integer => "INTEGER", forms::EventParamType::Single => "SINGLE", forms::EventParamType::Control => "CONTROL", } .into(), )); } params } fn handle_at(r: Rect, x: u16, y: u16) -> Option<(i8, i8)> { let columns = [(r.x, -1), (r.right() - 1, 1), (r.x + r.width / 2, 0)]; let rows = [(r.y, -1), (r.bottom() - 1, 1), (r.y + r.height / 2, 0)]; for (col, horizontal) in columns { for (row, vertical) in rows { if (horizontal != 0 || vertical != 0) && x == col && y == row { return Some(( if r.width == 1 { 0 } else { horizontal }, if r.height == 1 { 0 } else { vertical }, )); } } } None }