feat(core): first half of context menu handling

This commit is contained in:
Federico Terzi 2021-05-17 22:09:29 +02:00
parent 716c50cee1
commit cba94607fa
16 changed files with 334 additions and 9 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<espanso_ui::menu::MenuItem> =
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,
}
}

View File

@ -18,5 +18,6 @@
*/
pub mod clipboard_injector;
pub mod context_menu;
pub mod event_injector;
pub mod key_injector;

View File

@ -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<dyn UIRemote>,
exit_signal: Receiver<()>,
ui_event_receiver: Receiver<UIEvent>,
) -> Result<JoinHandle<()>> {
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);

View File

@ -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)> {

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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<UIEvent>,
pub sequencer: &'a Sequencer,
}
impl<'a> UISource<'a> {
pub fn new(ui_receiver: Receiver<UIEvent>, 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 })
}
},
}
}
}

View File

@ -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...");

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
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,
))
],
}

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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

View File

@ -17,7 +17,8 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod text_inject;
pub mod key_inject;
pub mod context_menu;
pub mod image_inject;
pub mod html_inject;
pub mod image_inject;
pub mod key_inject;
pub mod text_inject;

View File

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

View File

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

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
#[derive(Debug, Clone, PartialEq)]
pub struct ShowContextMenuEvent {
pub items: Vec<MenuItem>,
}
#[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<MenuItem>,
}

View File

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

View File

@ -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 <https://www.gnu.org/licenses/>.
*/
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

View File

@ -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;