1967 lines
76 KiB
Rust
1967 lines
76 KiB
Rust
//! 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<i32>);
|
||
type Drag = (u16, u16, Option<(i8, i8)>);
|
||
#[derive(Default)]
|
||
pub struct Designer {
|
||
pub document: Option<DocumentId>,
|
||
pub selections: BTreeMap<DocumentId, Vec<DesignId>>,
|
||
pub grid: bool,
|
||
pub tool: Option<(Class, i32)>,
|
||
pub property: usize,
|
||
pub value: Option<Field>,
|
||
clipboard: Vec<FormNode>,
|
||
pub viewport: Rect,
|
||
pub objects: Vec<(DesignId, Rect)>,
|
||
drag: Option<Drag>,
|
||
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<Value> {
|
||
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<DocumentId> {
|
||
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<Vec<(DesignId, FormNode)>> {
|
||
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<Vec<Key>> {
|
||
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<FormNode> {
|
||
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<DesignId> {
|
||
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<Value> {
|
||
ensure!(s.writable, "schreibgeschützt");
|
||
Ok(match s.ty {
|
||
PropertyType::Integer => {
|
||
let v = input.parse::<i32>()?;
|
||
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::<f32>()?;
|
||
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<FormNode>) {
|
||
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<String>,
|
||
renamed: &mut BTreeMap<String, String>,
|
||
) -> 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<bool> {
|
||
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<FormFile>) -> 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::<Vec<_>>()
|
||
.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<DocumentId, Content> = BTreeMap::new();
|
||
let mut edits: BTreeMap<DocumentId, BTreeMap<usize, (usize, String)>> = 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::<Vec<_>>()
|
||
.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::<Vec<_>>()
|
||
.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::<Vec<_>>()
|
||
.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::<Vec<_>>()
|
||
.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<bool> {
|
||
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<bool> {
|
||
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<bool> {
|
||
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<bool> {
|
||
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<usize> {
|
||
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<usize> {
|
||
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
|
||
}
|