First draft of macOS ui layer
This commit is contained in:
parent
bee2001e28
commit
7d357149ff
|
@ -59,10 +59,7 @@ fn cc_config() {
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn cc_config() {
|
fn cc_config() {
|
||||||
println!("cargo:rustc-link-lib=dylib=c++");
|
// TODO
|
||||||
println!("cargo:rustc-link-lib=static=macbridge");
|
|
||||||
println!("cargo:rustc-link-lib=framework=Cocoa");
|
|
||||||
println!("cargo:rustc-link-lib=framework=IOKit");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
|
|
@ -16,6 +16,9 @@ thiserror = "1.0.23"
|
||||||
widestring = "0.4.3"
|
widestring = "0.4.3"
|
||||||
lazycell = "1.3.0"
|
lazycell = "1.3.0"
|
||||||
|
|
||||||
|
[target.'cfg(target_os="macos")'.dependencies]
|
||||||
|
lazycell = "1.3.0"
|
||||||
|
|
||||||
[target.'cfg(target_os="linux")'.dependencies]
|
[target.'cfg(target_os="linux")'.dependencies]
|
||||||
notify-rust = "4.2.2"
|
notify-rust = "4.2.2"
|
||||||
|
|
||||||
|
|
|
@ -40,17 +40,24 @@ fn cc_config() {
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn cc_config() {
|
fn cc_config() {
|
||||||
// println!("cargo:rustc-link-search=native=/usr/lib/x86_64-linux-gnu/");
|
// Nothing to link on linux
|
||||||
// println!("cargo:rustc-link-lib=static=linuxbridge");
|
|
||||||
// println!("cargo:rustc-link-lib=dylib=X11");
|
|
||||||
// println!("cargo:rustc-link-lib=dylib=Xtst");
|
|
||||||
// println!("cargo:rustc-link-lib=dylib=xdo");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
fn cc_config() {
|
fn cc_config() {
|
||||||
|
println!("cargo:rerun-if-changed=src/mac/native.mm");
|
||||||
|
println!("cargo:rerun-if-changed=src/mac/native.h");
|
||||||
|
println!("cargo:rerun-if-changed=src/mac/AppDelegate.mm");
|
||||||
|
println!("cargo:rerun-if-changed=src/mac/AppDelegate.h");
|
||||||
|
cc::Build::new()
|
||||||
|
.cpp(true)
|
||||||
|
.include("src/mac/native.h")
|
||||||
|
.include("src/mac/AppDelegate.h")
|
||||||
|
.file("src/mac/native.mm")
|
||||||
|
.file("src/mac/AppDelegate.mm")
|
||||||
|
.compile("espansoui");
|
||||||
println!("cargo:rustc-link-lib=dylib=c++");
|
println!("cargo:rustc-link-lib=dylib=c++");
|
||||||
println!("cargo:rustc-link-lib=static=macbridge");
|
println!("cargo:rustc-link-lib=static=espansoui");
|
||||||
println!("cargo:rustc-link-lib=framework=Cocoa");
|
println!("cargo:rustc-link-lib=framework=Cocoa");
|
||||||
println!("cargo:rustc-link-lib=framework=IOKit");
|
println!("cargo:rustc-link-lib=framework=IOKit");
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,3 +7,6 @@ pub mod win32;
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub mod linux;
|
pub mod linux;
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub mod mac;
|
39
espanso-ui/src/mac/AppDelegate.h
Normal file
39
espanso-ui/src/mac/AppDelegate.h
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import <AppKit/AppKit.h>
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
|
||||||
|
#include "native.h"
|
||||||
|
|
||||||
|
@interface AppDelegate : NSObject <NSApplicationDelegate, NSUserNotificationCenterDelegate> {
|
||||||
|
@public NSStatusItem *statusItem;
|
||||||
|
@public UIOptions options;
|
||||||
|
@public void *rust_instance;
|
||||||
|
@public EventCallback event_callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification;
|
||||||
|
- (void) setIcon: (int32_t) iconIndex;
|
||||||
|
- (void) popupMenu: (NSString *) payload;
|
||||||
|
- (void) showNotification: (NSString *) message withDelay:(double) delay;
|
||||||
|
- (IBAction) statusIconClick: (id) sender;
|
||||||
|
- (IBAction) contextMenuClick: (id) sender;
|
||||||
|
|
||||||
|
@end
|
134
espanso-ui/src/mac/AppDelegate.mm
Normal file
134
espanso-ui/src/mac/AppDelegate.mm
Normal file
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#import "AppDelegate.h"
|
||||||
|
|
||||||
|
void addSeparatorMenu(NSMenu * parent);
|
||||||
|
void addSingleMenu(NSMenu * parent, id item);
|
||||||
|
void addSubMenu(NSMenu * parent, NSArray * items);
|
||||||
|
|
||||||
|
@implementation AppDelegate
|
||||||
|
|
||||||
|
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
|
||||||
|
{
|
||||||
|
if (options.show_icon) {
|
||||||
|
statusItem = [[[NSStatusBar systemStatusBar] statusItemWithLength:NSSquareStatusItemLength] retain];
|
||||||
|
[self setIcon: 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
[[NSUserNotificationCenter defaultUserNotificationCenter] setDelegate:self];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) setIcon: (int32_t)iconIndex {
|
||||||
|
if (options.show_icon) {
|
||||||
|
char * iconPath = options.icon_paths[iconIndex];
|
||||||
|
NSString *nsIconPath = [NSString stringWithUTF8String:iconPath];
|
||||||
|
|
||||||
|
NSImage *statusImage = [[NSImage alloc] initWithContentsOfFile:nsIconPath];
|
||||||
|
[statusImage setTemplate:YES];
|
||||||
|
|
||||||
|
[statusItem.button setImage:statusImage];
|
||||||
|
[statusItem setHighlightMode:YES];
|
||||||
|
[statusItem.button setAction:@selector(statusIconClick:)];
|
||||||
|
[statusItem.button setTarget:self];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction) statusIconClick: (id) sender {
|
||||||
|
UIEvent event = {};
|
||||||
|
event.event_type = UI_EVENT_TYPE_ICON_CLICK;
|
||||||
|
if (event_callback && rust_instance) {
|
||||||
|
event_callback(rust_instance, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) popupMenu: (NSString *) payload {
|
||||||
|
NSError *jsonError;
|
||||||
|
NSData *data = [payload dataUsingEncoding:NSUTF8StringEncoding];
|
||||||
|
NSArray *jsonMenuItems = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&jsonError];
|
||||||
|
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"Espanso"];
|
||||||
|
addSubMenu(menu, jsonMenuItems);
|
||||||
|
[statusItem popUpStatusItemMenu: menu];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (IBAction) contextMenuClick: (id) sender {
|
||||||
|
NSInteger itemId = [[sender valueForKey:@"tag"] integerValue];
|
||||||
|
|
||||||
|
UIEvent event = {};
|
||||||
|
event.event_type = UI_EVENT_TYPE_CONTEXT_MENU_CLICK;
|
||||||
|
event.context_menu_id = (uint32_t) [itemId intValue];
|
||||||
|
if (event_callback && rust_instance) {
|
||||||
|
event_callback(rust_instance, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) showNotification: (NSString *) message withDelay: (double) delay {
|
||||||
|
NSUserNotification *notification = [[NSUserNotification alloc] init];
|
||||||
|
notification.title = @"Espanso";
|
||||||
|
notification.informativeText = message;
|
||||||
|
notification.soundName = nil;
|
||||||
|
|
||||||
|
[[NSUserNotificationCenter defaultUserNotificationCenter] deliverNotification:notification];
|
||||||
|
[[NSUserNotificationCenter defaultUserNotificationCenter] performSelector:@selector(removeDeliveredNotification:) withObject:notification afterDelay:delay];
|
||||||
|
}
|
||||||
|
|
||||||
|
@end
|
||||||
|
|
||||||
|
// Menu utility methods
|
||||||
|
|
||||||
|
void addSeparatorMenu(NSMenu * parent)
|
||||||
|
{
|
||||||
|
[parent addItem: [NSMenuItem separatorItem]];
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSingleMenu(NSMenu * parent, id item)
|
||||||
|
{
|
||||||
|
id label = [item objectForKey:@"label"];
|
||||||
|
id raw_id = [item objectForKey:@"raw_id"];
|
||||||
|
if (label == nil || raw_id == nil)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
NSMenuItem *newMenu = [[NSMenuItem alloc] initWithTitle:label action:@selector(contextMenuClick:) keyEquivalent:@""];
|
||||||
|
[newMenu setTag:(NSInteger)raw_id];
|
||||||
|
[parent addItem: newMenu];
|
||||||
|
}
|
||||||
|
|
||||||
|
void addSubMenu(NSMenu * parent, NSArray * items)
|
||||||
|
{
|
||||||
|
for (id item in items) {
|
||||||
|
id type = [item objectForKey:@"type"];
|
||||||
|
if ([type isEqualToString:@"simple"])
|
||||||
|
{
|
||||||
|
addSingleMenu(parent, item);
|
||||||
|
}
|
||||||
|
else if ([type isEqualToString:@"separator"])
|
||||||
|
{
|
||||||
|
addSeparatorMenu(parent);
|
||||||
|
}
|
||||||
|
else if ([type isEqualToString:@"sub"])
|
||||||
|
{
|
||||||
|
NSMenuItem *menuItem = [[NSMenuItem alloc] initWithTitle:[item objectForKey:@"label"] action:nil keyEquivalent:@""];
|
||||||
|
NSMenu *subMenu = [[NSMenu alloc] init];
|
||||||
|
[parent addItem: menuItem];
|
||||||
|
addSubMenu(subMenu, [item objectForKey:@"items"]);
|
||||||
|
[menuItem setSubmenu: subMenu];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
241
espanso-ui/src/mac/mod.rs
Normal file
241
espanso-ui/src/mac/mod.rs
Normal file
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* This file is part of espanso.
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019-2021 Federico Terzi
|
||||||
|
*
|
||||||
|
* espanso is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* espanso is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use std::{cmp::min, collections::HashMap, ffi::{CString}, os::raw::c_char, thread::ThreadId};
|
||||||
|
|
||||||
|
use lazycell::LazyCell;
|
||||||
|
use log::{error, trace};
|
||||||
|
|
||||||
|
use crate::{event::UIEvent, icons::TrayIcon, menu::Menu};
|
||||||
|
|
||||||
|
// IMPORTANT: if you change these, also edit the native.h file.
|
||||||
|
const MAX_FILE_PATH: usize = 1024;
|
||||||
|
const MAX_ICON_COUNT: usize = 3;
|
||||||
|
|
||||||
|
const UI_EVENT_TYPE_ICON_CLICK: i32 = 1;
|
||||||
|
const UI_EVENT_TYPE_CONTEXT_MENU_CLICK: i32 = 2;
|
||||||
|
|
||||||
|
// Take a look at the native.h header file for an explanation of the fields
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct RawUIOptions {
|
||||||
|
pub show_icon: i32,
|
||||||
|
|
||||||
|
pub icon_paths: [[u8; MAX_FILE_PATH]; MAX_ICON_COUNT],
|
||||||
|
pub icon_paths_count: i32,
|
||||||
|
}
|
||||||
|
// Take a look at the native.h header file for an explanation of the fields
|
||||||
|
#[repr(C)]
|
||||||
|
pub struct RawUIEvent {
|
||||||
|
pub event_type: i32,
|
||||||
|
pub context_menu_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(improper_ctypes)]
|
||||||
|
#[link(name = "espansoui", kind = "static")]
|
||||||
|
extern "C" {
|
||||||
|
pub fn ui_initialize(
|
||||||
|
_self: *const MacEventLoop,
|
||||||
|
options: RawUIOptions,
|
||||||
|
);
|
||||||
|
pub fn ui_eventloop(
|
||||||
|
event_callback: extern "C" fn(_self: *mut MacEventLoop, event: RawUIEvent),
|
||||||
|
) -> i32;
|
||||||
|
pub fn ui_update_tray_icon(index: i32);
|
||||||
|
pub fn ui_show_notification(message: *const c_char, delay: f64);
|
||||||
|
pub fn ui_show_context_menu(payload: *const c_char);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MacUIOptions<'a> {
|
||||||
|
pub show_icon: bool,
|
||||||
|
pub icon_paths: &'a Vec<(TrayIcon, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create(options: MacUIOptions) -> (MacRemote, MacEventLoop) {
|
||||||
|
// Validate icons
|
||||||
|
if options.icon_paths.len() > MAX_ICON_COUNT {
|
||||||
|
panic!("MacOS UI received too many icon paths, please increase the MAX_ICON_COUNT constant to support more");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the icon paths to the internal representation
|
||||||
|
let mut icon_indexes: HashMap<TrayIcon, usize> = HashMap::new();
|
||||||
|
let mut icons = Vec::new();
|
||||||
|
for (index, (tray_icon, path)) in options.icon_paths.iter().enumerate() {
|
||||||
|
icon_indexes.insert(tray_icon.clone(), index);
|
||||||
|
icons.push(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let eventloop = MacEventLoop::new(
|
||||||
|
icons,
|
||||||
|
options.show_icon,
|
||||||
|
);
|
||||||
|
let remote = MacRemote::new(icon_indexes);
|
||||||
|
|
||||||
|
(remote, eventloop)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type MacUIEventCallback = Box<dyn Fn(UIEvent)>;
|
||||||
|
|
||||||
|
pub struct MacEventLoop {
|
||||||
|
show_icon: bool,
|
||||||
|
icons: Vec<String>,
|
||||||
|
|
||||||
|
// Internal
|
||||||
|
_event_callback: LazyCell<MacUIEventCallback>,
|
||||||
|
_init_thread_id: LazyCell<ThreadId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MacEventLoop {
|
||||||
|
pub(crate) fn new(
|
||||||
|
icons: Vec<String>,
|
||||||
|
show_icon: bool,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
icons,
|
||||||
|
show_icon,
|
||||||
|
_event_callback: LazyCell::new(),
|
||||||
|
_init_thread_id: LazyCell::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize(&mut self) {
|
||||||
|
// Convert the icon paths to the raw representation
|
||||||
|
let mut icon_paths: [[u8; MAX_FILE_PATH]; MAX_ICON_COUNT] =
|
||||||
|
[[0; MAX_FILE_PATH]; MAX_ICON_COUNT];
|
||||||
|
for (i, icon_path) in icon_paths.iter_mut().enumerate().take(self.icons.len()) {
|
||||||
|
let c_path = CString::new(self.icons[i].clone()).expect("unable to create CString for UI tray icon path");
|
||||||
|
let len = min(c_path.as_bytes().len(), MAX_FILE_PATH - 1);
|
||||||
|
icon_path[0..len].clone_from_slice(&c_path.as_bytes()[..len]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = RawUIOptions {
|
||||||
|
show_icon: if self.show_icon { 1 } else { 0 },
|
||||||
|
icon_paths,
|
||||||
|
icon_paths_count: self.icons.len() as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe { ui_initialize(self as *const MacEventLoop, options) };
|
||||||
|
|
||||||
|
// Make sure the run() method is called in the same thread as initialize()
|
||||||
|
self
|
||||||
|
._init_thread_id
|
||||||
|
.fill(std::thread::current().id())
|
||||||
|
.expect("Unable to set initialization thread id");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(&self, event_callback: MacUIEventCallback) {
|
||||||
|
// Make sure the run() method is called in the same thread as initialize()
|
||||||
|
if let Some(init_id) = self._init_thread_id.borrow() {
|
||||||
|
if init_id != &std::thread::current().id() {
|
||||||
|
panic!("MacEventLoop run() and initialize() methods should be called in the same thread");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._event_callback.fill(event_callback).is_err() {
|
||||||
|
panic!("Unable to set MacEventLoop callback");
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" fn callback(_self: *mut MacEventLoop, event: RawUIEvent) {
|
||||||
|
if let Some(callback) = unsafe { (*_self)._event_callback.borrow() } {
|
||||||
|
let event: Option<UIEvent> = event.into();
|
||||||
|
if let Some(event) = event {
|
||||||
|
callback(event)
|
||||||
|
} else {
|
||||||
|
trace!("Unable to convert raw event to input event");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let error_code = unsafe { ui_eventloop(callback) };
|
||||||
|
|
||||||
|
if error_code <= 0 {
|
||||||
|
panic!("MacEventLoop exited with <= 0 code")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MacRemote {
|
||||||
|
// Maps icon name to their index
|
||||||
|
icon_indexes: HashMap<TrayIcon, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MacRemote {
|
||||||
|
pub(crate) fn new(
|
||||||
|
icon_indexes: HashMap<TrayIcon, usize>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
icon_indexes,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_tray_icon(&self, icon: TrayIcon) {
|
||||||
|
if let Some(index) = self.icon_indexes.get(&icon) {
|
||||||
|
unsafe { ui_update_tray_icon((*index) as i32) }
|
||||||
|
} else {
|
||||||
|
error!("Unable to update tray icon, invalid icon id");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_notification(&self, message: &str) {
|
||||||
|
let c_string = CString::new(message);
|
||||||
|
match c_string {
|
||||||
|
Ok(message) => unsafe { ui_show_notification(message.as_ptr(), 3.0) },
|
||||||
|
Err(error) => {
|
||||||
|
error!(
|
||||||
|
"Unable to show notification {}",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_context_menu(&self, menu: &Menu) {
|
||||||
|
match menu.to_json() {
|
||||||
|
Ok(payload) => {
|
||||||
|
let c_string = CString::new(payload);
|
||||||
|
match c_string {
|
||||||
|
Ok(c_string) => unsafe { ui_show_context_menu(c_string.as_ptr()) },
|
||||||
|
Err(error) => error!(
|
||||||
|
"Unable to show context menu, impossible to convert payload to c_string: {}",
|
||||||
|
error
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
error!("Unable to show context menu, {}", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::single_match)] // TODO: remove after another match is used
|
||||||
|
impl From<RawUIEvent> for Option<UIEvent> {
|
||||||
|
fn from(raw: RawUIEvent) -> Option<UIEvent> {
|
||||||
|
match raw.event_type {
|
||||||
|
UI_EVENT_TYPE_ICON_CLICK => {
|
||||||
|
return Some(UIEvent::TrayIconClick);
|
||||||
|
}
|
||||||
|
UI_EVENT_TYPE_CONTEXT_MENU_CLICK => {
|
||||||
|
return Some(UIEvent::ContextMenuClick(raw.context_menu_id));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
73
espanso-ui/src/mac/native.h
Normal file
73
espanso-ui/src/mac/native.h
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef ESPANSO_UI_H
|
||||||
|
#define ESPANSO_UI_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
// Explicitly define this constant as we need to use it from the Rust side
|
||||||
|
#define MAX_FILE_PATH 1024
|
||||||
|
#define MAX_ICON_COUNT 3
|
||||||
|
|
||||||
|
#define UI_EVENT_TYPE_ICON_CLICK 1
|
||||||
|
#define UI_EVENT_TYPE_CONTEXT_MENU_CLICK 2
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t show_icon;
|
||||||
|
|
||||||
|
char icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH];
|
||||||
|
int32_t icon_paths_count;
|
||||||
|
} UIOptions;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int32_t event_type;
|
||||||
|
uint32_t context_menu_id;
|
||||||
|
} UIEvent;
|
||||||
|
|
||||||
|
typedef void (*EventCallback)(void * self, UIEvent data);
|
||||||
|
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
UIOptions options;
|
||||||
|
|
||||||
|
// Rust interop
|
||||||
|
void *rust_instance;
|
||||||
|
EventCallback event_callback;
|
||||||
|
} UIVariables;
|
||||||
|
|
||||||
|
// Initialize the Application delegate.
|
||||||
|
extern "C" void ui_initialize(void * self, UIOptions options);
|
||||||
|
|
||||||
|
// Run the event loop. Blocking call.
|
||||||
|
extern "C" int32_t ui_eventloop(EventCallback callback);
|
||||||
|
|
||||||
|
// Updates the tray icon to the given one. The method accepts an index that refers to
|
||||||
|
// the icon within the UIOptions.icon_paths array.
|
||||||
|
extern "C" void ui_update_tray_icon(int32_t index);
|
||||||
|
|
||||||
|
// Show a native notification
|
||||||
|
extern "C" void ui_show_notification(char * message, double delay);
|
||||||
|
|
||||||
|
// Display the context menu on the tray icon.
|
||||||
|
// Payload is passed as JSON as given the complex structure, parsing
|
||||||
|
// this manually would have been complex.
|
||||||
|
extern "C" void ui_show_context_menu(char * payload);
|
||||||
|
|
||||||
|
#endif //ESPANSO_UI_H
|
72
espanso-ui/src/mac/native.mm
Normal file
72
espanso-ui/src/mac/native.mm
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "native.h"
|
||||||
|
#include "AppDelegate.h"
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#include <IOKit/IOKitLib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <libproc.h>
|
||||||
|
|
||||||
|
void ui_initialize(void *_self, UIOptions _options)
|
||||||
|
{
|
||||||
|
AppDelegate *delegate = [[AppDelegate alloc] init];
|
||||||
|
delegate->options = _options;
|
||||||
|
delegate->rust_instance = _self;
|
||||||
|
|
||||||
|
NSApplication * application = [NSApplication sharedApplication];
|
||||||
|
[application setDelegate:delegate];
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t ui_eventloop(EventCallback _callback)
|
||||||
|
{
|
||||||
|
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
|
||||||
|
delegate->event_callback = _callback;
|
||||||
|
|
||||||
|
[NSApp run];
|
||||||
|
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_update_tray_icon(int32_t index)
|
||||||
|
{
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
|
||||||
|
[delegate setIcon: index];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_show_notification(char *message, double delay)
|
||||||
|
{
|
||||||
|
NSString *nsMessage = [NSString stringWithUTF8String:message];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
|
||||||
|
[delegate showNotification: nsMessage withDelay: delay];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void ui_show_context_menu(char *payload)
|
||||||
|
{
|
||||||
|
NSString *nsPayload = [NSString stringWithUTF8String:payload];
|
||||||
|
dispatch_async(dispatch_get_main_queue(), ^(void) {
|
||||||
|
AppDelegate *delegate = [[NSApplication sharedApplication] delegate];
|
||||||
|
[delegate popupMenu: nsPayload];
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
use espanso_detect::event::{InputEvent, Status};
|
use espanso_detect::event::{InputEvent, Status};
|
||||||
use espanso_ui::{linux::LinuxUIOptions, menu::*};
|
use espanso_ui::{icons::TrayIcon, mac::*, menu::*, event::UIEvent::*};
|
||||||
use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
|
use simplelog::{CombinedLogger, Config, LevelFilter, TermLogger, TerminalMode};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
@ -14,14 +14,24 @@ fn main() {
|
||||||
])
|
])
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
// let icon_paths = vec![
|
||||||
|
// (
|
||||||
|
// espanso_ui::icons::TrayIcon::Normal,
|
||||||
|
// r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(),
|
||||||
|
// ),
|
||||||
|
// (
|
||||||
|
// espanso_ui::icons::TrayIcon::Disabled,
|
||||||
|
// r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(),
|
||||||
|
// ),
|
||||||
|
// ];
|
||||||
let icon_paths = vec![
|
let icon_paths = vec![
|
||||||
(
|
(
|
||||||
espanso_ui::icons::TrayIcon::Normal,
|
espanso_ui::icons::TrayIcon::Normal,
|
||||||
r"C:\Users\Freddy\AppData\Local\espanso\espanso.ico".to_string(),
|
r"/Users/freddy/Library/Application Support/espanso/icon.png".to_string(),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
espanso_ui::icons::TrayIcon::Disabled,
|
espanso_ui::icons::TrayIcon::Disabled,
|
||||||
r"C:\Users\Freddy\AppData\Local\espanso\espansored.ico".to_string(),
|
r"/Users/freddy/Library/Application Support/espanso/icondisabled.png".to_string(),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -32,44 +42,55 @@ fn main() {
|
||||||
// notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
|
// notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png"
|
||||||
// .to_string(),
|
// .to_string(),
|
||||||
// });
|
// });
|
||||||
let (remote, mut eventloop) = espanso_ui::linux::create(LinuxUIOptions {
|
let (remote, mut eventloop) = espanso_ui::mac::create(MacUIOptions {
|
||||||
notification_icon_path: r"/home/freddy/insync/Development/Espanso/Images/icongreensmall.png".to_owned(),
|
show_icon: true,
|
||||||
|
icon_paths: &icon_paths,
|
||||||
});
|
});
|
||||||
|
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || {
|
||||||
|
|
||||||
//let mut source = espanso_detect::win32::Win32Source::new();
|
//let mut source = espanso_detect::win32::Win32Source::new();
|
||||||
let mut source = espanso_detect::evdev::EVDEVSource::new();
|
//let mut source = espanso_detect::x11::X11Source::new();
|
||||||
source.initialize();
|
// source.initialize();
|
||||||
source.eventloop(Box::new(move |event: InputEvent| {
|
// source.eventloop(Box::new(move |event: InputEvent| {
|
||||||
println!("ev {:?}", event);
|
// println!("ev {:?}", event);
|
||||||
match event {
|
// match event {
|
||||||
InputEvent::Mouse(_) => {}
|
// InputEvent::Mouse(_) => {}
|
||||||
InputEvent::Keyboard(evt) => {
|
// InputEvent::Keyboard(evt) => {
|
||||||
if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
|
// if evt.key == espanso_detect::event::Key::Shift && evt.status == Status::Pressed {
|
||||||
//remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
|
// //remote.update_tray_icon(espanso_ui::icons::TrayIcon::Disabled);
|
||||||
remote.show_notification("Espanso is running!");
|
// remote.show_notification("Espanso is running!");
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}));
|
// }));
|
||||||
});
|
});
|
||||||
|
|
||||||
// eventloop.initialize();
|
eventloop.initialize();
|
||||||
// eventloop.run(Box::new(move |event| {
|
eventloop.run(Box::new(move |event| {
|
||||||
// println!("ui {:?}", event);
|
println!("ui {:?}", event);
|
||||||
// let menu = Menu::from(vec![
|
let menu = Menu::from(vec![
|
||||||
// MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
|
MenuItem::Simple(SimpleMenuItem::new("open", "Open")),
|
||||||
// MenuItem::Separator,
|
MenuItem::Separator,
|
||||||
// MenuItem::Sub(SubMenuItem::new(
|
MenuItem::Sub(SubMenuItem::new(
|
||||||
// "Sub",
|
"Sub",
|
||||||
// vec![
|
vec![
|
||||||
// MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
|
MenuItem::Simple(SimpleMenuItem::new("sub1", "Sub 1")),
|
||||||
// MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
|
MenuItem::Simple(SimpleMenuItem::new("sub2", "Sub 2")),
|
||||||
// ],
|
],
|
||||||
// )),
|
)),
|
||||||
// ])
|
])
|
||||||
// .unwrap();
|
.unwrap();
|
||||||
// remote.show_context_menu(&menu);
|
match event {
|
||||||
// }))
|
TrayIconClick => {
|
||||||
eventloop.run();
|
remote.show_context_menu(&menu);
|
||||||
|
}
|
||||||
|
ContextMenuClick(raw_id) => {
|
||||||
|
//remote.update_tray_icon(TrayIcon::Disabled);
|
||||||
|
remote.show_notification("Hello there!");
|
||||||
|
println!("item {:?}", menu.get_item_id(raw_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,7 +38,7 @@ fn main() {
|
||||||
|
|
||||||
let handle = std::thread::spawn(move || {
|
let handle = std::thread::spawn(move || {
|
||||||
//let mut source = espanso_detect::win32::Win32Source::new();
|
//let mut source = espanso_detect::win32::Win32Source::new();
|
||||||
let mut source = espanso_detect::x11::X11Source::new();
|
let mut source = espanso_detect::evdev::EVDEVSource::new();
|
||||||
source.initialize();
|
source.initialize();
|
||||||
source.eventloop(Box::new(move |event: InputEvent| {
|
source.eventloop(Box::new(move |event: InputEvent| {
|
||||||
println!("ev {:?}", event);
|
println!("ev {:?}", event);
|
Loading…
Reference in New Issue
Block a user