303 lines
9.0 KiB
Rust
303 lines
9.0 KiB
Rust
use muda::accelerator::{Accelerator, CMD_OR_CTRL, Code, Modifiers};
|
|
use muda::{CheckMenuItem, Menu, MenuEvent, MenuItem, PredefinedMenuItem, Submenu};
|
|
|
|
use crate::app::DetailTab;
|
|
use crate::native_edit::EditCommand;
|
|
|
|
const PREFERENCES: &str = "preferences";
|
|
const MODEL_MANAGER: &str = "model-manager";
|
|
const NEW_CHAT: &str = "new-chat";
|
|
const EXPORT_CHAT: &str = "export-chat";
|
|
const TOGGLE_SIDEBAR: &str = "toggle-sidebar";
|
|
const SHOW_CHAT: &str = "show-chat";
|
|
const SHOW_A2UI: &str = "show-a2ui";
|
|
const SHOW_GIT: &str = "show-git";
|
|
const SHOW_STATS: &str = "show-stats";
|
|
const HELP: &str = "help";
|
|
const QUIT: &str = "quit";
|
|
const UNDO: &str = "undo";
|
|
const REDO: &str = "redo";
|
|
const CUT: &str = "cut";
|
|
const COPY: &str = "copy";
|
|
const PASTE: &str = "paste";
|
|
const SELECT_ALL: &str = "select-all";
|
|
|
|
pub(crate) struct NativeMenu {
|
|
_menu: Menu,
|
|
new_chat: MenuItem,
|
|
export_chat: MenuItem,
|
|
sidebar: CheckMenuItem,
|
|
chat: CheckMenuItem,
|
|
a2ui: CheckMenuItem,
|
|
git: CheckMenuItem,
|
|
stats: CheckMenuItem,
|
|
undo: MenuItem,
|
|
redo: MenuItem,
|
|
cut: MenuItem,
|
|
copy: MenuItem,
|
|
paste: MenuItem,
|
|
select_all: MenuItem,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
pub(crate) enum NativeMenuEvent {
|
|
Preferences,
|
|
ModelManager,
|
|
NewChat,
|
|
ExportChat,
|
|
ToggleSidebar,
|
|
ShowChat,
|
|
ShowA2ui,
|
|
ShowGit,
|
|
ShowStats,
|
|
Help,
|
|
Quit,
|
|
Edit(EditCommand),
|
|
}
|
|
|
|
pub(crate) fn install() -> Result<NativeMenu, String> {
|
|
let menu = Menu::new();
|
|
let application = Submenu::new("DS4Server", true);
|
|
let file = Submenu::new("File", true);
|
|
let edit = Submenu::new("Edit", true);
|
|
let view = Submenu::new("View", true);
|
|
let window = Submenu::new("Window", true);
|
|
let help = Submenu::new("Help", true);
|
|
let preferences = MenuItem::with_id(
|
|
PREFERENCES,
|
|
"Preferences…",
|
|
true,
|
|
Some(Accelerator::new(Some(Modifiers::SUPER), Code::Comma)),
|
|
);
|
|
let model_manager = MenuItem::with_id(
|
|
MODEL_MANAGER,
|
|
"Model Manager…",
|
|
true,
|
|
Some(Accelerator::new(
|
|
Some(Modifiers::SUPER | Modifiers::SHIFT),
|
|
Code::KeyM,
|
|
)),
|
|
);
|
|
let new_chat = MenuItem::with_id(
|
|
NEW_CHAT,
|
|
"New Chat",
|
|
false,
|
|
Some(Accelerator::new(Some(Modifiers::SUPER), Code::KeyN)),
|
|
);
|
|
let export_chat = MenuItem::with_id(
|
|
EXPORT_CHAT,
|
|
"Export Chat as Markdown…",
|
|
false,
|
|
Some(Accelerator::new(
|
|
Some(Modifiers::SUPER | Modifiers::SHIFT),
|
|
Code::KeyS,
|
|
)),
|
|
);
|
|
let sidebar = CheckMenuItem::with_id(
|
|
TOGGLE_SIDEBAR,
|
|
"Show Sidebar",
|
|
true,
|
|
true,
|
|
Some(Accelerator::new(Some(Modifiers::SUPER), Code::KeyB)),
|
|
);
|
|
let chat = view_item(SHOW_CHAT, "Chat", Code::Digit1, true);
|
|
let a2ui = view_item(SHOW_A2UI, "A2UI", Code::Digit2, false);
|
|
let git = view_item(SHOW_GIT, "Git", Code::Digit3, false);
|
|
let stats = view_item(SHOW_STATS, "Stats", Code::Digit4, false);
|
|
let open_help = MenuItem::with_id(
|
|
HELP,
|
|
"DS4Server Help",
|
|
true,
|
|
Some(Accelerator::new(
|
|
Some(Modifiers::SUPER | Modifiers::SHIFT),
|
|
Code::Slash,
|
|
)),
|
|
);
|
|
let undo = edit_item(UNDO, "Undo", Code::KeyZ, None);
|
|
let redo = edit_item(REDO, "Redo", Code::KeyZ, Some(Modifiers::SHIFT));
|
|
let cut = edit_item(CUT, "Cut", Code::KeyX, None);
|
|
let copy = edit_item(COPY, "Copy", Code::KeyC, None);
|
|
let paste = edit_item(PASTE, "Paste", Code::KeyV, None);
|
|
let select_all = edit_item(SELECT_ALL, "Select All", Code::KeyA, None);
|
|
let quit = MenuItem::with_id(
|
|
QUIT,
|
|
"Quit DS4Server",
|
|
true,
|
|
Some(Accelerator::new(Some(Modifiers::SUPER), Code::KeyQ)),
|
|
);
|
|
|
|
application
|
|
.append_items(&[
|
|
&PredefinedMenuItem::about(None, None),
|
|
&PredefinedMenuItem::separator(),
|
|
&preferences,
|
|
&PredefinedMenuItem::separator(),
|
|
&PredefinedMenuItem::services(None),
|
|
&PredefinedMenuItem::separator(),
|
|
&PredefinedMenuItem::hide(None),
|
|
&PredefinedMenuItem::hide_others(None),
|
|
&PredefinedMenuItem::show_all(None),
|
|
&PredefinedMenuItem::separator(),
|
|
&quit,
|
|
])
|
|
.map_err(|error| error.to_string())?;
|
|
file.append_items(&[
|
|
&new_chat,
|
|
&export_chat,
|
|
&PredefinedMenuItem::separator(),
|
|
&PredefinedMenuItem::close_window(None),
|
|
])
|
|
.map_err(|error| error.to_string())?;
|
|
edit.append_items(&[
|
|
&undo,
|
|
&redo,
|
|
&PredefinedMenuItem::separator(),
|
|
&cut,
|
|
©,
|
|
&paste,
|
|
&select_all,
|
|
])
|
|
.map_err(|error| error.to_string())?;
|
|
view.append_items(&[
|
|
&sidebar,
|
|
&PredefinedMenuItem::separator(),
|
|
&chat,
|
|
&a2ui,
|
|
&git,
|
|
&stats,
|
|
&PredefinedMenuItem::separator(),
|
|
&model_manager,
|
|
])
|
|
.map_err(|error| error.to_string())?;
|
|
window
|
|
.append_items(&[
|
|
&PredefinedMenuItem::minimize(None),
|
|
&PredefinedMenuItem::maximize(None),
|
|
&PredefinedMenuItem::close_window(None),
|
|
&PredefinedMenuItem::bring_all_to_front(None),
|
|
])
|
|
.map_err(|error| error.to_string())?;
|
|
help.append(&open_help).map_err(|error| error.to_string())?;
|
|
menu.append_items(&[&application, &file, &edit, &view, &window, &help])
|
|
.map_err(|error| error.to_string())?;
|
|
menu.init_for_nsapp();
|
|
window.set_as_windows_menu_for_nsapp();
|
|
Ok(NativeMenu {
|
|
_menu: menu,
|
|
new_chat,
|
|
export_chat,
|
|
sidebar,
|
|
chat,
|
|
a2ui,
|
|
git,
|
|
stats,
|
|
undo,
|
|
redo,
|
|
cut,
|
|
copy,
|
|
paste,
|
|
select_all,
|
|
})
|
|
}
|
|
|
|
impl NativeMenu {
|
|
pub(crate) fn update(
|
|
&self,
|
|
project_active: bool,
|
|
chat_active: bool,
|
|
sidebar_visible: bool,
|
|
detail_tab: DetailTab,
|
|
edit: crate::native_edit::EditAvailability,
|
|
) {
|
|
self.new_chat.set_enabled(project_active);
|
|
self.export_chat.set_enabled(chat_active);
|
|
self.sidebar.set_checked(sidebar_visible);
|
|
self.chat.set_checked(detail_tab == DetailTab::Chat);
|
|
self.a2ui.set_checked(detail_tab == DetailTab::A2ui);
|
|
self.git.set_checked(detail_tab == DetailTab::Git);
|
|
self.stats.set_checked(detail_tab == DetailTab::Stats);
|
|
self.undo.set_enabled(edit.undo);
|
|
self.redo.set_enabled(edit.redo);
|
|
self.cut.set_enabled(edit.cut);
|
|
self.copy.set_enabled(edit.copy);
|
|
self.paste.set_enabled(edit.paste);
|
|
self.select_all.set_enabled(edit.select_all);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn next_event() -> Option<NativeMenuEvent> {
|
|
while let Ok(event) = MenuEvent::receiver().try_recv() {
|
|
let event = match event.id.0.as_str() {
|
|
PREFERENCES => NativeMenuEvent::Preferences,
|
|
MODEL_MANAGER => NativeMenuEvent::ModelManager,
|
|
NEW_CHAT => NativeMenuEvent::NewChat,
|
|
EXPORT_CHAT => NativeMenuEvent::ExportChat,
|
|
TOGGLE_SIDEBAR => NativeMenuEvent::ToggleSidebar,
|
|
SHOW_CHAT => NativeMenuEvent::ShowChat,
|
|
SHOW_A2UI => NativeMenuEvent::ShowA2ui,
|
|
SHOW_GIT => NativeMenuEvent::ShowGit,
|
|
SHOW_STATS => NativeMenuEvent::ShowStats,
|
|
HELP => NativeMenuEvent::Help,
|
|
QUIT => NativeMenuEvent::Quit,
|
|
UNDO => NativeMenuEvent::Edit(EditCommand::Undo),
|
|
REDO => NativeMenuEvent::Edit(EditCommand::Redo),
|
|
CUT => NativeMenuEvent::Edit(EditCommand::Cut),
|
|
COPY => NativeMenuEvent::Edit(EditCommand::Copy),
|
|
PASTE => NativeMenuEvent::Edit(EditCommand::Paste),
|
|
SELECT_ALL => NativeMenuEvent::Edit(EditCommand::SelectAll),
|
|
_ => continue,
|
|
};
|
|
return Some(event);
|
|
}
|
|
None
|
|
}
|
|
|
|
fn view_item(id: &'static str, label: &'static str, code: Code, checked: bool) -> CheckMenuItem {
|
|
CheckMenuItem::with_id(
|
|
id,
|
|
label,
|
|
true,
|
|
checked,
|
|
Some(Accelerator::new(Some(Modifiers::SUPER), code)),
|
|
)
|
|
}
|
|
|
|
fn edit_item(
|
|
id: &'static str,
|
|
label: &'static str,
|
|
code: Code,
|
|
extra_modifier: Option<Modifiers>,
|
|
) -> MenuItem {
|
|
MenuItem::with_id(
|
|
id,
|
|
label,
|
|
true,
|
|
Some(Accelerator::new(
|
|
Some(CMD_OR_CTRL | extra_modifier.unwrap_or_else(Modifiers::empty)),
|
|
code,
|
|
)),
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn edit_items_are_custom_menu_events() {
|
|
let paste = edit_item(PASTE, "Paste", Code::KeyV, None);
|
|
assert_eq!(paste.id().0, PASTE);
|
|
assert!(paste.is_enabled());
|
|
}
|
|
|
|
#[test]
|
|
fn view_items_keep_the_selected_pane_checked() {
|
|
let chat = view_item(SHOW_CHAT, "Chat", Code::Digit1, true);
|
|
let stats = view_item(SHOW_STATS, "Stats", Code::Digit4, false);
|
|
assert_eq!(chat.id().0, SHOW_CHAT);
|
|
assert!(chat.is_checked());
|
|
assert_eq!(stats.id().0, SHOW_STATS);
|
|
assert!(!stats.is_checked());
|
|
}
|
|
}
|