feat(modulo): initial port of modulo within the espanso repo

This commit is contained in:
Federico Terzi 2021-05-21 22:03:15 +02:00
parent 04720212e5
commit 23895841e3
26 changed files with 2843 additions and 0 deletions

21
espanso-modulo/Cargo.toml Normal file
View 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
View 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();
}

View 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>

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

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

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

View 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())
],]
);
}
}

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

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

View 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()
}

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

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

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

View 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
}

View 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

View 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);

View 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;
}

View 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);
}
}

View 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
}

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

View 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);
}

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

View 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
}

View 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);
}

Binary file not shown.