1903 lines
68 KiB
Rust
1903 lines
68 KiB
Rust
use crate::{
|
|
commands::{self, Command, Menu},
|
|
documents::{Decision, Destination, DocumentId, Project, SavePlan, StartupRemoval, ViewId},
|
|
export::{Artifact, ExportRequest, ExportStatus},
|
|
options::{Options, ELEMENTS},
|
|
};
|
|
use anyhow::{anyhow, ensure, Result};
|
|
use crossterm::event::{
|
|
Event, KeyCode as K, KeyEvent, KeyEventKind, KeyModifiers as M, ModifierKeyCode, MouseButton,
|
|
MouseEventKind,
|
|
};
|
|
use ratatui::layout::Rect;
|
|
use std::path::{Path, PathBuf};
|
|
use tb_vm::project_io::Content;
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Mode {
|
|
Environment,
|
|
Designer,
|
|
}
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Execution {
|
|
Idle,
|
|
Compiling,
|
|
Paused,
|
|
Running,
|
|
Waiting,
|
|
Ended,
|
|
Error,
|
|
}
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum WindowKind {
|
|
Code(ViewId),
|
|
Project,
|
|
Output,
|
|
Calls,
|
|
Debug,
|
|
Immediate,
|
|
Help,
|
|
}
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum WindowState {
|
|
Normal,
|
|
Minimized,
|
|
Maximized,
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub struct Window {
|
|
pub id: u64,
|
|
pub kind: WindowKind,
|
|
pub normal: Rect,
|
|
pub state: WindowState,
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub enum FieldValue {
|
|
Text(String),
|
|
Choice { items: Vec<String>, selected: usize },
|
|
Toggle(bool),
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub struct Field {
|
|
pub label: String,
|
|
pub value: FieldValue,
|
|
pub cursor: usize,
|
|
pub selected: bool,
|
|
}
|
|
impl Field {
|
|
pub fn text(label: impl Into<String>, text: impl Into<String>) -> Self {
|
|
let text = text.into();
|
|
Self {
|
|
label: label.into(),
|
|
cursor: text.len(),
|
|
value: FieldValue::Text(text),
|
|
selected: true,
|
|
}
|
|
}
|
|
pub fn choice(label: impl Into<String>, items: Vec<String>, selected: usize) -> Self {
|
|
Self {
|
|
label: label.into(),
|
|
value: FieldValue::Choice { selected, items },
|
|
cursor: 0,
|
|
selected: false,
|
|
}
|
|
}
|
|
pub fn toggle(label: impl Into<String>, value: bool) -> Self {
|
|
Self {
|
|
label: label.into(),
|
|
value: FieldValue::Toggle(value),
|
|
cursor: 0,
|
|
selected: false,
|
|
}
|
|
}
|
|
pub fn string(&self) -> String {
|
|
match &self.value {
|
|
FieldValue::Text(s) => s.clone(),
|
|
FieldValue::Choice { items, selected } => {
|
|
items.get(*selected).cloned().unwrap_or_default()
|
|
}
|
|
FieldValue::Toggle(v) => v.to_string(),
|
|
}
|
|
}
|
|
pub fn index(&self) -> usize {
|
|
match self.value {
|
|
FieldValue::Choice { selected, .. } => selected,
|
|
_ => 0,
|
|
}
|
|
}
|
|
pub fn flag(&self) -> bool {
|
|
matches!(self.value, FieldValue::Toggle(true))
|
|
}
|
|
fn key(&mut self, key: KeyEvent) {
|
|
match &mut self.value {
|
|
FieldValue::Toggle(value) => {
|
|
if matches!(key.code, K::Char(' ') | K::Left | K::Right) {
|
|
*value = !*value
|
|
}
|
|
}
|
|
FieldValue::Choice { items, selected } => {
|
|
if !items.is_empty() {
|
|
match key.code {
|
|
K::Left => *selected = (*selected + items.len() - 1) % items.len(),
|
|
K::Right | K::Char(' ') => *selected = (*selected + 1) % items.len(),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
FieldValue::Text(s) => {
|
|
if key.modifiers.contains(M::CONTROL) && key.code == K::Char('a') {
|
|
self.selected = true;
|
|
return;
|
|
}
|
|
match key.code {
|
|
K::Char(c) if !key.modifiers.intersects(M::CONTROL | M::ALT) => {
|
|
if self.selected {
|
|
s.clear();
|
|
self.cursor = 0;
|
|
}
|
|
s.insert(self.cursor, c);
|
|
self.cursor += c.len_utf8();
|
|
}
|
|
K::Backspace => {
|
|
if self.selected {
|
|
s.clear();
|
|
self.cursor = 0;
|
|
} else if self.cursor > 0 {
|
|
let p = s[..self.cursor].char_indices().last().unwrap().0;
|
|
s.drain(p..self.cursor);
|
|
self.cursor = p;
|
|
}
|
|
}
|
|
K::Delete => {
|
|
if self.selected {
|
|
s.clear();
|
|
self.cursor = 0;
|
|
} else if self.cursor < s.len() {
|
|
let n = s[self.cursor..].chars().next().unwrap().len_utf8();
|
|
s.drain(self.cursor..self.cursor + n);
|
|
}
|
|
}
|
|
K::Left => {
|
|
self.cursor = s[..self.cursor]
|
|
.char_indices()
|
|
.last()
|
|
.map(|(i, _)| i)
|
|
.unwrap_or(0);
|
|
}
|
|
K::Right => {
|
|
self.cursor += s[self.cursor..]
|
|
.chars()
|
|
.next()
|
|
.map(char::len_utf8)
|
|
.unwrap_or(0);
|
|
}
|
|
K::Home => self.cursor = 0,
|
|
K::End => self.cursor = s.len(),
|
|
_ => return,
|
|
}
|
|
self.selected = false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub enum AfterSave {
|
|
Stay,
|
|
Exit,
|
|
NewProject,
|
|
OpenProject(PathBuf),
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub enum DialogKind {
|
|
Search {
|
|
view: ViewId,
|
|
selection: Option<std::ops::Range<usize>>,
|
|
},
|
|
Procedure(bool),
|
|
Procedures(Vec<usize>),
|
|
Diagnostics,
|
|
Browse {
|
|
return_to: Box<Dialog>,
|
|
field: usize,
|
|
entries: Vec<PathBuf>,
|
|
},
|
|
Message,
|
|
OpenProject,
|
|
NewModule,
|
|
AddFile,
|
|
ChooseCode,
|
|
ChooseForm,
|
|
Remove,
|
|
Startup,
|
|
Save {
|
|
project: bool,
|
|
ids: Vec<DocumentId>,
|
|
after: AfterSave,
|
|
},
|
|
Dirty(AfterSave),
|
|
LoadText,
|
|
SaveText,
|
|
Print,
|
|
CommandLine,
|
|
ResumeRevision,
|
|
Display,
|
|
Paths,
|
|
RightMouse,
|
|
SaveOptions,
|
|
Export(Artifact),
|
|
}
|
|
#[derive(Debug, Clone)]
|
|
pub struct Dialog {
|
|
pub title: String,
|
|
pub kind: DialogKind,
|
|
pub fields: Vec<Field>,
|
|
pub focus: usize,
|
|
pub error: String,
|
|
pub request: Option<ExportRequest>,
|
|
pub export_status: ExportStatus,
|
|
}
|
|
impl Dialog {
|
|
fn new(title: impl Into<String>, kind: DialogKind, fields: Vec<Field>) -> Self {
|
|
Self {
|
|
title: title.into(),
|
|
kind,
|
|
fields,
|
|
focus: 0,
|
|
error: String::new(),
|
|
request: None,
|
|
export_status: ExportStatus::Ready,
|
|
}
|
|
}
|
|
}
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub enum Hit {
|
|
Command(Command),
|
|
Menu(usize),
|
|
MenuItem(usize),
|
|
Window(u64),
|
|
ProjectMember(usize),
|
|
ProjectView(bool),
|
|
DialogField(usize),
|
|
DialogSubmit,
|
|
DialogCancel,
|
|
}
|
|
|
|
pub struct App {
|
|
pub session: crate::execution::Session,
|
|
pub editor: crate::editor::Editor,
|
|
pub project: Project,
|
|
pub options: Options,
|
|
pub mode: Mode,
|
|
pub execution: Execution,
|
|
pub windows: Vec<Window>,
|
|
pub active: u64,
|
|
pub size: (u16, u16),
|
|
pub menu: Option<(usize, usize)>,
|
|
pub control_menu: bool,
|
|
pub dialog: Option<Dialog>,
|
|
pub properties: bool,
|
|
pub value_focus: bool,
|
|
pub selected_member: usize,
|
|
pub message: String,
|
|
pub quit: bool,
|
|
pub last_command: Option<Command>,
|
|
pub hits: Vec<(Rect, Hit)>,
|
|
pub basic_events: Vec<Event>,
|
|
pub last_export: Option<(ExportRequest, ExportStatus)>,
|
|
pub config_path: PathBuf,
|
|
pub saved_options: Options,
|
|
config_disk: Option<Vec<u8>>,
|
|
next_window: u64,
|
|
geometry_action: Option<(bool, Rect)>,
|
|
base: PathBuf,
|
|
}
|
|
impl App {
|
|
pub fn load_initial_project(&mut self, path: PathBuf) -> Result<()> {
|
|
self.finish_change(AfterSave::OpenProject(path))
|
|
}
|
|
pub fn new(base: &Path, config_path: PathBuf, size: (u16, u16)) -> Result<Self> {
|
|
let mut project = Project::new(base)?;
|
|
let doc = untitled(&mut project)?;
|
|
let (options, errors, config_disk) = Options::load(&config_path);
|
|
project.include_paths = options.include_paths.clone();
|
|
let mut app = Self {
|
|
session: Default::default(),
|
|
editor: Default::default(),
|
|
project,
|
|
options: options.clone(),
|
|
saved_options: options,
|
|
config_path,
|
|
config_disk,
|
|
base: tb_vm::project_io::identity(base)?,
|
|
mode: Mode::Environment,
|
|
execution: Execution::Idle,
|
|
windows: vec![],
|
|
active: 0,
|
|
size,
|
|
menu: None,
|
|
control_menu: false,
|
|
dialog: None,
|
|
properties: false,
|
|
value_focus: false,
|
|
selected_member: 0,
|
|
message: format!(
|
|
"Terminal Basic {} · Copyright {} · {}",
|
|
env!("CARGO_PKG_VERSION"),
|
|
env!("CARGO_PKG_AUTHORS"),
|
|
errors.join("; ")
|
|
),
|
|
quit: false,
|
|
last_command: None,
|
|
hits: vec![],
|
|
basic_events: vec![],
|
|
last_export: None,
|
|
next_window: 1,
|
|
geometry_action: None,
|
|
};
|
|
let width = size.0.max(80);
|
|
let height = size.1.max(25);
|
|
let view = app.project.open_view(doc)?;
|
|
let code = app.add_window(
|
|
WindowKind::Code(view),
|
|
Rect::new(0, 1, width.saturating_sub(32), height - 3),
|
|
);
|
|
app.add_window(
|
|
WindowKind::Project,
|
|
Rect::new(width - 32, 1, 32, height - 3),
|
|
);
|
|
app.active = code;
|
|
Ok(app)
|
|
}
|
|
pub fn area(&self) -> Rect {
|
|
Rect::new(
|
|
0,
|
|
1,
|
|
self.size.0,
|
|
self.size
|
|
.1
|
|
.saturating_sub(if self.mode == Mode::Designer { 1 } else { 2 }),
|
|
)
|
|
}
|
|
pub fn rect(&self, w: &Window) -> Rect {
|
|
let area = self.area();
|
|
match w.state {
|
|
WindowState::Maximized => area,
|
|
WindowState::Minimized => Rect::new(
|
|
0,
|
|
area.bottom().saturating_sub(2),
|
|
24.min(area.width),
|
|
2.min(area.height),
|
|
),
|
|
WindowState::Normal => clamp(w.normal, area),
|
|
}
|
|
}
|
|
fn add_window(&mut self, kind: WindowKind, normal: Rect) -> u64 {
|
|
let id = self.next_window;
|
|
self.next_window += 1;
|
|
self.windows.push(Window {
|
|
id,
|
|
kind,
|
|
normal,
|
|
state: WindowState::Normal,
|
|
});
|
|
self.active = id;
|
|
id
|
|
}
|
|
pub fn active_window(&self) -> Option<&Window> {
|
|
self.windows.iter().find(|w| w.id == self.active)
|
|
}
|
|
pub fn active_document(&self) -> Option<DocumentId> {
|
|
match self.active_window()?.kind {
|
|
WindowKind::Code(v) => self.project.view(v).ok().map(|v| v.document()),
|
|
_ => self.project.members().get(self.selected_member).copied(),
|
|
}
|
|
}
|
|
pub fn title(&self, w: &Window) -> String {
|
|
match w.kind {
|
|
WindowKind::Code(v) => {
|
|
let doc = self
|
|
.project
|
|
.document(self.project.view(v).unwrap().document())
|
|
.unwrap();
|
|
let name = doc
|
|
.path()
|
|
.and_then(|p| p.file_name())
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
.unwrap_or_else(|| {
|
|
doc.source_path()
|
|
.file_stem()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
});
|
|
format!(
|
|
"[{}] {}{}{}",
|
|
w.id,
|
|
if self.editor.expansions.contains_key(&v) {
|
|
"Included Lines [read-only] · "
|
|
} else {
|
|
""
|
|
},
|
|
name,
|
|
if doc.is_dirty() { " *" } else { "" }
|
|
)
|
|
}
|
|
WindowKind::Project => format!(
|
|
"Project: {}",
|
|
self.project
|
|
.path()
|
|
.and_then(|p| p.file_name())
|
|
.map(|s| s.to_string_lossy().into_owned())
|
|
.unwrap_or_else(|| "Untitled".into())
|
|
)
|
|
.trim_end()
|
|
.to_string(),
|
|
WindowKind::Output if self.session.old_revision => "Output · ALTES KOMPILAT".into(),
|
|
_ => format!("{:?}", w.kind),
|
|
}
|
|
}
|
|
pub fn menus(&self) -> Vec<Menu> {
|
|
commands::menus(self.mode == Mode::Designer)
|
|
}
|
|
pub fn window_commands(&self) -> Vec<(String, Command)> {
|
|
self.windows
|
|
.iter()
|
|
.filter(|w| matches!(w.kind, WindowKind::Code(_)))
|
|
.filter(|w| self.mode!=Mode::Designer || matches!(w.kind,WindowKind::Code(v) if self.project.view(v).ok().and_then(|v|self.project.document(v.document()).ok()).is_some_and(|d|matches!(d.content(),Content::Form(_)))))
|
|
.map(|w| {
|
|
(
|
|
format!(
|
|
"{}{}{}",
|
|
if w.id == self.active { "• " } else { " " },
|
|
if self.mode==Mode::Designer {"Form: "}else{""},
|
|
self.title(w)
|
|
),
|
|
Command::FocusWindow(w.id),
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
pub fn availability(&self, command: Command) -> Option<String> {
|
|
if let Some(phase) = command.feature_phase() {
|
|
return Some(format!("Fachfunktion folgt in Phase-5-Change {phase:02}"));
|
|
}
|
|
use Command::*;
|
|
if command == Shell && self.session.host.shell_request.is_some() {
|
|
return Some("Shell-Übergabe bereits angefordert".into());
|
|
}
|
|
if matches!(
|
|
command,
|
|
Undo | Cut | Paste | Clear | LoadText | NewSub | NewFunction | Replace
|
|
) && self
|
|
.editor_view()
|
|
.is_ok_and(|v| self.editor.expansions.contains_key(&v))
|
|
{
|
|
return Some(
|
|
"Included Lines ist schreibgeschützt; Included File öffnet die Originaldatei"
|
|
.into(),
|
|
);
|
|
}
|
|
if matches!(
|
|
command,
|
|
LoadText
|
|
| Print
|
|
| SaveText
|
|
| Cut
|
|
| Copy
|
|
| Paste
|
|
| Clear
|
|
| NewSub
|
|
| NewFunction
|
|
| IncludedFile
|
|
| IncludedLines
|
|
| Find
|
|
| SelectedText
|
|
| FindNext
|
|
| Replace
|
|
| Procedures
|
|
| PreviousCode
|
|
| Diagnostics
|
|
) && !matches!(
|
|
self.active_window().map(|w| w.kind),
|
|
Some(WindowKind::Code(_))
|
|
) {
|
|
return Some("Kein Codefenster aktiv".into());
|
|
}
|
|
if matches!(
|
|
command,
|
|
SaveFile | SaveAs | LoadText | SaveText | Undo | NewWindow
|
|
) && self.active_document().is_none()
|
|
{
|
|
return Some("Kein Dokument ausgewählt".into());
|
|
}
|
|
if matches!(command, Form)
|
|
&& !self
|
|
.project
|
|
.documents()
|
|
.any(|(_, d)| matches!(d.content(), Content::Form(_)))
|
|
{
|
|
return Some("Kein Formular vorhanden".into());
|
|
}
|
|
if matches!(command, RemoveFile | Startup) && self.project.members().is_empty() {
|
|
return Some("Kein Projektmitglied vorhanden".into());
|
|
}
|
|
if matches!(
|
|
command,
|
|
CloseWindow | MoveWindow | SizeWindow | Minimize | Maximize | Restore | ControlMenu
|
|
) && self.active_window().is_none()
|
|
{
|
|
return Some("Kein Fenster aktiv".into());
|
|
}
|
|
None
|
|
}
|
|
pub fn execute(&mut self, command: Command) {
|
|
self.last_command = Some(command);
|
|
if let Some(reason) = self.availability(command) {
|
|
self.message = reason;
|
|
return;
|
|
}
|
|
self.menu = None;
|
|
self.control_menu = false;
|
|
if let Err(e) = self.action(command) {
|
|
self.message = format!("{e:#}");
|
|
}
|
|
}
|
|
fn action(&mut self, command: Command) -> Result<()> {
|
|
use Command::*;
|
|
let id = self.active_document();
|
|
match command {
|
|
Start => self.start_execution()?,
|
|
Restart => self.restart_target()?,
|
|
Continue => self.continue_execution()?,
|
|
Pause => self.pause_execution(),
|
|
OutputScreen => {
|
|
self.session.fullscreen = !self.session.fullscreen;
|
|
}
|
|
CommandLine => self.open_dialog(
|
|
"COMMAND$",
|
|
DialogKind::CommandLine,
|
|
vec![Field::text(
|
|
"Argumente für den nächsten Start",
|
|
&self.session.command,
|
|
)],
|
|
),
|
|
Shell => {
|
|
self.session.file_shell = true;
|
|
self.session.host.shell_request = Some(String::new());
|
|
}
|
|
Print => self.open_dialog(
|
|
"Print",
|
|
DialogKind::Print,
|
|
vec![
|
|
Field::text("UTF-8-Ausgabedatei", self.output_default("LPT1.TXT")),
|
|
Field::toggle("Bestehendes Ziel überschreiben", false),
|
|
Field::toggle(
|
|
"Nur Auswahl",
|
|
self.project
|
|
.view(self.editor_view()?)?
|
|
.selection()
|
|
.is_some(),
|
|
),
|
|
],
|
|
),
|
|
Cut | Copy | Paste | Clear | NewSub | NewFunction | IncludedFile | IncludedLines
|
|
| Find | SelectedText | FindNext | Replace | Procedures | PreviousCode
|
|
| Diagnostics => self.editor_command(command)?,
|
|
NewProject => self.change_project(AfterSave::NewProject)?,
|
|
OpenProject => self.open_dialog(
|
|
"Open Project",
|
|
DialogKind::OpenProject,
|
|
vec![Field::text("Datei", self.source_default())],
|
|
),
|
|
Exit => self.change_project(AfterSave::Exit)?,
|
|
NewModule => self.open_dialog(
|
|
"New Module",
|
|
DialogKind::NewModule,
|
|
vec![Field::text("Name", "Module1")],
|
|
),
|
|
NewForm => {
|
|
let mut n = 1;
|
|
while self
|
|
.project
|
|
.loader()?
|
|
.resolve(self.project.directory(), &format!("Form{n}.frm"))
|
|
.is_ok()
|
|
|| self.project.documents().any(|(_, d)| {
|
|
d.source_path()
|
|
.file_stem()
|
|
.is_some_and(|s| s.eq_ignore_ascii_case(format!("Form{n}").as_str()))
|
|
})
|
|
{
|
|
n += 1;
|
|
}
|
|
let id = self.project.new_form(&format!("Form{n}"))?;
|
|
self.show_document(id, true)?;
|
|
}
|
|
AddFile => self.open_dialog(
|
|
"Add File",
|
|
DialogKind::AddFile,
|
|
vec![Field::text("Datei", self.source_default())],
|
|
),
|
|
SaveFile => {
|
|
let id = id.unwrap();
|
|
if self.project.document(id)?.path().is_none()
|
|
|| self.project.save_file(id, None).is_err()
|
|
{
|
|
self.save_dialog(false, vec![id], AfterSave::Stay)?;
|
|
} else {
|
|
self.message = format!(
|
|
"Datei gespeichert · {}",
|
|
self.project.save_notices.join("; ")
|
|
);
|
|
}
|
|
}
|
|
SaveAs => self.save_dialog(false, vec![id.unwrap()], AfterSave::Stay)?,
|
|
SaveProject => self.save_dialog(
|
|
true,
|
|
self.project.documents().map(|(id, _)| id).collect(),
|
|
AfterSave::Stay,
|
|
)?,
|
|
RemoveFile => {
|
|
let ids = self.project.members();
|
|
let names = self.names(&ids);
|
|
let mut replacements = vec!["Bisheriger Standard".into()];
|
|
replacements.extend(names.clone());
|
|
self.open_dialog(
|
|
"Remove File",
|
|
DialogKind::Remove,
|
|
vec![
|
|
Field::choice("Mitglied", names, self.selected_member.min(ids.len() - 1)),
|
|
Field::choice("Ersatz für Startdatei", replacements, 0),
|
|
],
|
|
);
|
|
}
|
|
Startup => {
|
|
let ids = self.project.members();
|
|
let mut names = vec!["Bisheriger Standard".into()];
|
|
names.extend(self.names(&ids));
|
|
let selected = self
|
|
.project
|
|
.startup()
|
|
.and_then(|id| ids.iter().position(|i| *i == id))
|
|
.map(|n| n + 1)
|
|
.unwrap_or(0);
|
|
self.open_dialog(
|
|
"Set Start-up File",
|
|
DialogKind::Startup,
|
|
vec![Field::choice("Startdatei", names, selected)],
|
|
);
|
|
}
|
|
Code | Form => {
|
|
let ids: Vec<_> = self
|
|
.project
|
|
.documents()
|
|
.filter(|(_, d)| command == Code || matches!(d.content(), Content::Form(_)))
|
|
.map(|(id, _)| id)
|
|
.collect();
|
|
let selected = id
|
|
.and_then(|id| ids.iter().position(|i| *i == id))
|
|
.unwrap_or(0);
|
|
let names = self.names(&ids);
|
|
self.open_dialog(
|
|
if command == Code { "Code" } else { "Form" },
|
|
if command == Code {
|
|
DialogKind::ChooseCode
|
|
} else {
|
|
DialogKind::ChooseForm
|
|
},
|
|
vec![Field::choice("Dokument", names, selected)],
|
|
);
|
|
}
|
|
LoadText => self.open_dialog(
|
|
"Load Text",
|
|
DialogKind::LoadText,
|
|
vec![Field::text("Datei", self.source_default())],
|
|
),
|
|
SaveText => self.open_dialog(
|
|
"Save Text",
|
|
DialogKind::SaveText,
|
|
vec![
|
|
Field::text("Datei", self.output_default("text.txt")),
|
|
Field::toggle("Bestehendes Ziel überschreiben", false),
|
|
],
|
|
),
|
|
Undo => {
|
|
self.project.undo(id.unwrap())?;
|
|
}
|
|
NewWindow => {
|
|
let view = self.project.open_view(id.unwrap())?;
|
|
self.add_window(WindowKind::Code(view), self.cascade());
|
|
}
|
|
Arrange => self.arrange(),
|
|
NextWindow => self.cycle(1),
|
|
PreviousWindow => self.cycle(-1),
|
|
CloseWindow => {
|
|
if let Some(i) = self.windows.iter().position(|w| w.id == self.active) {
|
|
let w = self.windows.remove(i);
|
|
if let WindowKind::Code(v) = w.kind {
|
|
self.project.close_view(v)?;
|
|
}
|
|
self.active = self.windows.first().map(|w| w.id).unwrap_or(0);
|
|
}
|
|
}
|
|
MoveWindow | SizeWindow => {
|
|
if let Some(w) = self.windows.iter().find(|w| w.id == self.active) {
|
|
self.geometry_action = Some((command == SizeWindow, w.normal));
|
|
self.message = "Pfeile ändern Fenster; Enter bestätigt, Esc verwirft".into();
|
|
}
|
|
}
|
|
Minimize | Maximize | Restore => {
|
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == self.active) {
|
|
w.state = match command {
|
|
Minimize => WindowState::Minimized,
|
|
Maximize => WindowState::Maximized,
|
|
_ => WindowState::Normal,
|
|
};
|
|
}
|
|
}
|
|
ControlMenu => {
|
|
self.control_menu = true;
|
|
self.menu = Some((0, 0));
|
|
}
|
|
FocusWindow(id) => {
|
|
if self.windows.iter().any(|w| w.id == id) {
|
|
self.active = id;
|
|
}
|
|
}
|
|
Calls | Debug | HelpWindow | Immediate | Output | Project => {
|
|
let kind = match command {
|
|
Calls => WindowKind::Calls,
|
|
Debug => WindowKind::Debug,
|
|
HelpWindow => WindowKind::Help,
|
|
Immediate => WindowKind::Immediate,
|
|
Output => WindowKind::Output,
|
|
_ => WindowKind::Project,
|
|
};
|
|
self.show_tool(kind);
|
|
if command == Output {
|
|
self.session.fullscreen = false;
|
|
}
|
|
}
|
|
MenuBar => {
|
|
self.properties = !self.properties;
|
|
self.value_focus = false;
|
|
self.menu = if self.properties { None } else { Some((0, 0)) };
|
|
}
|
|
Display => {
|
|
let mut fields = vec![];
|
|
for (i, name) in ELEMENTS.iter().enumerate() {
|
|
fields.push(Field::choice(
|
|
format!("{name} Vordergrund"),
|
|
(0..16).map(|n| n.to_string()).collect(),
|
|
self.options.colors[i].0 as usize,
|
|
));
|
|
fields.push(Field::choice(
|
|
format!("{name} Hintergrund"),
|
|
(0..16).map(|n| n.to_string()).collect(),
|
|
self.options.colors[i].1 as usize,
|
|
));
|
|
}
|
|
fields.extend([
|
|
Field::text("Desktopzeichen", self.options.desktop.to_string()),
|
|
Field::text("Tabweite", self.options.tab_width.to_string()),
|
|
]);
|
|
self.open_dialog("Display", DialogKind::Display, fields);
|
|
}
|
|
Paths => self.open_dialog(
|
|
"Set Paths",
|
|
DialogKind::Paths,
|
|
vec![
|
|
Field::text("Quellen", self.options.source_dir.display().to_string()),
|
|
Field::text("Ausgaben", self.options.output_dir.display().to_string()),
|
|
Field::text(
|
|
"Include-Pfade (| getrennt)",
|
|
self.options
|
|
.include_paths
|
|
.iter()
|
|
.map(|p| p.display().to_string())
|
|
.collect::<Vec<_>>()
|
|
.join("|"),
|
|
),
|
|
],
|
|
),
|
|
RightMouse => self.open_dialog(
|
|
"Right Mouse",
|
|
DialogKind::RightMouse,
|
|
vec![Field::toggle(
|
|
"Rechtsklick öffnet Kontext-Hilfe",
|
|
self.options.right_help,
|
|
)],
|
|
),
|
|
SaveOptions => self.open_dialog("Save Options", DialogKind::SaveOptions, vec![]),
|
|
SyntaxChecking => {
|
|
self.options.syntax_checking = !self.options.syntax_checking;
|
|
self.message = format!("Syntax Checking: {}", self.options.syntax_checking);
|
|
}
|
|
About => self.open_dialog(
|
|
format!(
|
|
"Terminal Basic {} · Copyright {}",
|
|
env!("CARGO_PKG_VERSION"),
|
|
env!("CARGO_PKG_AUTHORS")
|
|
),
|
|
DialogKind::Message,
|
|
vec![],
|
|
),
|
|
MakeExe | MakeLibrary => {
|
|
let artifact = if command == MakeExe {
|
|
Artifact::Executable
|
|
} else {
|
|
Artifact::Library
|
|
};
|
|
let systems = vec!["linux".into(), "macos".into(), "windows".into()];
|
|
let selected = systems
|
|
.iter()
|
|
.position(|s| s == std::env::consts::OS)
|
|
.unwrap_or(0);
|
|
let architectures = vec!["x86_64".into(), "aarch64".into()];
|
|
let arch = architectures
|
|
.iter()
|
|
.position(|s| s == std::env::consts::ARCH)
|
|
.unwrap_or(0);
|
|
self.open_dialog(
|
|
if command == MakeExe {
|
|
"Make EXE File"
|
|
} else {
|
|
"Make Library"
|
|
},
|
|
DialogKind::Export(artifact),
|
|
vec![
|
|
Field::text("Artefakt", artifact.label()),
|
|
Field::choice("Zielsystem", systems, selected),
|
|
Field::choice("Architektur", architectures, arch),
|
|
Field::text(
|
|
"Ausgabepfad",
|
|
self.output_default(if artifact == Artifact::Executable {
|
|
"program"
|
|
} else {
|
|
"library"
|
|
}),
|
|
),
|
|
Field::toggle("Bestehendes Ziel überschreiben", false),
|
|
],
|
|
);
|
|
}
|
|
_ => {}
|
|
}
|
|
Ok(())
|
|
}
|
|
fn names(&self, ids: &[DocumentId]) -> Vec<String> {
|
|
ids.iter()
|
|
.map(|id| {
|
|
self.project
|
|
.document(*id)
|
|
.unwrap()
|
|
.source_path()
|
|
.file_name()
|
|
.unwrap_or_default()
|
|
.to_string_lossy()
|
|
.into_owned()
|
|
})
|
|
.collect()
|
|
}
|
|
fn source_default(&self) -> String {
|
|
if self.options.source_dir.as_os_str().is_empty() {
|
|
self.base.display().to_string()
|
|
} else {
|
|
self.options.source_dir.display().to_string()
|
|
}
|
|
}
|
|
fn output_default(&self, name: &str) -> String {
|
|
if self.options.output_dir.as_os_str().is_empty() {
|
|
self.base.join(name)
|
|
} else {
|
|
self.options.output_dir.join(name)
|
|
}
|
|
.display()
|
|
.to_string()
|
|
}
|
|
pub(crate) fn open_dialog(
|
|
&mut self,
|
|
title: impl Into<String>,
|
|
kind: DialogKind,
|
|
fields: Vec<Field>,
|
|
) {
|
|
self.dialog = Some(Dialog::new(title, kind, fields));
|
|
}
|
|
fn cascade(&self) -> Rect {
|
|
let offset = (self.windows.len() % 5) as u16 * 2;
|
|
clamp(Rect::new(offset, 1 + offset, 60, 16), self.area())
|
|
}
|
|
pub(crate) fn show_tool(&mut self, kind: WindowKind) {
|
|
if let Some(w) = self.windows.iter_mut().find(|w| w.kind == kind) {
|
|
self.active = w.id;
|
|
w.state = WindowState::Normal;
|
|
} else {
|
|
self.add_window(kind, self.cascade());
|
|
}
|
|
}
|
|
pub(crate) fn show_document(&mut self, id: DocumentId, form: bool) -> Result<()> {
|
|
if form {
|
|
ensure!(
|
|
matches!(self.project.document(id)?.content(), Content::Form(_)),
|
|
"Kein Formular"
|
|
);
|
|
}
|
|
if let Some(w)=self.windows.iter_mut().find(|w|matches!(w.kind,WindowKind::Code(v) if self.project.view(v).is_ok_and(|v|v.document()==id))){self.active=w.id;w.state=WindowState::Normal;}else{let view=self.project.open_view(id)?;self.add_window(WindowKind::Code(view),self.cascade());}
|
|
self.mode = if form {
|
|
Mode::Designer
|
|
} else {
|
|
Mode::Environment
|
|
};
|
|
self.properties = form;
|
|
self.value_focus = false;
|
|
Ok(())
|
|
}
|
|
fn cycle(&mut self, direction: isize) {
|
|
if self.windows.is_empty() {
|
|
return;
|
|
}
|
|
let i = self
|
|
.windows
|
|
.iter()
|
|
.position(|w| w.id == self.active)
|
|
.unwrap_or(0);
|
|
let i = (i as isize + direction).rem_euclid(self.windows.len() as isize) as usize;
|
|
self.active = self.windows[i].id;
|
|
}
|
|
fn arrange(&mut self) {
|
|
let area = self.area();
|
|
let n = self.windows.len();
|
|
if n == 0 {
|
|
return;
|
|
}
|
|
let cols = (n as f64).sqrt().ceil() as u16;
|
|
let rows = (n as u16).div_ceil(cols);
|
|
for (i, w) in self.windows.iter_mut().enumerate() {
|
|
let col = i as u16 % cols;
|
|
let row = i as u16 / cols;
|
|
let x = area.width * col / cols;
|
|
let right = area.width * (col + 1) / cols;
|
|
let y = area.height * row / rows;
|
|
let bottom = area.height * (row + 1) / rows;
|
|
w.normal = Rect::new(x, area.y + y, right - x, bottom - y);
|
|
w.state = WindowState::Normal;
|
|
}
|
|
}
|
|
fn change_project(&mut self, after: AfterSave) -> Result<()> {
|
|
if self.project.is_dirty() {
|
|
self.open_dialog(
|
|
"Ungespeicherte Änderungen",
|
|
DialogKind::Dirty(after),
|
|
vec![Field::choice(
|
|
"Entscheidung",
|
|
vec!["Abbrechen".into(), "Speichern".into(), "Verwerfen".into()],
|
|
0,
|
|
)],
|
|
);
|
|
} else {
|
|
self.finish_change(after)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
fn finish_change(&mut self, after: AfterSave) -> Result<()> {
|
|
match after {
|
|
AfterSave::Stay => return Ok(()),
|
|
AfterSave::Exit => {
|
|
self.session.finish();
|
|
self.basic_events.clear();
|
|
self.quit = true;
|
|
return Ok(());
|
|
}
|
|
AfterSave::NewProject => {
|
|
self.project.new_project(Decision::Discard)?;
|
|
untitled(&mut self.project)?;
|
|
}
|
|
AfterSave::OpenProject(path) => {
|
|
self.project.open_project(&path, Decision::Discard)?;
|
|
}
|
|
}
|
|
self.session = Default::default();
|
|
self.basic_events.clear();
|
|
self.execution = Execution::Idle;
|
|
self.base = self.project.directory().to_path_buf();
|
|
self.editor = Default::default();
|
|
self.windows.clear();
|
|
self.active = 0;
|
|
self.selected_member = 0;
|
|
self.mode = Mode::Environment;
|
|
self.properties = false;
|
|
if let Some(id) = self.project.members().first().copied() {
|
|
self.show_document(id, false)?;
|
|
}
|
|
let active = self.active;
|
|
self.show_tool(WindowKind::Project);
|
|
if active != 0 {
|
|
self.active = active;
|
|
}
|
|
self.arrange();
|
|
Ok(())
|
|
}
|
|
fn save_dialog(&mut self, project: bool, ids: Vec<DocumentId>, after: AfterSave) -> Result<()> {
|
|
let mut fields = vec![];
|
|
if project {
|
|
fields.push(Field::text(
|
|
"Projekt (.mak)",
|
|
self.project
|
|
.path()
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|| self.output_default("project.mak")),
|
|
));
|
|
fields.push(Field::toggle("Projektziel überschreiben", false));
|
|
}
|
|
for id in &ids {
|
|
let doc = self.project.document(*id)?;
|
|
fields.push(Field::text(
|
|
self.names(&[*id])[0].clone(),
|
|
doc.path()
|
|
.map(|p| p.display().to_string())
|
|
.unwrap_or_else(|| doc.source_path().display().to_string()),
|
|
));
|
|
fields.push(Field::toggle("Dateiziel überschreiben", false));
|
|
}
|
|
self.open_dialog(
|
|
if project {
|
|
"Save Project"
|
|
} else {
|
|
"Save File As"
|
|
},
|
|
DialogKind::Save {
|
|
project,
|
|
ids,
|
|
after,
|
|
},
|
|
fields,
|
|
);
|
|
Ok(())
|
|
}
|
|
fn submit_dialog(&mut self) {
|
|
let Some(mut d) = self.dialog.take() else {
|
|
return;
|
|
};
|
|
match self.submit(&mut d) {
|
|
Ok(keep) => {
|
|
if keep {
|
|
self.dialog = Some(d);
|
|
}
|
|
}
|
|
Err(e) => {
|
|
d.error = format!("{e:#}");
|
|
self.dialog = Some(d);
|
|
}
|
|
}
|
|
}
|
|
fn submit(&mut self, d: &mut Dialog) -> Result<bool> {
|
|
match d.kind.clone() {
|
|
DialogKind::Search { .. }
|
|
| DialogKind::Procedure(_)
|
|
| DialogKind::Procedures(_)
|
|
| DialogKind::Diagnostics => return self.editor_submit(d),
|
|
DialogKind::Browse {
|
|
mut return_to,
|
|
field,
|
|
entries,
|
|
} => {
|
|
let path = entries
|
|
.get(d.fields[0].index())
|
|
.ok_or_else(|| anyhow!("Keine Datei ausgewählt"))?;
|
|
if path.is_dir() {
|
|
self.dialog = Some(Self::browser(*return_to, field, path)?);
|
|
} else {
|
|
return_to.fields[field] = Field::text(
|
|
return_to.fields[field].label.clone(),
|
|
path.display().to_string(),
|
|
);
|
|
return_to.request = None;
|
|
self.dialog = Some(*return_to);
|
|
}
|
|
}
|
|
DialogKind::Message => {}
|
|
DialogKind::OpenProject => {
|
|
let path = self.base.join(d.fields[0].string());
|
|
Project::open(&path, self.options.include_paths.clone())?;
|
|
self.change_project(AfterSave::OpenProject(path))?;
|
|
}
|
|
DialogKind::NewModule => {
|
|
let id = self.project.new_module(&d.fields[0].string())?;
|
|
self.show_document(id, false)?;
|
|
}
|
|
DialogKind::AddFile => {
|
|
let id = self.project.add_file(Path::new(&d.fields[0].string()))?;
|
|
self.show_document(id, false)?;
|
|
}
|
|
DialogKind::ChooseCode | DialogKind::ChooseForm => {
|
|
let form = matches!(d.kind, DialogKind::ChooseForm);
|
|
let ids: Vec<_> = self
|
|
.project
|
|
.documents()
|
|
.filter(|(_, d)| !form || matches!(d.content(), Content::Form(_)))
|
|
.map(|(id, _)| id)
|
|
.collect();
|
|
let id = *ids
|
|
.get(d.fields[0].index())
|
|
.ok_or_else(|| anyhow!("Kein Dokument"))?;
|
|
self.show_document(id, form)?;
|
|
}
|
|
DialogKind::Remove => {
|
|
let ids = self.project.members();
|
|
let id = ids[d.fields[0].index()];
|
|
let replacement = d.fields[1].index();
|
|
self.project.remove_file(
|
|
id,
|
|
Some(if replacement == 0 {
|
|
StartupRemoval::Default
|
|
} else {
|
|
StartupRemoval::Replace(ids[replacement - 1])
|
|
}),
|
|
)?;
|
|
self.selected_member = self
|
|
.selected_member
|
|
.min(self.project.members().len().saturating_sub(1));
|
|
}
|
|
DialogKind::Startup => {
|
|
let ids = self.project.members();
|
|
let choice = d.fields[0].index();
|
|
self.project.set_startup(if choice == 0 {
|
|
None
|
|
} else {
|
|
Some(ids[choice - 1])
|
|
})?;
|
|
}
|
|
DialogKind::Dirty(after) => match d.fields[0].index() {
|
|
1 => self.save_dialog(
|
|
true,
|
|
self.project.documents().map(|(id, _)| id).collect(),
|
|
after,
|
|
)?,
|
|
2 => self.finish_change(after)?,
|
|
_ => {}
|
|
},
|
|
DialogKind::Save {
|
|
project,
|
|
ids,
|
|
after,
|
|
} => {
|
|
let mut plan = SavePlan::default();
|
|
let mut offset = 0;
|
|
if project {
|
|
plan.project = Some(Destination {
|
|
path: d.fields[0].string().into(),
|
|
overwrite: d.fields[1].flag(),
|
|
});
|
|
offset = 2;
|
|
}
|
|
for (i, id) in ids.iter().enumerate() {
|
|
let path = PathBuf::from(d.fields[offset + 2 * i].string());
|
|
let doc = self.project.document(*id)?;
|
|
// Unveränderte Mitglieder brauchen keinen erneuten Schreibzugriff (insbesondere Binärimporte).
|
|
if !project || doc.is_dirty() || doc.path() != Some(path.as_path()) {
|
|
plan.files.insert(
|
|
*id,
|
|
Destination {
|
|
path,
|
|
overwrite: d.fields[offset + 2 * i + 1].flag(),
|
|
},
|
|
);
|
|
}
|
|
}
|
|
if project {
|
|
self.project.save_project(&plan)?;
|
|
self.base = self.project.path().unwrap().parent().unwrap().to_path_buf();
|
|
} else {
|
|
self.project.save_file(ids[0], plan.files.get(&ids[0]))?;
|
|
}
|
|
self.message = format!("Gespeichert · {}", self.project.save_notices.join("; "));
|
|
self.finish_change(after)?;
|
|
}
|
|
DialogKind::LoadText => {
|
|
let text = std::fs::read_to_string(self.base.join(d.fields[0].string()))?;
|
|
self.editor_insert(&text)?;
|
|
}
|
|
DialogKind::CommandLine => {
|
|
self.session.command = d.fields[0].string();
|
|
}
|
|
DialogKind::ResumeRevision => {
|
|
if d.fields[0].index() == 0 {
|
|
self.restart_target()?;
|
|
} else {
|
|
self.resume_execution(true);
|
|
}
|
|
}
|
|
DialogKind::SaveText | DialogKind::Print => {
|
|
let (id, _) = self.code_cursor()?;
|
|
self.project.save_text(
|
|
id,
|
|
if matches!(d.kind, DialogKind::Print) && !d.fields[2].flag() {
|
|
None
|
|
} else {
|
|
self.project.view(self.editor_view()?)?.selection()
|
|
},
|
|
&Destination {
|
|
path: d.fields[0].string().into(),
|
|
overwrite: d.fields[1].flag(),
|
|
},
|
|
)?;
|
|
}
|
|
DialogKind::Display => {
|
|
let mut options = self.options.clone();
|
|
for i in 0..7 {
|
|
options.colors[i] = (
|
|
d.fields[2 * i].index() as u8,
|
|
d.fields[2 * i + 1].index() as u8,
|
|
);
|
|
}
|
|
let s = d.fields[14].string();
|
|
ensure!(s.chars().count() == 1, "Ein Desktopzeichen erforderlich");
|
|
options.desktop = s.chars().next().unwrap();
|
|
options.tab_width = d.fields[15].string().parse()?;
|
|
options.validate()?;
|
|
self.options = options;
|
|
}
|
|
DialogKind::Paths => {
|
|
let mut options = self.options.clone();
|
|
let path = |s: String| {
|
|
if s.is_empty() {
|
|
PathBuf::new()
|
|
} else {
|
|
self.base.join(s)
|
|
}
|
|
};
|
|
options.source_dir = path(d.fields[0].string());
|
|
options.output_dir = path(d.fields[1].string());
|
|
options.include_paths = d.fields[2]
|
|
.string()
|
|
.split('|')
|
|
.filter(|s| !s.is_empty())
|
|
.map(|s| path(s.into()))
|
|
.collect();
|
|
options.validate()?;
|
|
self.project.include_paths = options.include_paths.clone();
|
|
self.options = options;
|
|
}
|
|
DialogKind::RightMouse => self.options.right_help = d.fields[0].flag(),
|
|
DialogKind::SaveOptions => {
|
|
self.config_disk = Some(
|
|
self.options
|
|
.save(&self.config_path, self.config_disk.as_deref())?,
|
|
);
|
|
self.saved_options = self.options.clone();
|
|
self.message = format!("Optionen gespeichert: {}", self.config_path.display());
|
|
}
|
|
DialogKind::Export(artifact) => {
|
|
let request = ExportRequest::validate(
|
|
&self.project,
|
|
artifact,
|
|
&d.fields[1].string(),
|
|
&d.fields[2].string(),
|
|
&Destination {
|
|
path: d.fields[3].string().into(),
|
|
overwrite: d.fields[4].flag(),
|
|
},
|
|
)?;
|
|
d.request = Some(request);
|
|
d.export_status = ExportStatus::Ready;
|
|
d.error = crate::export::UNAVAILABLE.into();
|
|
return Ok(true);
|
|
}
|
|
}
|
|
Ok(false)
|
|
}
|
|
/// Die spätere Anbindung und Oberflächentests liefern denselben konkreten Auftrag zurück.
|
|
pub fn export_result(&mut self, request: &ExportRequest, status: ExportStatus) -> Result<()> {
|
|
let d = self
|
|
.dialog
|
|
.as_mut()
|
|
.ok_or_else(|| anyhow!("Kein Exportdialog offen"))?;
|
|
ensure!(
|
|
d.request.as_ref() == Some(request),
|
|
"Exportauftrag stimmt nicht mit dem Dialog überein"
|
|
);
|
|
ensure!(
|
|
request.project == crate::export::ProjectStamp::capture(&self.project),
|
|
"Projekt wurde seit dem Exportauftrag geändert"
|
|
);
|
|
d.export_status = status.clone();
|
|
d.error.clear();
|
|
self.last_export = Some((request.clone(), status));
|
|
Ok(())
|
|
}
|
|
fn code_cursor(&self) -> Result<(DocumentId, usize)> {
|
|
let Some(Window {
|
|
kind: WindowKind::Code(view),
|
|
..
|
|
}) = self.active_window()
|
|
else {
|
|
return Err(anyhow!("Kein Codefenster aktiv"));
|
|
};
|
|
let view = self.project.view(*view)?;
|
|
Ok((view.document(), view.cursor))
|
|
}
|
|
pub fn handle(&mut self, event: Event) {
|
|
let old = self.editor_position();
|
|
if !matches!(event, Event::Key(_) | Event::Resize(_, _)) {
|
|
self.editor.chord = None;
|
|
}
|
|
self.handle_event(event);
|
|
self.line_leave(old);
|
|
}
|
|
fn handle_event(&mut self, event: Event) {
|
|
if matches!(event, Event::Key(k) if k.kind != KeyEventKind::Release && k.code == K::Pause && k.modifiers.contains(M::CONTROL))
|
|
{
|
|
self.pause_execution();
|
|
return;
|
|
}
|
|
if let Event::Resize(w, h) = event {
|
|
self.size = (w, h);
|
|
if let Some(vm) = self.session.vm.as_mut() {
|
|
vm.rt.ereignis(tb_runtime::host::Ereignis::Groesse {
|
|
cols: w as usize,
|
|
rows: h as usize,
|
|
});
|
|
vm.forms.resize(w as usize, h as usize);
|
|
}
|
|
return;
|
|
}
|
|
if self.size.0 < 80 || self.size.1 < 25 {
|
|
return;
|
|
}
|
|
if let Event::Key(key) = event {
|
|
if key.kind == KeyEventKind::Release {
|
|
return;
|
|
}
|
|
self.key(key);
|
|
} else if let Event::Mouse(mouse) = event {
|
|
if self.dialog.is_none() && self.menu.is_none() && self.program_focus() {
|
|
if self.session.fullscreen {
|
|
self.basic_events.push(Event::Mouse(mouse));
|
|
return;
|
|
}
|
|
if let Some(w) = self.active_window() {
|
|
let r = self.rect(w);
|
|
let inner = Rect::new(
|
|
r.x + 1,
|
|
r.y + 1,
|
|
r.width.saturating_sub(2),
|
|
r.height.saturating_sub(2),
|
|
);
|
|
if inner.contains((mouse.column, mouse.row).into()) {
|
|
let mut mouse = mouse;
|
|
mouse.column -= inner.x;
|
|
mouse.row -= inner.y;
|
|
self.basic_events.push(Event::Mouse(mouse));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if mouse.kind == MouseEventKind::Down(MouseButton::Right) {
|
|
if self.dialog.is_none() && self.menu.is_none() && self.options.right_help {
|
|
self.execute(Command::Topic);
|
|
}
|
|
return;
|
|
}
|
|
if mouse.kind == MouseEventKind::Down(MouseButton::Left) {
|
|
let hit = self
|
|
.hits
|
|
.iter()
|
|
.rev()
|
|
.find(|(r, _)| r.contains((mouse.column, mouse.row).into()))
|
|
.map(|(_, hit)| *hit);
|
|
if let Some(hit) = hit {
|
|
self.click(hit);
|
|
}
|
|
} else if self.dialog.is_none() && self.menu.is_none() {
|
|
if let Some(Window {
|
|
kind: WindowKind::Code(v),
|
|
..
|
|
}) = self.active_window()
|
|
{
|
|
let v = *v;
|
|
if let Ok(view) = self.project.view_mut(v) {
|
|
match mouse.kind {
|
|
MouseEventKind::ScrollDown => view.scroll_line += 1,
|
|
MouseEventKind::ScrollUp => {
|
|
view.scroll_line = view.scroll_line.saturating_sub(1)
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else if self.program_focus() {
|
|
self.basic_events.push(event);
|
|
} else if let Event::Paste(text) = event {
|
|
if self.dialog.is_none() && self.menu.is_none() && self.mode == Mode::Environment {
|
|
if let Err(e) = self.editor_insert(&text) {
|
|
self.message = e.to_string();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
fn click(&mut self, hit: Hit) {
|
|
if self.dialog.is_some() {
|
|
match hit {
|
|
Hit::DialogField(i) => {
|
|
let d = self.dialog.as_mut().unwrap();
|
|
d.focus = i.min(d.fields.len());
|
|
if let Some(field) = d.fields.get_mut(i) {
|
|
if !matches!(field.value, FieldValue::Text(_)) {
|
|
field.key(KeyEvent::new(K::Right, M::NONE));
|
|
d.request = None;
|
|
d.export_status = ExportStatus::Ready;
|
|
}
|
|
}
|
|
}
|
|
Hit::DialogSubmit => self.submit_dialog(),
|
|
Hit::DialogCancel => self.cancel_dialog(),
|
|
_ => {}
|
|
}
|
|
return;
|
|
}
|
|
if self.menu.is_some() {
|
|
match hit {
|
|
Hit::Menu(i) => self.menu = Some((i, 0)),
|
|
Hit::MenuItem(i) => {
|
|
if let Some((menu, _)) = self.menu {
|
|
self.menu = Some((menu, i));
|
|
self.menu_enter();
|
|
}
|
|
}
|
|
_ => {
|
|
self.menu = None;
|
|
self.control_menu = false;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
match hit {
|
|
Hit::Command(c) => self.execute(c),
|
|
Hit::Menu(i) => {
|
|
self.properties = false;
|
|
self.menu = Some((i, 0));
|
|
}
|
|
Hit::Window(id) => self.active = id,
|
|
Hit::ProjectMember(i) => {
|
|
self.selected_member = i;
|
|
if let Some(w) = self.windows.iter().find(|w| w.kind == WindowKind::Project) {
|
|
self.active = w.id;
|
|
}
|
|
}
|
|
Hit::ProjectView(form) => {
|
|
self.last_command = Some(if form { Command::Form } else { Command::Code });
|
|
if let Some(id) = self.project.members().get(self.selected_member).copied() {
|
|
if let Err(e) = self.show_document(id, form) {
|
|
self.message = e.to_string();
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
fn cancel_dialog(&mut self) {
|
|
if let Some(d) = self.dialog.take() {
|
|
if let DialogKind::Browse { return_to, .. } = d.kind {
|
|
self.dialog = Some(*return_to);
|
|
return;
|
|
}
|
|
if let Some(request) = d.request {
|
|
self.last_export = Some((request, ExportStatus::Cancelled));
|
|
}
|
|
}
|
|
}
|
|
fn program_focus(&self) -> bool {
|
|
matches!(self.execution, Execution::Running | Execution::Waiting)
|
|
&& (self.session.fullscreen
|
|
|| matches!(
|
|
self.active_window().map(|w| w.kind),
|
|
Some(WindowKind::Output)
|
|
))
|
|
}
|
|
fn key(&mut self, key: KeyEvent) {
|
|
if self.session.fullscreen && self.dialog.is_none() && key.code == K::F(4) {
|
|
self.execute(Command::OutputScreen);
|
|
return;
|
|
}
|
|
if self.editor.chord.is_some()
|
|
&& self.dialog.is_none()
|
|
&& self.menu.is_none()
|
|
&& !self.program_focus()
|
|
{
|
|
if let Err(e) = self.editor_key(key) {
|
|
self.message = e.to_string();
|
|
}
|
|
return;
|
|
}
|
|
if key.code == K::F(2) && self.dialog.is_some() {
|
|
self.browse();
|
|
return;
|
|
}
|
|
if let Some(d) = self.dialog.as_mut() {
|
|
match key.code {
|
|
K::Esc => self.cancel_dialog(),
|
|
K::Enter => self.submit_dialog(),
|
|
K::Tab if key.modifiers.contains(M::SHIFT) => {
|
|
d.focus = (d.focus + d.fields.len()) % (d.fields.len() + 1)
|
|
}
|
|
K::BackTab | K::Up => d.focus = (d.focus + d.fields.len()) % (d.fields.len() + 1),
|
|
K::Tab | K::Down => d.focus = (d.focus + 1) % (d.fields.len() + 1),
|
|
_ => {
|
|
if let Some(field) = d.fields.get_mut(d.focus) {
|
|
if !(matches!(d.kind, DialogKind::Export(_)) && d.focus == 0) {
|
|
field.key(key);
|
|
d.request = None;
|
|
d.export_status = ExportStatus::Ready;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if self.mode == Mode::Designer
|
|
&& key.code == K::F(10)
|
|
&& !key.modifiers.contains(M::CONTROL)
|
|
{
|
|
self.menu = None;
|
|
self.execute(Command::MenuBar);
|
|
return;
|
|
}
|
|
if self.menu.is_some() {
|
|
if key.modifiers.contains(M::ALT) {
|
|
if let K::Char(c) = key.code {
|
|
if let Some(i) = self
|
|
.menus()
|
|
.iter()
|
|
.position(|m| m.mnemonic == c.to_ascii_lowercase())
|
|
{
|
|
self.control_menu = false;
|
|
self.menu = Some((i, 0));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
match key.code {
|
|
K::Esc => {
|
|
self.menu = None;
|
|
self.control_menu = false;
|
|
}
|
|
K::Left | K::Right if !self.control_menu => {
|
|
let n = self.menus().len();
|
|
let (m, _) = self.menu.unwrap();
|
|
self.menu = Some((
|
|
(m + n + if key.code == K::Right { 1 } else { n - 1 }) % n,
|
|
0,
|
|
));
|
|
}
|
|
K::Up | K::Down => {
|
|
let (m, i) = self.menu.unwrap();
|
|
let n = self.menu_entries(m).len();
|
|
if n > 0 {
|
|
self.menu =
|
|
Some((m, (i + n + if key.code == K::Down { 1 } else { n - 1 }) % n));
|
|
}
|
|
}
|
|
K::Enter => self.menu_enter(),
|
|
K::Char(c) => {
|
|
let (m, _) = self.menu.unwrap();
|
|
let entries = self.menu_entries(m);
|
|
if let Some(i) = entries.iter().position(|(label, _)| {
|
|
label
|
|
.split_once('&')
|
|
.and_then(|(_, s)| s.chars().next())
|
|
.is_some_and(|v| v.eq_ignore_ascii_case(&c))
|
|
}) {
|
|
self.menu = Some((m, i));
|
|
self.menu_enter();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
return;
|
|
}
|
|
if let Some((sizing, original)) = self.geometry_action {
|
|
match key.code {
|
|
K::Esc => {
|
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == self.active) {
|
|
w.normal = original;
|
|
}
|
|
self.geometry_action = None;
|
|
}
|
|
K::Enter => self.geometry_action = None,
|
|
K::Left | K::Right | K::Up | K::Down => {
|
|
let area = self.area();
|
|
if let Some(w) = self.windows.iter_mut().find(|w| w.id == self.active) {
|
|
let delta = if key.modifiers.contains(M::CONTROL) {
|
|
5
|
|
} else {
|
|
1
|
|
};
|
|
let grow = matches!(key.code, K::Right | K::Down);
|
|
let value = match (sizing, key.code) {
|
|
(true, K::Left | K::Right) => &mut w.normal.width,
|
|
(true, _) => &mut w.normal.height,
|
|
(false, K::Left | K::Right) => &mut w.normal.x,
|
|
(false, _) => &mut w.normal.y,
|
|
};
|
|
*value = if grow {
|
|
value.saturating_add(delta)
|
|
} else {
|
|
value.saturating_sub(delta)
|
|
};
|
|
w.normal = clamp(w.normal, area);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
return;
|
|
}
|
|
if key.code == K::F(11)
|
|
|| matches!(
|
|
key.code,
|
|
K::Modifier(ModifierKeyCode::LeftAlt | ModifierKeyCode::RightAlt)
|
|
)
|
|
{
|
|
self.properties = false;
|
|
self.menu = Some((0, 0));
|
|
return;
|
|
}
|
|
if key.modifiers.contains(M::ALT) {
|
|
match key.code {
|
|
K::Char('-') => {
|
|
self.execute(Command::ControlMenu);
|
|
return;
|
|
}
|
|
K::F(4) => {
|
|
self.execute(Command::Exit);
|
|
return;
|
|
}
|
|
K::Char(c) => {
|
|
if let Some(i) = self
|
|
.menus()
|
|
.iter()
|
|
.position(|m| m.mnemonic == c.to_ascii_lowercase())
|
|
{
|
|
self.properties = false;
|
|
self.menu = Some((i, 0));
|
|
return;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if self.mode == Mode::Designer
|
|
&& key.code == K::F(10)
|
|
&& !key.modifiers.contains(M::CONTROL)
|
|
{
|
|
self.execute(Command::MenuBar);
|
|
return;
|
|
}
|
|
if self.mode == Mode::Designer && key.code == K::F(2) {
|
|
self.properties = true;
|
|
self.value_focus = true;
|
|
self.message = "Properties Value · Bearbeitung folgt in Change 05".into();
|
|
return;
|
|
}
|
|
if self.program_focus() && key.modifiers.contains(M::CONTROL) && key.code == K::Char('c') {
|
|
self.pause_execution();
|
|
return;
|
|
}
|
|
if let Some(command) = shortcut(key) {
|
|
self.execute(command);
|
|
return;
|
|
}
|
|
if self.program_focus() {
|
|
self.basic_events.push(Event::Key(key));
|
|
return;
|
|
}
|
|
if matches!(
|
|
self.active_window().map(|w| w.kind),
|
|
Some(WindowKind::Project)
|
|
) {
|
|
let n = self.project.members().len();
|
|
match key.code {
|
|
K::Down if n > 0 => self.selected_member = (self.selected_member + 1) % n,
|
|
K::Up if n > 0 => self.selected_member = (self.selected_member + n - 1) % n,
|
|
K::Enter if n > 0 => {
|
|
let id = self.project.members()[self.selected_member];
|
|
let form = matches!(
|
|
self.project.document(id).unwrap().content(),
|
|
Content::Form(_)
|
|
);
|
|
if let Err(e) = self.show_document(id, form) {
|
|
self.message = e.to_string();
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
return;
|
|
}
|
|
if self.mode == Mode::Environment {
|
|
if let Err(e) = self.code_key(key) {
|
|
self.message = e.to_string();
|
|
}
|
|
}
|
|
}
|
|
fn browse(&mut self) {
|
|
let Some(dialog) = self.dialog.take() else {
|
|
return;
|
|
};
|
|
let index = dialog.focus;
|
|
let allowed = match dialog.kind {
|
|
DialogKind::OpenProject
|
|
| DialogKind::AddFile
|
|
| DialogKind::LoadText
|
|
| DialogKind::SaveText
|
|
| DialogKind::Print => index == 0,
|
|
DialogKind::Save { .. } => index % 2 == 0,
|
|
DialogKind::Export(_) => index == 3,
|
|
_ => false,
|
|
};
|
|
if !allowed || index >= dialog.fields.len() {
|
|
self.dialog = Some(dialog);
|
|
return;
|
|
}
|
|
let path = self.base.join(dialog.fields[index].string());
|
|
let dir = if path.is_dir() {
|
|
path
|
|
} else {
|
|
path.parent().unwrap_or(&self.base).to_path_buf()
|
|
};
|
|
match Self::browser(dialog.clone(), index, &dir) {
|
|
Ok(d) => self.dialog = Some(d),
|
|
Err(e) => {
|
|
let mut dialog = dialog;
|
|
dialog.error = e.to_string();
|
|
self.dialog = Some(dialog);
|
|
}
|
|
}
|
|
}
|
|
fn browser(return_to: Dialog, field: usize, dir: &Path) -> Result<Dialog> {
|
|
let dir = dir.canonicalize()?;
|
|
let mut entries = std::fs::read_dir(&dir)?
|
|
.map(|e| e.map(|e| e.path()))
|
|
.collect::<std::io::Result<Vec<_>>>()?;
|
|
entries.sort_by_key(|p| (!p.is_dir(), p.clone()));
|
|
if let Some(parent) = dir.parent() {
|
|
entries.insert(0, parent.to_path_buf());
|
|
}
|
|
let names = entries
|
|
.iter()
|
|
.map(|p| {
|
|
format!(
|
|
"{}{}",
|
|
p.file_name().unwrap_or_default().to_string_lossy(),
|
|
if p.is_dir() { "/" } else { "" }
|
|
)
|
|
})
|
|
.collect();
|
|
Ok(Dialog::new(
|
|
format!("Dateiwahl: {}", dir.display()),
|
|
DialogKind::Browse {
|
|
return_to: Box::new(return_to),
|
|
field,
|
|
entries,
|
|
},
|
|
vec![Field::choice("Datei/Verzeichnis", names, 0)],
|
|
))
|
|
}
|
|
fn code_key(&mut self, key: KeyEvent) -> Result<()> {
|
|
if self.editor_view().is_err() {
|
|
return Ok(());
|
|
}
|
|
self.editor_key(key)
|
|
}
|
|
pub fn menu_entries(&self, index: usize) -> Vec<(String, Option<Command>)> {
|
|
if self.control_menu {
|
|
return vec![
|
|
("&Restore".into(), Some(Command::Restore)),
|
|
("&Move".into(), Some(Command::MoveWindow)),
|
|
("&Size".into(), Some(Command::SizeWindow)),
|
|
("Mi&nimize".into(), Some(Command::Minimize)),
|
|
("Ma&ximize".into(), Some(Command::Maximize)),
|
|
("&Close".into(), Some(Command::CloseWindow)),
|
|
];
|
|
}
|
|
let menus = self.menus();
|
|
let mut entries = menus[index]
|
|
.items
|
|
.iter()
|
|
.map(|i| (i.label.into(), i.command))
|
|
.collect::<Vec<_>>();
|
|
if menus[index].title == "Window" {
|
|
entries.extend(
|
|
self.window_commands()
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(i, (label, c))| (format!("&{} {label}", i + 1), Some(c))),
|
|
);
|
|
}
|
|
entries
|
|
}
|
|
fn menu_enter(&mut self) {
|
|
if let Some((m, i)) = self.menu {
|
|
if let Some((_, Some(c))) = self.menu_entries(m).get(i) {
|
|
self.execute(*c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pub fn clamp(r: Rect, area: Rect) -> Rect {
|
|
let width = r.width.max(12).min(area.width);
|
|
let height = r.height.max(4).min(area.height);
|
|
Rect::new(
|
|
r.x.clamp(area.x, area.right().saturating_sub(width)),
|
|
r.y.clamp(area.y, area.bottom().saturating_sub(height)),
|
|
width,
|
|
height,
|
|
)
|
|
}
|
|
fn untitled(project: &mut Project) -> Result<DocumentId> {
|
|
let mut n = 0;
|
|
loop {
|
|
let name = if n == 0 {
|
|
"Untitled".into()
|
|
} else {
|
|
format!("Untitled{n}")
|
|
};
|
|
if project
|
|
.loader()?
|
|
.resolve(project.directory(), &format!("{name}.bas"))
|
|
.is_err()
|
|
{
|
|
return project.new_module(&name);
|
|
}
|
|
n += 1;
|
|
}
|
|
}
|
|
pub fn shortcut(key: KeyEvent) -> Option<Command> {
|
|
use Command::*;
|
|
let ctrl = key.modifiers.contains(M::CONTROL);
|
|
let shift = key.modifiers.contains(M::SHIFT);
|
|
Some(match key.code {
|
|
K::F(6) => {
|
|
if shift {
|
|
PreviousWindow
|
|
} else {
|
|
NextWindow
|
|
}
|
|
}
|
|
K::F(4) if ctrl => CloseWindow,
|
|
K::F(7) if ctrl => MoveWindow,
|
|
K::F(8) if ctrl => SizeWindow,
|
|
K::F(9) if ctrl => Minimize,
|
|
K::F(10) if ctrl => Maximize,
|
|
K::F(5) if ctrl => Restore,
|
|
K::F(1) if shift => UsingHelp,
|
|
K::F(1) => Topic,
|
|
K::F(2) if shift => Procedures,
|
|
K::F(2) if ctrl => PreviousCode,
|
|
K::F(2) => Code,
|
|
K::F(3) => FindNext,
|
|
K::F(4) => OutputScreen,
|
|
K::F(5) if shift => Start,
|
|
K::F(5) => Continue,
|
|
K::F(7) => RunToCursor,
|
|
K::F(8) if shift => HistoryBack,
|
|
K::F(8) => Step,
|
|
K::F(9) if shift => InstantWatch,
|
|
K::F(9) => Breakpoint,
|
|
K::F(10) if shift => HistoryForward,
|
|
K::F(10) => ProcedureStep,
|
|
K::F(12) if shift => Form,
|
|
K::F(12) => Events,
|
|
K::Insert if ctrl => Copy,
|
|
K::Insert if shift => Paste,
|
|
K::Delete if shift => Cut,
|
|
K::Backspace if key.modifiers.contains(M::ALT) => Undo,
|
|
K::Char('c') if ctrl => Copy,
|
|
K::Char('x') if ctrl => Cut,
|
|
K::Char('v') if ctrl => Paste,
|
|
K::Char('z') if ctrl => Undo,
|
|
K::Char('\\') if ctrl => SelectedText,
|
|
K::Delete => Clear,
|
|
_ => return None,
|
|
})
|
|
}
|