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