Files
TerminalBasic/crates/tb-ide/src/app.rs

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,
})
}