feat(modulo): initial port of modulo within the espanso repo
This commit is contained in:
parent
04720212e5
commit
23895841e3
21
espanso-modulo/Cargo.toml
Normal file
21
espanso-modulo/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "espanso-modulo"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Federico Terzi <federico-terzi@users.noreply.github.com>"]
|
||||||
|
edition = "2018"
|
||||||
|
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"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
regex = "1.4.3"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1.0.66"
|
||||||
|
regex = "1.4.3"
|
||||||
|
zip = "0.5.12"
|
||||||
|
winres = "0.1.11"
|
322
espanso-modulo/build.rs
Normal file
322
espanso-modulo/build.rs
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 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 std::path::{PathBuf};
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
use std::path::{Path};
|
||||||
|
|
||||||
|
const WX_WIDGETS_ARCHIVE_NAME: &str = "wxWidgets-3.1.5.zip";
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn build_native() {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let project_dir =
|
||||||
|
PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").expect("missing CARGO_MANIFEST_DIR"));
|
||||||
|
let wx_archive = project_dir.join("vendor").join(WX_WIDGETS_ARCHIVE_NAME);
|
||||||
|
if !wx_archive.is_file() {
|
||||||
|
panic!("could not find wxWidgets archive!");
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("missing OUT_DIR"));
|
||||||
|
let out_wx_dir = out_dir.join("wx");
|
||||||
|
|
||||||
|
if !out_wx_dir.is_dir() {
|
||||||
|
// Extract the wxWidgets archive
|
||||||
|
let wx_archive =
|
||||||
|
std::fs::File::open(&wx_archive).expect("unable to open wxWidgets source archive");
|
||||||
|
let mut archive = zip::ZipArchive::new(wx_archive).expect("unable to read wxWidgets archive");
|
||||||
|
archive
|
||||||
|
.extract(&out_wx_dir)
|
||||||
|
.expect("unable to extract wxWidgets source dir");
|
||||||
|
|
||||||
|
// Compile wxWidgets
|
||||||
|
let tool = cc::windows_registry::find_tool("msvc", "msbuild")
|
||||||
|
.expect("unable to locate MSVC compiler, did you install Visual Studio?");
|
||||||
|
let mut vcvars_path = None;
|
||||||
|
let mut current_root = tool.path();
|
||||||
|
while let Some(parent) = current_root.parent() {
|
||||||
|
let target = parent
|
||||||
|
.join("VC")
|
||||||
|
.join("Auxiliary")
|
||||||
|
.join("Build")
|
||||||
|
.join("vcvars64.bat");
|
||||||
|
if target.exists() {
|
||||||
|
vcvars_path = Some(target);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
current_root = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
let vcvars_path = vcvars_path.expect("unable to find vcvars64.bat file");
|
||||||
|
let mut handle = Command::new("cmd")
|
||||||
|
.current_dir(
|
||||||
|
out_wx_dir
|
||||||
|
.join("build")
|
||||||
|
.join("msw")
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
.args(&[
|
||||||
|
"/k",
|
||||||
|
&vcvars_path.to_string_lossy().to_string(),
|
||||||
|
"&",
|
||||||
|
"nmake",
|
||||||
|
"/f",
|
||||||
|
"makefile.vc",
|
||||||
|
"BUILD=release",
|
||||||
|
"TARGET_CPU=X64",
|
||||||
|
"&",
|
||||||
|
"exit",
|
||||||
|
])
|
||||||
|
.spawn()
|
||||||
|
.expect("failed to execute nmake");
|
||||||
|
handle.wait().expect("unable to wait for nmake command");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure wxWidgets is compiled
|
||||||
|
if !out_wx_dir
|
||||||
|
.join("build")
|
||||||
|
.join("msw")
|
||||||
|
.join("vc_mswu_x64")
|
||||||
|
.is_dir()
|
||||||
|
{
|
||||||
|
panic!("wxWidgets is not compiled correctly, missing 'build/msw/vc_mswu_x64' directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
let wx_include_dir = out_wx_dir.join("include");
|
||||||
|
let wx_include_msvc_dir = wx_include_dir.join("msvc");
|
||||||
|
let wx_lib_dir = out_wx_dir.join("lib").join("vc_x64_lib");
|
||||||
|
|
||||||
|
cc::Build::new()
|
||||||
|
.cpp(true)
|
||||||
|
.file("src/sys/form/form.cpp")
|
||||||
|
.file("src/sys/search/search.cpp")
|
||||||
|
.file("src/sys/common/common.cpp")
|
||||||
|
.flag("/EHsc")
|
||||||
|
.include(wx_include_dir)
|
||||||
|
.include(wx_include_msvc_dir)
|
||||||
|
.compile("espansomodulosys");
|
||||||
|
|
||||||
|
// Add resources (manifest)
|
||||||
|
let mut resources = winres::WindowsResource::new();
|
||||||
|
resources.set_manifest(include_str!("res/win.manifest"));
|
||||||
|
resources
|
||||||
|
.compile()
|
||||||
|
.expect("unable to compile resource file");
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"cargo:rustc-link-search=native={}",
|
||||||
|
wx_lib_dir.to_string_lossy()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add documentation for macos
|
||||||
|
// Install LLVM:
|
||||||
|
// brew install llvm
|
||||||
|
// Compile wxWidgets:
|
||||||
|
// mkdir build-cocoa
|
||||||
|
// cd build-cocoa
|
||||||
|
// ../configure --disable-shared --enable-macosx_arch=x86_64
|
||||||
|
// make -j6
|
||||||
|
//
|
||||||
|
// Run
|
||||||
|
// WXMAC=/Users/freddy/wxWidgets cargo run
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn build_native() {
|
||||||
|
let wx_location = std::env::var("WXMAC").expect(
|
||||||
|
"unable to find wxWidgets directory, please add a WXMAC env variable with the absolute path",
|
||||||
|
);
|
||||||
|
let wx_path = PathBuf::from(&wx_location);
|
||||||
|
println!("{}", wx_location);
|
||||||
|
if !wx_path.is_dir() {
|
||||||
|
panic!("The given WXMAC directory is not valid");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure wxWidgets is compiled
|
||||||
|
if !wx_path.join("build-cocoa").is_dir() {
|
||||||
|
panic!("wxWidgets is not compiled correctly, missing 'build-cocoa/' directory")
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_path = wx_path.join("build-cocoa").join("wx-config");
|
||||||
|
let cpp_flags = get_cpp_flags(&config_path);
|
||||||
|
|
||||||
|
let mut build = cc::Build::new();
|
||||||
|
build
|
||||||
|
.cpp(true)
|
||||||
|
.file("native/form.cpp")
|
||||||
|
.file("native/common.cpp")
|
||||||
|
.file("native/search.cpp")
|
||||||
|
.file("native/mac.mm");
|
||||||
|
build.flag("-std=c++17");
|
||||||
|
|
||||||
|
for flag in cpp_flags {
|
||||||
|
build.flag(&flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
build.compile("modulosys");
|
||||||
|
|
||||||
|
// Render linker flags
|
||||||
|
|
||||||
|
generate_linker_flags(&config_path);
|
||||||
|
|
||||||
|
// On (older) OSX we need to link against the clang runtime,
|
||||||
|
// which is hidden in some non-default path.
|
||||||
|
//
|
||||||
|
// More details at https://github.com/alexcrichton/curl-rust/issues/279.
|
||||||
|
if let Some(path) = macos_link_search_path() {
|
||||||
|
println!("cargo:rustc-link-lib=clang_rt.osx");
|
||||||
|
println!("cargo:rustc-link-search={}", path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn get_cpp_flags(wx_config_path: &Path) -> Vec<String> {
|
||||||
|
let config_output = std::process::Command::new(&wx_config_path)
|
||||||
|
.arg("--cxxflags")
|
||||||
|
.output()
|
||||||
|
.expect("unable to execute wx-config");
|
||||||
|
let config_libs =
|
||||||
|
String::from_utf8(config_output.stdout).expect("unable to parse wx-config output");
|
||||||
|
let cpp_flags: Vec<String> = config_libs
|
||||||
|
.split(" ")
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| {
|
||||||
|
if !s.trim().is_empty() {
|
||||||
|
Some(s.trim().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
cpp_flags
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
fn generate_linker_flags(wx_config_path: &Path) {
|
||||||
|
use regex::Regex;
|
||||||
|
let config_output = std::process::Command::new(&wx_config_path)
|
||||||
|
.arg("--libs")
|
||||||
|
.output()
|
||||||
|
.expect("unable to execute wx-config libs");
|
||||||
|
let config_libs =
|
||||||
|
String::from_utf8(config_output.stdout).expect("unable to parse wx-config libs output");
|
||||||
|
let linker_flags: Vec<String> = config_libs
|
||||||
|
.split(" ")
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|s| {
|
||||||
|
if !s.trim().is_empty() {
|
||||||
|
Some(s.trim().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let static_lib_extract = Regex::new(r"lib/lib(.*)\.a").unwrap();
|
||||||
|
|
||||||
|
// Translate the flags generated by `wx-config` to commands
|
||||||
|
// that cargo can understand.
|
||||||
|
for (i, flag) in linker_flags.iter().enumerate() {
|
||||||
|
if flag.starts_with("-L") {
|
||||||
|
let path = flag.trim_start_matches("-L");
|
||||||
|
println!("cargo:rustc-link-search=native={}", path);
|
||||||
|
} else if flag.starts_with("-framework") {
|
||||||
|
println!("cargo:rustc-link-lib=framework={}", linker_flags[i + 1]);
|
||||||
|
} else if flag.starts_with("/") {
|
||||||
|
let captures = static_lib_extract
|
||||||
|
.captures(flag)
|
||||||
|
.expect("unable to capture flag regex");
|
||||||
|
let libname = captures.get(1).expect("unable to find static libname");
|
||||||
|
println!("cargo:rustc-link-lib=static={}", libname.as_str());
|
||||||
|
} else if flag.starts_with("-l") {
|
||||||
|
let libname = flag.trim_start_matches("-l");
|
||||||
|
println!("cargo:rustc-link-lib=dylib={}", libname);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Taken from curl-rust: https://github.com/alexcrichton/curl-rust/pull/283/files
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn macos_link_search_path() -> Option<String> {
|
||||||
|
let output = std::process::Command::new("clang")
|
||||||
|
.arg("--print-search-dirs")
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
if !output.status.success() {
|
||||||
|
println!("failed to run 'clang --print-search-dirs', continuing without a link search path");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
for line in stdout.lines() {
|
||||||
|
if line.contains("libraries: =") {
|
||||||
|
let path = line.split('=').skip(1).next()?;
|
||||||
|
return Some(format!("{}/lib/darwin", path));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("failed to determine link search path, continuing without it");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: add documentation for linux
|
||||||
|
// Install LLVM:
|
||||||
|
// sudo apt install clang
|
||||||
|
// Install wxWidgets:
|
||||||
|
// sudo apt install libwxgtk3.0-0v5 libwxgtk3.0-dev
|
||||||
|
//
|
||||||
|
// cargo run
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn build_native() {
|
||||||
|
// Make sure wxWidgets is installed
|
||||||
|
if !std::process::Command::new("wx-config")
|
||||||
|
.arg("--version")
|
||||||
|
.output()
|
||||||
|
.is_ok()
|
||||||
|
{
|
||||||
|
panic!("wxWidgets is not installed, as `wx-config` cannot be exectued")
|
||||||
|
}
|
||||||
|
|
||||||
|
let config_path = PathBuf::from("wx-config");
|
||||||
|
let cpp_flags = get_cpp_flags(&config_path);
|
||||||
|
|
||||||
|
let mut build = cc::Build::new();
|
||||||
|
build
|
||||||
|
.cpp(true)
|
||||||
|
.file("native/form.cpp")
|
||||||
|
.file("native/search.cpp")
|
||||||
|
.file("native/common.cpp");
|
||||||
|
build.flag("-std=c++17");
|
||||||
|
|
||||||
|
for flag in cpp_flags {
|
||||||
|
build.flag(&flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
build.compile("modulosys");
|
||||||
|
|
||||||
|
// Render linker flags
|
||||||
|
|
||||||
|
generate_linker_flags(&config_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
build_native();
|
||||||
|
}
|
10
espanso-modulo/res/win.manifest
Normal file
10
espanso-modulo/res/win.manifest
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
|
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||||
|
<assemblyIdentity version="0.1.0.0" processorArchitecture="*" name="com.federicoterzi.modulo" type="win32"/>
|
||||||
|
<description>TODO</description>
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity name="Microsoft.Windows.Common-Controls" version="6.0.0.0" type="win32" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
202
espanso-modulo/src/form/config.rs
Normal file
202
espanso-modulo/src/form/config.rs
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
fn default_title() -> String {
|
||||||
|
"espanso".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_icon() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_fields() -> HashMap<String, FieldConfig> {
|
||||||
|
HashMap::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct FormConfig {
|
||||||
|
#[serde(default = "default_title")]
|
||||||
|
pub title: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_icon")]
|
||||||
|
pub icon: Option<String>,
|
||||||
|
|
||||||
|
pub layout: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_fields")]
|
||||||
|
pub fields: HashMap<String, FieldConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Clone)]
|
||||||
|
pub struct FieldConfig {
|
||||||
|
pub field_type: FieldTypeConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FieldConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
field_type: FieldTypeConfig::Text(TextFieldConfig {
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub enum FieldTypeConfig {
|
||||||
|
Text(TextFieldConfig),
|
||||||
|
Choice(ChoiceFieldConfig),
|
||||||
|
List(ListFieldConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct TextFieldConfig {
|
||||||
|
pub default: String,
|
||||||
|
pub multiline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TextFieldConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
default: "".to_owned(),
|
||||||
|
multiline: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ChoiceFieldConfig {
|
||||||
|
pub values: Vec<String>,
|
||||||
|
pub default: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ChoiceFieldConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
values: Vec::new(),
|
||||||
|
default: "".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct ListFieldConfig {
|
||||||
|
pub values: Vec<String>,
|
||||||
|
pub default: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ListFieldConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
values: Vec::new(),
|
||||||
|
default: "".to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> serde::Deserialize<'de> for FieldConfig {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: Deserializer<'de>,
|
||||||
|
{
|
||||||
|
let auto_field = AutoFieldConfig::deserialize(deserializer)?;
|
||||||
|
Ok(FieldConfig::from(&auto_field))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a AutoFieldConfig> for FieldConfig {
|
||||||
|
fn from(other: &'a AutoFieldConfig) -> Self {
|
||||||
|
let field_type = match other.field_type.as_ref() {
|
||||||
|
"text" => {
|
||||||
|
let mut config: TextFieldConfig = Default::default();
|
||||||
|
|
||||||
|
if let Some(default) = &other.default {
|
||||||
|
config.default = default.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
config.multiline = other.multiline;
|
||||||
|
|
||||||
|
FieldTypeConfig::Text(config)
|
||||||
|
}
|
||||||
|
"choice" => {
|
||||||
|
let mut config = ChoiceFieldConfig {
|
||||||
|
values: other.values.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(default) = &other.default {
|
||||||
|
config.default = default.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldTypeConfig::Choice(config)
|
||||||
|
}
|
||||||
|
"list" => {
|
||||||
|
let mut config = ListFieldConfig {
|
||||||
|
values: other.values.clone(),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(default) = &other.default {
|
||||||
|
config.default = default.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
FieldTypeConfig::List(config)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
panic!("invalid field type: {}", other.field_type);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Self { field_type }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_type() -> String {
|
||||||
|
"text".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_default() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_multiline() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_values() -> Vec<String> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct AutoFieldConfig {
|
||||||
|
#[serde(rename = "type", default = "default_type")]
|
||||||
|
pub field_type: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_default")]
|
||||||
|
pub default: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_multiline")]
|
||||||
|
pub multiline: bool,
|
||||||
|
|
||||||
|
#[serde(default = "default_values")]
|
||||||
|
pub values: Vec<String>,
|
||||||
|
}
|
99
espanso-modulo/src/form/generator.rs
Normal file
99
espanso-modulo/src/form/generator.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::config::{FieldConfig, FieldTypeConfig, FormConfig};
|
||||||
|
use super::parser::layout::Token;
|
||||||
|
use crate::sys::form::types::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
pub fn generate(config: FormConfig) -> Form {
|
||||||
|
let structure = super::parser::layout::parse_layout(&config.layout);
|
||||||
|
build_form(config, structure)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_field(token: &Token, field_map: &HashMap<String, FieldConfig>) -> Field {
|
||||||
|
match token {
|
||||||
|
Token::Text(text) => Field {
|
||||||
|
field_type: FieldType::Label(LabelMetadata { text: text.clone() }),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Token::Field(name) => {
|
||||||
|
let config = if let Some(config) = field_map.get(name) {
|
||||||
|
config.clone()
|
||||||
|
} else {
|
||||||
|
Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_type = match &config.field_type {
|
||||||
|
FieldTypeConfig::Text(config) => FieldType::Text(TextMetadata {
|
||||||
|
default_text: config.default.clone(),
|
||||||
|
multiline: config.multiline,
|
||||||
|
}),
|
||||||
|
FieldTypeConfig::Choice(config) => FieldType::Choice(ChoiceMetadata {
|
||||||
|
values: config.values.clone(),
|
||||||
|
choice_type: ChoiceType::Dropdown,
|
||||||
|
default_value: config.default.clone(),
|
||||||
|
}),
|
||||||
|
FieldTypeConfig::List(config) => FieldType::Choice(ChoiceMetadata {
|
||||||
|
values: config.values.clone(),
|
||||||
|
choice_type: ChoiceType::List,
|
||||||
|
default_value: config.default.clone(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
Field {
|
||||||
|
id: Some(name.clone()),
|
||||||
|
field_type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_form(form: FormConfig, structure: Vec<Vec<Token>>) -> Form {
|
||||||
|
let field_map = form.fields;
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
|
||||||
|
for row in structure.iter() {
|
||||||
|
let current_field = if row.len() == 1 {
|
||||||
|
// Single field
|
||||||
|
create_field(&row[0], &field_map)
|
||||||
|
} else {
|
||||||
|
// Row field
|
||||||
|
let inner_fields = row
|
||||||
|
.iter()
|
||||||
|
.map(|token| create_field(token, &field_map))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Field {
|
||||||
|
field_type: FieldType::Row(RowMetadata {
|
||||||
|
fields: inner_fields,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fields.push(current_field)
|
||||||
|
}
|
||||||
|
|
||||||
|
Form {
|
||||||
|
title: form.title,
|
||||||
|
icon: form.icon,
|
||||||
|
fields,
|
||||||
|
}
|
||||||
|
}
|
24
espanso-modulo/src/form/mod.rs
Normal file
24
espanso-modulo/src/form/mod.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub mod config;
|
||||||
|
pub mod generator;
|
||||||
|
pub mod parser;
|
||||||
|
|
||||||
|
pub use crate::sys::form::show;
|
108
espanso-modulo/src/form/parser/layout.rs
Normal file
108
espanso-modulo/src/form/parser/layout.rs
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use super::split::*;
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref FIELD_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Token {
|
||||||
|
Text(String), // Text
|
||||||
|
Field(String), // id: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_layout(layout: &str) -> Vec<Vec<Token>> {
|
||||||
|
let mut rows = Vec::new();
|
||||||
|
|
||||||
|
for line in layout.lines() {
|
||||||
|
let line = line.trim();
|
||||||
|
|
||||||
|
// Skip empty lines
|
||||||
|
if line.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut row: Vec<Token> = Vec::new();
|
||||||
|
|
||||||
|
let splitter = SplitCaptures::new(&FIELD_REGEX, line);
|
||||||
|
|
||||||
|
// Get the individual tokens
|
||||||
|
for state in splitter {
|
||||||
|
match state {
|
||||||
|
SplitState::Unmatched(text) => {
|
||||||
|
if !text.is_empty() {
|
||||||
|
row.push(Token::Text(text.to_owned()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SplitState::Captured(caps) => {
|
||||||
|
if let Some(name) = caps.get(1) {
|
||||||
|
let name = name.as_str().to_owned();
|
||||||
|
row.push(Token::Field(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
rows
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_layout() {
|
||||||
|
let layout = "Hey {{name}},\nHow are you?\n \nCheers";
|
||||||
|
let result = parse_layout(layout);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
vec![
|
||||||
|
vec![
|
||||||
|
Token::Text("Hey ".to_owned()),
|
||||||
|
Token::Field("name".to_owned()),
|
||||||
|
Token::Text(",".to_owned())
|
||||||
|
],
|
||||||
|
vec![Token::Text("How are you?".to_owned())],
|
||||||
|
vec![Token::Text("Cheers".to_owned())],
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_layout_2() {
|
||||||
|
let layout = "Hey {{name}} {{surname}},";
|
||||||
|
let result = parse_layout(layout);
|
||||||
|
assert_eq!(
|
||||||
|
result,
|
||||||
|
vec![vec![
|
||||||
|
Token::Text("Hey ".to_owned()),
|
||||||
|
Token::Field("name".to_owned()),
|
||||||
|
Token::Text(" ".to_owned()),
|
||||||
|
Token::Field("surname".to_owned()),
|
||||||
|
Token::Text(",".to_owned())
|
||||||
|
],]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
espanso-modulo/src/form/parser/mod.rs
Normal file
21
espanso-modulo/src/form/parser/mod.rs
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub mod layout;
|
||||||
|
mod split;
|
54
espanso-modulo/src/form/parser/split.rs
Normal file
54
espanso-modulo/src/form/parser/split.rs
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
// Taken from: https://github.com/rust-lang/regex/issues/330#issuecomment-274058261
|
||||||
|
use regex::{CaptureMatches, Captures, Regex};
|
||||||
|
|
||||||
|
pub struct SplitCaptures<'r, 't> {
|
||||||
|
finder: CaptureMatches<'r, 't>,
|
||||||
|
text: &'t str,
|
||||||
|
last: usize,
|
||||||
|
caps: Option<Captures<'t>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r, 't> SplitCaptures<'r, 't> {
|
||||||
|
pub fn new(re: &'r Regex, text: &'t str) -> SplitCaptures<'r, 't> {
|
||||||
|
SplitCaptures {
|
||||||
|
finder: re.captures_iter(text),
|
||||||
|
text,
|
||||||
|
last: 0,
|
||||||
|
caps: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum SplitState<'t> {
|
||||||
|
Unmatched(&'t str),
|
||||||
|
Captured(Captures<'t>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r, 't> Iterator for SplitCaptures<'r, 't> {
|
||||||
|
type Item = SplitState<'t>;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<SplitState<'t>> {
|
||||||
|
if let Some(caps) = self.caps.take() {
|
||||||
|
return Some(SplitState::Captured(caps));
|
||||||
|
}
|
||||||
|
match self.finder.next() {
|
||||||
|
None => {
|
||||||
|
if self.last >= self.text.len() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let s = &self.text[self.last..];
|
||||||
|
self.last = self.text.len();
|
||||||
|
Some(SplitState::Unmatched(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(caps) => {
|
||||||
|
let m = caps.get(0).unwrap();
|
||||||
|
let unmatched = &self.text[self.last..m.start()];
|
||||||
|
self.last = m.end();
|
||||||
|
self.caps = Some(caps);
|
||||||
|
Some(SplitState::Unmatched(unmatched))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
espanso-modulo/src/lib.rs
Normal file
25
espanso-modulo/src/lib.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
|
||||||
|
pub mod form;
|
||||||
|
pub mod search;
|
||||||
|
mod sys;
|
80
espanso-modulo/src/search/algorithm.rs
Normal file
80
espanso-modulo/src/search/algorithm.rs
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::sys::search::types::SearchItem;
|
||||||
|
|
||||||
|
pub fn get_algorithm(name: &str) -> Box<dyn Fn(&str, &[SearchItem]) -> Vec<usize>> {
|
||||||
|
match name {
|
||||||
|
"exact" => Box::new(exact_match),
|
||||||
|
"iexact" => Box::new(case_insensitive_exact_match),
|
||||||
|
"ikey" => Box::new(case_insensitive_keyword),
|
||||||
|
_ => panic!("unknown search algorithm: {}", name),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exact_match(query: &str, items: &[SearchItem]) -> Vec<usize> {
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, item)| {
|
||||||
|
item.label.contains(query) || item.trigger.as_deref().map_or(false, |t| t.contains(query))
|
||||||
|
})
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn case_insensitive_exact_match(query: &str, items: &[SearchItem]) -> Vec<usize> {
|
||||||
|
let lowercase_query = query.to_lowercase();
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, item)| {
|
||||||
|
item.label.to_lowercase().contains(&lowercase_query)
|
||||||
|
|| item
|
||||||
|
.trigger
|
||||||
|
.as_deref()
|
||||||
|
.map_or(false, |t| t.to_lowercase().contains(query))
|
||||||
|
})
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn case_insensitive_keyword(query: &str, items: &[SearchItem]) -> Vec<usize> {
|
||||||
|
let lowercase_query = query.to_lowercase();
|
||||||
|
let keywords: Vec<&str> = lowercase_query.split_whitespace().collect();
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, item)| {
|
||||||
|
for keyword in keywords.iter() {
|
||||||
|
if !item.label.to_lowercase().contains(keyword)
|
||||||
|
&& !item
|
||||||
|
.trigger
|
||||||
|
.as_deref()
|
||||||
|
.map_or(false, |t| t.to_lowercase().contains(keyword))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.map(|(i, _)| i)
|
||||||
|
.collect()
|
||||||
|
}
|
58
espanso-modulo/src/search/config.rs
Normal file
58
espanso-modulo/src/search/config.rs
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
fn default_title() -> String {
|
||||||
|
"espanso".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_icon() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_items() -> Vec<SearchItem> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_algorithm() -> String {
|
||||||
|
"ikey".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct SearchConfig {
|
||||||
|
#[serde(default = "default_title")]
|
||||||
|
pub title: String,
|
||||||
|
|
||||||
|
#[serde(default = "default_icon")]
|
||||||
|
pub icon: Option<String>,
|
||||||
|
|
||||||
|
#[serde(default = "default_items")]
|
||||||
|
pub items: Vec<SearchItem>,
|
||||||
|
|
||||||
|
#[serde(default = "default_algorithm")]
|
||||||
|
pub algorithm: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
pub struct SearchItem {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub trigger: Option<String>,
|
||||||
|
}
|
39
espanso-modulo/src/search/generator.rs
Normal file
39
espanso-modulo/src/search/generator.rs
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::search::config::SearchConfig;
|
||||||
|
use crate::sys::search::types;
|
||||||
|
|
||||||
|
pub fn generate(config: SearchConfig) -> types::Search {
|
||||||
|
let items = config
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| types::SearchItem {
|
||||||
|
id: item.id,
|
||||||
|
label: item.label,
|
||||||
|
trigger: item.trigger,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
types::Search {
|
||||||
|
title: config.title,
|
||||||
|
items,
|
||||||
|
icon: config.icon,
|
||||||
|
}
|
||||||
|
}
|
24
espanso-modulo/src/search/mod.rs
Normal file
24
espanso-modulo/src/search/mod.rs
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub mod algorithm;
|
||||||
|
pub mod config;
|
||||||
|
pub mod generator;
|
||||||
|
|
||||||
|
pub use crate::sys::search::show;
|
83
espanso-modulo/src/sys/common/common.cpp
Normal file
83
espanso-modulo/src/sys/common/common.cpp
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#ifdef __WXMSW__
|
||||||
|
#include <windows.h>
|
||||||
|
#endif
|
||||||
|
#ifdef __WXOSX__
|
||||||
|
#include "mac.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void setFrameIcon(const char * iconPath, wxFrame * frame) {
|
||||||
|
if (iconPath) {
|
||||||
|
wxString iconPath(iconPath);
|
||||||
|
wxBitmapType imgType = wxICON_DEFAULT_TYPE;
|
||||||
|
|
||||||
|
#ifdef __WXMSW__
|
||||||
|
imgType = wxBITMAP_TYPE_ICO;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
wxIcon icon;
|
||||||
|
icon.LoadFile(iconPath, imgType);
|
||||||
|
if (icon.IsOk()) {
|
||||||
|
frame->SetIcon(icon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Activate(wxFrame * frame) {
|
||||||
|
#ifdef __WXMSW__
|
||||||
|
|
||||||
|
HWND handle = frame->GetHandle();
|
||||||
|
if (handle == GetForegroundWindow()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsIconic(handle)) {
|
||||||
|
ShowWindow(handle, 9);
|
||||||
|
}
|
||||||
|
|
||||||
|
INPUT ip;
|
||||||
|
ip.type = INPUT_KEYBOARD;
|
||||||
|
ip.ki.wScan = 0;
|
||||||
|
ip.ki.time = 0;
|
||||||
|
ip.ki.dwExtraInfo = 0;
|
||||||
|
ip.ki.wVk = VK_MENU;
|
||||||
|
ip.ki.dwFlags = 0;
|
||||||
|
|
||||||
|
SendInput(1, &ip, sizeof(INPUT));
|
||||||
|
ip.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||||
|
|
||||||
|
SendInput(1, &ip, sizeof(INPUT));
|
||||||
|
|
||||||
|
SetForegroundWindow(handle);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
#ifdef __WXOSX__
|
||||||
|
ActivateApp();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetupWindowStyle(wxFrame * frame) {
|
||||||
|
#ifdef __WXOSX__
|
||||||
|
SetWindowStyles((NSWindow*) frame->MacGetTopLevelWindowRef());
|
||||||
|
#endif
|
||||||
|
}
|
36
espanso-modulo/src/sys/common/common.h
Normal file
36
espanso-modulo/src/sys/common/common.h
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MODULO_COMMON
|
||||||
|
#define MODULO_COMMON
|
||||||
|
|
||||||
|
#define _UNICODE
|
||||||
|
|
||||||
|
#include <wx/wxprec.h>
|
||||||
|
#ifndef WX_PRECOMP
|
||||||
|
#include <wx/wx.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void setFrameIcon(const char * iconPath, wxFrame * frame);
|
||||||
|
|
||||||
|
void Activate(wxFrame * frame);
|
||||||
|
|
||||||
|
void SetupWindowStyle(wxFrame * frame);
|
||||||
|
|
||||||
|
#endif
|
21
espanso-modulo/src/sys/common/mac.h
Normal file
21
espanso-modulo/src/sys/common/mac.h
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
void ActivateApp();
|
||||||
|
void SetWindowStyles(NSWindow * window);
|
30
espanso-modulo/src/sys/common/mac.mm
Normal file
30
espanso-modulo/src/sys/common/mac.mm
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
|
||||||
|
void ActivateApp() {
|
||||||
|
[[NSRunningApplication currentApplication] activateWithOptions:(NSApplicationActivateAllWindows | NSApplicationActivateIgnoringOtherApps)];
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetWindowStyles(NSWindow * window) {
|
||||||
|
window.titleVisibility = NSWindowTitleHidden;
|
||||||
|
window.styleMask &= ~NSWindowStyleMaskTitled;
|
||||||
|
window.movableByWindowBackground = true;
|
||||||
|
}
|
289
espanso-modulo/src/sys/form/form.cpp
Normal file
289
espanso-modulo/src/sys/form/form.cpp
Normal file
|
@ -0,0 +1,289 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#define _UNICODE
|
||||||
|
|
||||||
|
#include "../common/common.h"
|
||||||
|
#include "../interop/interop.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
// https://docs.wxwidgets.org/stable/classwx_frame.html
|
||||||
|
const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxCLOSE_BOX | wxCAPTION;
|
||||||
|
|
||||||
|
const int PADDING = 5;
|
||||||
|
const int MULTILINE_MIN_HEIGHT = 100;
|
||||||
|
const int MULTILINE_MIN_WIDTH = 100;
|
||||||
|
|
||||||
|
FormMetadata *formMetadata = nullptr;
|
||||||
|
std::vector<ValuePair> values;
|
||||||
|
|
||||||
|
// Field Wrappers
|
||||||
|
|
||||||
|
class FieldWrapper {
|
||||||
|
public:
|
||||||
|
virtual wxString getValue() = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class TextFieldWrapper {
|
||||||
|
wxTextCtrl * control;
|
||||||
|
public:
|
||||||
|
explicit TextFieldWrapper(wxTextCtrl * control): control(control) {}
|
||||||
|
|
||||||
|
virtual wxString getValue() {
|
||||||
|
return control->GetValue();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ChoiceFieldWrapper {
|
||||||
|
wxChoice * control;
|
||||||
|
public:
|
||||||
|
explicit ChoiceFieldWrapper(wxChoice * control): control(control) {}
|
||||||
|
|
||||||
|
virtual wxString getValue() {
|
||||||
|
return control->GetStringSelection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class ListFieldWrapper {
|
||||||
|
wxListBox * control;
|
||||||
|
public:
|
||||||
|
explicit ListFieldWrapper(wxListBox * control): control(control) {}
|
||||||
|
|
||||||
|
virtual wxString getValue() {
|
||||||
|
return control->GetStringSelection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// App Code
|
||||||
|
|
||||||
|
class FormApp: public wxApp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual bool OnInit();
|
||||||
|
};
|
||||||
|
class FormFrame: public wxFrame
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
FormFrame(const wxString& title, const wxPoint& pos, const wxSize& size);
|
||||||
|
|
||||||
|
wxPanel *panel;
|
||||||
|
std::vector<void *> fields;
|
||||||
|
std::unordered_map<const char *, std::unique_ptr<FieldWrapper>> idMap;
|
||||||
|
wxButton *submit;
|
||||||
|
private:
|
||||||
|
void AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata meta);
|
||||||
|
void Submit();
|
||||||
|
void OnSubmitBtn(wxCommandEvent& event);
|
||||||
|
void OnEscape(wxKeyEvent& event);
|
||||||
|
};
|
||||||
|
enum
|
||||||
|
{
|
||||||
|
ID_Submit = 20000
|
||||||
|
};
|
||||||
|
|
||||||
|
bool FormApp::OnInit()
|
||||||
|
{
|
||||||
|
FormFrame *frame = new FormFrame(formMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340) );
|
||||||
|
setFrameIcon(formMetadata->iconPath, frame);
|
||||||
|
frame->Show( true );
|
||||||
|
|
||||||
|
Activate(frame);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
FormFrame::FormFrame(const wxString& title, const wxPoint& pos, const wxSize& size)
|
||||||
|
: wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE)
|
||||||
|
{
|
||||||
|
panel = new wxPanel(this, wxID_ANY);
|
||||||
|
wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
|
||||||
|
panel->SetSizer(vbox);
|
||||||
|
|
||||||
|
for (int field = 0; field < formMetadata->fieldSize; field++) {
|
||||||
|
FieldMetadata meta = formMetadata->fields[field];
|
||||||
|
AddComponent(panel, vbox, meta);
|
||||||
|
}
|
||||||
|
|
||||||
|
submit = new wxButton(panel, ID_Submit, "Submit");
|
||||||
|
vbox->Add(submit, 1, wxEXPAND | wxALL, PADDING);
|
||||||
|
|
||||||
|
Bind(wxEVT_BUTTON, &FormFrame::OnSubmitBtn, this, ID_Submit);
|
||||||
|
Bind(wxEVT_CHAR_HOOK, &FormFrame::OnEscape, this, wxID_ANY);
|
||||||
|
// TODO: register ESC click handler: https://forums.wxwidgets.org/viewtopic.php?t=41926
|
||||||
|
|
||||||
|
this->SetClientSize(panel->GetBestSize());
|
||||||
|
this->CentreOnScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FormFrame::AddComponent(wxPanel *parent, wxBoxSizer *sizer, FieldMetadata meta) {
|
||||||
|
void * control = nullptr;
|
||||||
|
|
||||||
|
switch (meta.fieldType) {
|
||||||
|
case FieldType::LABEL:
|
||||||
|
{
|
||||||
|
const LabelMetadata *labelMeta = static_cast<const LabelMetadata*>(meta.specific);
|
||||||
|
auto label = new wxStaticText(parent, wxID_ANY, wxString::FromUTF8(labelMeta->text), wxDefaultPosition, wxDefaultSize);
|
||||||
|
control = label;
|
||||||
|
fields.push_back(label);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldType::TEXT:
|
||||||
|
{
|
||||||
|
const TextMetadata *textMeta = static_cast<const TextMetadata*>(meta.specific);
|
||||||
|
long style = 0;
|
||||||
|
if (textMeta->multiline) {
|
||||||
|
style |= wxTE_MULTILINE;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto textControl = new wxTextCtrl(parent, NewControlId(), wxString::FromUTF8(textMeta->defaultText), wxDefaultPosition, wxDefaultSize, style);
|
||||||
|
|
||||||
|
if (textMeta->multiline) {
|
||||||
|
textControl->SetMinSize(wxSize(MULTILINE_MIN_WIDTH, MULTILINE_MIN_HEIGHT));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the field wrapper
|
||||||
|
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new TextFieldWrapper(textControl));
|
||||||
|
idMap[meta.id] = std::move(field);
|
||||||
|
control = textControl;
|
||||||
|
fields.push_back(textControl);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldType::CHOICE:
|
||||||
|
{
|
||||||
|
const ChoiceMetadata *choiceMeta = static_cast<const ChoiceMetadata*>(meta.specific);
|
||||||
|
|
||||||
|
int selectedItem = -1;
|
||||||
|
wxArrayString choices;
|
||||||
|
for (int i = 0; i<choiceMeta->valueSize; i++) {
|
||||||
|
choices.Add(wxString::FromUTF8(choiceMeta->values[i]));
|
||||||
|
|
||||||
|
if (strcmp(choiceMeta->values[i], choiceMeta->defaultValue) == 0) {
|
||||||
|
selectedItem = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void * choice = nullptr;
|
||||||
|
if (choiceMeta->choiceType == ChoiceType::DROPDOWN) {
|
||||||
|
choice = (void*) new wxChoice(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
|
||||||
|
|
||||||
|
if (selectedItem >= 0) {
|
||||||
|
((wxChoice*)choice)->SetSelection(selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the field wrapper
|
||||||
|
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ChoiceFieldWrapper((wxChoice*) choice));
|
||||||
|
idMap[meta.id] = std::move(field);
|
||||||
|
}else {
|
||||||
|
choice = (void*) new wxListBox(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, choices);
|
||||||
|
|
||||||
|
if (selectedItem >= 0) {
|
||||||
|
((wxListBox*)choice)->SetSelection(selectedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the field wrapper
|
||||||
|
std::unique_ptr<FieldWrapper> field((FieldWrapper*) new ListFieldWrapper((wxListBox*) choice));
|
||||||
|
idMap[meta.id] = std::move(field);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
control = choice;
|
||||||
|
fields.push_back(choice);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case FieldType::ROW:
|
||||||
|
{
|
||||||
|
const RowMetadata *rowMeta = static_cast<const RowMetadata*>(meta.specific);
|
||||||
|
|
||||||
|
auto innerPanel = new wxPanel(panel, wxID_ANY);
|
||||||
|
wxBoxSizer *hbox = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
innerPanel->SetSizer(hbox);
|
||||||
|
sizer->Add(innerPanel, 0, wxEXPAND | wxALL, 0);
|
||||||
|
fields.push_back(innerPanel);
|
||||||
|
|
||||||
|
for (int field = 0; field < rowMeta->fieldSize; field++) {
|
||||||
|
FieldMetadata innerMeta = rowMeta->fields[field];
|
||||||
|
AddComponent(innerPanel, hbox, innerMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// TODO: handle unknown field type
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (control) {
|
||||||
|
sizer->Add((wxWindow*) control, 0, wxEXPAND | wxALL, PADDING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void FormFrame::Submit() {
|
||||||
|
for (auto& field: idMap) {
|
||||||
|
FieldWrapper * fieldWrapper = (FieldWrapper*) field.second.get();
|
||||||
|
wxString value {fieldWrapper->getValue()};
|
||||||
|
wxCharBuffer buffer {value.ToUTF8()};
|
||||||
|
char * id = strdup(field.first);
|
||||||
|
char * c_value = strdup(buffer.data());
|
||||||
|
ValuePair valuePair = {
|
||||||
|
id,
|
||||||
|
c_value,
|
||||||
|
};
|
||||||
|
values.push_back(valuePair);
|
||||||
|
}
|
||||||
|
|
||||||
|
Close(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void FormFrame::OnSubmitBtn(wxCommandEvent &event) {
|
||||||
|
Submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void FormFrame::OnEscape(wxKeyEvent& event) {
|
||||||
|
if (event.GetKeyCode() == WXK_ESCAPE) {
|
||||||
|
Close(true);
|
||||||
|
}else if(event.GetKeyCode() == WXK_RETURN && wxGetKeyState(WXK_RAW_CONTROL)) {
|
||||||
|
Submit();
|
||||||
|
}else{
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void interop_show_form(FormMetadata * _metadata, void (*callback)(ValuePair *values, int size, void *data), void *data) {
|
||||||
|
// Setup high DPI support on Windows
|
||||||
|
#ifdef __WXMSW__
|
||||||
|
SetProcessDPIAware();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
formMetadata = _metadata;
|
||||||
|
|
||||||
|
wxApp::SetInstance(new FormApp());
|
||||||
|
int argc = 0;
|
||||||
|
wxEntry(argc, (char **)nullptr);
|
||||||
|
|
||||||
|
callback(values.data(), values.size(), data);
|
||||||
|
|
||||||
|
// Free up values
|
||||||
|
for (auto pair: values) {
|
||||||
|
free((void*) pair.id);
|
||||||
|
free((void*) pair.value);
|
||||||
|
}
|
||||||
|
}
|
386
espanso-modulo/src/sys/form/mod.rs
Normal file
386
espanso-modulo/src/sys/form/mod.rs
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::os::raw::c_int;
|
||||||
|
|
||||||
|
// Form schema
|
||||||
|
|
||||||
|
pub mod types {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Form {
|
||||||
|
pub title: String,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub fields: Vec<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Field {
|
||||||
|
pub id: Option<String>,
|
||||||
|
pub field_type: FieldType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Field {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
id: None,
|
||||||
|
field_type: FieldType::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FieldType {
|
||||||
|
Unknown,
|
||||||
|
Row(RowMetadata),
|
||||||
|
Label(LabelMetadata),
|
||||||
|
Text(TextMetadata),
|
||||||
|
Choice(ChoiceMetadata),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct RowMetadata {
|
||||||
|
pub fields: Vec<Field>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LabelMetadata {
|
||||||
|
pub text: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct TextMetadata {
|
||||||
|
pub default_text: String,
|
||||||
|
pub multiline: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ChoiceType {
|
||||||
|
Dropdown,
|
||||||
|
List,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ChoiceMetadata {
|
||||||
|
pub values: Vec<String>,
|
||||||
|
pub choice_type: ChoiceType,
|
||||||
|
pub default_value: String,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form interop
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod interop {
|
||||||
|
use super::super::interop::*;
|
||||||
|
use super::types;
|
||||||
|
use std::ffi::{c_void, CString};
|
||||||
|
use std::os::raw::{c_char, c_int};
|
||||||
|
use std::ptr::null;
|
||||||
|
|
||||||
|
pub(crate) struct OwnedForm {
|
||||||
|
title: CString,
|
||||||
|
icon_path: CString,
|
||||||
|
fields: Vec<OwnedField>,
|
||||||
|
|
||||||
|
_metadata: Vec<FieldMetadata>,
|
||||||
|
_interop: Box<FormMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interoperable for OwnedForm {
|
||||||
|
fn as_ptr(&self) -> *const c_void {
|
||||||
|
&(*self._interop) as *const FormMetadata as *const c_void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::Form> for OwnedForm {
|
||||||
|
fn from(form: types::Form) -> Self {
|
||||||
|
let title = CString::new(form.title).expect("unable to convert form title to CString");
|
||||||
|
let fields: Vec<OwnedField> = form.fields.into_iter().map(|field| field.into()).collect();
|
||||||
|
|
||||||
|
let _metadata: Vec<FieldMetadata> = fields.iter().map(|field| field.metadata()).collect();
|
||||||
|
|
||||||
|
let icon_path = if let Some(icon_path) = form.icon.as_ref() {
|
||||||
|
icon_path.clone()
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon_path = CString::new(icon_path).expect("unable to convert form icon to CString");
|
||||||
|
|
||||||
|
let icon_path_ptr = if form.icon.is_some() {
|
||||||
|
icon_path.as_ptr()
|
||||||
|
} else {
|
||||||
|
std::ptr::null()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _interop = Box::new(FormMetadata {
|
||||||
|
windowTitle: title.as_ptr(),
|
||||||
|
iconPath: icon_path_ptr,
|
||||||
|
fields: _metadata.as_ptr(),
|
||||||
|
fieldSize: fields.len() as c_int,
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
icon_path,
|
||||||
|
fields,
|
||||||
|
_metadata,
|
||||||
|
_interop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OwnedField {
|
||||||
|
id: Option<CString>,
|
||||||
|
field_type: FieldType,
|
||||||
|
specific: Box<dyn Interoperable>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::Field> for OwnedField {
|
||||||
|
fn from(field: types::Field) -> Self {
|
||||||
|
let id = if let Some(id) = field.id {
|
||||||
|
Some(CString::new(id).expect("unable to create cstring for field id"))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let field_type = match field.field_type {
|
||||||
|
types::FieldType::Row(_) => FieldType_ROW,
|
||||||
|
types::FieldType::Label(_) => FieldType_LABEL,
|
||||||
|
types::FieldType::Text(_) => FieldType_TEXT,
|
||||||
|
types::FieldType::Choice(_) => FieldType_CHOICE,
|
||||||
|
types::FieldType::Unknown => panic!("unknown field type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: clean up this match
|
||||||
|
let specific: Box<dyn Interoperable> = match field.field_type {
|
||||||
|
types::FieldType::Row(metadata) => {
|
||||||
|
let owned_metadata: OwnedRowMetadata = metadata.into();
|
||||||
|
Box::new(owned_metadata)
|
||||||
|
}
|
||||||
|
types::FieldType::Label(metadata) => {
|
||||||
|
let owned_metadata: OwnedLabelMetadata = metadata.into();
|
||||||
|
Box::new(owned_metadata)
|
||||||
|
}
|
||||||
|
types::FieldType::Text(metadata) => {
|
||||||
|
let owned_metadata: OwnedTextMetadata = metadata.into();
|
||||||
|
Box::new(owned_metadata)
|
||||||
|
}
|
||||||
|
types::FieldType::Choice(metadata) => {
|
||||||
|
let owned_metadata: OwnedChoiceMetadata = metadata.into();
|
||||||
|
Box::new(owned_metadata)
|
||||||
|
}
|
||||||
|
types::FieldType::Unknown => panic!("unknown field type"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
field_type,
|
||||||
|
specific,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnedField {
|
||||||
|
pub fn metadata(&self) -> FieldMetadata {
|
||||||
|
let id_ptr = if let Some(id) = self.id.as_ref() {
|
||||||
|
id.as_ptr()
|
||||||
|
} else {
|
||||||
|
null()
|
||||||
|
};
|
||||||
|
|
||||||
|
FieldMetadata {
|
||||||
|
id: id_ptr,
|
||||||
|
fieldType: self.field_type,
|
||||||
|
specific: self.specific.as_ptr(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OwnedLabelMetadata {
|
||||||
|
text: CString,
|
||||||
|
_interop: Box<LabelMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interoperable for OwnedLabelMetadata {
|
||||||
|
fn as_ptr(&self) -> *const c_void {
|
||||||
|
&(*self._interop) as *const LabelMetadata as *const c_void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::LabelMetadata> for OwnedLabelMetadata {
|
||||||
|
fn from(label_metadata: types::LabelMetadata) -> Self {
|
||||||
|
let text =
|
||||||
|
CString::new(label_metadata.text).expect("unable to convert label text to CString");
|
||||||
|
let _interop = Box::new(LabelMetadata {
|
||||||
|
text: text.as_ptr(),
|
||||||
|
});
|
||||||
|
Self { text, _interop }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OwnedTextMetadata {
|
||||||
|
default_text: CString,
|
||||||
|
_interop: Box<TextMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interoperable for OwnedTextMetadata {
|
||||||
|
fn as_ptr(&self) -> *const c_void {
|
||||||
|
&(*self._interop) as *const TextMetadata as *const c_void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::TextMetadata> for OwnedTextMetadata {
|
||||||
|
fn from(text_metadata: types::TextMetadata) -> Self {
|
||||||
|
let default_text = CString::new(text_metadata.default_text)
|
||||||
|
.expect("unable to convert default text to CString");
|
||||||
|
let _interop = Box::new(TextMetadata {
|
||||||
|
defaultText: default_text.as_ptr(),
|
||||||
|
multiline: if text_metadata.multiline { 1 } else { 0 },
|
||||||
|
});
|
||||||
|
Self {
|
||||||
|
default_text,
|
||||||
|
_interop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OwnedChoiceMetadata {
|
||||||
|
values: Vec<CString>,
|
||||||
|
values_ptr_array: Vec<*const c_char>,
|
||||||
|
default_value: CString,
|
||||||
|
_interop: Box<ChoiceMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interoperable for OwnedChoiceMetadata {
|
||||||
|
fn as_ptr(&self) -> *const c_void {
|
||||||
|
&(*self._interop) as *const ChoiceMetadata as *const c_void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::ChoiceMetadata> for OwnedChoiceMetadata {
|
||||||
|
fn from(metadata: types::ChoiceMetadata) -> Self {
|
||||||
|
let values: Vec<CString> = metadata
|
||||||
|
.values
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| CString::new(value).expect("unable to convert choice value to string"))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let values_ptr_array: Vec<*const c_char> =
|
||||||
|
values.iter().map(|value| value.as_ptr()).collect();
|
||||||
|
|
||||||
|
let choice_type = match metadata.choice_type {
|
||||||
|
types::ChoiceType::Dropdown => ChoiceType_DROPDOWN,
|
||||||
|
types::ChoiceType::List => ChoiceType_LIST,
|
||||||
|
};
|
||||||
|
|
||||||
|
let default_value =
|
||||||
|
CString::new(metadata.default_value).expect("unable to convert default value to CString");
|
||||||
|
|
||||||
|
let _interop = Box::new(ChoiceMetadata {
|
||||||
|
values: values_ptr_array.as_ptr(),
|
||||||
|
valueSize: values.len() as c_int,
|
||||||
|
choiceType: choice_type,
|
||||||
|
defaultValue: default_value.as_ptr(),
|
||||||
|
});
|
||||||
|
Self {
|
||||||
|
values,
|
||||||
|
values_ptr_array,
|
||||||
|
default_value,
|
||||||
|
_interop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OwnedRowMetadata {
|
||||||
|
fields: Vec<OwnedField>,
|
||||||
|
|
||||||
|
_metadata: Vec<FieldMetadata>,
|
||||||
|
_interop: Box<RowMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interoperable for OwnedRowMetadata {
|
||||||
|
fn as_ptr(&self) -> *const c_void {
|
||||||
|
&(*self._interop) as *const RowMetadata as *const c_void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<types::RowMetadata> for OwnedRowMetadata {
|
||||||
|
fn from(row_metadata: types::RowMetadata) -> Self {
|
||||||
|
let fields: Vec<OwnedField> = row_metadata
|
||||||
|
.fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|field| field.into())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let _metadata: Vec<FieldMetadata> = fields.iter().map(|field| field.metadata()).collect();
|
||||||
|
|
||||||
|
let _interop = Box::new(RowMetadata {
|
||||||
|
fields: _metadata.as_ptr(),
|
||||||
|
fieldSize: _metadata.len() as c_int,
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
fields,
|
||||||
|
_metadata,
|
||||||
|
_interop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(form: types::Form) -> HashMap<String, String> {
|
||||||
|
use super::interop::*;
|
||||||
|
use std::os::raw::c_void;
|
||||||
|
|
||||||
|
let owned_form: interop::OwnedForm = form.into();
|
||||||
|
let metadata: *const FormMetadata = owned_form.as_ptr() as *const FormMetadata;
|
||||||
|
|
||||||
|
let mut value_map: HashMap<String, String> = HashMap::new();
|
||||||
|
|
||||||
|
extern "C" fn callback(values: *const ValuePair, size: c_int, map: *mut c_void) {
|
||||||
|
let values: &[ValuePair] =
|
||||||
|
unsafe { std::slice::from_raw_parts(values, size as usize) };
|
||||||
|
let map = map as *mut HashMap<String, String>;
|
||||||
|
let map = unsafe { &mut (*map) };
|
||||||
|
for pair in values.iter() {
|
||||||
|
unsafe {
|
||||||
|
let id = CStr::from_ptr(pair.id);
|
||||||
|
let value = CStr::from_ptr(pair.value);
|
||||||
|
|
||||||
|
let id = id.to_string_lossy().to_string();
|
||||||
|
let value = value.to_string_lossy().to_string();
|
||||||
|
map.insert(id, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// TODO: Nested rows should fail, add check
|
||||||
|
interop_show_form(
|
||||||
|
metadata,
|
||||||
|
callback,
|
||||||
|
&mut value_map as *mut HashMap<String, String> as *mut c_void,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
value_map
|
||||||
|
}
|
90
espanso-modulo/src/sys/interop/interop.h
Normal file
90
espanso-modulo/src/sys/interop/interop.h
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// FORM
|
||||||
|
|
||||||
|
typedef enum FieldType {
|
||||||
|
ROW,
|
||||||
|
LABEL,
|
||||||
|
TEXT,
|
||||||
|
CHOICE,
|
||||||
|
CHECKBOX,
|
||||||
|
} FieldType;
|
||||||
|
|
||||||
|
typedef struct LabelMetadata {
|
||||||
|
const char *text;
|
||||||
|
} LabelMetadata;
|
||||||
|
|
||||||
|
typedef struct TextMetadata {
|
||||||
|
const char *defaultText;
|
||||||
|
const int multiline;
|
||||||
|
} TextMetadata;
|
||||||
|
|
||||||
|
typedef enum ChoiceType {
|
||||||
|
DROPDOWN,
|
||||||
|
LIST,
|
||||||
|
} ChoiceType;
|
||||||
|
|
||||||
|
typedef struct ChoiceMetadata {
|
||||||
|
const char * const * values;
|
||||||
|
const int valueSize;
|
||||||
|
const char *defaultValue;
|
||||||
|
const ChoiceType choiceType;
|
||||||
|
} ChoiceMetadata;
|
||||||
|
|
||||||
|
typedef struct FieldMetadata {
|
||||||
|
const char * id;
|
||||||
|
FieldType fieldType;
|
||||||
|
const void * specific;
|
||||||
|
} FieldMetadata;
|
||||||
|
|
||||||
|
typedef struct RowMetadata {
|
||||||
|
const FieldMetadata *fields;
|
||||||
|
const int fieldSize;
|
||||||
|
} RowMetadata;
|
||||||
|
|
||||||
|
typedef struct FormMetadata {
|
||||||
|
const char *windowTitle;
|
||||||
|
const char *iconPath;
|
||||||
|
const FieldMetadata *fields;
|
||||||
|
const int fieldSize;
|
||||||
|
} FormMetadata;
|
||||||
|
|
||||||
|
typedef struct ValuePair {
|
||||||
|
const char *id;
|
||||||
|
const char *value;
|
||||||
|
} ValuePair;
|
||||||
|
|
||||||
|
// SEARCH
|
||||||
|
|
||||||
|
typedef struct SearchItem {
|
||||||
|
const char *id;
|
||||||
|
const char *label;
|
||||||
|
const char *trigger;
|
||||||
|
} SearchItem;
|
||||||
|
|
||||||
|
typedef struct SearchResults {
|
||||||
|
const SearchItem * items;
|
||||||
|
const int itemSize;
|
||||||
|
} SearchResults;
|
||||||
|
|
||||||
|
typedef struct SearchMetadata {
|
||||||
|
const char *windowTitle;
|
||||||
|
const char *iconPath;
|
||||||
|
} SearchMetadata;
|
133
espanso-modulo/src/sys/interop/mod.rs
Normal file
133
espanso-modulo/src/sys/interop/mod.rs
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
pub(crate) trait Interoperable {
|
||||||
|
fn as_ptr(&self) -> *const c_void;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FieldType_ROW: FieldType = 0;
|
||||||
|
pub const FieldType_LABEL: FieldType = 1;
|
||||||
|
pub const FieldType_TEXT: FieldType = 2;
|
||||||
|
pub const FieldType_CHOICE: FieldType = 3;
|
||||||
|
pub const FieldType_CHECKBOX: FieldType = 4;
|
||||||
|
pub type FieldType = i32;
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct LabelMetadata {
|
||||||
|
pub text: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct TextMetadata {
|
||||||
|
pub defaultText: *const ::std::os::raw::c_char,
|
||||||
|
pub multiline: ::std::os::raw::c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const ChoiceType_DROPDOWN: ChoiceType = 0;
|
||||||
|
pub const ChoiceType_LIST: ChoiceType = 1;
|
||||||
|
pub type ChoiceType = i32;
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct ChoiceMetadata {
|
||||||
|
pub values: *const *const ::std::os::raw::c_char,
|
||||||
|
pub valueSize: ::std::os::raw::c_int,
|
||||||
|
pub defaultValue: *const ::std::os::raw::c_char,
|
||||||
|
pub choiceType: ChoiceType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct FieldMetadata {
|
||||||
|
pub id: *const ::std::os::raw::c_char,
|
||||||
|
pub fieldType: FieldType,
|
||||||
|
pub specific: *const ::std::os::raw::c_void,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct RowMetadata {
|
||||||
|
pub fields: *const FieldMetadata,
|
||||||
|
pub fieldSize: ::std::os::raw::c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct FormMetadata {
|
||||||
|
pub windowTitle: *const ::std::os::raw::c_char,
|
||||||
|
pub iconPath: *const ::std::os::raw::c_char,
|
||||||
|
pub fields: *const FieldMetadata,
|
||||||
|
pub fieldSize: ::std::os::raw::c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct ValuePair {
|
||||||
|
pub id: *const ::std::os::raw::c_char,
|
||||||
|
pub value: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct SearchItem {
|
||||||
|
pub id: *const ::std::os::raw::c_char,
|
||||||
|
pub label: *const ::std::os::raw::c_char,
|
||||||
|
pub trigger: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct SearchResults {
|
||||||
|
pub items: *const SearchItem,
|
||||||
|
pub itemSize: ::std::os::raw::c_int,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct SearchMetadata {
|
||||||
|
pub windowTitle: *const ::std::os::raw::c_char,
|
||||||
|
pub iconPath: *const ::std::os::raw::c_char,
|
||||||
|
}
|
||||||
|
|
||||||
|
use std::os::raw::{c_char, c_int};
|
||||||
|
|
||||||
|
// Native bindings
|
||||||
|
|
||||||
|
#[allow(improper_ctypes)]
|
||||||
|
#[link(name = "espansomodulosys", kind = "static")]
|
||||||
|
extern "C" {
|
||||||
|
// FORM
|
||||||
|
pub(crate) fn interop_show_form(
|
||||||
|
metadata: *const FormMetadata,
|
||||||
|
callback: extern "C" fn(values: *const ValuePair, size: c_int, map: *mut c_void),
|
||||||
|
map: *mut c_void,
|
||||||
|
);
|
||||||
|
|
||||||
|
// SEARCH
|
||||||
|
pub(crate) fn interop_show_search(
|
||||||
|
metadata: *const SearchMetadata,
|
||||||
|
search_callback: extern "C" fn(query: *const c_char, app: *const c_void, data: *const c_void),
|
||||||
|
items: *const c_void,
|
||||||
|
result_callback: extern "C" fn(id: *const c_char, result: *mut c_void),
|
||||||
|
result: *mut c_void,
|
||||||
|
);
|
||||||
|
|
||||||
|
pub(crate) fn update_items(app: *const c_void, items: *const SearchItem, itemCount: c_int);
|
||||||
|
}
|
26
espanso-modulo/src/sys/mod.rs
Normal file
26
espanso-modulo/src/sys/mod.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub mod form;
|
||||||
|
pub mod search;
|
||||||
|
|
||||||
|
#[allow(non_upper_case_globals)]
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub mod interop;
|
191
espanso-modulo/src/sys/search/mod.rs
Normal file
191
espanso-modulo/src/sys/search/mod.rs
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::ffi::CStr;
|
||||||
|
use std::os::raw::{c_char, c_int, c_void};
|
||||||
|
|
||||||
|
pub mod types {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SearchItem {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub trigger: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Search {
|
||||||
|
pub title: String,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub items: Vec<SearchItem>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod interop {
|
||||||
|
use super::types;
|
||||||
|
use super::super::interop::*;
|
||||||
|
use std::ffi::{c_void, CString};
|
||||||
|
|
||||||
|
pub(crate) struct OwnedSearch {
|
||||||
|
title: CString,
|
||||||
|
icon_path: CString,
|
||||||
|
items: Vec<OwnedSearchItem>,
|
||||||
|
pub(crate) interop_items: Vec<SearchItem>,
|
||||||
|
_interop: Box<SearchMetadata>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interoperable for OwnedSearch {
|
||||||
|
fn as_ptr(&self) -> *const c_void {
|
||||||
|
&(*self._interop) as *const SearchMetadata as *const c_void
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&types::Search> for OwnedSearch {
|
||||||
|
fn from(search: &types::Search) -> Self {
|
||||||
|
let title =
|
||||||
|
CString::new(search.title.clone()).expect("unable to convert search title to CString");
|
||||||
|
|
||||||
|
let items: Vec<OwnedSearchItem> = search.items.iter().map(|item| item.into()).collect();
|
||||||
|
|
||||||
|
let interop_items: Vec<SearchItem> = items.iter().map(|item| item.to_search_item()).collect();
|
||||||
|
|
||||||
|
let icon_path = if let Some(icon_path) = search.icon.as_ref() {
|
||||||
|
icon_path.clone()
|
||||||
|
} else {
|
||||||
|
"".to_owned()
|
||||||
|
};
|
||||||
|
|
||||||
|
let icon_path = CString::new(icon_path).expect("unable to convert search icon to CString");
|
||||||
|
|
||||||
|
let icon_path_ptr = if search.icon.is_some() {
|
||||||
|
icon_path.as_ptr()
|
||||||
|
} else {
|
||||||
|
std::ptr::null()
|
||||||
|
};
|
||||||
|
|
||||||
|
let _interop = Box::new(SearchMetadata {
|
||||||
|
iconPath: icon_path_ptr,
|
||||||
|
windowTitle: title.as_ptr(),
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
title,
|
||||||
|
items,
|
||||||
|
icon_path,
|
||||||
|
interop_items,
|
||||||
|
_interop,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct OwnedSearchItem {
|
||||||
|
id: CString,
|
||||||
|
label: CString,
|
||||||
|
trigger: CString,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnedSearchItem {
|
||||||
|
fn to_search_item(&self) -> SearchItem {
|
||||||
|
SearchItem {
|
||||||
|
id: self.id.as_ptr(),
|
||||||
|
label: self.label.as_ptr(),
|
||||||
|
trigger: self.trigger.as_ptr(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&types::SearchItem> for OwnedSearchItem {
|
||||||
|
fn from(item: &types::SearchItem) -> Self {
|
||||||
|
let id = CString::new(item.id.clone()).expect("unable to convert item id to CString");
|
||||||
|
let label =
|
||||||
|
CString::new(item.label.clone()).expect("unable to convert item label to CString");
|
||||||
|
|
||||||
|
let trigger = if let Some(trigger) = item.trigger.as_deref() {
|
||||||
|
CString::new(trigger.to_string()).expect("unable to convert item trigger to CString")
|
||||||
|
} else {
|
||||||
|
CString::new("".to_string()).expect("unable to convert item trigger to CString")
|
||||||
|
};
|
||||||
|
|
||||||
|
Self { id, label, trigger }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SearchData {
|
||||||
|
owned_search: interop::OwnedSearch,
|
||||||
|
items: Vec<types::SearchItem>,
|
||||||
|
algorithm: Box<dyn Fn(&str, &[types::SearchItem]) -> Vec<usize>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show(
|
||||||
|
search: types::Search,
|
||||||
|
algorithm: Box<dyn Fn(&str, &[types::SearchItem]) -> Vec<usize>>,
|
||||||
|
) -> Option<String> {
|
||||||
|
use super::interop::*;
|
||||||
|
|
||||||
|
let owned_search: interop::OwnedSearch = (&search).into();
|
||||||
|
let metadata: *const SearchMetadata = owned_search.as_ptr() as *const SearchMetadata;
|
||||||
|
|
||||||
|
let search_data = SearchData {
|
||||||
|
owned_search,
|
||||||
|
items: search.items,
|
||||||
|
algorithm,
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C" fn search_callback(query: *const c_char, app: *const c_void, data: *const c_void) {
|
||||||
|
let query = unsafe { CStr::from_ptr(query) };
|
||||||
|
let query = query.to_string_lossy().to_string();
|
||||||
|
|
||||||
|
let search_data = data as *const SearchData;
|
||||||
|
let search_data = unsafe { &*search_data };
|
||||||
|
|
||||||
|
let indexes = (*search_data.algorithm)(&query, &search_data.items);
|
||||||
|
let items: Vec<SearchItem> = indexes
|
||||||
|
.into_iter()
|
||||||
|
.map(|index| search_data.owned_search.interop_items[index])
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
update_items(app, items.as_ptr(), items.len() as c_int);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result: Option<String> = None;
|
||||||
|
|
||||||
|
extern "C" fn result_callback(id: *const c_char, result: *mut c_void) {
|
||||||
|
let id = unsafe { CStr::from_ptr(id) };
|
||||||
|
let id = id.to_string_lossy().to_string();
|
||||||
|
let result: *mut Option<String> = result as *mut Option<String>;
|
||||||
|
unsafe {
|
||||||
|
*result = Some(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
interop_show_search(
|
||||||
|
metadata,
|
||||||
|
search_callback,
|
||||||
|
&search_data as *const SearchData as *const c_void,
|
||||||
|
result_callback,
|
||||||
|
&mut result as *mut Option<String> as *mut c_void,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
471
espanso-modulo/src/sys/search/search.cpp
Normal file
471
espanso-modulo/src/sys/search/search.cpp
Normal file
|
@ -0,0 +1,471 @@
|
||||||
|
/*
|
||||||
|
* This file is part of modulo.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* modulo 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.
|
||||||
|
*
|
||||||
|
* modulo 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 modulo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mouse dragging mechanism greatly inspired by: https://developpaper.com/wxwidgets-implementing-the-drag-effect-of-titleless-bar-window/
|
||||||
|
|
||||||
|
#define _UNICODE
|
||||||
|
|
||||||
|
#include "../common/common.h"
|
||||||
|
#include "../interop/interop.h"
|
||||||
|
|
||||||
|
#include "wx/htmllbox.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
// Platform-specific styles
|
||||||
|
#ifdef __WXMSW__
|
||||||
|
const int SEARCH_BAR_FONT_SIZE = 16;
|
||||||
|
const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW;
|
||||||
|
#endif
|
||||||
|
#ifdef __WXOSX__
|
||||||
|
const int SEARCH_BAR_FONT_SIZE = 20;
|
||||||
|
const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxRESIZE_BORDER;
|
||||||
|
#endif
|
||||||
|
#ifdef __LINUX__
|
||||||
|
const int SEARCH_BAR_FONT_SIZE = 20;
|
||||||
|
const long DEFAULT_STYLE = wxSTAY_ON_TOP | wxFRAME_TOOL_WINDOW | wxBORDER_NONE;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
const wxColour SELECTION_LIGHT_BG = wxColour(164, 210, 253);
|
||||||
|
const wxColour SELECTION_DARK_BG = wxColour(49, 88, 126);
|
||||||
|
|
||||||
|
// https://docs.wxwidgets.org/stable/classwx_frame.html
|
||||||
|
const int MIN_WIDTH = 500;
|
||||||
|
const int MIN_HEIGHT = 80;
|
||||||
|
|
||||||
|
typedef void (*QueryCallback)(const char *query, void *app, void *data);
|
||||||
|
typedef void (*ResultCallback)(const char *id, void *data);
|
||||||
|
|
||||||
|
SearchMetadata *searchMetadata = nullptr;
|
||||||
|
QueryCallback queryCallback = nullptr;
|
||||||
|
ResultCallback resultCallback = nullptr;
|
||||||
|
void *data = nullptr;
|
||||||
|
void *resultData = nullptr;
|
||||||
|
wxArrayString wxItems;
|
||||||
|
wxArrayString wxTriggers;
|
||||||
|
wxArrayString wxIds;
|
||||||
|
|
||||||
|
// App Code
|
||||||
|
|
||||||
|
class SearchApp : public wxApp
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
virtual bool OnInit();
|
||||||
|
};
|
||||||
|
|
||||||
|
class ResultListBox : public wxHtmlListBox
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ResultListBox() {}
|
||||||
|
ResultListBox(wxWindow *parent, bool isDark, const wxWindowID id, const wxPoint &pos, const wxSize &size);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// override this method to return data to be shown in the listbox (this is
|
||||||
|
// mandatory)
|
||||||
|
virtual wxString OnGetItem(size_t n) const;
|
||||||
|
|
||||||
|
// change the appearance by overriding these functions (this is optional)
|
||||||
|
virtual void OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const;
|
||||||
|
|
||||||
|
bool isDark;
|
||||||
|
|
||||||
|
public:
|
||||||
|
wxDECLARE_NO_COPY_CLASS(ResultListBox);
|
||||||
|
wxDECLARE_DYNAMIC_CLASS(ResultListBox);
|
||||||
|
};
|
||||||
|
|
||||||
|
wxIMPLEMENT_DYNAMIC_CLASS(ResultListBox, wxHtmlListBox);
|
||||||
|
|
||||||
|
ResultListBox::ResultListBox(wxWindow *parent, bool isDark, const wxWindowID id, const wxPoint &pos, const wxSize &size)
|
||||||
|
: wxHtmlListBox(parent, id, pos, size, 0)
|
||||||
|
{
|
||||||
|
this->isDark = isDark;
|
||||||
|
SetMargins(5, 5);
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ResultListBox::OnDrawBackground(wxDC &dc, const wxRect &rect, size_t n) const
|
||||||
|
{
|
||||||
|
if (IsSelected(n))
|
||||||
|
{
|
||||||
|
if (isDark)
|
||||||
|
{
|
||||||
|
dc.SetBrush(wxBrush(SELECTION_DARK_BG));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dc.SetBrush(wxBrush(SELECTION_LIGHT_BG));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dc.SetBrush(*wxTRANSPARENT_BRUSH);
|
||||||
|
}
|
||||||
|
dc.SetPen(*wxTRANSPARENT_PEN);
|
||||||
|
dc.DrawRectangle(0, 0, rect.GetRight(), rect.GetBottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
wxString ResultListBox::OnGetItem(size_t n) const
|
||||||
|
{
|
||||||
|
wxString textColor = isDark ? "white" : "";
|
||||||
|
wxString shortcut = (n < 8) ? wxString::Format(wxT("Alt+%i"), (int)n + 1) : " ";
|
||||||
|
return wxString::Format(wxT("<font color='%s'><table width='100%%'><tr><td>%s</td><td align='right'><b>%s</b> <font color='#636e72'> %s</font></td></tr></table></font>"), textColor, wxItems[n], wxTriggers[n], shortcut);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SearchFrame : public wxFrame
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SearchFrame(const wxString &title, const wxPoint &pos, const wxSize &size);
|
||||||
|
|
||||||
|
wxPanel *panel;
|
||||||
|
wxTextCtrl *searchBar;
|
||||||
|
wxStaticBitmap *iconPanel;
|
||||||
|
ResultListBox *resultBox;
|
||||||
|
void SetItems(SearchItem *items, int itemSize);
|
||||||
|
|
||||||
|
private:
|
||||||
|
void OnCharEvent(wxKeyEvent &event);
|
||||||
|
void OnQueryChange(wxCommandEvent &event);
|
||||||
|
void OnItemClickEvent(wxCommandEvent &event);
|
||||||
|
void OnActivate(wxActivateEvent &event);
|
||||||
|
|
||||||
|
// Mouse events
|
||||||
|
void OnMouseCaptureLost(wxMouseCaptureLostEvent &event);
|
||||||
|
void OnMouseLeave(wxMouseEvent &event);
|
||||||
|
void OnMouseMove(wxMouseEvent &event);
|
||||||
|
void OnMouseLUp(wxMouseEvent &event);
|
||||||
|
void OnMouseLDown(wxMouseEvent &event);
|
||||||
|
wxPoint mLastPt;
|
||||||
|
|
||||||
|
// Selection
|
||||||
|
void SelectNext();
|
||||||
|
void SelectPrevious();
|
||||||
|
void Submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
bool SearchApp::OnInit()
|
||||||
|
{
|
||||||
|
SearchFrame *frame = new SearchFrame(searchMetadata->windowTitle, wxPoint(50, 50), wxSize(450, 340));
|
||||||
|
frame->Show(true);
|
||||||
|
SetupWindowStyle(frame);
|
||||||
|
Activate(frame);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
SearchFrame::SearchFrame(const wxString &title, const wxPoint &pos, const wxSize &size)
|
||||||
|
: wxFrame(NULL, wxID_ANY, title, pos, size, DEFAULT_STYLE)
|
||||||
|
{
|
||||||
|
wxInitAllImageHandlers();
|
||||||
|
|
||||||
|
#if wxCHECK_VERSION(3, 1, 3)
|
||||||
|
bool isDark = wxSystemSettings::GetAppearance().IsDark();
|
||||||
|
#else
|
||||||
|
// Workaround needed for previous versions of wxWidgets
|
||||||
|
const wxColour bg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
|
||||||
|
const wxColour fg = wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
|
||||||
|
unsigned int bgSum = (bg.Red() + bg.Blue() + bg.Green());
|
||||||
|
unsigned int fgSum = (fg.Red() + fg.Blue() + fg.Green());
|
||||||
|
bool isDark = fgSum > bgSum;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
panel = new wxPanel(this, wxID_ANY);
|
||||||
|
wxBoxSizer *vbox = new wxBoxSizer(wxVERTICAL);
|
||||||
|
panel->SetSizer(vbox);
|
||||||
|
|
||||||
|
wxBoxSizer *topBox = new wxBoxSizer(wxHORIZONTAL);
|
||||||
|
|
||||||
|
int iconId = NewControlId();
|
||||||
|
iconPanel = nullptr;
|
||||||
|
if (searchMetadata->iconPath)
|
||||||
|
{
|
||||||
|
wxString iconPath = wxString(searchMetadata->iconPath);
|
||||||
|
if (wxFileExists(iconPath))
|
||||||
|
{
|
||||||
|
wxBitmap bitmap = wxBitmap(iconPath, wxBITMAP_TYPE_PNG);
|
||||||
|
if (bitmap.IsOk())
|
||||||
|
{
|
||||||
|
wxImage image = bitmap.ConvertToImage();
|
||||||
|
image.Rescale(32, 32, wxIMAGE_QUALITY_HIGH);
|
||||||
|
wxBitmap resizedBitmap = wxBitmap(image, -1);
|
||||||
|
iconPanel = new wxStaticBitmap(panel, iconId, resizedBitmap, wxDefaultPosition, wxSize(32, 32));
|
||||||
|
topBox->Add(iconPanel, 0, wxEXPAND | wxLEFT | wxUP | wxDOWN, 10);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int textId = NewControlId();
|
||||||
|
searchBar = new wxTextCtrl(panel, textId, "", wxDefaultPosition, wxDefaultSize);
|
||||||
|
wxFont font = searchBar->GetFont();
|
||||||
|
font.SetPointSize(SEARCH_BAR_FONT_SIZE);
|
||||||
|
searchBar->SetFont(font);
|
||||||
|
topBox->Add(searchBar, 1, wxEXPAND | wxALL, 10);
|
||||||
|
|
||||||
|
vbox->Add(topBox, 1, wxEXPAND);
|
||||||
|
|
||||||
|
wxArrayString choices;
|
||||||
|
int resultId = NewControlId();
|
||||||
|
resultBox = new ResultListBox(panel, isDark, resultId, wxDefaultPosition, wxSize(MIN_WIDTH, MIN_HEIGHT));
|
||||||
|
vbox->Add(resultBox, 5, wxEXPAND | wxALL, 0);
|
||||||
|
|
||||||
|
Bind(wxEVT_CHAR_HOOK, &SearchFrame::OnCharEvent, this, wxID_ANY);
|
||||||
|
Bind(wxEVT_TEXT, &SearchFrame::OnQueryChange, this, textId);
|
||||||
|
Bind(wxEVT_LISTBOX_DCLICK, &SearchFrame::OnItemClickEvent, this, resultId);
|
||||||
|
Bind(wxEVT_ACTIVATE, &SearchFrame::OnActivate, this, wxID_ANY);
|
||||||
|
|
||||||
|
// Events to handle the mouse drag
|
||||||
|
if (iconPanel)
|
||||||
|
{
|
||||||
|
iconPanel->Bind(wxEVT_LEFT_UP, &SearchFrame::OnMouseLUp, this);
|
||||||
|
iconPanel->Bind(wxEVT_LEFT_DOWN, &SearchFrame::OnMouseLDown, this);
|
||||||
|
Bind(wxEVT_MOTION, &SearchFrame::OnMouseMove, this);
|
||||||
|
Bind(wxEVT_LEFT_UP, &SearchFrame::OnMouseLUp, this);
|
||||||
|
Bind(wxEVT_LEFT_DOWN, &SearchFrame::OnMouseLDown, this);
|
||||||
|
Bind(wxEVT_MOUSE_CAPTURE_LOST, &SearchFrame::OnMouseCaptureLost, this);
|
||||||
|
Bind(wxEVT_LEAVE_WINDOW, &SearchFrame::OnMouseLeave, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
this->SetClientSize(panel->GetBestSize());
|
||||||
|
this->CentreOnScreen();
|
||||||
|
|
||||||
|
// Trigger the first data update
|
||||||
|
queryCallback("", (void *)this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnCharEvent(wxKeyEvent &event)
|
||||||
|
{
|
||||||
|
if (event.GetKeyCode() == WXK_ESCAPE)
|
||||||
|
{
|
||||||
|
Close(true);
|
||||||
|
}
|
||||||
|
else if (event.GetKeyCode() == WXK_TAB)
|
||||||
|
{
|
||||||
|
if (wxGetKeyState(WXK_SHIFT))
|
||||||
|
{
|
||||||
|
SelectPrevious();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event.GetKeyCode() >= 49 && event.GetKeyCode() <= 56)
|
||||||
|
{ // Alt + num shortcut
|
||||||
|
int index = event.GetKeyCode() - 49;
|
||||||
|
if (wxGetKeyState(WXK_ALT))
|
||||||
|
{
|
||||||
|
if (resultBox->GetItemCount() > index)
|
||||||
|
{
|
||||||
|
resultBox->SetSelection(index);
|
||||||
|
Submit();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (event.GetKeyCode() == WXK_DOWN)
|
||||||
|
{
|
||||||
|
SelectNext();
|
||||||
|
}
|
||||||
|
else if (event.GetKeyCode() == WXK_UP)
|
||||||
|
{
|
||||||
|
SelectPrevious();
|
||||||
|
}
|
||||||
|
else if (event.GetKeyCode() == WXK_RETURN)
|
||||||
|
{
|
||||||
|
Submit();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnQueryChange(wxCommandEvent &event)
|
||||||
|
{
|
||||||
|
wxString queryString = searchBar->GetValue();
|
||||||
|
const char *query = queryString.ToUTF8();
|
||||||
|
queryCallback(query, (void *)this, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnItemClickEvent(wxCommandEvent &event)
|
||||||
|
{
|
||||||
|
resultBox->SetSelection(event.GetInt());
|
||||||
|
Submit();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnActivate(wxActivateEvent &event)
|
||||||
|
{
|
||||||
|
if (!event.GetActive())
|
||||||
|
{
|
||||||
|
Close(true);
|
||||||
|
}
|
||||||
|
event.Skip();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnMouseMove(wxMouseEvent &event)
|
||||||
|
{
|
||||||
|
if (event.LeftIsDown() && event.Dragging())
|
||||||
|
{
|
||||||
|
wxPoint pt = event.GetPosition();
|
||||||
|
wxPoint wndLeftTopPt = GetPosition();
|
||||||
|
int distanceX = pt.x - mLastPt.x;
|
||||||
|
int distanceY = pt.y - mLastPt.y;
|
||||||
|
|
||||||
|
wxPoint desPt;
|
||||||
|
desPt.x = distanceX + wndLeftTopPt.x - 24;
|
||||||
|
desPt.y = distanceY + wndLeftTopPt.y - 24;
|
||||||
|
this->Move(desPt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.LeftDown())
|
||||||
|
{
|
||||||
|
this->CaptureMouse();
|
||||||
|
mLastPt = event.GetPosition();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnMouseLeave(wxMouseEvent &event)
|
||||||
|
{
|
||||||
|
if (event.LeftIsDown() && event.Dragging())
|
||||||
|
{
|
||||||
|
wxPoint pt = event.GetPosition();
|
||||||
|
wxPoint wndLeftTopPt = GetPosition();
|
||||||
|
int distanceX = pt.x - mLastPt.x;
|
||||||
|
int distanceY = pt.y - mLastPt.y;
|
||||||
|
|
||||||
|
wxPoint desPt;
|
||||||
|
desPt.x = distanceX + wndLeftTopPt.x - 24;
|
||||||
|
desPt.y = distanceY + wndLeftTopPt.y - 24;
|
||||||
|
this->Move(desPt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnMouseLDown(wxMouseEvent &event)
|
||||||
|
{
|
||||||
|
if (!HasCapture())
|
||||||
|
this->CaptureMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnMouseLUp(wxMouseEvent &event)
|
||||||
|
{
|
||||||
|
if (HasCapture())
|
||||||
|
ReleaseMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::OnMouseCaptureLost(wxMouseCaptureLostEvent &event)
|
||||||
|
{
|
||||||
|
if (HasCapture())
|
||||||
|
ReleaseMouse();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::SetItems(SearchItem *items, int itemSize)
|
||||||
|
{
|
||||||
|
wxItems.Clear();
|
||||||
|
wxIds.Clear();
|
||||||
|
wxTriggers.Clear();
|
||||||
|
|
||||||
|
for (int i = 0; i < itemSize; i++)
|
||||||
|
{
|
||||||
|
wxString item = items[i].label;
|
||||||
|
wxItems.Add(item);
|
||||||
|
|
||||||
|
wxString id = items[i].id;
|
||||||
|
wxIds.Add(id);
|
||||||
|
|
||||||
|
wxString trigger = items[i].trigger;
|
||||||
|
wxTriggers.Add(trigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox->SetItemCount(itemSize);
|
||||||
|
|
||||||
|
if (itemSize > 0)
|
||||||
|
{
|
||||||
|
resultBox->SetSelection(0);
|
||||||
|
}
|
||||||
|
resultBox->RefreshAll();
|
||||||
|
resultBox->Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::SelectNext()
|
||||||
|
{
|
||||||
|
if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND)
|
||||||
|
{
|
||||||
|
int newSelected = 0;
|
||||||
|
if (resultBox->GetSelection() < (resultBox->GetItemCount() - 1))
|
||||||
|
{
|
||||||
|
newSelected = resultBox->GetSelection() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox->SetSelection(newSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::SelectPrevious()
|
||||||
|
{
|
||||||
|
if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND)
|
||||||
|
{
|
||||||
|
int newSelected = resultBox->GetItemCount() - 1;
|
||||||
|
if (resultBox->GetSelection() > 0)
|
||||||
|
{
|
||||||
|
newSelected = resultBox->GetSelection() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
resultBox->SetSelection(newSelected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void SearchFrame::Submit()
|
||||||
|
{
|
||||||
|
if (resultBox->GetItemCount() > 0 && resultBox->GetSelection() != wxNOT_FOUND)
|
||||||
|
{
|
||||||
|
long index = resultBox->GetSelection();
|
||||||
|
wxString id = wxIds[index];
|
||||||
|
if (resultCallback)
|
||||||
|
{
|
||||||
|
resultCallback(id.ToUTF8(), resultData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void interop_show_search(SearchMetadata *_metadata, QueryCallback _queryCallback, void *_data, ResultCallback _resultCallback, void *_resultData)
|
||||||
|
{
|
||||||
|
// Setup high DPI support on Windows
|
||||||
|
#ifdef __WXMSW__
|
||||||
|
SetProcessDPIAware();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
searchMetadata = _metadata;
|
||||||
|
queryCallback = _queryCallback;
|
||||||
|
resultCallback = _resultCallback;
|
||||||
|
data = _data;
|
||||||
|
resultData = _resultData;
|
||||||
|
|
||||||
|
wxApp::SetInstance(new SearchApp());
|
||||||
|
int argc = 0;
|
||||||
|
wxEntry(argc, (char **)nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void update_items(void *app, SearchItem *items, int itemSize)
|
||||||
|
{
|
||||||
|
SearchFrame *frame = (SearchFrame *)app;
|
||||||
|
frame->SetItems(items, itemSize);
|
||||||
|
}
|
BIN
espanso-modulo/vendor/wxWidgets-3.1.5.zip
vendored
Normal file
BIN
espanso-modulo/vendor/wxWidgets-3.1.5.zip
vendored
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user