Files
ElyOffice/crates/ely-ui/src/commands.rs
T

217 lines
6.6 KiB
Rust

use ely_platform::AppId;
use std::rc::Rc;
use gpui::{Action, App, Global, KeyBinding, Menu, MenuItem, SharedString, Window, actions};
use crate::i18n;
actions!(
office,
[
New,
Open,
Save,
SaveAs,
Rename,
Export,
Print,
CloseTab,
ToggleCommandPalette,
OpenSettings,
CheckForUpdates,
Quit
]
);
actions!(
apps,
[OpenElyDoc, OpenElyTable, OpenElyShow, OpenElyPdf, OpenElyMail, OpenElyTree, OpenElyMark, OpenElyAgent]
);
/// The main window's own quit, run from any window.
struct QuitRoute(Rc<dyn Fn(&mut App)>);
impl Global for QuitRoute {}
/// Quit from every window, settings too, runs `quit`.
pub fn route_quit(cx: &mut App, quit: impl Fn(&mut App) + 'static) {
cx.set_global(QuitRoute(Rc::new(quit)));
}
/// Quit, from any window, goes through `quit`.
pub fn bind_quit(cx: &mut App) {
cx.on_action(|_: &Quit, cx| quit(cx));
}
/// Quits through the main window's route, if one is set.
pub(crate) fn quit(cx: &mut App) {
match cx.try_global::<QuitRoute>().map(|route| Rc::clone(&route.0)) {
Some(route) => route(cx),
None => cx.quit(),
}
}
/// The action that starts `app` (§16 app switching).
pub fn switch_to(app: AppId) -> Box<dyn Action> {
match app {
AppId::Doc => Box::new(OpenElyDoc),
AppId::Table => Box::new(OpenElyTable),
AppId::Show => Box::new(OpenElyShow),
AppId::Pdf => Box::new(OpenElyPdf),
AppId::Mail => Box::new(OpenElyMail),
AppId::Tree => Box::new(OpenElyTree),
AppId::Mark => Box::new(OpenElyMark),
AppId::Agent => Box::new(OpenElyAgent),
}
}
/// One command: an id and a chord.
pub struct Command {
pub id: &'static str,
pub keys: &'static str,
pub blocked: Option<&'static str>,
pub action: fn() -> Box<dyn Action>,
}
const BLOCKED: &str = "command.blocked.no_document";
const NEW: Command = Command {
id: "file.new",
keys: "secondary-n",
blocked: None,
action: || Box::new(New),
};
const OPEN: Command = Command {
id: "file.open",
keys: "secondary-o",
blocked: None,
action: || Box::new(Open),
};
const SAVE: Command = Command {
id: "file.save",
keys: "secondary-s",
blocked: Some(BLOCKED),
action: || Box::new(Save),
};
const SAVE_AS: Command = Command {
id: "file.save_as",
keys: "secondary-shift-s",
blocked: Some(BLOCKED),
action: || Box::new(SaveAs),
};
const RENAME: Command = Command {
id: "file.rename",
keys: "f2",
blocked: Some(BLOCKED),
action: || Box::new(Rename),
};
const EXPORT: Command = Command {
id: "file.export",
keys: "secondary-e",
blocked: Some(BLOCKED),
action: || Box::new(Export),
};
const PRINT: Command = Command {
id: "file.print",
keys: "secondary-p",
blocked: Some(BLOCKED),
action: || Box::new(Print),
};
pub const FILE: [&Command; 6] = [&NEW, &OPEN, &SAVE, &SAVE_AS, &EXPORT, &PRINT];
/// A document app that also renames its files.
pub const RENAMING: [&Command; 7] = [&NEW, &OPEN, &SAVE, &SAVE_AS, &RENAME, &EXPORT, &PRINT];
/// A tool that starts and opens, but saves nothing itself.
pub const OPENING: [&Command; 2] = [&NEW, &OPEN];
pub const TOGGLE_PALETTE: &str = "secondary-shift-p";
pub const CONTEXT: &str = "office";
fn label(command: &Command, cx: &App) -> SharedString {
i18n::text(cx, &format!("command.{}", command.id), &[])
}
/// Why `id` cannot run; a document lifts the no-document block.
pub fn blocked(id: &str, document: bool, cx: &App) -> Option<SharedString> {
if id == PRINT.id && !ely_platform::print::BUILT {
return Some(i18n::text(cx, "command.blocked.unbuilt", &[]));
}
let command = RENAMING.iter().find(|command| command.id == id)?;
let key = command.blocked.filter(|_| !document)?;
Some(i18n::text(cx, key, &[]))
}
/// The app menu, File with `file`'s commands, then `extra`.
pub fn menus(
app: AppId,
file: &[&Command],
document: bool,
extra: Vec<Menu>,
cx: &App,
) -> Vec<Menu> {
let items = file.iter().map(|command| {
let off = blocked(command.id, document, cx).is_some();
MenuItem::Action {
name: label(command, cx),
action: (command.action)(),
os_action: None,
checked: false,
disabled: off,
}
});
// The other installed Ely apps, to switch to.
let others: Vec<MenuItem> = AppId::ALL
.into_iter()
.filter(|other| *other != app && ely_platform::apps::installed(*other))
.map(|other| MenuItem::Action {
name: other.name().into(),
action: switch_to(other),
os_action: None,
checked: false,
disabled: false,
})
.collect();
let mut first = Vec::new();
if !others.is_empty() {
first.push(MenuItem::submenu(Menu::new(i18n::text(cx, "menu.app.switch", &[])).items(others)));
first.push(MenuItem::separator());
}
first.push(MenuItem::action(i18n::text(cx, "menu.app.settings", &[]), OpenSettings));
// Only an installed app can update itself.
if crate::updates::updater(cx).is_some() {
first.push(MenuItem::action(i18n::text(cx, "menu.app.updates", &[]), CheckForUpdates));
}
first.push(MenuItem::separator());
first.push(MenuItem::action(i18n::text(cx, "menu.app.quit", &[("app", app.name())]), Quit));
let mut menus = vec![
Menu::new(app.name()).items(first),
Menu::new(i18n::text(cx, "menu.file", &[])).items(items.collect::<Vec<_>>()),
];
menus.extend(extra);
menus
}
pub fn bind_keys(cx: &mut App) {
// Without a print panel, Print stays off the keyboard.
if ely_platform::print::BUILT {
cx.bind_keys([KeyBinding::new(PRINT.keys, Print, Some(CONTEXT))]);
}
cx.bind_keys([
KeyBinding::new(NEW.keys, New, Some(CONTEXT)),
KeyBinding::new(OPEN.keys, Open, Some(CONTEXT)),
KeyBinding::new(SAVE.keys, Save, Some(CONTEXT)),
KeyBinding::new(SAVE_AS.keys, SaveAs, Some(CONTEXT)),
KeyBinding::new(RENAME.keys, Rename, Some(CONTEXT)),
KeyBinding::new(EXPORT.keys, Export, Some(CONTEXT)),
KeyBinding::new("secondary-w", CloseTab, Some(CONTEXT)),
KeyBinding::new(TOGGLE_PALETTE, ToggleCommandPalette, Some(CONTEXT)),
KeyBinding::new("secondary-q", Quit, None),
KeyBinding::new("secondary-,", OpenSettings, None),
]);
}
pub fn run(id: &str, window: &mut Window, cx: &mut App) {
match RENAMING.iter().find(|command| command.id == id) {
Some(command) => window.dispatch_action((command.action)(), cx),
None => log::warn!("commands: {id} is not a command"),
}
}