From cba94607fac89c4262b2f30c0559c3c78cd60fb1 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 17 May 2021 22:09:29 +0200 Subject: [PATCH] feat(core): first half of context menu handling --- .../worker/engine/executor/context_menu.rs | 70 +++++++++++++++++++ espanso/src/cli/worker/engine/executor/mod.rs | 1 + espanso/src/cli/worker/engine/mod.rs | 8 ++- espanso/src/cli/worker/engine/source/mod.rs | 1 + espanso/src/cli/worker/engine/source/ui.rs | 64 +++++++++++++++++ espanso/src/cli/worker/mod.rs | 8 ++- espanso/src/engine/dispatch/default.rs | 6 +- .../engine/dispatch/executor/context_menu.rs | 53 ++++++++++++++ espanso/src/engine/dispatch/executor/mod.rs | 7 +- espanso/src/engine/dispatch/mod.rs | 3 + espanso/src/engine/event/input.rs | 5 ++ espanso/src/engine/event/mod.rs | 6 ++ espanso/src/engine/event/ui.rs | 42 +++++++++++ espanso/src/engine/process/default.rs | 3 +- .../engine/process/middleware/context_menu.rs | 65 +++++++++++++++++ espanso/src/engine/process/middleware/mod.rs | 1 + 16 files changed, 334 insertions(+), 9 deletions(-) create mode 100644 espanso/src/cli/worker/engine/executor/context_menu.rs create mode 100644 espanso/src/cli/worker/engine/source/ui.rs create mode 100644 espanso/src/engine/dispatch/executor/context_menu.rs create mode 100644 espanso/src/engine/event/ui.rs create mode 100644 espanso/src/engine/process/middleware/context_menu.rs diff --git a/espanso/src/cli/worker/engine/executor/context_menu.rs b/espanso/src/cli/worker/engine/executor/context_menu.rs new file mode 100644 index 0000000..55822b8 --- /dev/null +++ b/espanso/src/cli/worker/engine/executor/context_menu.rs @@ -0,0 +1,70 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use espanso_ui::UIRemote; + +use crate::engine::dispatch::ContextMenuHandler; + +pub struct ContextMenuHandlerAdapter<'a> { + remote: &'a dyn UIRemote, +} + +impl<'a> ContextMenuHandlerAdapter<'a> { + pub fn new(remote: &'a dyn UIRemote) -> Self { + Self { remote } + } +} + +impl<'a> ContextMenuHandler for ContextMenuHandlerAdapter<'a> { + fn show_context_menu(&self, items: &[crate::engine::event::ui::MenuItem]) -> anyhow::Result<()> { + let ui_menu_items: Vec = + items.iter().map(convert_to_ui_menu_item).collect(); + let ui_menu = espanso_ui::menu::Menu { + items: ui_menu_items, + }; + + self.remote.show_context_menu(&ui_menu); + + Ok(()) + } +} + +fn convert_to_ui_menu_item( + item: &crate::engine::event::ui::MenuItem, +) -> espanso_ui::menu::MenuItem { + match item { + crate::engine::event::ui::MenuItem::Simple(simple) => { + espanso_ui::menu::MenuItem::Simple(espanso_ui::menu::SimpleMenuItem { + id: simple.id, + label: simple.label.clone(), + }) + } + crate::engine::event::ui::MenuItem::Sub(sub) => { + espanso_ui::menu::MenuItem::Sub(espanso_ui::menu::SubMenuItem { + label: sub.label.clone(), + items: sub + .items + .iter() + .map(|item| convert_to_ui_menu_item(item)) + .collect(), + }) + } + crate::engine::event::ui::MenuItem::Separator => espanso_ui::menu::MenuItem::Separator, + } +} diff --git a/espanso/src/cli/worker/engine/executor/mod.rs b/espanso/src/cli/worker/engine/executor/mod.rs index 162e979..ad18615 100644 --- a/espanso/src/cli/worker/engine/executor/mod.rs +++ b/espanso/src/cli/worker/engine/executor/mod.rs @@ -18,5 +18,6 @@ */ pub mod clipboard_injector; +pub mod context_menu; pub mod event_injector; pub mod key_injector; \ No newline at end of file diff --git a/espanso/src/cli/worker/engine/mod.rs b/espanso/src/cli/worker/engine/mod.rs index d2f39cf..f8e96a9 100644 --- a/espanso/src/cli/worker/engine/mod.rs +++ b/espanso/src/cli/worker/engine/mod.rs @@ -23,7 +23,7 @@ use anyhow::Result; use crossbeam::channel::Receiver; use espanso_config::{config::ConfigStore, matches::store::MatchStore}; use espanso_path::Paths; -use espanso_ui::UIRemote; +use espanso_ui::{UIRemote, event::UIEvent}; use log::info; use ui::selector::MatchSelectorAdapter; @@ -47,6 +47,7 @@ pub fn initialize_and_spawn( icon_paths: IconPaths, ui_remote: Box, exit_signal: Receiver<()>, + ui_event_receiver: Receiver, ) -> Result> { let handle = std::thread::Builder::new() .name("engine thread".to_string()) @@ -68,7 +69,8 @@ pub fn initialize_and_spawn( let (detect_source, modifier_state_store, sequencer) = super::engine::source::init_and_spawn().expect("failed to initialize detector module"); let exit_source = super::engine::source::exit::ExitSource::new(exit_signal, &sequencer); - let sources: Vec<&dyn crate::engine::funnel::Source> = vec![&detect_source, &exit_source]; + let ui_source = super::engine::source::ui::UISource::new(ui_event_receiver, &sequencer); + let sources: Vec<&dyn crate::engine::funnel::Source> = vec![&detect_source, &exit_source, &ui_source]; let funnel = crate::engine::funnel::default(&sources); let rolling_matcher = super::engine::matcher::rolling::RollingMatcherAdapter::new( @@ -140,6 +142,7 @@ pub fn initialize_and_spawn( &config_manager, ); let key_injector = super::engine::executor::key_injector::KeyInjectorAdapter::new(&*injector); + let context_menu_adapter = super::engine::executor::context_menu::ContextMenuHandlerAdapter::new(&*ui_remote); let dispatcher = crate::engine::dispatch::default( &event_injector, &clipboard_injector, @@ -147,6 +150,7 @@ pub fn initialize_and_spawn( &key_injector, &clipboard_injector, &clipboard_injector, + &context_menu_adapter, ); let mut engine = crate::engine::Engine::new(&funnel, &mut processor, &dispatcher); diff --git a/espanso/src/cli/worker/engine/source/mod.rs b/espanso/src/cli/worker/engine/source/mod.rs index 57b6be1..29a5224 100644 --- a/espanso/src/cli/worker/engine/source/mod.rs +++ b/espanso/src/cli/worker/engine/source/mod.rs @@ -30,6 +30,7 @@ pub mod detect; pub mod exit; pub mod modifier; pub mod sequencer; +pub mod ui; // TODO: pass options pub fn init_and_spawn() -> Result<(DetectSource, ModifierStateStore, Sequencer)> { diff --git a/espanso/src/cli/worker/engine/source/ui.rs b/espanso/src/cli/worker/engine/source/ui.rs new file mode 100644 index 0000000..1ebd3a1 --- /dev/null +++ b/espanso/src/cli/worker/engine/source/ui.rs @@ -0,0 +1,64 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use crossbeam::channel::{Receiver, Select, SelectedOperation}; +use espanso_ui::event::UIEvent; + +use crate::engine::{ + event::{input::ContextMenuClickedEvent, Event, EventType}, + funnel, +}; + +use super::sequencer::Sequencer; + +pub struct UISource<'a> { + pub ui_receiver: Receiver, + pub sequencer: &'a Sequencer, +} + +impl<'a> UISource<'a> { + pub fn new(ui_receiver: Receiver, sequencer: &'a Sequencer) -> Self { + UISource { + ui_receiver, + sequencer, + } + } +} + +impl<'a> funnel::Source<'a> for UISource<'a> { + fn register(&'a self, select: &mut Select<'a>) -> usize { + select.recv(&self.ui_receiver) + } + + fn receive(&self, op: SelectedOperation) -> Event { + let ui_event = op + .recv(&self.ui_receiver) + .expect("unable to select data from UISource receiver"); + + Event { + source_id: self.sequencer.next_id(), + etype: match ui_event { + UIEvent::TrayIconClick => EventType::TrayIconClicked, + UIEvent::ContextMenuClick(context_item_id) => { + EventType::ContextMenuClicked(ContextMenuClickedEvent { context_item_id }) + } + }, + } + } +} diff --git a/espanso/src/cli/worker/mod.rs b/espanso/src/cli/worker/mod.rs index 7d445d2..ff1f9ee 100644 --- a/espanso/src/cli/worker/mod.rs +++ b/espanso/src/cli/worker/mod.rs @@ -80,6 +80,7 @@ fn worker_main(args: CliModuleArgs) -> i32 { .expect("unable to initialize UI module"); let (engine_exit_notify, engine_exit_receiver) = unbounded(); + let (engine_ui_event_sender, engine_ui_event_receiver) = unbounded(); // Initialize the engine on another thread and start it engine::initialize_and_spawn( @@ -89,6 +90,7 @@ fn worker_main(args: CliModuleArgs) -> i32 { icon_paths, remote, engine_exit_receiver, + engine_ui_event_receiver, ) .expect("unable to initialize engine"); @@ -97,8 +99,10 @@ fn worker_main(args: CliModuleArgs) -> i32 { .expect("unable to initialize IPC server"); eventloop.run(Box::new(move |event| { - // TODO: handle event - })); + if let Err(error) = engine_ui_event_sender.send(event) { + error!("unable to send UIEvent to engine: {}", error); + } + })).expect("unable to run main eventloop"); info!("exiting worker process..."); diff --git a/espanso/src/engine/dispatch/default.rs b/espanso/src/engine/dispatch/default.rs index 53051e7..af91f5a 100644 --- a/espanso/src/engine/dispatch/default.rs +++ b/espanso/src/engine/dispatch/default.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use super::{Event, ImageInjector}; +use super::{ContextMenuHandler, Event, ImageInjector}; use super::{ModeProvider, Dispatcher, Executor, KeyInjector, TextInjector, HtmlInjector}; pub struct DefaultDispatcher<'a> { @@ -32,6 +32,7 @@ impl<'a> DefaultDispatcher<'a> { key_injector: &'a dyn KeyInjector, html_injector: &'a dyn HtmlInjector, image_injector: &'a dyn ImageInjector, + context_menu_handler: &'a dyn ContextMenuHandler, ) -> Self { Self { executors: vec![ @@ -48,6 +49,9 @@ impl<'a> DefaultDispatcher<'a> { )), Box::new(super::executor::image_inject::ImageInjectExecutor::new( image_injector, + )), + Box::new(super::executor::context_menu::ContextMenuExecutor::new( + context_menu_handler, )) ], } diff --git a/espanso/src/engine/dispatch/executor/context_menu.rs b/espanso/src/engine/dispatch/executor/context_menu.rs new file mode 100644 index 0000000..9345c29 --- /dev/null +++ b/espanso/src/engine/dispatch/executor/context_menu.rs @@ -0,0 +1,53 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use super::super::{Event, Executor}; +use crate::engine::event::{ui::MenuItem, EventType}; +use anyhow::Result; +use log::error; + +pub trait ContextMenuHandler { + fn show_context_menu(&self, items: &[MenuItem]) -> Result<()>; +} + +pub struct ContextMenuExecutor<'a> { + handler: &'a dyn ContextMenuHandler, +} + +impl<'a> ContextMenuExecutor<'a> { + pub fn new(handler: &'a dyn ContextMenuHandler) -> Self { + Self { handler } + } +} + +impl<'a> Executor for ContextMenuExecutor<'a> { + fn execute(&self, event: &Event) -> bool { + if let EventType::ShowContextMenu(context_menu_event) = &event.etype { + if let Err(error) = self.handler.show_context_menu(&context_menu_event.items) { + error!("context menu handler reported an error: {:?}", error); + } + + return true; + } + + false + } +} + +// TODO: test diff --git a/espanso/src/engine/dispatch/executor/mod.rs b/espanso/src/engine/dispatch/executor/mod.rs index b6a0d63..158d6d8 100644 --- a/espanso/src/engine/dispatch/executor/mod.rs +++ b/espanso/src/engine/dispatch/executor/mod.rs @@ -17,7 +17,8 @@ * along with espanso. If not, see . */ -pub mod text_inject; -pub mod key_inject; +pub mod context_menu; +pub mod image_inject; pub mod html_inject; -pub mod image_inject; \ No newline at end of file +pub mod key_inject; +pub mod text_inject; \ No newline at end of file diff --git a/espanso/src/engine/dispatch/mod.rs b/espanso/src/engine/dispatch/mod.rs index 46ad9b8..fc710c0 100644 --- a/espanso/src/engine/dispatch/mod.rs +++ b/espanso/src/engine/dispatch/mod.rs @@ -36,6 +36,7 @@ pub trait Dispatcher { pub use executor::html_inject::HtmlInjector; pub use executor::text_inject::{Mode, ModeProvider, TextInjector}; pub use executor::image_inject::{ImageInjector}; +pub use executor::context_menu::{ContextMenuHandler}; // TODO: move into module pub trait KeyInjector { @@ -49,6 +50,7 @@ pub fn default<'a>( key_injector: &'a dyn KeyInjector, html_injector: &'a dyn HtmlInjector, image_injector: &'a dyn ImageInjector, + context_menu_handler: &'a dyn ContextMenuHandler, ) -> impl Dispatcher + 'a { default::DefaultDispatcher::new( event_injector, @@ -57,5 +59,6 @@ pub fn default<'a>( key_injector, html_injector, image_injector, + context_menu_handler, ) } diff --git a/espanso/src/engine/event/input.rs b/espanso/src/engine/event/input.rs index 190c126..b667837 100644 --- a/espanso/src/engine/event/input.rs +++ b/espanso/src/engine/event/input.rs @@ -110,4 +110,9 @@ pub enum Key { // Other keys, includes the raw code provided by the operating system Other(i32), +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ContextMenuClickedEvent { + pub context_item_id: u32, } \ No newline at end of file diff --git a/espanso/src/engine/event/mod.rs b/espanso/src/engine/event/mod.rs index b3e3c95..aadbd15 100644 --- a/espanso/src/engine/event/mod.rs +++ b/espanso/src/engine/event/mod.rs @@ -20,6 +20,7 @@ pub mod input; pub mod effect; pub mod internal; +pub mod ui; pub type SourceId = u32; @@ -54,6 +55,8 @@ pub enum EventType { Keyboard(input::KeyboardEvent), Mouse(input::MouseEvent), // TODO: hotkeys + TrayIconClicked, + ContextMenuClicked(input::ContextMenuClickedEvent), // Internal MatchesDetected(internal::MatchesDetectedEvent), @@ -76,4 +79,7 @@ pub enum EventType { MarkdownInject(effect::MarkdownInjectRequest), HtmlInject(effect::HtmlInjectRequest), ImageInject(effect::ImageInjectRequest), + + // UI + ShowContextMenu(ui::ShowContextMenuEvent), } \ No newline at end of file diff --git a/espanso/src/engine/event/ui.rs b/espanso/src/engine/event/ui.rs new file mode 100644 index 0000000..1412264 --- /dev/null +++ b/espanso/src/engine/event/ui.rs @@ -0,0 +1,42 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +#[derive(Debug, Clone, PartialEq)] +pub struct ShowContextMenuEvent { + pub items: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum MenuItem { + Simple(SimpleMenuItem), + Sub(SubMenuItem), + Separator, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SimpleMenuItem { + pub id: u32, + pub label: String, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct SubMenuItem { + pub label: String, + pub items: Vec, +} \ No newline at end of file diff --git a/espanso/src/engine/process/default.rs b/espanso/src/engine/process/default.rs index 93e769e..ed21150 100644 --- a/espanso/src/engine/process/default.rs +++ b/espanso/src/engine/process/default.rs @@ -25,7 +25,7 @@ use super::{MatchFilter, MatchInfoProvider, MatchSelector, Matcher, Middleware, delay_modifiers::{DelayForModifierReleaseMiddleware, ModifierStatusProvider}, markdown::MarkdownMiddleware, past_discard::PastEventsDiscardMiddleware, }}; -use crate::engine::{event::{Event, EventType}, process::middleware::{exit::ExitMiddleware, image_resolve::ImageResolverMiddleware}}; +use crate::engine::{event::{Event, EventType}, process::middleware::{context_menu::ContextMenuMiddleware, exit::ExitMiddleware, image_resolve::ImageResolverMiddleware}}; use std::collections::VecDeque; pub struct DefaultProcessor<'a> { @@ -50,6 +50,7 @@ impl<'a> DefaultProcessor<'a> { middleware: vec![ Box::new(PastEventsDiscardMiddleware::new()), Box::new(MatcherMiddleware::new(matchers)), + Box::new(ContextMenuMiddleware::new()), Box::new(MatchSelectMiddleware::new(match_filter, match_selector)), Box::new(CauseCompensateMiddleware::new()), Box::new(MultiplexMiddleware::new(multiplexer)), diff --git a/espanso/src/engine/process/middleware/context_menu.rs b/espanso/src/engine/process/middleware/context_menu.rs new file mode 100644 index 0000000..b85d04d --- /dev/null +++ b/espanso/src/engine/process/middleware/context_menu.rs @@ -0,0 +1,65 @@ +/* + * This file is part of espanso. + * + * Copyright id: (), label: () id: (), label: () id: (), label: ()(C) 2019-2021 Federico Terzi + * + * espanso is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use super::super::Middleware; +use crate::engine::{event::{Event, EventType, ui::{MenuItem, ShowContextMenuEvent, SimpleMenuItem}}}; + +pub struct ContextMenuMiddleware { +} + +impl ContextMenuMiddleware { + pub fn new() -> Self { + Self {} + } +} + +impl Middleware for ContextMenuMiddleware { + fn name(&self) -> &'static str { + "context_menu" + } + + fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event { + if let EventType::TrayIconClicked = event.etype { + // TODO: fetch top matches for the active config to be added + + // TODO: my idea is to use a set of reserved u32 ids for built-in + // actions such as Exit, Open Editor etc + // then we need some u32 for the matches, so we need to create + // a mapping structure match_id <-> context-menu-id + return Event::caused_by( + event.source_id, + EventType::ShowContextMenu(ShowContextMenuEvent { + // TODO: add actual entries + items: vec![ + MenuItem::Simple(SimpleMenuItem { + id: 0, + label: "Exit espanso".to_string(), + }) + ] + }), + ) + } + + // TODO: handle context menu clicks + + event + } +} + +// TODO: test diff --git a/espanso/src/engine/process/middleware/mod.rs b/espanso/src/engine/process/middleware/mod.rs index 49076e8..929acff 100644 --- a/espanso/src/engine/process/middleware/mod.rs +++ b/espanso/src/engine/process/middleware/mod.rs @@ -19,6 +19,7 @@ pub mod action; pub mod cause; +pub mod context_menu; pub mod cursor_hint; pub mod delay_modifiers; pub mod exit;