Implement and archive Phase 5 form designer
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -591,6 +591,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"tb-frontend",
|
"tb-frontend",
|
||||||
|
"tb-ide",
|
||||||
"tb-runtime",
|
"tb-runtime",
|
||||||
"tb-ui",
|
"tb-ui",
|
||||||
"tb-vm",
|
"tb-vm",
|
||||||
|
|||||||
@@ -16,3 +16,6 @@ tb-vm.workspace = true
|
|||||||
tb-runtime.workspace = true
|
tb-runtime.workspace = true
|
||||||
tb-ui = { workspace = true, features = ["terminal"] }
|
tb-ui = { workspace = true, features = ["terminal"] }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tb-ide = { path = "../tb-ide" }
|
||||||
|
|||||||
@@ -234,3 +234,38 @@ fn explicit_startup_order_and_form_selection_survive_tbc() {
|
|||||||
}
|
}
|
||||||
std::fs::remove_dir_all(dir).unwrap();
|
std::fs::remove_dir_all(dir).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn designer_frm_is_accepted_by_the_standalone_cli_compiler() {
|
||||||
|
use tb_frontend::forms::ObjectClass;
|
||||||
|
use tb_ide::{app::App, commands::Command as IdeCommand, documents::Destination};
|
||||||
|
let dir = std::env::temp_dir().join(format!("tb-designer-cli-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let dir = dir.canonicalize().unwrap();
|
||||||
|
let mut app = App::new(&dir, dir.join("options"), (100, 30)).unwrap();
|
||||||
|
app.execute(IdeCommand::NewForm);
|
||||||
|
let root = app.design_objects().unwrap()[0].0;
|
||||||
|
let button = app.design_place(ObjectClass::CommandButton, 0, root, Default::default());
|
||||||
|
assert!(button.is_err()); // ungültige Größe schreibt nichts
|
||||||
|
let mut rect = app.area();
|
||||||
|
rect.x = 2;
|
||||||
|
rect.y = 2;
|
||||||
|
rect.width = 8;
|
||||||
|
rect.height = 2;
|
||||||
|
let control = app
|
||||||
|
.design_place(ObjectClass::CommandButton, 0, root, rect)
|
||||||
|
.unwrap();
|
||||||
|
app.design_property("INDEX", "0").unwrap();
|
||||||
|
app.design_event(control, "CLICK").unwrap();
|
||||||
|
let doc = app.active_document().unwrap();
|
||||||
|
let path = dir.join("designed.frm");
|
||||||
|
app.project
|
||||||
|
.save_file(doc, Some(&Destination::new(&path)))
|
||||||
|
.unwrap();
|
||||||
|
let built = tbc(&dir, "build", "designed.frm");
|
||||||
|
assert!(built.status.success(), "{built:?}");
|
||||||
|
let bytes = std::fs::read(dir.join("designed.tbc")).unwrap();
|
||||||
|
assert_eq!(&bytes[..6], b"TBC\0\x04\0");
|
||||||
|
tb_vm::project_io::load_program(&dir.join("designed.tbc")).unwrap();
|
||||||
|
std::fs::remove_dir_all(dir).unwrap();
|
||||||
|
}
|
||||||
|
|||||||
@@ -751,3 +751,17 @@ impl FormCatalog {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tastenkombinationen, die der Forms-Host für Menü-Shortcuts liefert.
|
||||||
|
pub fn menu_shortcuts() -> Vec<String> {
|
||||||
|
let mut out = vec![String::new()];
|
||||||
|
for prefix in ["", "Ctrl+", "Alt+", "Shift+", "Ctrl+Shift+"] {
|
||||||
|
for n in 1..=10 {
|
||||||
|
out.push(format!("{prefix}F{n}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for c in ('A'..='Z').filter(|c| *c != 'C') {
|
||||||
|
out.push(format!("Ctrl+{c}"));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|||||||
@@ -421,6 +421,23 @@ pub fn lower_with_forms(
|
|||||||
(hir, s.diags)
|
(hir, s.diags)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Semantisch gebundene Objekt-/Ereignisnamen für transaktionale IDE-Umbenennungen.
|
||||||
|
/// Aufrufer validieren die gesamte Übersetzungseinheit einschließlich ihrer Imports.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct BoundFormReferences {
|
||||||
|
pub objects: Vec<(SourcePos, String, u16)>,
|
||||||
|
pub procedures: Vec<(SourcePos, String)>,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
pub fn bound_form_references(module: &Module, catalog: &FormCatalog) -> BoundFormReferences {
|
||||||
|
let mut s = new_sema(module, catalog);
|
||||||
|
s.references = Some(BoundFormReferences::default());
|
||||||
|
s.run(module);
|
||||||
|
let mut references = s.references.unwrap();
|
||||||
|
references.diagnostics = s.diags;
|
||||||
|
references
|
||||||
|
}
|
||||||
|
|
||||||
fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
|
fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
|
||||||
Sema {
|
Sema {
|
||||||
diags: Vec::new(),
|
diags: Vec::new(),
|
||||||
@@ -446,6 +463,7 @@ fn new_sema(module: &Module, catalog: &FormCatalog) -> Sema {
|
|||||||
forms: catalog.clone(),
|
forms: catalog.clone(),
|
||||||
module_name: module.name.clone(),
|
module_name: module.name.clone(),
|
||||||
event_procs: Vec::new(),
|
event_procs: Vec::new(),
|
||||||
|
references: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,6 +568,7 @@ struct Sema {
|
|||||||
forms: FormCatalog,
|
forms: FormCatalog,
|
||||||
module_name: String,
|
module_name: String,
|
||||||
event_procs: Vec<hir::HEventProc>,
|
event_procs: Vec<hir::HEventProc>,
|
||||||
|
references: Option<BoundFormReferences>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sema {
|
impl Sema {
|
||||||
@@ -942,6 +961,9 @@ impl Sema {
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if let Some(r) = &mut self.references {
|
||||||
|
r.objects.push((proc.pos, proc.sig.name.clone(), object));
|
||||||
|
}
|
||||||
let mut expected: Vec<(&str, forms::EventParamType)> = Vec::new();
|
let mut expected: Vec<(&str, forms::EventParamType)> = Vec::new();
|
||||||
if self.forms.objects[object as usize].array {
|
if self.forms.objects[object as usize].array {
|
||||||
expected.push(("INDEX", forms::EventParamType::Integer));
|
expected.push(("INDEX", forms::EventParamType::Integer));
|
||||||
@@ -1049,6 +1071,14 @@ impl Sema {
|
|||||||
) -> Option<(u16, forms::FormObject)> {
|
) -> Option<(u16, forms::FormObject)> {
|
||||||
if let Some(parent) = parent {
|
if let Some(parent) = parent {
|
||||||
if let Some((id, object)) = self.forms.find_in(name, parent) {
|
if let Some((id, object)) = self.forms.find_in(name, parent) {
|
||||||
|
if let Some(r) = &mut self.references {
|
||||||
|
r.objects.push((pos, name.into(), id));
|
||||||
|
}
|
||||||
|
if let Some((form, _)) = self.forms.find(parent) {
|
||||||
|
if let Some(r) = &mut self.references {
|
||||||
|
r.objects.push((pos, parent.into(), form));
|
||||||
|
}
|
||||||
|
}
|
||||||
return Some((id, object.clone()));
|
return Some((id, object.clone()));
|
||||||
}
|
}
|
||||||
self.err(
|
self.err(
|
||||||
@@ -1067,7 +1097,11 @@ impl Sema {
|
|||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
) -> Option<(u16, crate::forms::FormObject)> {
|
) -> Option<(u16, crate::forms::FormObject)> {
|
||||||
if let Some((id, object)) = self.find_object(name) {
|
if let Some((id, object)) = self.find_object(name) {
|
||||||
return Some((id, object.clone()));
|
let object = object.clone();
|
||||||
|
if let Some(r) = &mut self.references {
|
||||||
|
r.objects.push((pos, name.into(), id));
|
||||||
|
}
|
||||||
|
return Some((id, object));
|
||||||
}
|
}
|
||||||
self.err(pos, format!("Unknown object '{name}'"));
|
self.err(pos, format!("Unknown object '{name}'"));
|
||||||
None
|
None
|
||||||
@@ -2391,7 +2425,11 @@ impl Sema {
|
|||||||
OptionKind::Base(b) => self.option_base = *b,
|
OptionKind::Base(b) => self.option_base = *b,
|
||||||
},
|
},
|
||||||
Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert
|
Stmt::TypeDecl { .. } => {} // bereits in Pass 1 registriert
|
||||||
Stmt::Declare { .. } => {} // bereits in Pass 1 registriert
|
Stmt::Declare { sig, pos } => {
|
||||||
|
if let Some(r) = &mut self.references {
|
||||||
|
r.procedures.push((*pos, sig.name.clone()));
|
||||||
|
}
|
||||||
|
} // in Pass 1 registriert
|
||||||
Stmt::Call {
|
Stmt::Call {
|
||||||
name, args, pos, ..
|
name, args, pos, ..
|
||||||
} => {
|
} => {
|
||||||
@@ -3377,6 +3415,9 @@ impl Sema {
|
|||||||
pos: SourcePos,
|
pos: SourcePos,
|
||||||
out: &mut Vec<HStmt>,
|
out: &mut Vec<HStmt>,
|
||||||
) {
|
) {
|
||||||
|
if let Some(r) = &mut self.references {
|
||||||
|
r.procedures.push((pos, name.into()));
|
||||||
|
}
|
||||||
if name == "CLIPBOARD.ADDITEM" {
|
if name == "CLIPBOARD.ADDITEM" {
|
||||||
if args.len() != 1 {
|
if args.len() != 1 {
|
||||||
self.err(pos, "Argument-count mismatch for CLIPBOARD.ADDITEM");
|
self.err(pos, "Argument-count mismatch for CLIPBOARD.ADDITEM");
|
||||||
@@ -4834,6 +4875,9 @@ impl Sema {
|
|||||||
.map(|(id, info)| (id, info.clone()))
|
.map(|(id, info)| (id, info.clone()))
|
||||||
{
|
{
|
||||||
if args.is_none() || info.array {
|
if args.is_none() || info.array {
|
||||||
|
if let Some(r) = &mut self.references {
|
||||||
|
r.objects.push((pos, name.into(), object));
|
||||||
|
}
|
||||||
let Ok(index) = self.object_index(object, args, scope, pos) else {
|
let Ok(index) = self.object_index(object, args, scope, pos) else {
|
||||||
return (HExpr::Int(0), Ty::Unknown);
|
return (HExpr::Int(0), Ty::Unknown);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,6 +30,9 @@ pub enum Execution {
|
|||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum WindowKind {
|
pub enum WindowKind {
|
||||||
|
Toolbox,
|
||||||
|
Palette,
|
||||||
|
MenuDesign,
|
||||||
Code(ViewId),
|
Code(ViewId),
|
||||||
Project,
|
Project,
|
||||||
Output,
|
Output,
|
||||||
@@ -108,7 +111,7 @@ impl Field {
|
|||||||
pub fn flag(&self) -> bool {
|
pub fn flag(&self) -> bool {
|
||||||
matches!(self.value, FieldValue::Toggle(true))
|
matches!(self.value, FieldValue::Toggle(true))
|
||||||
}
|
}
|
||||||
fn key(&mut self, key: KeyEvent) {
|
pub(crate) fn key(&mut self, key: KeyEvent) {
|
||||||
match &mut self.value {
|
match &mut self.value {
|
||||||
FieldValue::Toggle(value) => {
|
FieldValue::Toggle(value) => {
|
||||||
if matches!(key.code, K::Char(' ') | K::Left | K::Right) {
|
if matches!(key.code, K::Char(' ') | K::Left | K::Right) {
|
||||||
@@ -189,6 +192,11 @@ pub enum AfterSave {
|
|||||||
}
|
}
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum DialogKind {
|
pub enum DialogKind {
|
||||||
|
DesignProperty,
|
||||||
|
DesignPalette,
|
||||||
|
DesignMenu((String, Option<i32>)),
|
||||||
|
DesignObject(Vec<u64>),
|
||||||
|
DesignEvent(u64),
|
||||||
Search {
|
Search {
|
||||||
view: ViewId,
|
view: ViewId,
|
||||||
selection: Option<std::ops::Range<usize>>,
|
selection: Option<std::ops::Range<usize>>,
|
||||||
@@ -251,6 +259,10 @@ impl Dialog {
|
|||||||
}
|
}
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
pub enum Hit {
|
pub enum Hit {
|
||||||
|
DesignTool(usize),
|
||||||
|
DesignPaint(&'static str, u8),
|
||||||
|
DesignObject(u64),
|
||||||
|
DesignAction(&'static str),
|
||||||
Command(Command),
|
Command(Command),
|
||||||
Menu(usize),
|
Menu(usize),
|
||||||
MenuItem(usize),
|
MenuItem(usize),
|
||||||
@@ -263,6 +275,7 @@ pub enum Hit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct App {
|
pub struct App {
|
||||||
|
pub designer: crate::designer::Designer,
|
||||||
pub session: crate::execution::Session,
|
pub session: crate::execution::Session,
|
||||||
pub editor: crate::editor::Editor,
|
pub editor: crate::editor::Editor,
|
||||||
pub project: Project,
|
pub project: Project,
|
||||||
@@ -301,6 +314,7 @@ impl App {
|
|||||||
let (options, errors, config_disk) = Options::load(&config_path);
|
let (options, errors, config_disk) = Options::load(&config_path);
|
||||||
project.include_paths = options.include_paths.clone();
|
project.include_paths = options.include_paths.clone();
|
||||||
let mut app = Self {
|
let mut app = Self {
|
||||||
|
designer: Default::default(),
|
||||||
session: Default::default(),
|
session: Default::default(),
|
||||||
editor: Default::default(),
|
editor: Default::default(),
|
||||||
project,
|
project,
|
||||||
@@ -389,6 +403,9 @@ impl App {
|
|||||||
pub fn active_document(&self) -> Option<DocumentId> {
|
pub fn active_document(&self) -> Option<DocumentId> {
|
||||||
match self.active_window()?.kind {
|
match self.active_window()?.kind {
|
||||||
WindowKind::Code(v) => self.project.view(v).ok().map(|v| v.document()),
|
WindowKind::Code(v) => self.project.view(v).ok().map(|v| v.document()),
|
||||||
|
WindowKind::Toolbox | WindowKind::Palette | WindowKind::MenuDesign => {
|
||||||
|
self.designer.document
|
||||||
|
}
|
||||||
_ => self.project.members().get(self.selected_member).copied(),
|
_ => self.project.members().get(self.selected_member).copied(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -477,30 +494,33 @@ impl App {
|
|||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if matches!(
|
if !(self.mode == Mode::Designer && matches!(command, Cut | Copy | Paste | Clear))
|
||||||
command,
|
&& matches!(
|
||||||
LoadText
|
command,
|
||||||
| Print
|
LoadText
|
||||||
| SaveText
|
| Print
|
||||||
| Cut
|
| SaveText
|
||||||
| Copy
|
| Cut
|
||||||
| Paste
|
| Copy
|
||||||
| Clear
|
| Paste
|
||||||
| NewSub
|
| Clear
|
||||||
| NewFunction
|
| NewSub
|
||||||
| IncludedFile
|
| NewFunction
|
||||||
| IncludedLines
|
| IncludedFile
|
||||||
| Find
|
| IncludedLines
|
||||||
| SelectedText
|
| Find
|
||||||
| FindNext
|
| SelectedText
|
||||||
| Replace
|
| FindNext
|
||||||
| Procedures
|
| Replace
|
||||||
| PreviousCode
|
| Procedures
|
||||||
| Diagnostics
|
| PreviousCode
|
||||||
) && !matches!(
|
| Diagnostics
|
||||||
self.active_window().map(|w| w.kind),
|
)
|
||||||
Some(WindowKind::Code(_))
|
&& !matches!(
|
||||||
) {
|
self.active_window().map(|w| w.kind),
|
||||||
|
Some(WindowKind::Code(_))
|
||||||
|
)
|
||||||
|
{
|
||||||
return Some("Kein Codefenster aktiv".into());
|
return Some("Kein Codefenster aktiv".into());
|
||||||
}
|
}
|
||||||
if matches!(
|
if matches!(
|
||||||
@@ -540,9 +560,19 @@ impl App {
|
|||||||
self.control_menu = false;
|
self.control_menu = false;
|
||||||
if let Err(e) = self.action(command) {
|
if let Err(e) = self.action(command) {
|
||||||
self.message = format!("{e:#}");
|
self.message = format!("{e:#}");
|
||||||
|
if self.mode == Mode::Designer {
|
||||||
|
self.open_dialog(
|
||||||
|
"Designer",
|
||||||
|
DialogKind::Message,
|
||||||
|
vec![Field::text("Fehler", self.message.clone())],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn action(&mut self, command: Command) -> Result<()> {
|
fn action(&mut self, command: Command) -> Result<()> {
|
||||||
|
if self.designer_command(command)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
use Command::*;
|
use Command::*;
|
||||||
let id = self.active_document();
|
let id = self.active_document();
|
||||||
match command {
|
match command {
|
||||||
@@ -742,6 +772,18 @@ impl App {
|
|||||||
FocusWindow(id) => {
|
FocusWindow(id) => {
|
||||||
if self.windows.iter().any(|w| w.id == id) {
|
if self.windows.iter().any(|w| w.id == id) {
|
||||||
self.active = id;
|
self.active = id;
|
||||||
|
if self.mode == Mode::Designer {
|
||||||
|
if let Some(WindowKind::Code(v)) = self.active_window().map(|w| w.kind) {
|
||||||
|
let doc = self.project.view(v)?.document();
|
||||||
|
if matches!(self.project.document(doc)?.content(), Content::Form(_)) {
|
||||||
|
self.design_enter(doc)?;
|
||||||
|
} else {
|
||||||
|
self.mode = Mode::Environment;
|
||||||
|
self.properties = false;
|
||||||
|
self.value_focus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Calls | Debug | HelpWindow | Immediate | Output | Project => {
|
Calls | Debug | HelpWindow | Immediate | Output | Project => {
|
||||||
@@ -930,6 +972,22 @@ impl App {
|
|||||||
};
|
};
|
||||||
self.properties = form;
|
self.properties = form;
|
||||||
self.value_focus = false;
|
self.value_focus = false;
|
||||||
|
if form {
|
||||||
|
self.design_enter(id)?;
|
||||||
|
if !self.windows.iter().any(|w| w.kind == WindowKind::Toolbox) {
|
||||||
|
let active = self.active;
|
||||||
|
self.add_window(WindowKind::Toolbox, Rect::new(0, 1, 20, 21));
|
||||||
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == active) {
|
||||||
|
w.normal = Rect::new(
|
||||||
|
20,
|
||||||
|
1,
|
||||||
|
self.size.0.saturating_sub(20),
|
||||||
|
self.size.1.saturating_sub(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.active = active;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn cycle(&mut self, direction: isize) {
|
fn cycle(&mut self, direction: isize) {
|
||||||
@@ -942,7 +1000,9 @@ impl App {
|
|||||||
.position(|w| w.id == self.active)
|
.position(|w| w.id == self.active)
|
||||||
.unwrap_or(0);
|
.unwrap_or(0);
|
||||||
let i = (i as isize + direction).rem_euclid(self.windows.len() as isize) as usize;
|
let i = (i as isize + direction).rem_euclid(self.windows.len() as isize) as usize;
|
||||||
self.active = self.windows[i].id;
|
if let Err(e) = self.action(Command::FocusWindow(self.windows[i].id)) {
|
||||||
|
self.message = format!("{e:#}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
fn arrange(&mut self) {
|
fn arrange(&mut self) {
|
||||||
let area = self.area();
|
let area = self.area();
|
||||||
@@ -997,6 +1057,7 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.session = Default::default();
|
self.session = Default::default();
|
||||||
|
self.designer = Default::default();
|
||||||
self.basic_events.clear();
|
self.basic_events.clear();
|
||||||
self.execution = Execution::Idle;
|
self.execution = Execution::Idle;
|
||||||
self.base = self.project.directory().to_path_buf();
|
self.base = self.project.directory().to_path_buf();
|
||||||
@@ -1072,6 +1133,14 @@ impl App {
|
|||||||
}
|
}
|
||||||
fn submit(&mut self, d: &mut Dialog) -> Result<bool> {
|
fn submit(&mut self, d: &mut Dialog) -> Result<bool> {
|
||||||
match d.kind.clone() {
|
match d.kind.clone() {
|
||||||
|
DialogKind::DesignProperty
|
||||||
|
| DialogKind::DesignPalette
|
||||||
|
| DialogKind::DesignMenu(_)
|
||||||
|
| DialogKind::DesignObject(_)
|
||||||
|
| DialogKind::DesignEvent(_) => {
|
||||||
|
self.design_submit(d)?;
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
DialogKind::Search { .. }
|
DialogKind::Search { .. }
|
||||||
| DialogKind::Procedure(_)
|
| DialogKind::Procedure(_)
|
||||||
| DialogKind::Procedures(_)
|
| DialogKind::Procedures(_)
|
||||||
@@ -1350,6 +1419,19 @@ impl App {
|
|||||||
}
|
}
|
||||||
self.key(key);
|
self.key(key);
|
||||||
} else if let Event::Mouse(mouse) = event {
|
} else if let Event::Mouse(mouse) = event {
|
||||||
|
match self.design_mouse(mouse) {
|
||||||
|
Ok(true) => return,
|
||||||
|
Err(e) => {
|
||||||
|
self.message = format!("{e:#}");
|
||||||
|
self.open_dialog(
|
||||||
|
"Designer",
|
||||||
|
DialogKind::Message,
|
||||||
|
vec![Field::text("Fehler", self.message.clone())],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
if self.dialog.is_none() && self.menu.is_none() && self.program_focus() {
|
if self.dialog.is_none() && self.menu.is_none() && self.program_focus() {
|
||||||
if self.session.fullscreen {
|
if self.session.fullscreen {
|
||||||
self.basic_events.push(Event::Mouse(mouse));
|
self.basic_events.push(Event::Mouse(mouse));
|
||||||
@@ -1453,12 +1535,29 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match hit {
|
match hit {
|
||||||
|
Hit::DesignTool(_) => {}
|
||||||
|
Hit::DesignObject(id) => {
|
||||||
|
if let Err(e) = self.design_select(id, false) {
|
||||||
|
self.message = e.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Hit::DesignAction(action) => {
|
||||||
|
if let Err(e) = self.design_action(action) {
|
||||||
|
self.message = format!("{e:#}");
|
||||||
|
self.open_dialog(
|
||||||
|
"Designer",
|
||||||
|
DialogKind::Message,
|
||||||
|
vec![Field::text("Fehler", self.message.clone())],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Hit::Command(c) => self.execute(c),
|
Hit::Command(c) => self.execute(c),
|
||||||
Hit::Menu(i) => {
|
Hit::Menu(i) => {
|
||||||
self.properties = false;
|
self.properties = false;
|
||||||
self.menu = Some((i, 0));
|
self.menu = Some((i, 0));
|
||||||
}
|
}
|
||||||
Hit::Window(id) => self.active = id,
|
Hit::Window(id) => self.execute(Command::FocusWindow(id)),
|
||||||
Hit::ProjectMember(i) => {
|
Hit::ProjectMember(i) => {
|
||||||
self.selected_member = i;
|
self.selected_member = i;
|
||||||
if let Some(w) = self.windows.iter().find(|w| w.kind == WindowKind::Project) {
|
if let Some(w) = self.windows.iter().find(|w| w.kind == WindowKind::Project) {
|
||||||
@@ -1674,19 +1773,61 @@ impl App {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.mode == Mode::Designer && key.code == K::F(2) {
|
if self.mode == Mode::Designer && key.code == K::F(2) {
|
||||||
self.properties = true;
|
if let Err(e) = self.design_value_focus() {
|
||||||
self.value_focus = true;
|
self.message = format!("{e:#}");
|
||||||
self.message = "Properties Value · Bearbeitung folgt in Change 05".into();
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.program_focus() && key.modifiers.contains(M::CONTROL) && key.code == K::Char('c') {
|
if self.program_focus() && key.modifiers.contains(M::CONTROL) && key.code == K::Char('c') {
|
||||||
self.pause_execution();
|
self.pause_execution();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if self.mode == Mode::Designer && self.value_focus {
|
||||||
|
if let Err(e) = self.design_key(key) {
|
||||||
|
self.message = format!("{e:#}");
|
||||||
|
self.open_dialog(
|
||||||
|
"Property",
|
||||||
|
DialogKind::Message,
|
||||||
|
vec![Field::text("Fehler", self.message.clone())],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if matches!(
|
||||||
|
self.active_window().map(|w| w.kind),
|
||||||
|
Some(WindowKind::MenuDesign | WindowKind::Toolbox | WindowKind::Palette)
|
||||||
|
) {
|
||||||
|
match self.design_key(key) {
|
||||||
|
Ok(true) => return,
|
||||||
|
Err(e) => {
|
||||||
|
self.message = format!("{e:#}");
|
||||||
|
self.open_dialog(
|
||||||
|
"Designer",
|
||||||
|
DialogKind::Message,
|
||||||
|
vec![Field::text("Fehler", self.message.clone())],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
if let Some(command) = shortcut(key) {
|
if let Some(command) = shortcut(key) {
|
||||||
self.execute(command);
|
self.execute(command);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
match self.design_key(key) {
|
||||||
|
Ok(true) => return,
|
||||||
|
Err(e) => {
|
||||||
|
self.message = format!("{e:#}");
|
||||||
|
self.open_dialog(
|
||||||
|
"Designer",
|
||||||
|
DialogKind::Message,
|
||||||
|
vec![Field::text("Fehler", self.message.clone())],
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
if self.program_focus() {
|
if self.program_focus() {
|
||||||
self.basic_events.push(Event::Key(key));
|
self.basic_events.push(Event::Key(key));
|
||||||
return;
|
return;
|
||||||
@@ -1783,7 +1924,7 @@ impl App {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
fn code_key(&mut self, key: KeyEvent) -> Result<()> {
|
fn code_key(&mut self, key: KeyEvent) -> Result<()> {
|
||||||
if self.editor_view().is_err() {
|
if self.mode == Mode::Designer || self.editor_view().is_err() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
self.editor_key(key)
|
self.editor_key(key)
|
||||||
@@ -1803,7 +1944,7 @@ impl App {
|
|||||||
let mut entries = menus[index]
|
let mut entries = menus[index]
|
||||||
.items
|
.items
|
||||||
.iter()
|
.iter()
|
||||||
.map(|i| (i.label.into(), i.command))
|
.map(|i| (i.label.clone(), i.command))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if menus[index].title == "Window" {
|
if menus[index].title == "Window" {
|
||||||
entries.extend(
|
entries.extend(
|
||||||
|
|||||||
@@ -101,7 +101,6 @@ impl Command {
|
|||||||
pub fn feature_phase(self) -> Option<u8> {
|
pub fn feature_phase(self) -> Option<u8> {
|
||||||
use Command::*;
|
use Command::*;
|
||||||
match self {
|
match self {
|
||||||
Events | Grid | Palette | MenuDesign | Toolbox | Tool(_) => Some(5),
|
|
||||||
NextStatement | AddWatch | InstantWatch | Watchpoint | DeleteWatch | DeleteWatches
|
NextStatement | AddWatch | InstantWatch | Watchpoint | DeleteWatch | DeleteWatches
|
||||||
| Trace | History | Breakpoint | ClearBreakpoints | BreakErrors | SetStatement
|
| Trace | History | Breakpoint | ClearBreakpoints | BreakErrors | SetStatement
|
||||||
| RunToCursor | Step | ProcedureStep | HistoryBack | HistoryForward => Some(6),
|
| RunToCursor | Step | ProcedureStep | HistoryBack | HistoryForward => Some(6),
|
||||||
@@ -113,7 +112,7 @@ impl Command {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Item {
|
pub struct Item {
|
||||||
/// & markiert das sichtbare Mnemonic, … einen Dialog.
|
/// & markiert das sichtbare Mnemonic, … einen Dialog.
|
||||||
pub label: &'static str,
|
pub label: String,
|
||||||
pub command: Option<Command>,
|
pub command: Option<Command>,
|
||||||
}
|
}
|
||||||
impl Item {
|
impl Item {
|
||||||
@@ -136,12 +135,12 @@ pub struct Menu {
|
|||||||
}
|
}
|
||||||
pub fn menus(designer: bool) -> Vec<Menu> {
|
pub fn menus(designer: bool) -> Vec<Menu> {
|
||||||
use Command::*;
|
use Command::*;
|
||||||
let item = |label, command| Item {
|
let item = |label: &str, command| Item {
|
||||||
label,
|
label: label.into(),
|
||||||
command: Some(command),
|
command: Some(command),
|
||||||
};
|
};
|
||||||
let sep = || Item {
|
let sep = || Item {
|
||||||
label: "────────",
|
label: "────────".into(),
|
||||||
command: None,
|
command: None,
|
||||||
};
|
};
|
||||||
let menu = |title, mnemonic, items| Menu {
|
let menu = |title, mnemonic, items| Menu {
|
||||||
@@ -211,27 +210,30 @@ pub fn menus(designer: bool) -> Vec<Menu> {
|
|||||||
result[2]
|
result[2]
|
||||||
.items
|
.items
|
||||||
.extend([sep(), item("&Menu Bar", MenuBar), item("&Grid Lines", Grid)]);
|
.extend([sep(), item("&Menu Bar", MenuBar), item("&Grid Lines", Grid)]);
|
||||||
result.push(menu(
|
result.push(menu("Tools", 't', {
|
||||||
"Tools",
|
let mut used = std::collections::BTreeSet::new();
|
||||||
't',
|
crate::designer::tools()
|
||||||
vec![
|
.into_iter()
|
||||||
item("&Check Box", Tool("CheckBox")),
|
.map(|(name, _, _)| {
|
||||||
item("C&ombo Box", Tool("ComboBox")),
|
let mut label = name.to_owned();
|
||||||
item("Command &Button", Tool("CommandButton")),
|
if let Some((at, c)) = name
|
||||||
item("&Dir List", Tool("DirListBox")),
|
.char_indices()
|
||||||
item("D&rive List", Tool("DriveListBox")),
|
.find(|(_, c)| !used.contains(&c.to_ascii_lowercase()))
|
||||||
item("&File List", Tool("FileListBox")),
|
{
|
||||||
item("Fr&ame", Tool("Frame")),
|
used.insert(c.to_ascii_lowercase());
|
||||||
item("&HScrollBar", Tool("HScrollBar")),
|
label.insert(at, '&');
|
||||||
item("&Label", Tool("Label")),
|
} else {
|
||||||
item("L&ist Box", Tool("ListBox")),
|
let c = ('1'..='9').find(|c| !used.contains(c)).unwrap();
|
||||||
item("O&ption Button", Tool("OptionButton")),
|
used.insert(c);
|
||||||
item("Pict&ure Box", Tool("PictureBox")),
|
label = format!("&{c} {label}");
|
||||||
item("&Text Box", Tool("TextBox")),
|
}
|
||||||
item("Ti&mer", Tool("Timer")),
|
Item {
|
||||||
item("&VScrollBar", Tool("VScrollBar")),
|
label,
|
||||||
],
|
command: Some(Tool(name)),
|
||||||
));
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
result.extend([
|
result.extend([
|
||||||
menu(
|
menu(
|
||||||
|
|||||||
1925
crates/tb-ide/src/designer.rs
Normal file
1925
crates/tb-ide/src/designer.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -51,6 +51,8 @@ impl View {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct Edit {
|
struct Edit {
|
||||||
content: Content,
|
content: Content,
|
||||||
|
design_ids: BTreeMap<(String, Option<i32>), u64>,
|
||||||
|
group: Option<(u64, Vec<DocumentId>)>,
|
||||||
views: Vec<(ViewId, View)>,
|
views: Vec<(ViewId, View)>,
|
||||||
}
|
}
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -64,6 +66,7 @@ pub struct Document {
|
|||||||
binary_source: Option<PathBuf>,
|
binary_source: Option<PathBuf>,
|
||||||
warnings: Vec<frm::BinaryWarning>,
|
warnings: Vec<frm::BinaryWarning>,
|
||||||
undo: Vec<Edit>,
|
undo: Vec<Edit>,
|
||||||
|
design_ids: BTreeMap<(String, Option<i32>), u64>,
|
||||||
}
|
}
|
||||||
impl Document {
|
impl Document {
|
||||||
pub fn path(&self) -> Option<&Path> {
|
pub fn path(&self) -> Option<&Path> {
|
||||||
@@ -257,6 +260,7 @@ impl Project {
|
|||||||
binary_source: read.binary.then_some(path),
|
binary_source: read.binary.then_some(path),
|
||||||
warnings: read.warnings,
|
warnings: read.warnings,
|
||||||
undo: Vec::new(),
|
undo: Vec::new(),
|
||||||
|
design_ids: BTreeMap::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
Ok(id)
|
Ok(id)
|
||||||
@@ -308,6 +312,7 @@ impl Project {
|
|||||||
binary_source: None,
|
binary_source: None,
|
||||||
warnings: Vec::new(),
|
warnings: Vec::new(),
|
||||||
undo: Vec::new(),
|
undo: Vec::new(),
|
||||||
|
design_ids: BTreeMap::new(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.manifest.lines.push(ProjectLine::File(path));
|
self.manifest.lines.push(ProjectLine::File(path));
|
||||||
@@ -344,6 +349,14 @@ impl Project {
|
|||||||
properties: BTreeMap::new(),
|
properties: BTreeMap::new(),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
};
|
};
|
||||||
|
let mut root = root;
|
||||||
|
for (property, value) in [("WIDTH", 40), ("HEIGHT", 15)] {
|
||||||
|
let p = tb_frontend::forms::property(root.class, property)
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
root.properties
|
||||||
|
.insert(p, tb_ui::forms::PropertyValue::Integer(value));
|
||||||
|
}
|
||||||
self.create(
|
self.create(
|
||||||
name,
|
name,
|
||||||
Content::Form(Box::new(FormFile::new("1.00", root, ""))),
|
Content::Form(Box::new(FormFile::new("1.00", root, ""))),
|
||||||
@@ -423,7 +436,82 @@ impl Project {
|
|||||||
pub fn view_code(&self, id: ViewId) -> Result<&str> {
|
pub fn view_code(&self, id: ViewId) -> Result<&str> {
|
||||||
Ok(self.document(self.view(id)?.document)?.code())
|
Ok(self.document(self.view(id)?.document)?.code())
|
||||||
}
|
}
|
||||||
|
pub fn design_ids(&mut self, id: DocumentId) -> Result<&BTreeMap<(String, Option<i32>), u64>> {
|
||||||
|
self.document(id)?;
|
||||||
|
let doc = self.documents.get_mut(&id).unwrap();
|
||||||
|
let mut keys = Vec::new();
|
||||||
|
if let Content::Form(f) = &doc.content {
|
||||||
|
fn visit(n: &FormNode, keys: &mut Vec<(String, Option<i32>)>) {
|
||||||
|
keys.push(crate::designer::node_key(n));
|
||||||
|
for child in &n.children {
|
||||||
|
visit(child, keys);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
visit(&f.root, &mut keys);
|
||||||
|
}
|
||||||
|
doc.design_ids.retain(|k, _| keys.contains(k));
|
||||||
|
for key in keys {
|
||||||
|
doc.design_ids.entry(key).or_insert_with(next_id);
|
||||||
|
}
|
||||||
|
Ok(&doc.design_ids)
|
||||||
|
}
|
||||||
|
/// Alle Kandidaten zuerst prüfen; danach eine gemeinsame Undo-Einheit.
|
||||||
|
pub(crate) fn commit_transaction(&mut self, changes: Vec<(DocumentId, Content)>) -> Result<()> {
|
||||||
|
let mut ids = Vec::new();
|
||||||
|
for (id, content) in &changes {
|
||||||
|
let old = self.document(*id)?;
|
||||||
|
ensure!(!ids.contains(id), "Doppeltes Dokument in Transaktion");
|
||||||
|
if let Content::Form(f) = content {
|
||||||
|
frm::read_text(&old.source_path.display().to_string(), &frm::write_text(f))?;
|
||||||
|
}
|
||||||
|
ids.push(*id);
|
||||||
|
}
|
||||||
|
let changes: Vec<_> = changes
|
||||||
|
.into_iter()
|
||||||
|
.filter(|(id, c)| self.documents[id].content != *c)
|
||||||
|
.collect();
|
||||||
|
let ids: Vec<_> = changes.iter().map(|(id, _)| *id).collect();
|
||||||
|
let group = next_id();
|
||||||
|
for (id, content) in changes {
|
||||||
|
self.commit_edit(id, content)?;
|
||||||
|
self.documents
|
||||||
|
.get_mut(&id)
|
||||||
|
.unwrap()
|
||||||
|
.undo
|
||||||
|
.last_mut()
|
||||||
|
.unwrap()
|
||||||
|
.group = Some((group, ids.clone()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub(crate) fn rekey_design_id(
|
||||||
|
&mut self,
|
||||||
|
id: DocumentId,
|
||||||
|
old: &(String, Option<i32>),
|
||||||
|
new: (String, Option<i32>),
|
||||||
|
stable: u64,
|
||||||
|
) {
|
||||||
|
let doc = self.documents.get_mut(&id).unwrap();
|
||||||
|
doc.design_ids.remove(old);
|
||||||
|
doc.design_ids.insert(new, stable);
|
||||||
|
}
|
||||||
|
pub(crate) fn rename_design_ids(
|
||||||
|
&mut self,
|
||||||
|
id: DocumentId,
|
||||||
|
old: &str,
|
||||||
|
new: &str,
|
||||||
|
before: &BTreeMap<(String, Option<i32>), u64>,
|
||||||
|
) {
|
||||||
|
let doc = self.documents.get_mut(&id).unwrap();
|
||||||
|
for ((name, index), value) in before {
|
||||||
|
if name.eq_ignore_ascii_case(old) {
|
||||||
|
doc.design_ids.remove(&(name.clone(), *index));
|
||||||
|
doc.design_ids.insert((new.to_uppercase(), *index), *value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
fn commit_edit(&mut self, id: DocumentId, content: Content) -> Result<()> {
|
fn commit_edit(&mut self, id: DocumentId, content: Content) -> Result<()> {
|
||||||
|
self.design_ids(id)?;
|
||||||
let old = self.document(id)?;
|
let old = self.document(id)?;
|
||||||
if old.content == content {
|
if old.content == content {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -456,6 +544,8 @@ impl Project {
|
|||||||
};
|
};
|
||||||
let undo = Edit {
|
let undo = Edit {
|
||||||
content: old.content.clone(),
|
content: old.content.clone(),
|
||||||
|
design_ids: old.design_ids.clone(),
|
||||||
|
group: None,
|
||||||
views: self
|
views: self
|
||||||
.views
|
.views
|
||||||
.iter()
|
.iter()
|
||||||
@@ -546,12 +636,31 @@ impl Project {
|
|||||||
self.commit_edit(id, Content::Form(form))
|
self.commit_edit(id, Content::Form(form))
|
||||||
}
|
}
|
||||||
pub fn undo(&mut self, id: DocumentId) -> Result<bool> {
|
pub fn undo(&mut self, id: DocumentId) -> Result<bool> {
|
||||||
|
let group = self.document(id)?.undo.last().and_then(|e| e.group.clone());
|
||||||
|
if let Some((number, ids)) = group {
|
||||||
|
ensure!(
|
||||||
|
ids.iter().all(|id| self.documents[id]
|
||||||
|
.undo
|
||||||
|
.last()
|
||||||
|
.and_then(|e| e.group.as_ref())
|
||||||
|
.is_some_and(|g| g.0 == number)),
|
||||||
|
"Spätere Änderungen in verbundenen Dokumenten zuerst rückgängig machen"
|
||||||
|
);
|
||||||
|
for id in ids {
|
||||||
|
self.undo_one(id)?;
|
||||||
|
}
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
self.undo_one(id)
|
||||||
|
}
|
||||||
|
fn undo_one(&mut self, id: DocumentId) -> Result<bool> {
|
||||||
self.document(id)?;
|
self.document(id)?;
|
||||||
let doc = self.documents.get_mut(&id).unwrap();
|
let doc = self.documents.get_mut(&id).unwrap();
|
||||||
let Some(edit) = doc.undo.pop() else {
|
let Some(edit) = doc.undo.pop() else {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
};
|
};
|
||||||
doc.content = edit.content;
|
doc.content = edit.content;
|
||||||
|
doc.design_ids = edit.design_ids;
|
||||||
doc.revision += 1;
|
doc.revision += 1;
|
||||||
for (id, view) in edit.views {
|
for (id, view) in edit.views {
|
||||||
if let Some(current) = self.views.get_mut(&id) {
|
if let Some(current) = self.views.get_mut(&id) {
|
||||||
|
|||||||
@@ -563,61 +563,14 @@ impl App {
|
|||||||
let v = self.editor_view()?;
|
let v = self.editor_view()?;
|
||||||
let view = self.project.view(v)?.clone();
|
let view = self.project.view(v)?.clone();
|
||||||
let id = view.document();
|
let id = view.document();
|
||||||
let code = self.project.document(id)?.code();
|
let addition = self.new_procedure_text(
|
||||||
let input = self.project.sources()?;
|
id,
|
||||||
let name = d.fields[0].string();
|
view.cursor,
|
||||||
for unit in &input.units {
|
&d.fields[0].string(),
|
||||||
let (module, _) = unit.parse(0, &mut Vec::new());
|
|
||||||
ensure!(
|
|
||||||
!module
|
|
||||||
.procs
|
|
||||||
.iter()
|
|
||||||
.any(|p| p.sig.name.eq_ignore_ascii_case(&name)),
|
|
||||||
"Prozedurname bereits im Projekt vorhanden"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let path = self
|
|
||||||
.project
|
|
||||||
.document(id)?
|
|
||||||
.source_path()
|
|
||||||
.display()
|
|
||||||
.to_string();
|
|
||||||
let unit = input
|
|
||||||
.units
|
|
||||||
.iter()
|
|
||||||
.find(|u| u.segments.iter().any(|s| s.file == path))
|
|
||||||
.ok_or_else(|| anyhow!("Keine Übersetzungseinheit"))?;
|
|
||||||
let first = match self.project.document(id)?.content() {
|
|
||||||
tb_vm::project_io::Content::Form(f) => {
|
|
||||||
tb_ui::frm::read_text(&path, &tb_ui::frm::write_text(f))?.code_line()
|
|
||||||
}
|
|
||||||
_ => 1,
|
|
||||||
};
|
|
||||||
let line =
|
|
||||||
first + code[..view.cursor].bytes().filter(|b| *b == b'\n').count() as u32;
|
|
||||||
let col = view.cursor - line_start(code, view.cursor);
|
|
||||||
let mut expanded = String::new();
|
|
||||||
let mut at = None;
|
|
||||||
for segment in &unit.segments {
|
|
||||||
for (i, text) in segment.text.split_inclusive('\n').enumerate() {
|
|
||||||
if segment.file == path && segment.first_line + i as u32 == line {
|
|
||||||
at = Some(
|
|
||||||
expanded.len() + col.min(text.trim_end_matches(['\n', '\r']).len()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
expanded.push_str(text);
|
|
||||||
if !text.ends_with('\n') {
|
|
||||||
expanded.push('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let addition = tb_frontend::editing::new_procedure(
|
|
||||||
&expanded,
|
|
||||||
at.unwrap_or(expanded.len()),
|
|
||||||
&name,
|
|
||||||
function,
|
function,
|
||||||
)
|
false,
|
||||||
.map_err(|e| anyhow!(e))?;
|
)?;
|
||||||
|
let code = self.project.document(id)?.code();
|
||||||
let end = code.len();
|
let end = code.len();
|
||||||
self.project.replace_text(id, end..end, &addition)?;
|
self.project.replace_text(id, end..end, &addition)?;
|
||||||
self.project.view_mut(v)?.cursor = end + 1;
|
self.project.view_mut(v)?.cursor = end + 1;
|
||||||
@@ -733,6 +686,75 @@ impl App {
|
|||||||
self.message = format!("{} Treffer ersetzt", ranges.len());
|
self.message = format!("{} Treffer ersetzt", ranges.len());
|
||||||
self.editor_scroll()
|
self.editor_scroll()
|
||||||
}
|
}
|
||||||
|
pub(crate) fn new_procedure_text(
|
||||||
|
&self,
|
||||||
|
id: DocumentId,
|
||||||
|
cursor: usize,
|
||||||
|
name: &str,
|
||||||
|
function: bool,
|
||||||
|
module_local: bool,
|
||||||
|
) -> Result<String> {
|
||||||
|
let code = self.project.document(id)?.code();
|
||||||
|
let input = self.project.sources()?;
|
||||||
|
for unit in &input.units {
|
||||||
|
if module_local
|
||||||
|
&& !unit.segments.iter().any(|s| {
|
||||||
|
s.file
|
||||||
|
== self
|
||||||
|
.project
|
||||||
|
.document(id)
|
||||||
|
.unwrap()
|
||||||
|
.source_path()
|
||||||
|
.display()
|
||||||
|
.to_string()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (module, _) = unit.parse(0, &mut Vec::new());
|
||||||
|
ensure!(
|
||||||
|
!module
|
||||||
|
.procs
|
||||||
|
.iter()
|
||||||
|
.any(|p| p.sig.name.eq_ignore_ascii_case(name)),
|
||||||
|
"Prozedurname bereits im Projekt vorhanden"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let path = self
|
||||||
|
.project
|
||||||
|
.document(id)?
|
||||||
|
.source_path()
|
||||||
|
.display()
|
||||||
|
.to_string();
|
||||||
|
let unit = input
|
||||||
|
.units
|
||||||
|
.iter()
|
||||||
|
.find(|u| u.segments.iter().any(|s| s.file == path))
|
||||||
|
.ok_or_else(|| anyhow!("Keine Übersetzungseinheit"))?;
|
||||||
|
let first = match self.project.document(id)?.content() {
|
||||||
|
tb_vm::project_io::Content::Form(f) => {
|
||||||
|
tb_ui::frm::read_text(&path, &tb_ui::frm::write_text(f))?.code_line()
|
||||||
|
}
|
||||||
|
_ => 1,
|
||||||
|
};
|
||||||
|
let line = first + code[..cursor].bytes().filter(|b| *b == b'\n').count() as u32;
|
||||||
|
let col = cursor - line_start(code, cursor);
|
||||||
|
let mut expanded = String::new();
|
||||||
|
let mut at = None;
|
||||||
|
for segment in &unit.segments {
|
||||||
|
for (i, text) in segment.text.split_inclusive('\n').enumerate() {
|
||||||
|
if segment.file == path && segment.first_line + i as u32 == line {
|
||||||
|
at = Some(expanded.len() + col.min(text.trim_end_matches(['\n', '\r']).len()));
|
||||||
|
}
|
||||||
|
expanded.push_str(text);
|
||||||
|
if !text.ends_with('\n') {
|
||||||
|
expanded.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tb_frontend::editing::new_procedure(&expanded, at.unwrap_or(expanded.len()), name, function)
|
||||||
|
.map_err(|e| anyhow!(e))
|
||||||
|
}
|
||||||
pub(crate) fn diagnostic_dialog(&mut self) {
|
pub(crate) fn diagnostic_dialog(&mut self) {
|
||||||
if !self.revision_current() {
|
if !self.revision_current() {
|
||||||
if let Err(e) = self.compile_current() {
|
if let Err(e) = self.compile_current() {
|
||||||
@@ -840,6 +862,9 @@ impl App {
|
|||||||
.then_some(self.editor.diagnostics.as_slice())
|
.then_some(self.editor.diagnostics.as_slice())
|
||||||
}
|
}
|
||||||
pub(crate) fn editor_position(&self) -> Option<(ViewId, DocumentId, usize)> {
|
pub(crate) fn editor_position(&self) -> Option<(ViewId, DocumentId, usize)> {
|
||||||
|
if self.mode == crate::app::Mode::Designer {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let v = self.editor_view().ok()?;
|
let v = self.editor_view().ok()?;
|
||||||
let view = self.project.view(v).ok()?;
|
let view = self.project.view(v).ok()?;
|
||||||
let code = self.project.document(view.document()).ok()?.code();
|
let code = self.project.document(view.document()).ok()?.code();
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ pub mod terminal;
|
|||||||
pub mod editor;
|
pub mod editor;
|
||||||
|
|
||||||
pub mod execution;
|
pub mod execution;
|
||||||
|
|
||||||
|
pub mod designer;
|
||||||
|
|||||||
@@ -147,15 +147,10 @@ impl App {
|
|||||||
let v = self.project.view(view).unwrap();
|
let v = self.project.view(view).unwrap();
|
||||||
let doc = self.project.document(v.document()).unwrap();
|
let doc = self.project.document(v.document()).unwrap();
|
||||||
if self.mode == Mode::Designer
|
if self.mode == Mode::Designer
|
||||||
&& active
|
&& Some(v.document()) == self.designer.document
|
||||||
&& matches!(doc.content(), tb_vm::project_io::Content::Form(_))
|
&& matches!(doc.content(), tb_vm::project_io::Content::Form(_))
|
||||||
{
|
{
|
||||||
put(
|
self.design_render(f, inner);
|
||||||
f,
|
|
||||||
inner,
|
|
||||||
"Formular · Designerwerkzeuge folgen in Change 05",
|
|
||||||
content_style,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
let expansion = self.editor.expansions.get(&view);
|
let expansion = self.editor.expansions.get(&view);
|
||||||
let code = expansion.map(String::as_str).unwrap_or(doc.code());
|
let code = expansion.map(String::as_str).unwrap_or(doc.code());
|
||||||
@@ -334,6 +329,9 @@ impl App {
|
|||||||
self.hits.push((r, Hit::ProjectMember(row)));
|
self.hits.push((r, Hit::ProjectMember(row)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
kind @ (WindowKind::Toolbox | WindowKind::Palette | WindowKind::MenuDesign) => {
|
||||||
|
self.design_tool_render(f, kind, inner)
|
||||||
|
}
|
||||||
WindowKind::Output => {
|
WindowKind::Output => {
|
||||||
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), inner)
|
f.render_widget(tb_ui::screen::ScreenWidget(self.session.screen()), inner)
|
||||||
}
|
}
|
||||||
@@ -400,22 +398,27 @@ impl App {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if self.mode == Mode::Designer && self.properties {
|
if self.mode == Mode::Designer && self.properties {
|
||||||
|
let (left, geometry) = self.design_bar();
|
||||||
|
let width = (geometry.width() as u16 + 1).min(area.width);
|
||||||
put(
|
put(
|
||||||
f,
|
f,
|
||||||
Rect::new(0, 0, area.width, 1),
|
Rect::new(0, 0, area.width - width, 1),
|
||||||
format!(
|
left,
|
||||||
"Property: [Caption]↓ Value: […]↓ │ Spalte,Zeile │ BxH{}",
|
style(self, 0),
|
||||||
if self.value_focus {
|
);
|
||||||
" · Value aktiv"
|
put(
|
||||||
} else {
|
f,
|
||||||
""
|
Rect::new(area.width - width, 0, width, 1),
|
||||||
}
|
geometry,
|
||||||
),
|
|
||||||
style(self, 0),
|
style(self, 0),
|
||||||
);
|
);
|
||||||
self.hits.push((
|
self.hits.push((
|
||||||
Rect::new(0, 0, area.width, 1),
|
Rect::new(0, 0, area.width / 2, 1),
|
||||||
Hit::Command(Command::MenuBar),
|
Hit::DesignAction("property"),
|
||||||
|
));
|
||||||
|
self.hits.push((
|
||||||
|
Rect::new(area.width / 2, 0, area.width - area.width / 2, 1),
|
||||||
|
Hit::DesignAction("value"),
|
||||||
));
|
));
|
||||||
} else {
|
} else {
|
||||||
put(
|
put(
|
||||||
|
|||||||
@@ -140,7 +140,8 @@ fn start_snapshot_and_reference_menus_have_all_commands() {
|
|||||||
let name = item.text();
|
let name = item.text();
|
||||||
assert!(
|
assert!(
|
||||||
reference.contains(name.trim_end_matches('…'))
|
reference.contains(name.trim_end_matches('…'))
|
||||||
|| item.command == Some(Command::Diagnostics),
|
|| item.command == Some(Command::Diagnostics)
|
||||||
|
|| matches!(item.command, Some(Command::Tool(_))),
|
||||||
"Nicht in Referenz: {name}"
|
"Nicht in Referenz: {name}"
|
||||||
);
|
);
|
||||||
assert!(item.mnemonic().is_some());
|
assert!(item.mnemonic().is_some());
|
||||||
|
|||||||
926
crates/tb-ide/tests/designer.rs
Normal file
926
crates/tb-ide/tests/designer.rs
Normal file
@@ -0,0 +1,926 @@
|
|||||||
|
use crossterm::event::{
|
||||||
|
Event, KeyCode as K, KeyEvent, KeyModifiers as M, MouseButton as MB, MouseEvent,
|
||||||
|
MouseEventKind as MK,
|
||||||
|
};
|
||||||
|
use ratatui::{backend::TestBackend, layout::Rect, Terminal};
|
||||||
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::PathBuf,
|
||||||
|
sync::atomic::{AtomicUsize, Ordering},
|
||||||
|
};
|
||||||
|
use tb_frontend::forms::{self, ObjectClass as C};
|
||||||
|
use tb_ide::{
|
||||||
|
app::{App, DialogKind, Field, Hit, Mode, WindowKind},
|
||||||
|
commands::Command,
|
||||||
|
designer::{self, DesignId},
|
||||||
|
documents::{Destination, Project},
|
||||||
|
};
|
||||||
|
use tb_ui::{forms::PropertyValue as V, frm};
|
||||||
|
struct Temp(PathBuf);
|
||||||
|
impl Temp {
|
||||||
|
fn new() -> Self {
|
||||||
|
static N: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
let p = std::env::temp_dir().join(format!(
|
||||||
|
"tb-designer-{}-{}",
|
||||||
|
std::process::id(),
|
||||||
|
N.fetch_add(1, Ordering::Relaxed)
|
||||||
|
));
|
||||||
|
fs::create_dir_all(&p).unwrap();
|
||||||
|
Self(p.canonicalize().unwrap())
|
||||||
|
}
|
||||||
|
fn app(&self) -> App {
|
||||||
|
let mut a = App::new(&self.0, self.0.join("options"), (100, 30)).unwrap();
|
||||||
|
a.options.syntax_checking = false;
|
||||||
|
a.execute(Command::NewForm);
|
||||||
|
assert_eq!(a.mode, Mode::Designer);
|
||||||
|
a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Temp {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = fs::remove_dir_all(&self.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn key(a: &mut App, k: K, m: M) {
|
||||||
|
a.handle(Event::Key(KeyEvent::new(k, m)));
|
||||||
|
}
|
||||||
|
fn plain(a: &mut App, k: K) {
|
||||||
|
key(a, k, M::NONE);
|
||||||
|
}
|
||||||
|
fn root(a: &mut App) -> DesignId {
|
||||||
|
a.design_objects().unwrap()[0].0
|
||||||
|
}
|
||||||
|
fn node(a: &mut App, id: DesignId) -> frm::FormNode {
|
||||||
|
a.design_objects()
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.find(|(i, _)| *i == id)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
}
|
||||||
|
fn number(a: &mut App, id: DesignId, p: &str) -> i32 {
|
||||||
|
match designer::value(&node(a, id), p).unwrap() {
|
||||||
|
V::Integer(n) => n,
|
||||||
|
_ => panic!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn draw(a: &mut App) -> String {
|
||||||
|
let mut t = Terminal::new(TestBackend::new(100, 30)).unwrap();
|
||||||
|
t.draw(|f| a.render(f)).unwrap();
|
||||||
|
t.backend()
|
||||||
|
.buffer()
|
||||||
|
.content
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.symbol())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
fn mouse(a: &mut App, kind: MK, x: u16, y: u16, m: M) {
|
||||||
|
a.handle(Event::Mouse(MouseEvent {
|
||||||
|
kind,
|
||||||
|
column: x,
|
||||||
|
row: y,
|
||||||
|
modifiers: m,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
fn click(a: &mut App, x: u16, y: u16) {
|
||||||
|
mouse(a, MK::Down(MB::Left), x, y, M::NONE);
|
||||||
|
mouse(a, MK::Up(MB::Left), x, y, M::NONE);
|
||||||
|
}
|
||||||
|
fn property(a: &mut App, p: &str, v: &str) {
|
||||||
|
a.design_property(p, v).unwrap();
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn all_metadata_tools_persist_and_preview_matches_runtime_without_events() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
property(&mut a, "WIDTH", "70");
|
||||||
|
property(&mut a, "HEIGHT", "24");
|
||||||
|
for (i, (_, class, style)) in designer::tools().iter().enumerate() {
|
||||||
|
a.design_place(
|
||||||
|
*class,
|
||||||
|
*style,
|
||||||
|
root,
|
||||||
|
Rect::new(1 + (i % 8) as u16 * 8, 1 + (i / 8) as u16 * 6, 6, 3),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
let timer = a
|
||||||
|
.design_objects()
|
||||||
|
.unwrap()
|
||||||
|
.into_iter()
|
||||||
|
.find(|(_, n)| n.class == C::Timer)
|
||||||
|
.unwrap()
|
||||||
|
.0;
|
||||||
|
a.design_select(timer, false).unwrap();
|
||||||
|
property(&mut a, "INTERVAL", "1");
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
a.project.replace_text(doc,0..0,"SUB Form_Load\nPRINT \"NO PREVIEW EXECUTION\"\nEND SUB\nSUB Timer1_Timer\nPRINT \"NO TIMER\"\nEND SUB\n").unwrap();
|
||||||
|
let form = a.design_form().unwrap().clone();
|
||||||
|
let (mut model, screen) = designer::preview(&form, (100, 30)).unwrap();
|
||||||
|
assert!(model.events.is_empty());
|
||||||
|
assert!(model.next_deadline().is_none());
|
||||||
|
model.timers(10_000);
|
||||||
|
assert!(model.next_event().is_none());
|
||||||
|
assert!(a.session.vm.is_none());
|
||||||
|
let mut runtime = tb_ui::forms::FormsModel::new(form.catalog().objects, 100, 30);
|
||||||
|
form.apply(&mut runtime).unwrap();
|
||||||
|
runtime.show(0, false).unwrap();
|
||||||
|
let mut expected = tb_runtime::screen::TextScreen::with_size(100, 30);
|
||||||
|
runtime.render(&mut expected);
|
||||||
|
for y in 1..=30 {
|
||||||
|
for x in 1..=100 {
|
||||||
|
assert_eq!(screen.cell(y, x), expected.cell(y, x));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let path = t.0.join("designed.frm");
|
||||||
|
a.project
|
||||||
|
.save_file(doc, Some(&Destination::new(&path)))
|
||||||
|
.unwrap();
|
||||||
|
let reopened = Project::open(&path, vec![]).unwrap();
|
||||||
|
let tb_vm::project_io::Content::Form(f) =
|
||||||
|
reopened.document(reopened.members()[0]).unwrap().content()
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_equivalent(&f.root, &form.root);
|
||||||
|
assert_eq!(f.root.children.len(), designer::tools().len());
|
||||||
|
tb_vm::project_io::load_program(&path).unwrap();
|
||||||
|
let listed = a
|
||||||
|
.menus()
|
||||||
|
.into_iter()
|
||||||
|
.find(|m| m.title == "Tools")
|
||||||
|
.unwrap()
|
||||||
|
.items;
|
||||||
|
assert_eq!(listed.len(), designer::tools().len());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn container_multiselect_keyboard_drag_resize_and_undo() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
let frame = a
|
||||||
|
.design_place(C::Frame, 0, root, Rect::new(3, 2, 28, 10))
|
||||||
|
.unwrap();
|
||||||
|
let b1 = a
|
||||||
|
.design_place(C::CommandButton, 0, frame, Rect::new(2, 2, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let b2 = a
|
||||||
|
.design_place(C::TextBox, 0, frame, Rect::new(12, 2, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
a.design_select(b1, false).unwrap();
|
||||||
|
a.design_select(b2, true).unwrap();
|
||||||
|
key(&mut a, K::Right, M::CONTROL);
|
||||||
|
assert_eq!(number(&mut a, b1, "LEFT"), 7);
|
||||||
|
assert_eq!(number(&mut a, b2, "LEFT"), 17);
|
||||||
|
assert_eq!(number(&mut a, frame, "LEFT"), 3);
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
assert_eq!(number(&mut a, b1, "LEFT"), 2);
|
||||||
|
assert_eq!(number(&mut a, b2, "LEFT"), 12);
|
||||||
|
a.design_select(b1, false).unwrap();
|
||||||
|
key(&mut a, K::Right, M::SHIFT);
|
||||||
|
assert_eq!(number(&mut a, b1, "WIDTH"), 9);
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
draw(&mut a);
|
||||||
|
let r = a
|
||||||
|
.designer
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(id, _)| *id == b1)
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
assert_eq!(r.x, a.designer.viewport.x + 3 + 2);
|
||||||
|
assert_eq!(r.y, a.designer.viewport.y + 2 + 2);
|
||||||
|
mouse(&mut a, MK::Down(MB::Left), r.x + 2, r.y, M::NONE);
|
||||||
|
mouse(&mut a, MK::Drag(MB::Left), r.x + 3, r.y + 1, M::NONE);
|
||||||
|
mouse(&mut a, MK::Up(MB::Left), r.x + 3, r.y + 1, M::NONE);
|
||||||
|
assert_eq!(number(&mut a, b1, "LEFT"), 3);
|
||||||
|
assert_eq!(number(&mut a, b1, "TOP"), 3);
|
||||||
|
draw(&mut a);
|
||||||
|
let r = a
|
||||||
|
.designer
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(id, _)| *id == b1)
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
mouse(
|
||||||
|
&mut a,
|
||||||
|
MK::Down(MB::Left),
|
||||||
|
r.right() - 1,
|
||||||
|
r.bottom() - 1,
|
||||||
|
M::NONE,
|
||||||
|
);
|
||||||
|
mouse(&mut a, MK::Up(MB::Left), r.right(), r.bottom(), M::NONE);
|
||||||
|
assert_eq!(number(&mut a, b1, "WIDTH"), 9);
|
||||||
|
assert_eq!(number(&mut a, b1, "HEIGHT"), 3);
|
||||||
|
let before = a.design_form().unwrap().clone();
|
||||||
|
assert!(a.design_geometry(-100, 0, false).is_err());
|
||||||
|
assert_eq!(*a.design_form().unwrap(), before);
|
||||||
|
a.design_select(b1, false).unwrap();
|
||||||
|
plain(&mut a, K::Tab);
|
||||||
|
assert_eq!(
|
||||||
|
a.designer.selections[&a.design_document().unwrap()],
|
||||||
|
vec![b2]
|
||||||
|
);
|
||||||
|
plain(&mut a, K::BackTab);
|
||||||
|
assert_eq!(
|
||||||
|
a.designer.selections[&a.design_document().unwrap()],
|
||||||
|
vec![b1]
|
||||||
|
);
|
||||||
|
draw(&mut a);
|
||||||
|
let r = a
|
||||||
|
.designer
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(id, _)| *id == b2)
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
mouse(&mut a, MK::Down(MB::Left), r.x + 1, r.y, M::CONTROL);
|
||||||
|
mouse(&mut a, MK::Up(MB::Left), r.x + 1, r.y, M::CONTROL);
|
||||||
|
assert_eq!(
|
||||||
|
a.designer.selections[&a.design_document().unwrap()].len(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn property_bar_types_readonly_and_no_status_line() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::CommandButton, 0, root, Rect::new(2, 3, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let before = a.design_form().unwrap().clone();
|
||||||
|
for (p, v) in [
|
||||||
|
("HEIGHT", "0"),
|
||||||
|
("WIDTH", "x"),
|
||||||
|
("BACKCOLOR", "16"),
|
||||||
|
("ENABLED", "yes"),
|
||||||
|
("PARENT", "anything"),
|
||||||
|
] {
|
||||||
|
let e = a.design_property(p, v).unwrap_err();
|
||||||
|
assert!(format!("{e:#}").contains(p));
|
||||||
|
assert_eq!(*a.design_form().unwrap(), before);
|
||||||
|
}
|
||||||
|
a.designer.property = forms::properties(C::CommandButton)
|
||||||
|
.iter()
|
||||||
|
.position(|p| p.name == "CAPTION")
|
||||||
|
.unwrap();
|
||||||
|
plain(&mut a, K::F(2));
|
||||||
|
assert!(a.value_focus);
|
||||||
|
key(&mut a, K::Char('a'), M::CONTROL);
|
||||||
|
for c in "Hallo".chars() {
|
||||||
|
plain(&mut a, K::Char(c));
|
||||||
|
}
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert_eq!(
|
||||||
|
designer::value(&node(&mut a, b), "CAPTION"),
|
||||||
|
Some(V::String("Hallo".into()))
|
||||||
|
);
|
||||||
|
let screen = draw(&mut a);
|
||||||
|
assert!(screen.contains("2,3"));
|
||||||
|
assert!(screen.contains("8×2"));
|
||||||
|
assert!(!screen.contains("Shift+F1=Help"));
|
||||||
|
plain(&mut a, K::F(10));
|
||||||
|
assert!(!a.properties && a.menu.is_some());
|
||||||
|
plain(&mut a, K::F(10));
|
||||||
|
assert!(a.properties && a.menu.is_none());
|
||||||
|
let spin = a
|
||||||
|
.design_place(C::Spin, 0, root, Rect::new(15, 3, 1, 2))
|
||||||
|
.unwrap();
|
||||||
|
assert!(a.design_property("WIDTH", "4").is_err());
|
||||||
|
property(&mut a, "STYLE", "1");
|
||||||
|
assert_eq!(number(&mut a, spin, "WIDTH"), 2);
|
||||||
|
assert_eq!(number(&mut a, spin, "HEIGHT"), 1);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn clipboard_children_arrays_and_delete_preserve_code() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
let frame = a
|
||||||
|
.design_place(C::Frame, 0, root, Rect::new(1, 1, 20, 10))
|
||||||
|
.unwrap();
|
||||||
|
let b = a
|
||||||
|
.design_place(C::CommandButton, 0, frame, Rect::new(1, 1, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
property(&mut a, "INDEX", "0");
|
||||||
|
a.design_event(b, "CLICK").unwrap();
|
||||||
|
a.design_event(b, "CLICK").unwrap();
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
let code = a.design_form().unwrap().code.clone();
|
||||||
|
a.design_select(frame, false).unwrap();
|
||||||
|
a.execute(Command::Copy);
|
||||||
|
a.design_select(root, false).unwrap();
|
||||||
|
a.execute(Command::Paste);
|
||||||
|
assert!(a.dialog.is_none(), "{:?}", a.dialog);
|
||||||
|
let f = a.design_form().unwrap();
|
||||||
|
assert_eq!(f.root.children.len(), 2);
|
||||||
|
let copied = &f.root.children[1];
|
||||||
|
assert_eq!(copied.children.len(), 1);
|
||||||
|
assert_ne!(copied.children[0].name, f.root.children[0].children[0].name);
|
||||||
|
assert_eq!(designer::node_key(&copied.children[0]).1, Some(0));
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
assert_eq!(a.design_form().unwrap().root.children.len(), 1);
|
||||||
|
a.design_select(frame, false).unwrap();
|
||||||
|
a.execute(Command::Cut);
|
||||||
|
assert_eq!(a.design_form().unwrap().code, code);
|
||||||
|
assert!(a.message.contains("ungebunden"));
|
||||||
|
plain(&mut a, K::Esc);
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
assert_eq!(a.design_form().unwrap().root.children.len(), 1);
|
||||||
|
assert_eq!(node(&mut a, b).class, C::CommandButton);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn bound_rename_across_documents_is_atomic_and_undo_restores_all() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::CommandButton, 0, root, Rect::new(2, 2, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let name = node(&mut a, b).name;
|
||||||
|
let form = a.design_form().unwrap().root.name.clone();
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
let code=format!("SUB {name}_Click\n{name}.Caption = \"{name} unchanged\"\n' {name}.Caption unchanged\nEND SUB\n");
|
||||||
|
a.project.replace_text(doc, 0..0, &code).unwrap();
|
||||||
|
let other = a.project.new_module("Other").unwrap();
|
||||||
|
let external = format!("PRINT {form}!{name}.Caption\n");
|
||||||
|
a.project.replace_text(other, 0..0, &external).unwrap();
|
||||||
|
a.design_rename("GoButton").unwrap();
|
||||||
|
assert_eq!(node(&mut a, b).name, "GoButton");
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(doc)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("SUB GoButton_CLICK"));
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(doc)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("GoButton.Caption"));
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(doc)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains(&format!("\"{name} unchanged\"")));
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(other)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("!GoButton.Caption"));
|
||||||
|
a.project.undo(other).unwrap();
|
||||||
|
assert_eq!(a.project.document(other).unwrap().code(), external);
|
||||||
|
assert_eq!(a.project.document(doc).unwrap().code(), code);
|
||||||
|
assert_eq!(node(&mut a, b).name, name);
|
||||||
|
let conflict = a
|
||||||
|
.design_place(C::TextBox, 0, root, Rect::new(14, 2, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let conflict_name = node(&mut a, conflict).name;
|
||||||
|
a.design_select(b, false).unwrap();
|
||||||
|
let before = a.design_form().unwrap().clone();
|
||||||
|
assert!(a.design_rename(&conflict_name).is_err());
|
||||||
|
assert_eq!(*a.design_form().unwrap(), before);
|
||||||
|
a.project.replace_text(other, 0..0, "IF \n").unwrap();
|
||||||
|
assert!(a.design_rename("Unsafe").is_err());
|
||||||
|
assert_eq!(*a.design_form().unwrap(), before);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn event_array_signature_twice_and_shared_form_identity() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::TextBox, 0, root, Rect::new(2, 2, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
property(&mut a, "INDEX", "0");
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
let structure = a.design_form().unwrap().root.clone();
|
||||||
|
a.design_event(b, "DRAGOVER").unwrap();
|
||||||
|
assert_eq!(a.mode, Mode::Environment);
|
||||||
|
let code = a.project.document(doc).unwrap().code().to_owned();
|
||||||
|
assert!(code.contains(
|
||||||
|
"Index AS INTEGER, SOURCE AS CONTROL, X AS SINGLE, Y AS SINGLE, STATE AS INTEGER"
|
||||||
|
));
|
||||||
|
a.compile_current().unwrap();
|
||||||
|
let view = if let WindowKind::Code(v) = a.active_window().unwrap().kind {
|
||||||
|
v
|
||||||
|
} else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let cursor = a.project.view(view).unwrap().cursor;
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
assert_eq!(a.mode, Mode::Designer);
|
||||||
|
assert_eq!(a.active_document(), Some(doc));
|
||||||
|
assert_eq!(a.design_form().unwrap().root, structure);
|
||||||
|
assert_eq!(a.designer.selections[&doc], vec![b]);
|
||||||
|
a.design_event(b, "DRAGOVER").unwrap();
|
||||||
|
assert_eq!(a.project.document(doc).unwrap().code(), code);
|
||||||
|
assert_eq!(a.project.view(view).unwrap().cursor, cursor);
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
plain(&mut a, K::F(12));
|
||||||
|
assert!(matches!(
|
||||||
|
a.dialog.as_ref().unwrap().kind,
|
||||||
|
DialogKind::DesignObject(_)
|
||||||
|
));
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert!(matches!(
|
||||||
|
a.dialog.as_ref().unwrap().kind,
|
||||||
|
DialogKind::DesignEvent(_)
|
||||||
|
));
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert_eq!(a.mode, Mode::Environment);
|
||||||
|
assert!(a.dialog.is_none());
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn menu_hierarchy_arrays_shortcuts_and_palette_use_events() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
let title = a.design_place(C::Menu, 0, root, Rect::default()).unwrap();
|
||||||
|
property(&mut a, "CAPTION", "&File");
|
||||||
|
let sub = a.design_place(C::Menu, 0, title, Rect::default()).unwrap();
|
||||||
|
property(&mut a, "CAPTION", "&Open");
|
||||||
|
property(&mut a, "INDEX", "0");
|
||||||
|
property(&mut a, "SHORTCUT", "F3");
|
||||||
|
let second = a.design_place(C::Menu, 0, title, Rect::default()).unwrap();
|
||||||
|
property(&mut a, "CAPTION", "-");
|
||||||
|
property(&mut a, "SEPARATOR", "true");
|
||||||
|
assert!(a.design_property("CHECKED", "true").is_err());
|
||||||
|
a.design_select(sub, false).unwrap();
|
||||||
|
a.design_menu_move("down").unwrap();
|
||||||
|
a.design_menu_move("up").unwrap();
|
||||||
|
assert!(a.design_property("SHORTCUT", "F11").is_err());
|
||||||
|
a.execute(Command::MenuDesign);
|
||||||
|
draw(&mut a);
|
||||||
|
assert_eq!(a.active_window().unwrap().kind, WindowKind::MenuDesign);
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert!(matches!(
|
||||||
|
a.dialog.as_ref().unwrap().kind,
|
||||||
|
DialogKind::DesignMenu(_)
|
||||||
|
));
|
||||||
|
a.dialog.as_mut().unwrap().fields[2] = Field::text("Tag", "kept");
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert!(a.dialog.is_none(), "{:?}", a.dialog);
|
||||||
|
a.design_select(root, false).unwrap();
|
||||||
|
a.execute(Command::Palette);
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
a.dialog.as_mut().unwrap().fields[0] =
|
||||||
|
Field::choice("ForeColor", (0..16).map(|n| n.to_string()).collect(), 4);
|
||||||
|
a.dialog.as_mut().unwrap().fields[1] =
|
||||||
|
Field::choice("BackColor", (0..16).map(|n| n.to_string()).collect(), 1);
|
||||||
|
a.dialog.as_mut().unwrap().fields[2] = Field::toggle("Form", true);
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert_eq!(number(&mut a, root, "FORECOLOR"), 4);
|
||||||
|
assert_eq!(number(&mut a, root, "BACKCOLOR"), 1);
|
||||||
|
let path = t.0.join("menu.frm");
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
a.project
|
||||||
|
.save_file(doc, Some(&Destination::new(&path)))
|
||||||
|
.unwrap();
|
||||||
|
let reopened = frm::read_text("menu", &fs::read_to_string(&path).unwrap()).unwrap();
|
||||||
|
assert_equivalent(&reopened.root, &a.design_form().unwrap().root);
|
||||||
|
tb_vm::project_io::load_program(&path).unwrap();
|
||||||
|
assert_eq!(node(&mut a, second).name, "Menu3");
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn toolbox_double_click_tool_drag_grid_and_window_form_selection() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let root = root(&mut a);
|
||||||
|
a.execute(Command::Toolbox);
|
||||||
|
draw(&mut a);
|
||||||
|
let r = a
|
||||||
|
.hits
|
||||||
|
.iter()
|
||||||
|
.find_map(|(r, h)| matches!(h, Hit::DesignTool(2)).then_some(*r))
|
||||||
|
.unwrap();
|
||||||
|
click(&mut a, r.x + 1, r.y);
|
||||||
|
click(&mut a, r.x + 1, r.y);
|
||||||
|
assert_eq!(a.design_form().unwrap().root.children.len(), 1);
|
||||||
|
a.execute(Command::Tool("TextBox"));
|
||||||
|
draw(&mut a);
|
||||||
|
let r = a
|
||||||
|
.designer
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(i, _)| *i == root)
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
mouse(&mut a, MK::Down(MB::Left), r.x + 15, r.y + 3, M::NONE);
|
||||||
|
mouse(&mut a, MK::Drag(MB::Left), r.x + 22, r.y + 4, M::NONE);
|
||||||
|
mouse(&mut a, MK::Up(MB::Left), r.x + 22, r.y + 4, M::NONE);
|
||||||
|
assert!(a.dialog.is_none(), "{:?}", a.dialog);
|
||||||
|
let textbox = a.design_form().unwrap().root.children.last().unwrap();
|
||||||
|
assert_eq!(textbox.class, C::TextBox);
|
||||||
|
assert_eq!(designer::value(textbox, "LEFT"), Some(V::Integer(15)));
|
||||||
|
assert_eq!(designer::value(textbox, "WIDTH"), Some(V::Integer(8)));
|
||||||
|
a.execute(Command::Grid);
|
||||||
|
assert!(a.designer.grid);
|
||||||
|
assert!(draw(&mut a).contains('·'));
|
||||||
|
a.execute(Command::Grid);
|
||||||
|
assert!(!a.designer.grid);
|
||||||
|
let first = a.design_document().unwrap();
|
||||||
|
a.execute(Command::NewForm);
|
||||||
|
let second = a.design_document().unwrap();
|
||||||
|
assert_ne!(first, second);
|
||||||
|
let command=a.window_commands().into_iter().find(|(_,c)|matches!(c,Command::FocusWindow(id) if a.windows.iter().any(|w|w.id==*id&&matches!(w.kind,WindowKind::Code(v) if a.project.view(v).unwrap().document()==first)))).unwrap().1;
|
||||||
|
a.execute(command);
|
||||||
|
assert_eq!(a.design_document().unwrap(), first);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn binary_designer_import_requires_text_target_and_compiles() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let bytes: Vec<u8> = include_str!("../../tb-ui/tests/data/new.frm.hex")
|
||||||
|
.split_whitespace()
|
||||||
|
.map(|s| u8::from_str_radix(s, 16).unwrap())
|
||||||
|
.collect();
|
||||||
|
let source = t.0.join("binary.frm");
|
||||||
|
fs::write(&source, &bytes).unwrap();
|
||||||
|
let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap();
|
||||||
|
a.load_initial_project(source.clone()).unwrap();
|
||||||
|
a.execute(Command::Form);
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
let root = root(&mut a);
|
||||||
|
a.design_select(root, false).unwrap();
|
||||||
|
property(&mut a, "CAPTION", "Imported and edited");
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
assert!(a.project.save_file(doc, None).is_err());
|
||||||
|
let path = t.0.join("text.frm");
|
||||||
|
a.project
|
||||||
|
.save_file(doc, Some(&Destination::new(&path)))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(fs::read(&source).unwrap(), bytes);
|
||||||
|
tb_vm::project_io::load_program(&path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_equivalent(a: &frm::FormNode, b: &frm::FormNode) {
|
||||||
|
assert_eq!(
|
||||||
|
(a.class, &a.name, designer::node_key(a)),
|
||||||
|
(b.class, &b.name, designer::node_key(b))
|
||||||
|
);
|
||||||
|
for p in forms::properties(a.class) {
|
||||||
|
assert_eq!(
|
||||||
|
designer::value(a, p.name),
|
||||||
|
designer::value(b, p.name),
|
||||||
|
"{}.{}",
|
||||||
|
a.name,
|
||||||
|
p.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(a.children.len(), b.children.len());
|
||||||
|
for (a, b) in a.children.iter().zip(&b.children) {
|
||||||
|
assert_equivalent(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rename_preserves_shadowed_names_and_handles_form_aliases_and_include_calls() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::CommandButton, 0, r, Rect::new(2, 2, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
let name = node(&mut a, b).name;
|
||||||
|
let form = node(&mut a, r).name;
|
||||||
|
let code=format!("SUB {name}_Click\nDIM {name}_Click AS INTEGER\n{name}_Click = 7\nPRINT {name}_Click\nEND SUB\nSUB {form}_Load\n{name}.Caption=\"Hi\"\nEND SUB\n");
|
||||||
|
a.project.replace_text(doc, 0..0, &code).unwrap();
|
||||||
|
// In eine andere Datei gebundene Aufrufe müssen mit umbenannt werden.
|
||||||
|
let other = a.project.new_module("Calls").unwrap();
|
||||||
|
let call = format!("DECLARE SUB {name}_Click ()\nCALL {name}_Click\n");
|
||||||
|
a.project.replace_text(other, 0..0, &call).unwrap();
|
||||||
|
a.design_rename("Launch").unwrap();
|
||||||
|
let changed = a.project.document(doc).unwrap().code();
|
||||||
|
assert!(changed.contains(&format!("DIM {name}_Click AS INTEGER")));
|
||||||
|
assert!(changed.contains(&format!("PRINT {name}_Click")));
|
||||||
|
assert!(changed.contains("SUB Launch_CLICK"));
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(other)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("CALL Launch_CLICK"));
|
||||||
|
a.design_select(r, false).unwrap();
|
||||||
|
a.design_event(r, "LOAD").unwrap();
|
||||||
|
assert!(!a
|
||||||
|
.project
|
||||||
|
.document(doc)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("SUB Form_LOAD"));
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
a.design_select(r, false).unwrap();
|
||||||
|
a.design_rename("MainForm").unwrap();
|
||||||
|
assert_eq!(node(&mut a, r).name, "MainForm");
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(doc)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("SUB MainForm_LOAD"));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn existing_default_typed_event_and_all_metadata_signatures_compile() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::CommandButton, 0, r, Rect::new(2, 2, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
property(&mut a, "INDEX", "0");
|
||||||
|
let name = node(&mut a, b).name;
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
let code = format!("DEFINT I,K,S\nSUB {name}_KeyDown (Index, KeyCode, Shift)\nEND SUB\n");
|
||||||
|
a.project.replace_text(doc, 0..0, &code).unwrap();
|
||||||
|
a.design_event(b, "KEYDOWN").unwrap();
|
||||||
|
assert_eq!(a.project.document(doc).unwrap().code(), code);
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
for event in forms::events(C::CommandButton) {
|
||||||
|
a.design_event(b, event).unwrap();
|
||||||
|
a.compile_current().unwrap();
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn all_eight_handles_resize_transactionally_and_preserve_identity_after_structure_edits() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::TextBox, 0, r, Rect::new(8, 5, 10, 5))
|
||||||
|
.unwrap();
|
||||||
|
for (horizontal, vertical) in [
|
||||||
|
(-1, -1),
|
||||||
|
(0, -1),
|
||||||
|
(1, -1),
|
||||||
|
(-1, 0),
|
||||||
|
(1, 0),
|
||||||
|
(-1, 1),
|
||||||
|
(0, 1),
|
||||||
|
(1, 1),
|
||||||
|
] {
|
||||||
|
a.design_select(b, false).unwrap();
|
||||||
|
draw(&mut a);
|
||||||
|
let rect = a
|
||||||
|
.designer
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(id, _)| *id == b)
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
let x = match horizontal {
|
||||||
|
-1 => rect.x,
|
||||||
|
0 => rect.x + rect.width / 2,
|
||||||
|
_ => rect.right() - 1,
|
||||||
|
};
|
||||||
|
let y = match vertical {
|
||||||
|
-1 => rect.y,
|
||||||
|
0 => rect.y + rect.height / 2,
|
||||||
|
_ => rect.bottom() - 1,
|
||||||
|
};
|
||||||
|
let before = a.design_form().unwrap().clone();
|
||||||
|
mouse(&mut a, MK::Down(MB::Left), x, y, M::NONE);
|
||||||
|
mouse(&mut a, MK::Up(MB::Left), x + 1, y + 1, M::NONE);
|
||||||
|
assert!(a.dialog.is_none(), "{:?}", a.dialog);
|
||||||
|
assert_eq!(number(&mut a, b, "WIDTH"), 10 + horizontal);
|
||||||
|
assert_eq!(number(&mut a, b, "HEIGHT"), 5 + vertical);
|
||||||
|
assert_eq!(number(&mut a, b, "LEFT"), 8 + i32::from(horizontal < 0));
|
||||||
|
assert_eq!(number(&mut a, b, "TOP"), 5 + i32::from(vertical < 0));
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
assert_eq!(*a.design_form().unwrap(), before);
|
||||||
|
}
|
||||||
|
let old_id = b;
|
||||||
|
a.design_select(b, false).unwrap();
|
||||||
|
property(&mut a, "INDEX", "0");
|
||||||
|
assert_eq!(node(&mut a, old_id).class, C::TextBox);
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
assert_eq!(designer::node_key(&node(&mut a, old_id)).1, None);
|
||||||
|
a.design_select(b, false).unwrap();
|
||||||
|
a.execute(Command::Clear);
|
||||||
|
plain(&mut a, K::Esc);
|
||||||
|
let replacement = a
|
||||||
|
.design_place(C::TextBox, 0, r, Rect::new(1, 1, 5, 2))
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(replacement, b);
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
assert_eq!(node(&mut a, b).class, C::TextBox);
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn menu_indent_outdent_rename_and_field_changes_have_one_undo() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r = root(&mut a);
|
||||||
|
let title = a.design_place(C::Menu, 0, r, Rect::default()).unwrap();
|
||||||
|
let child = a.design_place(C::Menu, 0, title, Rect::default()).unwrap();
|
||||||
|
let sibling = a.design_place(C::Menu, 0, title, Rect::default()).unwrap();
|
||||||
|
a.design_menu_move("in").unwrap();
|
||||||
|
assert_eq!(node(&mut a, child).children.len(), 1);
|
||||||
|
a.design_menu_move("out").unwrap();
|
||||||
|
assert_eq!(node(&mut a, title).children.len(), 2);
|
||||||
|
let before = a.design_form().unwrap().clone();
|
||||||
|
a.execute(Command::MenuDesign);
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
let d = a.dialog.as_mut().unwrap();
|
||||||
|
d.fields[0] = Field::text("CtlName", "SaveMenu");
|
||||||
|
d.fields[1] = Field::text("Caption", "&Save");
|
||||||
|
d.fields[2] = Field::text("Tag", "saved");
|
||||||
|
d.fields[3] = Field::text("Index", "0");
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert!(a.dialog.is_none(), "{:?}", a.dialog);
|
||||||
|
assert_eq!(node(&mut a, sibling).name, "SaveMenu");
|
||||||
|
a.execute(Command::Undo);
|
||||||
|
assert_eq!(*a.design_form().unwrap(), before);
|
||||||
|
assert_eq!(node(&mut a, sibling).name, "Menu3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn property_menu_buttons_palette_drag_and_modal_mouse_are_connected() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r = root(&mut a);
|
||||||
|
let before = a.design_form().unwrap().code.clone();
|
||||||
|
plain(&mut a, K::Char('x'));
|
||||||
|
assert_eq!(a.design_form().unwrap().code, before);
|
||||||
|
draw(&mut a);
|
||||||
|
click(&mut a, 1, 0);
|
||||||
|
assert!(matches!(
|
||||||
|
a.dialog.as_ref().unwrap().kind,
|
||||||
|
DialogKind::DesignProperty
|
||||||
|
));
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
assert!(a.value_focus);
|
||||||
|
plain(&mut a, K::Esc);
|
||||||
|
a.execute(Command::Palette);
|
||||||
|
draw(&mut a);
|
||||||
|
let swatch = a
|
||||||
|
.hits
|
||||||
|
.iter()
|
||||||
|
.find_map(|(r, h)| matches!(h, Hit::DesignPaint("BACKCOLOR", 5)).then_some(*r))
|
||||||
|
.unwrap();
|
||||||
|
let form = a
|
||||||
|
.designer
|
||||||
|
.objects
|
||||||
|
.iter()
|
||||||
|
.find(|(id, _)| *id == r)
|
||||||
|
.unwrap()
|
||||||
|
.1;
|
||||||
|
mouse(&mut a, MK::Down(MB::Left), swatch.x, swatch.y, M::NONE);
|
||||||
|
mouse(
|
||||||
|
&mut a,
|
||||||
|
MK::Drag(MB::Left),
|
||||||
|
form.right() - 3,
|
||||||
|
form.bottom() - 3,
|
||||||
|
M::NONE,
|
||||||
|
);
|
||||||
|
mouse(
|
||||||
|
&mut a,
|
||||||
|
MK::Up(MB::Left),
|
||||||
|
form.right() - 3,
|
||||||
|
form.bottom() - 3,
|
||||||
|
M::NONE,
|
||||||
|
);
|
||||||
|
assert_eq!(number(&mut a, r, "BACKCOLOR"), 5);
|
||||||
|
a.execute(Command::MenuDesign);
|
||||||
|
draw(&mut a);
|
||||||
|
let insert = a
|
||||||
|
.hits
|
||||||
|
.iter()
|
||||||
|
.find_map(|(r, h)| matches!(h, Hit::DesignAction("insert")).then_some(*r))
|
||||||
|
.unwrap();
|
||||||
|
click(&mut a, insert.x, insert.y);
|
||||||
|
assert_eq!(a.design_form().unwrap().root.children.len(), 1);
|
||||||
|
draw(&mut a);
|
||||||
|
let edit = a
|
||||||
|
.hits
|
||||||
|
.iter()
|
||||||
|
.find_map(|(r, h)| matches!(h, Hit::DesignAction("menu")).then_some(*r))
|
||||||
|
.unwrap();
|
||||||
|
click(&mut a, edit.x, edit.y);
|
||||||
|
assert!(matches!(
|
||||||
|
a.dialog.as_ref().unwrap().kind,
|
||||||
|
DialogKind::DesignMenu(_)
|
||||||
|
));
|
||||||
|
draw(&mut a);
|
||||||
|
let before = a.design_form().unwrap().clone();
|
||||||
|
click(&mut a, 1, 0);
|
||||||
|
assert_eq!(*a.design_form().unwrap(), before);
|
||||||
|
assert!(matches!(
|
||||||
|
a.dialog.as_ref().unwrap().kind,
|
||||||
|
DialogKind::DesignMenu(_)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn shared_include_ambiguity_aborts_rename_and_event_defaults_use_includes() {
|
||||||
|
let t = Temp::new();
|
||||||
|
fs::write(t.0.join("shared.bi"), "PRINT Button.Caption\n").unwrap();
|
||||||
|
for (file, name) in [("a.frm", "AForm"), ("b.frm", "BForm")] {
|
||||||
|
fs::write(t.0.join(file),format!("VERSION 1.00\nBegin Form {name}\n Width=40\n Height=15\n Begin CommandButton Button\n Width=8\n End\nEnd\n'$INCLUDE: 'shared.bi'\n")).unwrap();
|
||||||
|
}
|
||||||
|
fs::write(t.0.join("p.mak"), "a.frm\nb.frm\n").unwrap();
|
||||||
|
let mut a = App::new(&t.0, t.0.join("options"), (100, 30)).unwrap();
|
||||||
|
a.load_initial_project(t.0.join("p.mak")).unwrap();
|
||||||
|
a.options.syntax_checking = false;
|
||||||
|
a.execute(Command::Form);
|
||||||
|
plain(&mut a, K::Enter);
|
||||||
|
let b = a.design_objects().unwrap()[1].0;
|
||||||
|
a.design_select(b, false).unwrap();
|
||||||
|
let before: Vec<_> = a
|
||||||
|
.project
|
||||||
|
.documents()
|
||||||
|
.map(|(id, d)| (id, d.content().clone()))
|
||||||
|
.collect();
|
||||||
|
assert!(a.design_rename("Changed").is_err());
|
||||||
|
for (id, c) in before {
|
||||||
|
assert_eq!(*a.project.document(id).unwrap().content(), c);
|
||||||
|
}
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::CommandButton, 0, r, Rect::new(1, 1, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
fs::write(t.0.join("defaults.bi"), "DEFINT A-Z\n").unwrap();
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
a.project
|
||||||
|
.replace_text(doc, 0..0, "'$INCLUDE: 'defaults.bi'\n")
|
||||||
|
.unwrap();
|
||||||
|
a.design_event(b, "CLICK").unwrap();
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(doc)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("DEFINT A-Z\n\nEND SUB"));
|
||||||
|
a.compile_current().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn imported_type_shadow_is_not_an_object_reference_during_rename() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r = root(&mut a);
|
||||||
|
let b = a
|
||||||
|
.design_place(C::CommandButton, 0, r, Rect::new(1, 1, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let doc = a.design_document().unwrap();
|
||||||
|
let name = node(&mut a, b).name;
|
||||||
|
let types = a.project.new_module("Types").unwrap();
|
||||||
|
a.project
|
||||||
|
.replace_text(types, 0..0, "TYPE Imported\nCaption AS STRING\nEND TYPE\n")
|
||||||
|
.unwrap();
|
||||||
|
let code = format!(
|
||||||
|
"DIM {name} AS Imported\n{name}.Caption = \"UDT field\"\nSUB {name}_Click\nEND SUB\n"
|
||||||
|
);
|
||||||
|
a.project.replace_text(doc, 0..0, &code).unwrap();
|
||||||
|
a.design_rename("RunButton").unwrap();
|
||||||
|
let code = a.project.document(doc).unwrap().code();
|
||||||
|
assert!(code.contains(&format!("{name}.Caption = \"UDT field\"")));
|
||||||
|
assert!(code.contains("SUB RunButton_CLICK"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn events_with_the_same_name_are_local_to_their_form() {
|
||||||
|
let t = Temp::new();
|
||||||
|
let mut a = t.app();
|
||||||
|
let r1 = root(&mut a);
|
||||||
|
let b1 = a
|
||||||
|
.design_place(C::CommandButton, 0, r1, Rect::new(1, 1, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let d1 = a.design_document().unwrap();
|
||||||
|
a.design_event(b1, "CLICK").unwrap();
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
a.design_event(r1, "LOAD").unwrap();
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
a.execute(Command::NewForm);
|
||||||
|
let r2 = root(&mut a);
|
||||||
|
let b2 = a
|
||||||
|
.design_place(C::CommandButton, 0, r2, Rect::new(1, 1, 8, 2))
|
||||||
|
.unwrap();
|
||||||
|
let d2 = a.design_document().unwrap();
|
||||||
|
a.design_event(b2, "CLICK").unwrap();
|
||||||
|
assert_eq!(a.active_document(), Some(d2));
|
||||||
|
key(&mut a, K::F(12), M::SHIFT);
|
||||||
|
a.design_event(r2, "LOAD").unwrap();
|
||||||
|
assert_eq!(a.active_document(), Some(d2));
|
||||||
|
a.compile_current().unwrap();
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(d1)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("SUB Form_LOAD"));
|
||||||
|
assert!(a
|
||||||
|
.project
|
||||||
|
.document(d2)
|
||||||
|
.unwrap()
|
||||||
|
.code()
|
||||||
|
.contains("SUB Form_LOAD"));
|
||||||
|
}
|
||||||
@@ -293,6 +293,15 @@ impl Host for CaptureHost {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shared child command, inheriting the foreground terminal and stdio.
|
||||||
|
pub fn shell_command(command: &str) -> std::process::Command {
|
||||||
|
let mut child = std::process::Command::new(if cfg!(windows) { "cmd" } else { "sh" });
|
||||||
|
if !command.is_empty() {
|
||||||
|
child.args([if cfg!(windows) { "/C" } else { "-c" }, command]);
|
||||||
|
}
|
||||||
|
child
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -397,12 +406,3 @@ mod tests {
|
|||||||
assert_eq!(h.next_event(false), None);
|
assert_eq!(h.next_event(false), None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared child command, inheriting the foreground terminal and stdio.
|
|
||||||
pub fn shell_command(command: &str) -> std::process::Command {
|
|
||||||
let mut child = std::process::Command::new(if cfg!(windows) { "cmd" } else { "sh" });
|
|
||||||
if !command.is_empty() {
|
|
||||||
child.args([if cfg!(windows) { "/C" } else { "-c" }, command]);
|
|
||||||
}
|
|
||||||
child
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ pub enum PropertyValue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyValue {
|
impl PropertyValue {
|
||||||
fn from_default(default: PropertyDefault, ty: PropertyType) -> Self {
|
pub fn from_default(default: PropertyDefault, ty: PropertyType) -> Self {
|
||||||
match default {
|
match default {
|
||||||
PropertyDefault::Integer(v) if ty == PropertyType::IntegerArray => {
|
PropertyDefault::Integer(v) if ty == PropertyType::IntegerArray => {
|
||||||
Self::IntegerArray(vec![v; 18])
|
Self::IntegerArray(vec![v; 18])
|
||||||
@@ -1659,7 +1659,7 @@ impl FormsModel {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rect(&self, key: ObjectKey) -> Option<(usize, usize, usize, usize)> {
|
pub fn rect(&self, key: ObjectKey) -> Option<(usize, usize, usize, usize)> {
|
||||||
self.instance(key).ok()?;
|
self.instance(key).ok()?;
|
||||||
let mut left = self.integer(key, "LEFT").unwrap_or(0).max(0) as usize + 1;
|
let mut left = self.integer(key, "LEFT").unwrap_or(0).max(0) as usize + 1;
|
||||||
let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1;
|
let mut top = self.integer(key, "TOP").unwrap_or(0).max(0) as usize + 1;
|
||||||
|
|||||||
@@ -60,6 +60,26 @@ pub fn compile_project(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectCompiler {
|
impl ProjectCompiler {
|
||||||
|
/// Bindungen aus den tatsächlich übersetzten Modulen einschließlich ihrer Imports.
|
||||||
|
pub fn bound_form_references(
|
||||||
|
&self,
|
||||||
|
) -> Result<tb_frontend::sema::BoundFormReferences, Vec<Diagnostic>> {
|
||||||
|
let mut result = tb_frontend::sema::BoundFormReferences::default();
|
||||||
|
for product in &self.products {
|
||||||
|
let bound = tb_frontend::sema::bound_form_references(&product.module, &product.catalog);
|
||||||
|
result.objects.extend(bound.objects);
|
||||||
|
// Importierte Prototypen im Cache haben keine physische Quellposition.
|
||||||
|
result
|
||||||
|
.procedures
|
||||||
|
.extend(bound.procedures.into_iter().filter(|(pos, _)| pos.line > 0));
|
||||||
|
result.diagnostics.extend(bound.diagnostics);
|
||||||
|
}
|
||||||
|
if result.diagnostics.is_empty() {
|
||||||
|
Ok(result)
|
||||||
|
} else {
|
||||||
|
Err(result.diagnostics)
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn compile(
|
pub fn compile(
|
||||||
&mut self,
|
&mut self,
|
||||||
name: &str,
|
name: &str,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ Mausmodus und Alternate Screen zurück.
|
|||||||
- Die Umgebung zeigt unten anklickbare Tastenkürzel und `00001:001` als
|
- Die Umgebung zeigt unten anklickbare Tastenkürzel und `00001:001` als
|
||||||
Zeile/Spalte. Der Designer besitzt stattdessen die Properties Bar.
|
Zeile/Spalte. Der Designer besitzt stattdessen die Properties Bar.
|
||||||
F10 wechselt dort zwischen Menü- und Properties Bar, F2 fokussiert Value.
|
F10 wechselt dort zwischen Menü- und Properties Bar, F2 fokussiert Value.
|
||||||
Die fachliche Formularbearbeitung folgt in Change 05.
|
Die Formularbearbeitung ist unten beschrieben.
|
||||||
|
|
||||||
## Speichern und Wechseln
|
## Speichern und Wechseln
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ Library-Symbole/Aufrufvertrag und der native Verbraucher-Nachweis stehen
|
|||||||
explizit in [PLAN.md, Phase 6](../PLAN.md). Ein umbenanntes TBC erfüllt
|
explizit in [PLAN.md, Phase 6](../PLAN.md). Ein umbenanntes TBC erfüllt
|
||||||
keines der beiden Exportziele.
|
keines der beiden Exportziele.
|
||||||
|
|
||||||
## Befehlsabdeckung nach Change 03
|
## Befehlsabdeckung nach Change 05
|
||||||
|
|
||||||
Alle Referenzbefehle sind in ihrer Menüfolge vorhanden. Fachbefehle der
|
Alle Referenzbefehle sind in ihrer Menüfolge vorhanden. Fachbefehle der
|
||||||
folgenden Changes sind sichtbar deaktiviert und zeigen einen
|
folgenden Changes sind sichtbar deaktiviert und zeigen einen
|
||||||
@@ -138,17 +138,17 @@ Output-Fokus unterbricht Ctrl+C sofort die Sitzung, ohne eine BASIC-Taste einzur
|
|||||||
| Bereich | Rahmenaktion jetzt | Fachaktion im Folgechange |
|
| Bereich | Rahmenaktion jetzt | Fachaktion im Folgechange |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| File | New/Open/Save Project, New Form/Module, Add/Remove File, Save File/As, Load/Save Text, Print (UTF-8-Datei), Shell, Exit | — |
|
| File | New/Open/Save Project, New Form/Module, Add/Remove File, Save File/As, Load/Save Text, Print (UTF-8-Datei), Shell, Exit | — |
|
||||||
| Edit | Undo, Cut/Copy/Paste/Clear, New Sub/Function | Event Procedures: 05 |
|
| Edit | Undo, Cut/Copy/Paste/Clear, New Sub/Function, Event Procedures | — |
|
||||||
| View | Code/Form-Auswahl, Prozedurauswahl, Included File/Lines, Diagnostics, Designer Menu Bar/Value-Fokus, Output Screen | Grid Lines: 05; Next Statement: 06 |
|
| View | Code/Form-Auswahl, Prozedurauswahl, Included File/Lines, Diagnostics, Designer Menu Bar/Value-Fokus, Grid Lines, Output Screen | Next Statement: 06 |
|
||||||
| Search | Find, Selected Text, Repeat Last Find, Change | — |
|
| Search | Find, Selected Text, Repeat Last Find, Change | — |
|
||||||
| Run | Start/Restart/Continue/Pause, COMMAND$, wirksame Startdatei und beide Make-Dialoge | Native Erzeugung: Phase 6 |
|
| Run | Start/Restart/Continue/Pause, COMMAND$, wirksame Startdatei und beide Make-Dialoge | Native Erzeugung: Phase 6 |
|
||||||
| Debug | Vollständiges Menü und Kürzelzuordnung, einschließlich F7/F8/F9/F10 und History | Watches, Watchpoints, Trace, History, Breakpoints, Break on Errors, Set Next Statement: 06 |
|
| Debug | Vollständiges Menü und Kürzelzuordnung, einschließlich F7/F8/F9/F10 und History | Watches, Watchpoints, Trace, History, Breakpoints, Break on Errors, Set Next Statement: 06 |
|
||||||
| Options | Display, Set Paths, Right Mouse, Save, Syntax Checking einschließlich Zeilenprüfung | Kontext-Hilfe: 07 |
|
| Options | Display, Set Paths, Right Mouse, Save, Syntax Checking einschließlich Zeilenprüfung | Kontext-Hilfe: 07 |
|
||||||
| Window | New Window, Arrange All, Calls/Debug/Help/Immediate/Output/Project, aktive Codeliste, alle Fensterkürzel | Debug/Calls/Immediate-Inhalte: 06; Hilfeinhalt: 07 |
|
| Window | New Window, Arrange All, Calls/Debug/Help/Immediate/Output/Project, aktive Codeliste, alle Fensterkürzel | Debug/Calls/Immediate-Inhalte: 06; Hilfeinhalt: 07 |
|
||||||
| Designer Tools/Window | Tools-Menü, Formliste und gemeinsamer Fenster-/Modusrahmen | Controls/Toolbox, Color Palette, Menu Design Window: 05 |
|
| Designer Tools/Window | Vollständige Toolbox einschließlich Spin, Formliste, Color Palette, Menu Design Window | — |
|
||||||
| Help | Vollständiges Menü, About und Help-Fensterrahmen | Index, Contents, Keyboard, Topic, Using Help, Tutorial: 07 |
|
| Help | Vollständiges Menü, About und Help-Fensterrahmen | Index, Contents, Keyboard, Topic, Using Help, Tutorial: 07 |
|
||||||
|
|
||||||
Editor, inkrementelle Übersetzung und Ausführung sind umgesetzt. Die gesamte
|
Editor, inkrementelle Übersetzung, Ausführung und Formulardesigner sind umgesetzt. Die gesamte
|
||||||
Phase-5-Abnahme erfolgt in Change 08.
|
Phase-5-Abnahme erfolgt in Change 08.
|
||||||
|
|
||||||
## Programme ausführen
|
## Programme ausführen
|
||||||
@@ -298,7 +298,7 @@ Erfolge und Diagnosen gehören zu Projektrevision, expandierten Quellen und Form
|
|||||||
Stand zurück; ein Fehler macht keinen früheren Erfolg aktuell. Geöffnete
|
Stand zurück; ein Fehler macht keinen früheren Erfolg aktuell. Geöffnete
|
||||||
Include-Puffer bleiben maßgeblich gegenüber externen Dateiänderungen. Noch nicht
|
Include-Puffer bleiben maßgeblich gegenüber externen Dateiänderungen. Noch nicht
|
||||||
geöffnete Includes werden erneut vom Loader gelesen und verglichen. Start und
|
geöffnete Includes werden erneut vom Loader gelesen und verglichen. Start und
|
||||||
Fortsetzen der VM werden in Change 04 an diese Schnittstelle angebunden.
|
Fortsetzen der VM verwenden diese Schnittstelle.
|
||||||
|
|
||||||
Release-Messungen und Aufruf stehen in [tbvm-design.md](tbvm-design.md).
|
Release-Messungen und Aufruf stehen in [tbvm-design.md](tbvm-design.md).
|
||||||
|
|
||||||
@@ -307,3 +307,80 @@ Die Ausführungstests liegen in `crates/tb-ide/tests/execution.rs` und
|
|||||||
`cargo build -p tb-ide && python3 tests/support/ide-execution-pty.py` unter Unix
|
`cargo build -p tb-ide && python3 tests/support/ide-execution-pty.py` unter Unix
|
||||||
in einem unsichtbaren PTY die echten Shell-/Terminalübergaben und das Fortsetzen
|
in einem unsichtbaren PTY die echten Shell-/Terminalübergaben und das Fortsetzen
|
||||||
nach STOP. Das Skript verwendet ausschließlich temporäre Projekte.
|
nach STOP. Das Skript verwendet ausschließlich temporäre Projekte.
|
||||||
|
|
||||||
|
|
||||||
|
## Formulare gestalten
|
||||||
|
|
||||||
|
File → New Form öffnet ein neues FRM-Dokument mit 40×15 Zellen. Toolbox
|
||||||
|
links und Tools-Menü verwenden dieselben Klassenmetadaten: auch Datei-,
|
||||||
|
Verzeichnis- und Laufwerkslisten, Timer sowie VSpin/HSpin sind enthalten.
|
||||||
|
Doppelklick auf einen Toolboxeintrag platziert einen gültigen Standard im
|
||||||
|
gewählten Container (sonst auf der Form); mit Tastatur: Window → Toolbox,
|
||||||
|
Pfeile, Enter. Ein einfacher Klick oder Tools wählt das Werkzeug zum
|
||||||
|
Aufziehen auf der Designfläche. Esc beendet die Werkzeugwahl.
|
||||||
|
|
||||||
|
Die Vorschau verwendet FormsModel und TextScreen, führt aber keinen
|
||||||
|
Ereigniscode und keine Timer aus. Timer erscheinen als `[T]`-Marken.
|
||||||
|
Klicken wählt auch deaktivierte/unsichtbare Controls aus, Ctrl+Klick bildet
|
||||||
|
Mehrfachauswahlen. Tab/Shift+Tab folgt TabIndex. Drag verschiebt, die acht
|
||||||
|
Handles skalieren. Pfeile bewegen um eine, Ctrl+Pfeile um fünf Zellen;
|
||||||
|
Shift+Pfeile ändern die Größe (mit Ctrl ebenfalls fünf). View → Grid Lines
|
||||||
|
schaltet das Raster. Koordinaten sind relativ zum tatsächlichen Container.
|
||||||
|
Unzulässige Größen/Positionen werden vor der gesamten Änderung abgewiesen.
|
||||||
|
|
||||||
|
Oben wechseln F10 und F11 zwischen Properties Bar und Menü. Klicken auf
|
||||||
|
die Property-Auswahl öffnet die Klassenproperties; Bild auf/ab blättert
|
||||||
|
sie auf der Designfläche. F2 fokussiert Value, Ctrl+A ersetzt den Wert,
|
||||||
|
Enter übernimmt und Esc verwirft die Eingabe. Der rechte Bereich zeigt
|
||||||
|
Position und Größe auch bei langen Namen/Werten. Boolean-Werte sind
|
||||||
|
`true`/`false` beziehungsweise `-1`/`0`. Typen, Metadatenbereiche und
|
||||||
|
Laufzeitregeln werden geprüft; schreibgeschützte Properties bleiben erhalten.
|
||||||
|
Spin.Style wählt 0 (vertikal, 1×2) oder 1 (horizontal, 2×1); Width/Height
|
||||||
|
sind bei Spin schreibgeschützt. Explizites Index=0 erzeugt ein Control-Array.
|
||||||
|
|
||||||
|
Cut/Copy/Paste/Clear und Undo wirken im Designer auf Controls; die
|
||||||
|
Textzwischenablage des Editors bleibt getrennt. Container nehmen ihre
|
||||||
|
Kinder mit. Paste verlangt einen Container und vergibt freie Namen,
|
||||||
|
wobei die Arraygruppen und expliziten Indizes erhalten bleiben. Eine
|
||||||
|
Mehrfachaktion ist genau eine Undo-Einheit. Nach Clear/Cut weist ein Dialog
|
||||||
|
auf erhaltenen, nun ungebundenen Code hin. Code wird nicht gelöscht.
|
||||||
|
|
||||||
|
CtlName/FormName ändern auch gebundene Codeverweise und Ereignisnamen
|
||||||
|
in betroffenen Projektdateien und Includes. Kommentare, Strings und
|
||||||
|
unabhängige Variablen bleiben erhalten. Beide Projektstände werden
|
||||||
|
übersetzt und ihre Objektbindungen verglichen. Konflikte oder mehrdeutige
|
||||||
|
Include-Verweise verhindern die gesamte Änderung. Undo aus einer beteiligten
|
||||||
|
Datei nimmt sie überall zurück; spätere Änderungen in einer anderen
|
||||||
|
beteiligten Datei müssen zuerst rückgängig gemacht werden.
|
||||||
|
|
||||||
|
Window → Menu Design Window zeigt die Menühierarchie. `+`/Insert fügt unter
|
||||||
|
dem gewählten Menü (sonst unter der Form) ein, Del löscht, Enter/Edit öffnet
|
||||||
|
Caption, CtlName, Tag, Index, Flags und Shortcut. `&` markiert Access-Keys,
|
||||||
|
`-` einen Separator. Leerer Index bedeutet skalar; `0` ist ein Arrayindex.
|
||||||
|
Die Pfeilbuttons beziehungsweise Ctrl+Links/Rechts ändern die Ebene,
|
||||||
|
Ctrl+Auf/Ab die Reihenfolge. Änderungen des Dialogs sind gemeinsam rücknehmbar.
|
||||||
|
Angeboten werden F1–F10 mit unterstützten Modifikatoren sowie Ctrl+Buchstaben
|
||||||
|
außer Ctrl+C, das dem Programmabbruch vorbehalten bleibt. Unbekannte Importwerte
|
||||||
|
werden nicht als gültige Shortcuts angeboten; Menütitel und Separatoren tragen
|
||||||
|
keinen Shortcut.
|
||||||
|
|
||||||
|
Window → Color Palette bietet getrennte ForeColor-/BackColor-Zeilen. Eine
|
||||||
|
Farbfläche auf Form/Control ziehen trägt diese Farbe auf. Enter öffnet
|
||||||
|
zusätzlich den Dialog zum gemeinsamen Auftragen beider Farben auf die
|
||||||
|
Auswahl oder ausdrücklich die Form. Window bietet außerdem Toolbox, Help
|
||||||
|
und die Formliste; der Help-Inhalt folgt in Change 07.
|
||||||
|
|
||||||
|
F12/Edit → Event Procedures wählt Objekt und erlaubtes Ereignis. Eine
|
||||||
|
vorhandene gebundene Prozedur wird geöffnet; sonst entsteht ein New-Sub-Rumpf
|
||||||
|
mit den bestehenden Eventtypen, BYREF-Parametern und gegebenenfalls führendem
|
||||||
|
`Index AS INTEGER`. DEFtype aus Includes bleibt wirksam. Zweiter Aufruf
|
||||||
|
öffnet dieselbe Prozedur. Shift+F12/View → Form führt zum zugehörigen
|
||||||
|
Dokument und Control zurück; View → Code behält die Codeposition. Struktur
|
||||||
|
und Code bleiben dabei ungespeichert im gemeinsamen Dokument.
|
||||||
|
|
||||||
|
Save File/As/Project verwendet den vorhandenen FRM-Writer. Dieser lässt
|
||||||
|
überflüssige Defaultwerte aus, erhält aber explizites Index=0. Binärimporte
|
||||||
|
benötigen weiter ein separates Textziel. Die Designer-Tests prüfen alle
|
||||||
|
Klassen, Geometrie, Properties, Maus/Tastatur, Transaktionen, Ereignisse und
|
||||||
|
Speicher-Roundtrips. Der CLI-Integrationstest erzeugt eine FRM über dieselben
|
||||||
|
Designeraktionen und übersetzt sie mit dem echten `tbc build`-Prozess.
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ Besondere Risiken sind unverändert ausgeführte alte Kompilate nach einer Text
|
|||||||
| 02 | [IDE-Rahmen](../2026-09-06-phase-5-02-ide-rahmen/proposal.md) | 01 | Terminalbesitz, Menüs, Fenster, Dialoge, Optionen und Eingabetestpfad |
|
| 02 | [IDE-Rahmen](../2026-09-06-phase-5-02-ide-rahmen/proposal.md) | 01 | Terminalbesitz, Menüs, Fenster, Dialoge, Optionen und Eingabetestpfad |
|
||||||
| 03 | [Editor und inkrementelle Übersetzung](../2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md) | 01, 02 | Vollständige Codebearbeitung, Diagnose und schneller aktueller Compile |
|
| 03 | [Editor und inkrementelle Übersetzung](../2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md) | 01, 02 | Vollständige Codebearbeitung, Diagnose und schneller aktueller Compile |
|
||||||
| 04 | [Ausführung und Output](../2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md) | 01, 02, 03 | Fortsetzbare VM-Sitzung, Reset, Ausgabe, Shell und Textdruck |
|
| 04 | [Ausführung und Output](../2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md) | 01, 02, 03 | Fortsetzbare VM-Sitzung, Reset, Ausgabe, Shell und Textdruck |
|
||||||
| 05 | [Formular-Designer](../../phase-5-05-formular-designer/proposal.md) | 01, 02, 03 | Visuelles Gestalten und konsistenter Ereigniscode |
|
| 05 | [Formular-Designer](../2026-09-06-phase-5-05-formular-designer/proposal.md) | 01, 02, 03 | Visuelles Gestalten und konsistenter Ereigniscode |
|
||||||
| 06 | [Debugger und Direktfenster](../../phase-5-06-debugger-und-direktfenster/proposal.md) | 03, 04 | Quellgenaues Debuggen, Watches, Immediate, History und Fehlerhalte |
|
| 06 | [Debugger und Direktfenster](../../phase-5-06-debugger-und-direktfenster/proposal.md) | 03, 04 | Quellgenaues Debuggen, Watches, Immediate, History und Fehlerhalte |
|
||||||
| 07 | [Hilfesystem](../../phase-5-07-hilfesystem/proposal.md) | 02, 03 | Offline-Markdown-Hilfe und Kontextnavigation |
|
| 07 | [Hilfesystem](../../phase-5-07-hilfesystem/proposal.md) | 02, 03 | Offline-Markdown-Hilfe und Kontextnavigation |
|
||||||
| 08 | [Integration und Phasenabnahme](../../phase-5-08-integration-und-phasenabnahme/proposal.md) | 01–07 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status |
|
| 08 | [Integration und Phasenabnahme](../../phase-5-08-integration-und-phasenabnahme/proposal.md) | 01–07 | Durchgängiger Arbeitsablauf, Befehlsabdeckung und nachgewiesener PLAN-Status |
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ Keine bestehenden Anforderungen werden ersetzt; die neue IDE-Fähigkeit ergänzt
|
|||||||
|
|
||||||
tb-ide-Designer, vorhandenes tb-ui::frm::FormFile, FormsModel und tb-frontend-Forms-Metadaten. Keine zweite FRM-Implementierung und keine Programmausführung als Designvorschau.
|
tb-ide-Designer, vorhandenes tb-ui::frm::FormFile, FormsModel und tb-frontend-Forms-Metadaten. Keine zweite FRM-Implementierung und keine Programmausführung als Designvorschau.
|
||||||
|
|
||||||
**Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../archive/2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../archive/2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md).
|
**Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md).
|
||||||
|
|
||||||
**Gesamtplanung:** [Phase-5-Übersicht](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch.
|
**Gesamtplanung:** [Phase-5-Übersicht](../2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch.
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
## 1. Designmodell und Vorschau
|
## 1. Designmodell und Vorschau
|
||||||
|
|
||||||
- [ ] 1.1 Den FormFile-Dokumentbaum aus 01 mit stabilen Designobjekt-IDs an einen isolierten Preview-FormsModel/TextScreen anbinden; Tests belegen identische Geometrie und ausbleibende Load-/Timer-Nebenwirkungen.
|
- [x] 1.1 Den FormFile-Dokumentbaum aus 01 mit stabilen Designobjekt-IDs an einen isolierten Preview-FormsModel/TextScreen anbinden; Tests belegen identische Geometrie und ausbleibende Load-/Timer-Nebenwirkungen.
|
||||||
- [ ] 1.2 Toolbox und Tools-Menü aus den vorhandenen Klassenmetadaten ableiten und Platzieren per Doppelklick/Aufziehen umsetzen; ein tabellarischer Test erzeugt jede unterstützte Designzeitklasse einschließlich Spinvarianten und Timer.
|
- [x] 1.2 Toolbox und Tools-Menü aus den vorhandenen Klassenmetadaten ableiten und Platzieren per Doppelklick/Aufziehen umsetzen; ein tabellarischer Test erzeugt jede unterstützte Designzeitklasse einschließlich Spinvarianten und Timer.
|
||||||
- [ ] 1.3 Auswahl, Tab-Reihenfolge, Ctrl+Klick-Mehrfachauswahl, Drag, Handles und Raster implementieren; Maus-/Tastentests belegen Containerkoordinaten sowie 1-/5-Zellen-Schritte und Shift-Skalierung.
|
- [x] 1.3 Auswahl, Tab-Reihenfolge, Ctrl+Klick-Mehrfachauswahl, Drag, Handles und Raster implementieren; Maus-/Tastentests belegen Containerkoordinaten sowie 1-/5-Zellen-Schritte und Shift-Skalierung.
|
||||||
|
|
||||||
## 2. Eigenschaften und strukturierte Änderungen
|
## 2. Eigenschaften und strukturierte Änderungen
|
||||||
|
|
||||||
- [ ] 2.1 Properties Bar mit F2-/F10-Fokus und typisierter Propertybearbeitung implementieren; Tests prüfen gültige/ungültige Werte, sichtbare Koordinaten und fehlende Designer-Statuszeile.
|
- [x] 2.1 Properties Bar mit F2-/F10-Fokus und typisierter Propertybearbeitung implementieren; Tests prüfen gültige/ungültige Werte, sichtbare Koordinaten und fehlende Designer-Statuszeile.
|
||||||
- [ ] 2.2 Control-Cut/Copy/Paste/Clear und Undo über gemeinsame Dokumenttransaktionen umsetzen; Tests belegen Containerkinder, konfliktfreie Namen/Array-Indizes und vollständige Rücknahme einer Mehrfachaktion.
|
- [x] 2.2 Control-Cut/Copy/Paste/Clear und Undo über gemeinsame Dokumenttransaktionen umsetzen; Tests belegen Containerkinder, konfliktfreie Namen/Array-Indizes und vollständige Rücknahme einer Mehrfachaktion.
|
||||||
- [ ] 2.3 Den Menüdesigner für Caption, CtlName, Tag, Index, Flags, Shortcut, Ebenen und Reihenfolge implementieren; ein FRM-Roundtrip prüft Access-Keys, Separatoren und indizierte Untermenüs.
|
- [x] 2.3 Den Menüdesigner für Caption, CtlName, Tag, Index, Flags, Shortcut, Ebenen und Reihenfolge implementieren; ein FRM-Roundtrip prüft Access-Keys, Separatoren und indizierte Untermenüs.
|
||||||
- [ ] 2.4 Color Palette und Designer-Window-Befehle anbinden; Eventtests prüfen Fore-/BackColor-Auftrag, Werkzeugfenster und Formauswahl.
|
- [x] 2.4 Color Palette und Designer-Window-Befehle anbinden; Eventtests prüfen Fore-/BackColor-Auftrag, Werkzeugfenster und Formauswahl.
|
||||||
- [ ] 2.5 Form-/Control-Umbenennung über gebundene Codeverweise transaktional ausführen und Löschfolgen sichtbar machen; Tests belegen Handleranpassung, konfliktbedingten Gesamtabbruch und Erhalt vorhandenen Codes.
|
- [x] 2.5 Form-/Control-Umbenennung über gebundene Codeverweise transaktional ausführen und Löschfolgen sichtbar machen; Tests belegen Handleranpassung, konfliktbedingten Gesamtabbruch und Erhalt vorhandenen Codes.
|
||||||
|
|
||||||
## 3. Ereignisse und Dateivertrag
|
## 3. Ereignisse und Dateivertrag
|
||||||
|
|
||||||
- [ ] 3.1 F12/Event Procedures mit vorhandenen Eventsignaturen an die Prozedurerzeugung aus 03 binden; Tests prüfen Wiederaufruf ohne Duplikat, BYREF-/Typ- und Control-Array-Indexparameter.
|
- [x] 3.1 F12/Event Procedures mit vorhandenen Eventsignaturen an die Prozedurerzeugung aus 03 binden; Tests prüfen Wiederaufruf ohne Duplikat, BYREF-/Typ- und Control-Array-Indexparameter.
|
||||||
- [ ] 3.2 Shift+F12/View→Form und Codewechsel an dieselbe Dokumentidentität binden; eine Eingabefolge belegt unveränderte ungespeicherte Struktur und Cursor-/Controlauswahl.
|
- [x] 3.2 Shift+F12/View→Form und Codewechsel an dieselbe Dokumentidentität binden; eine Eingabefolge belegt unveränderte ungespeicherte Struktur und Cursor-/Controlauswahl.
|
||||||
- [ ] 3.3 Save/Reopen und Binärimport→Textziel für gestaltete Formulare prüfen; bestehender FRM-Parser und CLI-Compiler müssen Struktur, Array-Indizes, Properties und Code gemeinsam akzeptieren.
|
- [x] 3.3 Save/Reopen und Binärimport→Textziel für gestaltete Formulare prüfen; bestehender FRM-Parser und CLI-Compiler müssen Struktur, Array-Indizes, Properties und Code gemeinsam akzeptieren.
|
||||||
- [ ] 3.4 Designerbedienung dokumentieren und alle Spec-Szenarien in verification.md zuordnen; IDE-/FRM-/Forms-Metadaten-Tests und Formatprüfung müssen bestehen.
|
- [x] 3.4 Designerbedienung dokumentieren und alle Spec-Szenarien in verification.md zuordnen; IDE-/FRM-/Forms-Metadaten-Tests und Formatprüfung müssen bestehen.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Verification Report: phase-5-05-formular-designer
|
||||||
|
|
||||||
|
Stand: 2026-09-06. Geprüft wurden Proposal, Design, alle zwölf Tasks und alle
|
||||||
|
sechs Anforderungen samt Szenarien gegen den aktuellen Arbeitsstand.
|
||||||
|
|
||||||
|
## Ergebnis
|
||||||
|
|
||||||
|
| Dimension | Ergebnis |
|
||||||
|
| --- | --- |
|
||||||
|
| Completeness | 12/12 Aufgaben; 6/6 Anforderungen umgesetzt |
|
||||||
|
| Correctness | Alle sechs Spec-Szenarien durch ausführbare Prüfungen abgedeckt |
|
||||||
|
| Coherence | Gemeinsames FRM-Dokument, vorhandene Metadaten, FormsModel/TextScreen, gemeinsamer Prozedur- und Undo-Pfad |
|
||||||
|
| Offene Befunde | 0 CRITICAL, 0 WARNING, 0 SUGGESTION |
|
||||||
|
|
||||||
|
## Anforderungen und Szenarien
|
||||||
|
|
||||||
|
Die Designerprüfungen liegen in `crates/tb-ide/tests/designer.rs` und bedienen
|
||||||
|
die produktiven App-Aktionen sowie den realen Event-Dispatcher/Renderer.
|
||||||
|
|
||||||
|
| Anforderung / Scenario | Implementierung | Nachweis |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Designfläche und vollständige Toolbox / Jede Controlklasse | `designer::tools`, `design_place`, `preview`, `design_mouse`, `design_render`; `commands::menus` | `all_metadata_tools_persist_and_preview_matches_runtime_without_events`: alle 17 Toolboxvarianten, einschließlich VSpin/HSpin, Browserklassen und Timer, persistieren mit gleichen wirksamen Properties. Pixelvergleich mit FormsModel-Rendering; aktivierter 1-ms-Timer bleibt in der Vorschau ohne Deadline/Ereignis, Load-/Timer-Code wird nicht ausgeführt. `toolbox_double_click_tool_drag_grid_and_window_form_selection` prüft Doppelklick und Tools-Aufziehen. Menu wird durch die Menütests abgedeckt. |
|
||||||
|
| Auswahl und Geometrie / Container und Mehrfachauswahl | `design_select`, `design_geometry`, `design_resize`, `design_tab`, `design_mouse`; öffentliche Laufzeitgeometrie `FormsModel::rect` | `container_multiselect_keyboard_drag_resize_and_undo`: tatsächliche Framekoordinaten, Ctrl+Klick, Tab/Shift+Tab, 1-/5-Zellen-Bewegungen, Shift-Skalierung, Drag und gemeinsames Undo. `all_eight_handles_resize_transactionally_and_preserve_identity_after_structure_edits`: alle acht Handles, Ablehnung unzulässiger Geometrie und stabile IDs über Indexänderung, Löschen, Neuanlage und Undo. |
|
||||||
|
| Properties Bar und Bearbeitungsaktionen / Ungültige Eigenschaft | `design_property`, `design_value_focus`, `design_bar`, `design_copy/paste/delete`; `Project::edit_form/undo` | `property_bar_types_readonly_and_no_status_line`: Typen/Bereiche/readonly, F2/F10, sichtbare Geometrie, keine Statuszeile, Spin-Größenvertrag. Runtime-Setter validieren zusätzlich kontextabhängige Propertyregeln in einer isolierten Vorschau. `clipboard_children_arrays_and_delete_preserve_code`: Containerkinder, neue Namen, erhaltene Array-Indizes, Cut/Paste/Clear und vollständiges Undo. |
|
||||||
|
| Menüdesigner und Farbpalette / Menü mit Array und Untermenü | `design_action`, `design_submit`, `design_menu_move/commit`, `design_tool_render`; `forms::menu_shortcuts` | `menu_hierarchy_arrays_shortcuts_and_palette_use_events`: Access-Key, Separator, Index=0, Shortcut, Hierarchie/Reihenfolge, FRM-Roundtrip und Kompilierung; Fore-/BackColor-Auftrag. `menu_indent_outdent_rename_and_field_changes_have_one_undo`: Ebenenwechsel und eine Undo-Einheit für Name und Dialogfelder. `property_menu_buttons_palette_drag_and_modal_mouse_are_connected`: echte Property-/Menübuttons, Farbdrop und Modalität. |
|
||||||
|
| Dokumentkonsistenz und Umbenennen / Umbenennen mit Ereigniscode | `design_rename_with`; `ProjectCompiler::bound_form_references`; opt-in Referenzaufzeichnung im regulären Sema; `Project::commit_transaction/undo` | `bound_rename_across_documents_is_atomic_and_undo_restores_all`: Control, Handler und qualifizierte Verweise in anderen Modulen; Undo aus einer beteiligten Datei; Konflikt-/Syntaxabbruch ohne Teiländerung. `rename_preserves_shadowed_names_and_handles_form_aliases_and_include_calls`, `shared_include_ambiguity_aborts_rename_and_event_defaults_use_includes`, `imported_type_shadow_is_not_an_object_reference_during_rename`: Kommentare/Strings, gleichnamige Variablen, importierte UDT-Felder, Prototypen/Aufrufe, Formularalias und mehrdeutig gemeinsam genutzte Includes. Löschen erhält Code und meldet ungebundene Handler sichtbar. |
|
||||||
|
| Ereignisprozeduren und Moduswechsel / Ereignis zweimal öffnen | `design_event`, `event_parameters`, gemeinsames `App::new_procedure_text`; `show_document/design_enter` | `clipboard_children_arrays_and_delete_preserve_code` öffnet Array-Click zweimal ohne Duplikat. `event_array_signature_twice_and_shared_form_identity` prüft außerdem DragOver zweimal mit Index/CONTROL/SINGLE/INTEGER, gleiche Dokumentidentität, Cursor und Controlauswahl. `existing_default_typed_event_and_all_metadata_signatures_compile`: vorhandene DEFtype-Signatur und alle CommandButton-Events kompilieren. `events_with_the_same_name_are_local_to_their_form`: gleichnamige Ereignisse zweier Formulare bleiben getrennt. |
|
||||||
|
|
||||||
|
## Persistenz und Compilergrenze
|
||||||
|
|
||||||
|
- `binary_designer_import_requires_text_target_and_compiles` bearbeitet einen
|
||||||
|
echten Binärimport, verlangt ein separates Textziel, erhält die Originalbytes
|
||||||
|
und lädt das Ergebnis über den bestehenden Compilerpfad.
|
||||||
|
- `crates/tb-cli/tests/project.rs::designer_frm_is_accepted_by_the_standalone_cli_compiler`
|
||||||
|
erzeugt ein Formular samt Control-Array und Ereigniscode über App-Aktionen,
|
||||||
|
speichert es mit dem vorhandenen Writer, startet den echten `tbc build`-Prozess
|
||||||
|
und lädt das erzeugte TBC erneut. Die zusätzliche Abhängigkeit auf tb-ide
|
||||||
|
gilt ausschließlich für CLI-Tests; es gibt keine neue externe Bibliothek.
|
||||||
|
- Der FRM-Writer lässt explizite Defaultwerte weg. Die Roundtripprüfung
|
||||||
|
vergleicht Klassen, Namen, Hierarchie, explizite Array-Indizes und sämtliche
|
||||||
|
wirksamen Metadatenproperties, statt unterschiedliche interne Maps als
|
||||||
|
Formatfehler zu behandeln.
|
||||||
|
|
||||||
|
## Behobene Befunde während der Verifikation
|
||||||
|
|
||||||
|
- Acht Größenhandles einschließlich linker/oberer Kanten und zusammenfallender
|
||||||
|
Handles kleiner Controls werden korrekt aufgelöst.
|
||||||
|
- Designerzeichen erreichen den Quelltexteditor nicht. Property-/Menübuttons
|
||||||
|
sind im nichtmodalen Dispatcher angebunden; Dialoge sperren Hintergrundaktionen.
|
||||||
|
- Fenster-/Formwechsel aktualisieren die Designeridentität. Die ursprüngliche
|
||||||
|
Befehlsidentität beim Weiterschalten eines Fensters bleibt erhalten.
|
||||||
|
- Positions-/Größenanzeige bleibt rechts sichtbar, auch wenn Propertytext lang ist.
|
||||||
|
- Umbenennen nutzt die Importkontexte der tatsächlich übersetzten Module.
|
||||||
|
Synthetische Importprototypen ohne physische Quellposition werden nicht editiert.
|
||||||
|
Echte Prototypen und Prozeduraufrufe werden semantisch erfasst; keine globale
|
||||||
|
Bezeichnerersetzung beschädigt unabhängige Variablen/UDT-Felder.
|
||||||
|
- Ereigniserzeugung verwendet denselben includebewussten New-Sub-Pfad wie der
|
||||||
|
Editor. Vorhandene Formularalias-/DEFtype-Signaturen und gleichnamige Events
|
||||||
|
unterschiedlicher Formulare werden korrekt zugeordnet.
|
||||||
|
- Aktivierte Timer bleiben im Dokument erhalten, sind in der Vorschau aber
|
||||||
|
ausdrücklich deaktiviert. Es existiert dort keine VM oder Event-Pump.
|
||||||
|
|
||||||
|
- Der workspaceweite Clippy-Lauf fand die bereits vorhandene Shell-Hilfsfunktion
|
||||||
|
hinter dem Testmodul in `tb-runtime/src/host.rs`. Sie wurde unverändert vor
|
||||||
|
das Testmodul verschoben; das beseitigt die Ordnungswarnung ohne Verhaltensänderung.
|
||||||
|
|
||||||
|
## Abschlussprüfungen
|
||||||
|
|
||||||
|
- `cargo test --workspace`: 567 bestanden, 0 fehlgeschlagen, 2 bestehende
|
||||||
|
Ignore-Fälle (Golden-Output-Generator und externe VBDOS-Referenzsuite).
|
||||||
|
Darin: 17 Designer-Integrationstests und der zusätzliche echte CLI-Compilerpfad.
|
||||||
|
- `cargo clippy --workspace --all-targets -- -D warnings`: bestanden.
|
||||||
|
- `cargo fmt --all -- --check`: bestanden.
|
||||||
|
- `git diff --check`: bestanden.
|
||||||
|
- `openspec validate --all --strict`: 23/23 gültig. Vorhandene INFO-Hinweise
|
||||||
|
zur Länge von Requirement-Texten sind keine Validierungsfehler.
|
||||||
|
|
||||||
|
Keine angeforderte Prüfdimension wurde ausgelassen. Die Tests sind headless;
|
||||||
|
für diesen Change wurde keine interaktive GUI-/Emulatorsitzung benötigt.
|
||||||
|
Die gemeinsame Phase-5-Abnahme bleibt Aufgabe von Change 08. Der Change ist
|
||||||
|
vollständig umgesetzt und zur separaten Synchronisierung/Archivierung bereit.
|
||||||
@@ -24,6 +24,6 @@ Keine bestehenden Anforderungen werden ersetzt; die neue IDE-Fähigkeit ergänzt
|
|||||||
|
|
||||||
tb-ide-Integrationstests, vorhandene CLI-/VM-/Forms-Tests und Benchmarks, docs/ sowie PLAN.md. Featureimplementierung bleibt jeweils beim verantwortlichen Change; dieser Change schließt Integration und Nachweise.
|
tb-ide-Integrationstests, vorhandene CLI-/VM-/Forms-Tests und Benchmarks, docs/ sowie PLAN.md. Featureimplementierung bleibt jeweils beim verantwortlichen Change; dieser Change schließt Integration und Nachweise.
|
||||||
|
|
||||||
**Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../archive/2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../archive/2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md), [phase-5-04-ausfuehrung-und-output](../archive/2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md), [phase-5-05-formular-designer](../phase-5-05-formular-designer/proposal.md), [phase-5-06-debugger-und-direktfenster](../phase-5-06-debugger-und-direktfenster/proposal.md), [phase-5-07-hilfesystem](../phase-5-07-hilfesystem/proposal.md).
|
**Abhängigkeiten:** [phase-5-01-projekt-und-dokumentmodell](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/proposal.md), [phase-5-02-ide-rahmen](../archive/2026-09-06-phase-5-02-ide-rahmen/proposal.md), [phase-5-03-editor-und-inkrementelle-uebersetzung](../archive/2026-09-06-phase-5-03-editor-und-inkrementelle-uebersetzung/proposal.md), [phase-5-04-ausfuehrung-und-output](../archive/2026-09-06-phase-5-04-ausfuehrung-und-output/proposal.md), [phase-5-05-formular-designer](../archive/2026-09-06-phase-5-05-formular-designer/proposal.md), [phase-5-06-debugger-und-direktfenster](../phase-5-06-debugger-und-direktfenster/proposal.md), [phase-5-07-hilfesystem](../phase-5-07-hilfesystem/proposal.md).
|
||||||
|
|
||||||
**Gesamtplanung:** [Phase-5-Übersicht](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch.
|
**Gesamtplanung:** [Phase-5-Übersicht](../archive/2026-09-06-phase-5-01-projekt-und-dokumentmodell/phase-5-uebersicht.md). Die Nummern geben eine gültige Umsetzungsreihenfolge an; OpenSpec erzwingt Change-Abhängigkeiten nicht automatisch.
|
||||||
|
|||||||
49
openspec/specs/ide-formulardesigner/spec.md
Normal file
49
openspec/specs/ide-formulardesigner/spec.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# ide-formulardesigner Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Ermöglicht das visuelle Erstellen und Bearbeiten der bestehenden Formular- und Controlmodelle mit gemeinsamem Ereigniscode und kompatibler FRM-Persistenz.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Designfläche und vollständige Toolbox
|
||||||
|
Der Designer SHALL Formularfläche, Toolbox und gleichwertiges Tools-Menü anbieten. Alle in den bestehenden Forms-Metadaten unterstützten Designzeit-Controlklassen SHALL platzierbar sein, einschließlich Datei-/Verzeichnis-/Laufwerkslisten, Timer, Container, Scroll- und Spin-Controls. Werkzeugwahl mit Aufziehen und Toolbox-Doppelklick SHALL neue Controls mit gültigen Defaults und eindeutigen Namen erzeugen. Die Designvorschau MUST NOT Ereigniscode oder Laufzeittimer ausführen.
|
||||||
|
|
||||||
|
#### Scenario: Jede Controlklasse
|
||||||
|
- **WHEN** jede unterstützte Designzeitklasse einmal aus Toolbox oder Tools eingefügt wird
|
||||||
|
- **THEN** entsteht ein gültiger Eintrag im Formulardokument und nach Speichern/Wiederöffnen dieselbe Klasse mit ihren Eigenschaften
|
||||||
|
|
||||||
|
### Requirement: Auswahl und Geometrie
|
||||||
|
Ausgewählte Controls SHALL Sizing Handles zeigen. Drag SHALL verschieben und Handles SHALL skalieren; Pfeile SHALL um eine Zelle, Ctrl+Pfeile um fünf Zellen und Shift+Pfeile die Größe ändern. Tab/Shift+Tab SHALL die Controlauswahl wechseln, Ctrl+Klick SHALL eine Mehrfachauswahl bilden, Grid Lines SHALL das Raster umschalten. Koordinaten SHALL sich auf den tatsächlichen Container beziehen und ungültige Größen SHALL ohne Dokumentbeschädigung abgewiesen werden.
|
||||||
|
|
||||||
|
#### Scenario: Container und Mehrfachauswahl
|
||||||
|
- **WHEN** mehrere Controls innerhalb eines Frame ausgewählt und per Tastatur verschoben werden
|
||||||
|
- **THEN** ändern sich ihre relativen Positionen um denselben Betrag, andere Controls bleiben unverändert und Undo stellt die gesamte Aktion zurück
|
||||||
|
|
||||||
|
### Requirement: Properties Bar und Bearbeitungsaktionen
|
||||||
|
Die Properties Bar SHALL Property-/Value-Auswahl, Spalte/Zeile und Breite/Höhe anzeigen. F2 SHALL die Value-Box fokussieren und F10 zwischen Menüleiste und Properties Bar wechseln. Im Designer SHALL keine Statuszeile erscheinen. Edit→Cut/Copy/Paste/Clear/Undo SHALL auf die ausgewählten Controls wirken und beim Einfügen gültige Namen, Eltern und Array-Indizes herstellen. Klassenabhängige Propertytypen, Wertebereiche und schreibgeschützte Eigenschaften SHALL berücksichtigt werden.
|
||||||
|
|
||||||
|
#### Scenario: Ungültige Eigenschaft
|
||||||
|
- **WHEN** eine Eigenschaft außerhalb ihres gültigen Bereichs oder mit falschem Typ eingegeben wird
|
||||||
|
- **THEN** bleibt ihr alter Wert erhalten und die Fehlermeldung benennt Control und Eigenschaft
|
||||||
|
|
||||||
|
### Requirement: Menüdesigner und Farbpalette
|
||||||
|
Menu Design Window SHALL Caption mit Access-Key/Separator, CtlName, Tag, Index, Checked, Enabled, Visible, Separator und unterstützte Shortcuts bearbeiten. Einfügen, Löschen, Ebene und Reihenfolge SHALL die Menühierarchie konsistent ändern. Color Palette SHALL ForeColor/BackColor und das Auftragen auf die Form unterstützen. Window SHALL Toolbox, Color Palette, Menu Design Window, Help und die Formliste bedienen.
|
||||||
|
|
||||||
|
#### Scenario: Menü mit Array und Untermenü
|
||||||
|
- **WHEN** ein Untermenü mit Access-Key und ein indiziertes Menüelement erstellt, umgeordnet und gespeichert werden
|
||||||
|
- **THEN** bleiben Hierarchie, Reihenfolge, Index, Shortcut und Darstellung nach Wiederöffnen und Kompilieren erhalten
|
||||||
|
|
||||||
|
### Requirement: Dokumentkonsistenz und Umbenennen
|
||||||
|
Designer und Code SHALL dasselbe FRM-Dokument ändern. Änderungen an Formularstruktur, Properties und Code SHALL gemeinsam gespeichert und rückgängig gemacht werden können. Control-/Form-Umbenennen SHALL gebundene Ereignisnamen und eindeutig auflösbare Codeverweise anpassen; Namenskonflikte oder nicht sicher auflösbare betroffene Verweise SHALL die Umbenennung ohne Teiländerung verhindern. Löschen SHALL vorhandenen Code nicht stillschweigend entfernen.
|
||||||
|
|
||||||
|
#### Scenario: Umbenennen mit Ereigniscode
|
||||||
|
- **WHEN** ein Button mit Click-Prozedur umbenannt wird
|
||||||
|
- **THEN** sind Control, Handler und eindeutig gebundene Verweise konsistent umbenannt; Undo stellt alle betroffenen Teile gemeinsam wieder her
|
||||||
|
|
||||||
|
### Requirement: Ereignisprozeduren und Moduswechsel
|
||||||
|
F12/Event Procedures SHALL Objekt und zulässiges Ereignis wählen und die vorhandene passende Prozedur öffnen oder genau eine Prozedur mit der dokumentierten Signatur erzeugen. Control-Arrays SHALL die richtigen Indexparameter erhalten. Shift+F12/View→Form SHALL zum zugehörigen Formular und Control zurückkehren. Der Wechsel MUST ungespeicherte Änderungen erhalten.
|
||||||
|
|
||||||
|
#### Scenario: Ereignis zweimal öffnen
|
||||||
|
- **WHEN** das Click-Ereignis eines Control-Arrays zweimal über F12 geöffnet wird
|
||||||
|
- **THEN** existiert genau eine signaturgerechte Prozedur mit Indexparameter und beide Aufrufe führen dorthin
|
||||||
Reference in New Issue
Block a user