First draft of context menu handling on Windows
This commit is contained in:
parent
567c35eb0e
commit
65f9811db3
186
Cargo.lock
generated
186
Cargo.lock
generated
|
@ -1,5 +1,15 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.66"
|
||||
|
@ -10,6 +20,18 @@ name = "cfg-if"
|
|||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.84 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-as-inner"
|
||||
version = "0.3.3"
|
||||
|
@ -28,6 +50,7 @@ dependencies = [
|
|||
"espanso-detect 0.1.0",
|
||||
"espanso-ui 0.1.0",
|
||||
"maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"simplelog 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -45,9 +68,13 @@ dependencies = [
|
|||
name = "espanso-ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.123 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde_json 1.0.61 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thiserror 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
|
@ -59,11 +86,21 @@ dependencies = [
|
|||
"unicode-segmentation 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "lazycell"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.84"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.14"
|
||||
|
@ -77,6 +114,23 @@ name = "maplit"
|
|||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "num-integer"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.24"
|
||||
|
@ -93,6 +147,49 @@ dependencies = [
|
|||
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.123"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"serde_derive 1.0.123 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.123"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.60 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"itoa 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"serde 1.0.123 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.60"
|
||||
|
@ -103,6 +200,42 @@ dependencies = [
|
|||
"unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"thiserror-impl 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"syn 1.0.60 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.84 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.7.1"
|
||||
|
@ -113,22 +246,75 @@ name = "unicode-xid"
|
|||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "widestring"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum anyhow 1.0.38 (registry+https://github.com/rust-lang/crates.io-index)" = "afddf7f520a80dbf76e6f50a35bca42a2331ef227a28b3b6dc5c2e2338d114b1"
|
||||
"checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
|
||||
"checksum cc 1.0.66 (registry+https://github.com/rust-lang/crates.io-index)" = "4c0496836a84f8d0495758516b8621a622beb77c0fed418570e50764093ced48"
|
||||
"checksum cfg-if 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
"checksum chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)" = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
||||
"checksum enum-as-inner 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
|
||||
"checksum heck 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "87cbf45460356b7deeb5e3415b5563308c0a9b057c85e12b06ad551f98d0a6ac"
|
||||
"checksum itoa 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
|
||||
"checksum lazycell 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
"checksum libc 0.2.84 (registry+https://github.com/rust-lang/crates.io-index)" = "1cca32fa0182e8c0989459524dc356b8f2b5c10f1b9eb521b7d182c03cf8c5ff"
|
||||
"checksum log 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
|
||||
"checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
|
||||
"checksum num-integer 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
||||
"checksum num-traits 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290"
|
||||
"checksum proc-macro2 1.0.24 (registry+https://github.com/rust-lang/crates.io-index)" = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||
"checksum quote 1.0.8 (registry+https://github.com/rust-lang/crates.io-index)" = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
||||
"checksum ryu 1.0.5 (registry+https://github.com/rust-lang/crates.io-index)" = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
|
||||
"checksum serde 1.0.123 (registry+https://github.com/rust-lang/crates.io-index)" = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
|
||||
"checksum serde_derive 1.0.123 (registry+https://github.com/rust-lang/crates.io-index)" = "9391c295d64fc0abb2c556bad848f33cb8296276b1ad2677d1ae1ace4f258f31"
|
||||
"checksum serde_json 1.0.61 (registry+https://github.com/rust-lang/crates.io-index)" = "4fceb2595057b6891a4ee808f70054bd2d12f0e97f1cbb78689b59f676df325a"
|
||||
"checksum simplelog 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4bc0ffd69814a9b251d43afcabf96dad1b29f5028378056257be9e3fecc9f720"
|
||||
"checksum syn 1.0.60 (registry+https://github.com/rust-lang/crates.io-index)" = "c700597eca8a5a762beb35753ef6b94df201c81cca676604f547495a0d7f0081"
|
||||
"checksum termcolor 1.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
|
||||
"checksum thiserror 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "76cc616c6abf8c8928e2fdcc0dbfab37175edd8fb49a4641066ad1364fdab146"
|
||||
"checksum thiserror-impl 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||
"checksum time 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
||||
"checksum unicode-segmentation 1.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||
"checksum unicode-xid 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
|
||||
"checksum wasi 0.10.0+wasi-snapshot-preview1 (registry+https://github.com/rust-lang/crates.io-index)" = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
"checksum widestring 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "c168940144dd21fd8046987c16a46a33d5fc84eec29ef9dcddc2ac9e31526b7c"
|
||||
"checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
|
|
@ -7,6 +7,10 @@ build="build.rs"
|
|||
|
||||
[dependencies]
|
||||
log = "0.4.14"
|
||||
serde_json = "1.0.61"
|
||||
serde = { version = "1.0.123", features = ["derive"] }
|
||||
anyhow = "1.0.38"
|
||||
thiserror = "1.0.23"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
widestring = "0.4.3"
|
||||
|
|
|
@ -25,6 +25,7 @@ fn cc_config() {
|
|||
.cpp(true)
|
||||
.include("src/win32/native.h")
|
||||
.include("src/win32/WinToast/wintoastlib.h")
|
||||
.include("src/win32/json/json.hpp")
|
||||
.file("src/win32/native.cpp")
|
||||
.file("src/win32/WinToast/wintoastlib.cpp")
|
||||
.compile("espansoui");
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
pub mod menu;
|
||||
pub mod event;
|
||||
pub mod icons;
|
||||
|
||||
|
|
203
espanso-ui/src/menu.rs
Normal file
203
espanso-ui/src/menu.rs
Normal file
|
@ -0,0 +1,203 @@
|
|||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Menu {
|
||||
items: Vec<MenuItem>,
|
||||
|
||||
// Mapping between the raw numeric ids and string ids
|
||||
raw_ids: HashMap<u32, String>,
|
||||
}
|
||||
|
||||
impl Menu {
|
||||
pub fn from(mut items: Vec<MenuItem>) -> Result<Self> {
|
||||
// Generate the raw ids, also checking for duplicate ids
|
||||
let mut raw_ids: HashMap<u32, String> = HashMap::new();
|
||||
let mut ids: HashSet<String> = HashSet::new();
|
||||
let mut current = 0;
|
||||
for item in items.iter_mut() {
|
||||
Self::generate_raw_id(&mut raw_ids, &mut ids, &mut current, item)?;
|
||||
}
|
||||
|
||||
Ok(Self { items, raw_ids })
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Result<String> {
|
||||
Ok(serde_json::to_string(&self.items)?)
|
||||
}
|
||||
|
||||
pub fn get_item_id(&self, raw_id: u32) -> Option<String> {
|
||||
self.raw_ids.get(&raw_id).cloned()
|
||||
}
|
||||
|
||||
fn generate_raw_id(raw_ids: &mut HashMap<u32, String>, ids: &mut HashSet<String>, current: &mut u32, item: &mut MenuItem) -> Result<()> {
|
||||
match item {
|
||||
MenuItem::Simple(simple_item) => {
|
||||
if ids.contains::<str>(&simple_item.id) { // Duplicate id, throw error
|
||||
Err(MenuError::DuplicateMenuId(simple_item.id.to_string()).into())
|
||||
} else {
|
||||
ids.insert(simple_item.id.to_string());
|
||||
raw_ids.insert(*current, simple_item.id.to_string());
|
||||
simple_item.raw_id = Some(*current);
|
||||
*current += 1;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
MenuItem::Sub(SubMenuItem { items, ..}) => {
|
||||
for sub_item in items.iter_mut() {
|
||||
Self::generate_raw_id(raw_ids, ids, current, sub_item)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
MenuItem::Separator => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum MenuError {
|
||||
#[error("json serialization error")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
#[error("two or more menu items share the same id")]
|
||||
DuplicateMenuId(String),
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MenuItem {
|
||||
Simple(SimpleMenuItem),
|
||||
Sub(SubMenuItem),
|
||||
Separator,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SimpleMenuItem {
|
||||
id: String,
|
||||
label: String,
|
||||
raw_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl SimpleMenuItem {
|
||||
pub fn new(id: &str, label: &str) -> Self {
|
||||
Self {
|
||||
id: id.to_string(),
|
||||
label: label.to_string(),
|
||||
raw_id: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct SubMenuItem {
|
||||
label: String,
|
||||
items: Vec<MenuItem>,
|
||||
}
|
||||
|
||||
impl SubMenuItem {
|
||||
pub fn new(label: &str, items: Vec<MenuItem>) -> Self {
|
||||
Self {
|
||||
label: label.to_string(),
|
||||
items,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_context_menu_serializes_correctly() {
|
||||
let menu = Menu::from(vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"open",
|
||||
"Open",
|
||||
)),
|
||||
MenuItem::Separator,
|
||||
MenuItem::Sub(SubMenuItem::new(
|
||||
"Sub",
|
||||
vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"sub1",
|
||||
"Sub 1",
|
||||
)),
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"sub2",
|
||||
"Sub 2",
|
||||
)),
|
||||
]),
|
||||
),
|
||||
]).unwrap();
|
||||
assert_eq!(
|
||||
menu.to_json().unwrap(),
|
||||
r#"[{"type":"simple","id":"open","label":"Open","raw_id":0},{"type":"separator"},{"type":"sub","label":"Sub","items":[{"type":"simple","id":"sub1","label":"Sub 1","raw_id":1},{"type":"simple","id":"sub2","label":"Sub 2","raw_id":2}]}]"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_menu_raw_ids_work_correctly() {
|
||||
let menu = Menu::from(vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"open",
|
||||
"Open",
|
||||
)),
|
||||
MenuItem::Separator,
|
||||
MenuItem::Sub(SubMenuItem::new(
|
||||
"Sub",
|
||||
vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"sub1",
|
||||
"Sub 1",
|
||||
)),
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"sub2",
|
||||
"Sub 2",
|
||||
)),
|
||||
]),
|
||||
),
|
||||
]).unwrap();
|
||||
assert_eq!(
|
||||
menu.get_item_id(0).unwrap(),
|
||||
"open"
|
||||
);
|
||||
assert_eq!(
|
||||
menu.get_item_id(1).unwrap(),
|
||||
"sub1"
|
||||
);
|
||||
assert_eq!(
|
||||
menu.get_item_id(2).unwrap(),
|
||||
"sub2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_menu_detects_duplicate_ids() {
|
||||
let menu = Menu::from(vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"open",
|
||||
"Open",
|
||||
)),
|
||||
MenuItem::Separator,
|
||||
MenuItem::Sub(SubMenuItem::new(
|
||||
"Sub",
|
||||
vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"open",
|
||||
"Sub 1",
|
||||
)),
|
||||
MenuItem::Simple(SimpleMenuItem::new(
|
||||
"sub2",
|
||||
"Sub 2",
|
||||
)),
|
||||
]),
|
||||
),
|
||||
]);
|
||||
assert!(matches!(menu.unwrap_err().downcast::<MenuError>().unwrap(),
|
||||
MenuError::DuplicateMenuId(id) if id == "open"));
|
||||
}
|
||||
}
|
25447
espanso-ui/src/win32/json/json.hpp
Normal file
25447
espanso-ui/src/win32/json/json.hpp
Normal file
File diff suppressed because it is too large
Load Diff
|
@ -17,16 +17,23 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::{cmp::min, collections::HashMap, ffi::c_void, sync::{
|
||||
use std::{
|
||||
cmp::min,
|
||||
collections::HashMap,
|
||||
ffi::{c_void, CString},
|
||||
os::raw::c_char,
|
||||
sync::{
|
||||
atomic::{AtomicPtr, Ordering},
|
||||
Arc,
|
||||
}, thread::ThreadId};
|
||||
},
|
||||
thread::ThreadId,
|
||||
};
|
||||
|
||||
use lazycell::LazyCell;
|
||||
use log::{error, trace};
|
||||
use widestring::{WideCString};
|
||||
use widestring::WideCString;
|
||||
|
||||
use crate::{event::UIEvent, icons::TrayIcon};
|
||||
use crate::{event::UIEvent, icons::TrayIcon, menu::Menu};
|
||||
|
||||
// IMPORTANT: if you change these, also edit the native.h file.
|
||||
const MAX_FILE_PATH: usize = 260;
|
||||
|
@ -66,6 +73,7 @@ extern "C" {
|
|||
pub fn ui_destroy(window_handle: *const c_void) -> i32;
|
||||
pub fn ui_update_tray_icon(window_handle: *const c_void, index: i32);
|
||||
pub fn ui_show_notification(window_handle: *const c_void, message: *const u16);
|
||||
pub fn ui_show_context_menu(window_handle: *const c_void, payload: *const c_char);
|
||||
}
|
||||
|
||||
pub struct Win32UIOptions<'a> {
|
||||
|
@ -142,14 +150,16 @@ impl Win32EventLoop {
|
|||
let mut icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT] =
|
||||
[[0; MAX_FILE_PATH]; MAX_ICON_COUNT];
|
||||
for (i, icon_path) in icon_paths.iter_mut().enumerate().take(self.icons.len()) {
|
||||
let wide_path = WideCString::from_str(&self.icons[i]).expect("Error while converting icon to wide string");
|
||||
let wide_path =
|
||||
WideCString::from_str(&self.icons[i]).expect("Error while converting icon to wide string");
|
||||
let len = min(wide_path.len(), MAX_FILE_PATH - 1);
|
||||
icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]);
|
||||
// TODO: test overflow, correct case
|
||||
}
|
||||
|
||||
let wide_notification_icon_path =
|
||||
widestring::WideCString::from_str(&self.notification_icon_path).expect("Error while converting notification icon to wide string");
|
||||
widestring::WideCString::from_str(&self.notification_icon_path)
|
||||
.expect("Error while converting notification icon to wide string");
|
||||
let mut wide_notification_icon_path_buffer: [u16; MAX_FILE_PATH] = [0; MAX_FILE_PATH];
|
||||
wide_notification_icon_path_buffer[..wide_notification_icon_path.as_slice().len()]
|
||||
.clone_from_slice(wide_notification_icon_path.as_slice());
|
||||
|
@ -279,7 +289,34 @@ impl Win32Remote {
|
|||
match wide_message {
|
||||
Ok(wide_message) => unsafe { ui_show_notification(handle, wide_message.as_ptr()) },
|
||||
Err(error) => {
|
||||
error!("Unable to show notification, invalid message encoding {}", error);
|
||||
error!(
|
||||
"Unable to show notification, invalid message encoding {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_context_menu(&self, menu: &Menu) {
|
||||
let handle = self.handle.load(Ordering::Acquire);
|
||||
if handle.is_null() {
|
||||
error!("Unable to show context menu, pointer is null");
|
||||
return;
|
||||
}
|
||||
|
||||
match menu.to_json() {
|
||||
Ok(payload) => {
|
||||
let c_string = CString::new(payload);
|
||||
match c_string {
|
||||
Ok(c_string) => unsafe { ui_show_context_menu(handle, c_string.as_ptr()) },
|
||||
Err(error) => error!(
|
||||
"Unable to show context menu, impossible to convert payload to c_string: {}",
|
||||
error
|
||||
),
|
||||
}
|
||||
}
|
||||
Err(error) => {
|
||||
error!("Unable to show context menu, {}", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,9 @@
|
|||
#include "WinToast/wintoastlib.h"
|
||||
using namespace WinToastLib;
|
||||
|
||||
#include "json/json.hpp"
|
||||
using json = nlohmann::json;
|
||||
|
||||
#define APPWM_ICON_CLICK (WM_APP + 1)
|
||||
#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 2)
|
||||
#define APPWM_UPDATE_TRAY_ICON (WM_APP + 3)
|
||||
|
@ -111,6 +114,7 @@ LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, L
|
|||
if (flags == 0)
|
||||
{
|
||||
std::cout << "click menu" << std::flush;
|
||||
// TODO: click
|
||||
//context_menu_click_callback(manager_instance, (int32_t)idItem);
|
||||
}
|
||||
|
||||
|
@ -118,27 +122,12 @@ LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, L
|
|||
}
|
||||
case APPWM_SHOW_CONTEXT_MENU: // Request to show context menu
|
||||
{
|
||||
HMENU hPopupMenu = CreatePopupMenu();
|
||||
|
||||
// Create the menu
|
||||
|
||||
/*
|
||||
int32_t count = static_cast<int32_t>(lp);
|
||||
std::unique_ptr<MenuItem[]> items(reinterpret_cast<MenuItem*>(wp));
|
||||
|
||||
for (int i = 0; i<count; i++) {
|
||||
if (items[i].type == 1) {
|
||||
InsertMenu(hPopupMenu, i, MF_BYPOSITION | MF_STRING, items[i].id, items[i].name);
|
||||
}else{
|
||||
InsertMenu(hPopupMenu, i, MF_BYPOSITION | MF_SEPARATOR, items[i].id, NULL);
|
||||
}
|
||||
}
|
||||
|
||||
HMENU menu = (HMENU)lp;
|
||||
POINT pt;
|
||||
GetCursorPos(&pt);
|
||||
SetForegroundWindow(nw);
|
||||
TrackPopupMenu(hPopupMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, nw, NULL);
|
||||
*/
|
||||
SetForegroundWindow(window);
|
||||
TrackPopupMenu(menu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, window, NULL);
|
||||
|
||||
break;
|
||||
}
|
||||
case APPWM_UPDATE_TRAY_ICON: // Request to update the tray icon
|
||||
|
@ -288,7 +277,7 @@ void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
|
|||
|
||||
// Initialize the notification handler
|
||||
WinToast::instance()->setAppName(L"Espanso");
|
||||
const auto aumi = WinToast::configureAUMI(L"federico.terzi", L"Espanso", L"Espanso", L"1.0.0");
|
||||
const auto aumi = WinToast::configureAUMI(L"federico.terzi", L"Espanso", L"Core", L"1.0.0");
|
||||
WinToast::instance()->setAppUserModelId(aumi);
|
||||
if (!WinToast::instance()->initialize())
|
||||
{
|
||||
|
@ -342,3 +331,68 @@ int32_t ui_show_notification(void *window, wchar_t *message)
|
|||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Menu related methods
|
||||
|
||||
void _insert_separator_menu(HMENU parent)
|
||||
{
|
||||
InsertMenu(parent, -1, MF_BYPOSITION | MF_SEPARATOR, 0, NULL);
|
||||
}
|
||||
|
||||
void _insert_single_menu(HMENU parent, json item)
|
||||
{
|
||||
if (!item["label"].is_string() || !item["raw_id"].is_number())
|
||||
{
|
||||
return;
|
||||
}
|
||||
std::string label = item["label"];
|
||||
uint32_t raw_id = item["raw_id"];
|
||||
|
||||
// Convert to wide chars
|
||||
std::wstring wide_label(label.length(), L'#');
|
||||
mbstowcs(&wide_label[0], label.c_str(), label.length());
|
||||
|
||||
InsertMenu(parent, -1, MF_BYPOSITION | MF_STRING, raw_id, wide_label.c_str());
|
||||
}
|
||||
|
||||
void _insert_sub_menu(HMENU parent, json items)
|
||||
{
|
||||
for (auto &item : items)
|
||||
{
|
||||
if (item["type"] == "simple")
|
||||
{
|
||||
_insert_single_menu(parent, item);
|
||||
}
|
||||
else if (item["type"] == "separator")
|
||||
{
|
||||
_insert_separator_menu(parent);
|
||||
}
|
||||
else if (item["type"] == "sub")
|
||||
{
|
||||
HMENU subMenu = CreatePopupMenu();
|
||||
std::string label = item["label"];
|
||||
|
||||
// Convert to wide chars
|
||||
std::wstring wide_label(label.length(), L'#');
|
||||
mbstowcs(&wide_label[0], label.c_str(), label.length());
|
||||
|
||||
InsertMenu(parent, -1, MF_BYPOSITION | MF_POPUP, (UINT_PTR)subMenu, wide_label.c_str());
|
||||
_insert_sub_menu(subMenu, item["items"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int32_t ui_show_context_menu(void *window, char *payload)
|
||||
{
|
||||
if (window)
|
||||
{
|
||||
auto j_menu = json::parse(payload);
|
||||
// Generate the menu from the JSON payload
|
||||
HMENU parentMenu = CreatePopupMenu();
|
||||
_insert_sub_menu(parentMenu, j_menu);
|
||||
|
||||
PostMessage((HWND)window, APPWM_SHOW_CONTEXT_MENU, 0, (LPARAM)parentMenu);
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
|
@ -60,4 +60,9 @@ extern "C" void ui_update_tray_icon(void * window, int32_t index);
|
|||
// Show a native Windows 10 notification
|
||||
extern "C" int32_t ui_show_notification(void * window, wchar_t * message);
|
||||
|
||||
// Display the context menu on the tray icon.
|
||||
// Payload is passed as JSON as given the complex structure, parsing
|
||||
// this manually would have been complex.
|
||||
extern "C" int32_t ui_show_context_menu(void * window, char * payload);
|
||||
|
||||
#endif //ESPANSO_UI_H
|
|
@ -12,3 +12,4 @@ edition = "2018"
|
|||
espanso-detect = { path = "../espanso-detect" }
|
||||
espanso-ui = { path = "../espanso-ui" }
|
||||
maplit = "1.0.2"
|
||||
simplelog = "0.9.0"
|
|
@ -1,7 +1,18 @@
|
|||
use espanso_detect::event::{InputEvent, Status};
|
||||
use espanso_ui::menu::*;
|
||||
use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
|
||||
|
||||
fn main() {
|
||||
println!("Hello, world!z");
|
||||
CombinedLogger::init(vec![
|
||||
TermLogger::new(LevelFilter::Debug, Config::default(), TerminalMode::Mixed),
|
||||
// WriteLogger::new(
|
||||
// LevelFilter::Info,
|
||||
// Config::default(),
|
||||
// File::create("my_rust_binary.log").unwrap(),
|
||||
// ),
|
||||
])
|
||||
.unwrap();
|
||||
|
||||
let icon_paths = vec![
|
||||
(
|
||||
|
@ -17,7 +28,8 @@ fn main() {
|
|||
let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions {
|
||||
show_icon: true,
|
||||
icon_paths: &icon_paths,
|
||||
notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png".to_string(),
|
||||
notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
|
||||
.to_string(),
|
||||
});
|
||||
|
||||
std::thread::spawn(move || {
|
||||
|
@ -30,7 +42,7 @@ fn main() {
|
|||
InputEvent::Keyboard(evt) => {
|
||||
if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
|
||||
//remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
|
||||
remote.show_notification("Espanso is running!");
|
||||
//remote.show_notification("Espanso is running!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +50,20 @@ fn main() {
|
|||
});
|
||||
|
||||
eventloop.initialize();
|
||||
eventloop.run(Box::new(|event| {
|
||||
eventloop.run(Box::new(move |event| {
|
||||
println!("ui {:?}", event);
|
||||
let menu = Menu::from(vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
|
||||
MenuItem::Separator,
|
||||
MenuItem::Sub(SubMenuItem::new(
|
||||
"Sub",
|
||||
vec![
|
||||
MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
|
||||
MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
|
||||
],
|
||||
)),
|
||||
])
|
||||
.unwrap();
|
||||
remote.show_context_menu(&menu);
|
||||
}))
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user