Add native windows notifications to espanso-ui package

This commit is contained in:
Federico Terzi 2021-01-30 21:20:09 +01:00
parent 4c213db5fb
commit 567c35eb0e
8 changed files with 1518 additions and 50 deletions

View File

@ -24,7 +24,9 @@ fn cc_config() {
cc::Build::new() cc::Build::new()
.cpp(true) .cpp(true)
.include("src/win32/native.h") .include("src/win32/native.h")
.include("src/win32/WinToast/wintoastlib.h")
.file("src/win32/native.cpp") .file("src/win32/native.cpp")
.file("src/win32/WinToast/wintoastlib.cpp")
.compile("espansoui"); .compile("espansoui");
println!("cargo:rustc-link-lib=static=espansoui"); println!("cargo:rustc-link-lib=static=espansoui");

View File

@ -0,0 +1,3 @@
In order to support native notifications on Windows, espanso wraps
WinToast, a great and lightweight C++ library.
More info: https://github.com/mohabouje/WinToast

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,217 @@
/* * Copyright (C) 2016-2019 Mohammed Boujemaoui <mohabouje@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WINTOASTLIB_H
#define WINTOASTLIB_H
#include <Windows.h>
#include <sdkddkver.h>
#include <WinUser.h>
#include <ShObjIdl.h>
#include <wrl/implements.h>
#include <wrl/event.h>
#include <windows.ui.notifications.h>
#include <strsafe.h>
#include <Psapi.h>
#include <ShlObj.h>
#include <roapi.h>
#include <propvarutil.h>
#include <functiondiscoverykeys.h>
#include <iostream>
#include <winstring.h>
#include <string.h>
#include <vector>
#include <map>
using namespace Microsoft::WRL;
using namespace ABI::Windows::Data::Xml::Dom;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::UI::Notifications;
using namespace Windows::Foundation;
namespace WinToastLib {
class IWinToastHandler {
public:
enum WinToastDismissalReason {
UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled,
ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden,
TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut
};
virtual ~IWinToastHandler() = default;
virtual void toastActivated() const = 0;
virtual void toastActivated(int actionIndex) const = 0;
virtual void toastDismissed(WinToastDismissalReason state) const = 0;
virtual void toastFailed() const = 0;
};
class WinToastTemplate {
public:
enum Duration { System, Short, Long };
enum AudioOption { Default = 0, Silent, Loop };
enum TextField { FirstLine = 0, SecondLine, ThirdLine };
enum WinToastTemplateType {
ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01,
ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02,
ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03,
ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04,
Text01 = ToastTemplateType::ToastTemplateType_ToastText01,
Text02 = ToastTemplateType::ToastTemplateType_ToastText02,
Text03 = ToastTemplateType::ToastTemplateType_ToastText03,
Text04 = ToastTemplateType::ToastTemplateType_ToastText04,
};
enum AudioSystemFile {
DefaultSound,
IM,
Mail,
Reminder,
SMS,
Alarm,
Alarm2,
Alarm3,
Alarm4,
Alarm5,
Alarm6,
Alarm7,
Alarm8,
Alarm9,
Alarm10,
Call,
Call1,
Call2,
Call3,
Call4,
Call5,
Call6,
Call7,
Call8,
Call9,
Call10,
};
WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02);
~WinToastTemplate();
void setFirstLine(_In_ const std::wstring& text);
void setSecondLine(_In_ const std::wstring& text);
void setThirdLine(_In_ const std::wstring& text);
void setTextField(_In_ const std::wstring& txt, _In_ TextField pos);
void setAttributionText(_In_ const std::wstring & attributionText);
void setImagePath(_In_ const std::wstring& imgPath);
void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio);
void setAudioPath(_In_ const std::wstring& audioPath);
void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption);
void setDuration(_In_ Duration duration);
void setExpiration(_In_ INT64 millisecondsFromNow);
void addAction(_In_ const std::wstring& label);
std::size_t textFieldsCount() const;
std::size_t actionsCount() const;
bool hasImage() const;
const std::vector<std::wstring>& textFields() const;
const std::wstring& textField(_In_ TextField pos) const;
const std::wstring& actionLabel(_In_ std::size_t pos) const;
const std::wstring& imagePath() const;
const std::wstring& audioPath() const;
const std::wstring& attributionText() const;
INT64 expiration() const;
WinToastTemplateType type() const;
WinToastTemplate::AudioOption audioOption() const;
Duration duration() const;
private:
std::vector<std::wstring> _textFields{};
std::vector<std::wstring> _actions{};
std::wstring _imagePath{};
std::wstring _audioPath{};
std::wstring _attributionText{};
INT64 _expiration{0};
AudioOption _audioOption{WinToastTemplate::AudioOption::Default};
WinToastTemplateType _type{WinToastTemplateType::Text01};
Duration _duration{Duration::System};
};
class WinToast {
public:
enum WinToastError {
NoError = 0,
NotInitialized,
SystemNotSupported,
ShellLinkNotCreated,
InvalidAppUserModelID,
InvalidParameters,
InvalidHandler,
NotDisplayed,
UnknownError
};
enum ShortcutResult {
SHORTCUT_UNCHANGED = 0,
SHORTCUT_WAS_CHANGED = 1,
SHORTCUT_WAS_CREATED = 2,
SHORTCUT_MISSING_PARAMETERS = -1,
SHORTCUT_INCOMPATIBLE_OS = -2,
SHORTCUT_COM_INIT_FAILURE = -3,
SHORTCUT_CREATE_FAILED = -4
};
WinToast(void);
virtual ~WinToast();
static WinToast* instance();
static bool isCompatible();
static bool isSupportingModernFeatures();
static std::wstring configureAUMI(_In_ const std::wstring& companyName,
_In_ const std::wstring& productName,
_In_ const std::wstring& subProduct = std::wstring(),
_In_ const std::wstring& versionInformation = std::wstring());
static const std::wstring& strerror(_In_ WinToastError error);
virtual bool initialize(_Out_ WinToastError* error = nullptr);
virtual bool isInitialized() const;
virtual bool hideToast(_In_ INT64 id);
virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error = nullptr);
virtual void clear();
virtual enum ShortcutResult createShortcut();
const std::wstring& appName() const;
const std::wstring& appUserModelId() const;
void setAppUserModelId(_In_ const std::wstring& aumi);
void setAppName(_In_ const std::wstring& appName);
protected:
bool _isInitialized{false};
bool _hasCoInitialized{false};
std::wstring _appName{};
std::wstring _aumi{};
std::map<INT64, ComPtr<IToastNotification>> _buffer{};
HRESULT validateShellLinkHelper(_Out_ bool& wasChanged);
HRESULT createShellLinkHelper();
HRESULT setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path);
HRESULT setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default);
HRESULT setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos);
HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text);
HRESULT addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& action, _In_ const std::wstring& arguments);
HRESULT addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration);
ComPtr<IToastNotifier> notifier(_In_ bool* succeded) const;
void setError(_Out_ WinToastError* error, _In_ WinToastError value);
};
}
#endif // WINTOASTLIB_H

View File

@ -17,20 +17,14 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::{ use std::{cmp::min, collections::HashMap, ffi::c_void, sync::{
cmp::min,
collections::HashMap,
ffi::c_void,
sync::{
atomic::{AtomicPtr, Ordering}, atomic::{AtomicPtr, Ordering},
Arc, Arc,
}, }, thread::ThreadId};
thread::ThreadId,
};
use lazycell::LazyCell; use lazycell::LazyCell;
use log::{error, trace}; use log::{error, trace};
use widestring::WideString; use widestring::{WideCString};
use crate::{event::UIEvent, icons::TrayIcon}; use crate::{event::UIEvent, icons::TrayIcon};
@ -48,6 +42,8 @@ pub struct RawUIOptions {
pub icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT], pub icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT],
pub icon_paths_count: i32, pub icon_paths_count: i32,
pub notification_icon_path: [u16; MAX_FILE_PATH],
} }
// Take a look at the native.h header file for an explanation of the fields // Take a look at the native.h header file for an explanation of the fields
#[repr(C)] #[repr(C)]
@ -58,18 +54,24 @@ pub struct RawUIEvent {
#[allow(improper_ctypes)] #[allow(improper_ctypes)]
#[link(name = "espansoui", kind = "static")] #[link(name = "espansoui", kind = "static")]
extern "C" { extern "C" {
pub fn ui_initialize(_self: *const Win32EventLoop, options: RawUIOptions) -> *mut c_void; pub fn ui_initialize(
_self: *const Win32EventLoop,
options: RawUIOptions,
error_code: *mut i32,
) -> *mut c_void;
pub fn ui_eventloop( pub fn ui_eventloop(
window_handle: *const c_void, window_handle: *const c_void,
event_callback: extern "C" fn(_self: *mut Win32EventLoop, event: RawUIEvent), event_callback: extern "C" fn(_self: *mut Win32EventLoop, event: RawUIEvent),
) -> i32; ) -> i32;
pub fn ui_destroy(window_handle: *const c_void) -> i32; pub fn ui_destroy(window_handle: *const c_void) -> i32;
pub fn ui_update_tray_icon(window_handle: *const c_void, index: i32); pub fn ui_update_tray_icon(window_handle: *const c_void, index: i32);
pub fn ui_show_notification(window_handle: *const c_void, message: *const u16);
} }
pub struct Win32UIOptions<'a> { pub struct Win32UIOptions<'a> {
pub show_icon: bool, pub show_icon: bool,
pub icon_paths: &'a Vec<(TrayIcon, String)>, pub icon_paths: &'a Vec<(TrayIcon, String)>,
pub notification_icon_path: String,
} }
pub fn create(options: Win32UIOptions) -> (Win32Remote, Win32EventLoop) { pub fn create(options: Win32UIOptions) -> (Win32Remote, Win32EventLoop) {
@ -88,7 +90,12 @@ pub fn create(options: Win32UIOptions) -> (Win32Remote, Win32EventLoop) {
icons.push(path.clone()); icons.push(path.clone());
} }
let eventloop = Win32EventLoop::new(handle.clone(), icons, options.show_icon); let eventloop = Win32EventLoop::new(
handle.clone(),
icons,
options.show_icon,
options.notification_icon_path,
);
let remote = Win32Remote::new(handle, icon_indexes); let remote = Win32Remote::new(handle, icon_indexes);
(remote, eventloop) (remote, eventloop)
@ -101,6 +108,7 @@ pub struct Win32EventLoop {
show_icon: bool, show_icon: bool,
icons: Vec<String>, icons: Vec<String>,
notification_icon_path: String,
// Internal // Internal
_event_callback: LazyCell<Win32UIEventCallback>, _event_callback: LazyCell<Win32UIEventCallback>,
@ -108,11 +116,17 @@ pub struct Win32EventLoop {
} }
impl Win32EventLoop { impl Win32EventLoop {
pub(crate) fn new(handle: Arc<AtomicPtr<c_void>>, icons: Vec<String>, show_icon: bool) -> Self { pub(crate) fn new(
handle: Arc<AtomicPtr<c_void>>,
icons: Vec<String>,
show_icon: bool,
notification_icon_path: String,
) -> Self {
Self { Self {
handle, handle,
icons, icons,
show_icon, show_icon,
notification_icon_path,
_event_callback: LazyCell::new(), _event_callback: LazyCell::new(),
_init_thread_id: LazyCell::new(), _init_thread_id: LazyCell::new(),
} }
@ -128,22 +142,35 @@ impl Win32EventLoop {
let mut icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT] = let mut icon_paths: [[u16; MAX_FILE_PATH]; MAX_ICON_COUNT] =
[[0; 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()) { for (i, icon_path) in icon_paths.iter_mut().enumerate().take(self.icons.len()) {
let wide_path = WideString::from_str(&self.icons[i]); let wide_path = WideCString::from_str(&self.icons[i]).expect("Error while converting icon to wide string");
let len = min(wide_path.len(), MAX_FILE_PATH - 1); let len = min(wide_path.len(), MAX_FILE_PATH - 1);
icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]); icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]);
// TODO: test overflow, correct case // TODO: test overflow, correct case
} }
let wide_notification_icon_path =
widestring::WideCString::from_str(&self.notification_icon_path).expect("Error while converting notification icon to wide string");
let mut wide_notification_icon_path_buffer: [u16; MAX_FILE_PATH] = [0; MAX_FILE_PATH];
wide_notification_icon_path_buffer[..wide_notification_icon_path.as_slice().len()]
.clone_from_slice(wide_notification_icon_path.as_slice());
let options = RawUIOptions { let options = RawUIOptions {
show_icon: if self.show_icon { 1 } else { 0 }, show_icon: if self.show_icon { 1 } else { 0 },
icon_paths, icon_paths,
icon_paths_count: self.icons.len() as i32, icon_paths_count: self.icons.len() as i32,
notification_icon_path: wide_notification_icon_path_buffer,
}; };
let handle = unsafe { ui_initialize(self as *const Win32EventLoop, options) }; let mut error_code = 0;
let handle = unsafe { ui_initialize(self as *const Win32EventLoop, options, &mut error_code) };
if handle.is_null() { if handle.is_null() {
panic!("Unable to initialize Win32EventLoop"); match error_code {
-1 => panic!("Unable to initialize Win32EventLoop, error registering window class"),
-2 => panic!("Unable to initialize Win32EventLoop, error creating window"),
-3 => panic!("Unable to initialize Win32EventLoop, initializing notifications"),
_ => panic!("Unable to initialize Win32EventLoop, unknown error"),
}
} }
self.handle.store(handle, Ordering::Release); self.handle.store(handle, Ordering::Release);
@ -240,6 +267,22 @@ impl Win32Remote {
error!("Unable to update tray icon, invalid icon id"); error!("Unable to update tray icon, invalid icon id");
} }
} }
pub fn show_notification(&self, message: &str) {
let handle = self.handle.load(Ordering::Acquire);
if handle.is_null() {
error!("Unable to show notification, pointer is null");
return;
}
let wide_message = widestring::WideCString::from_str(message);
match wide_message {
Ok(wide_message) => unsafe { ui_show_notification(handle, wide_message.as_ptr()) },
Err(error) => {
error!("Unable to show notification, invalid message encoding {}", error);
}
}
}
} }
#[allow(clippy::single_match)] // TODO: remove after another match is used #[allow(clippy::single_match)] // TODO: remove after another match is used

View File

@ -36,6 +36,7 @@
#include <windows.h> #include <windows.h>
#include <winuser.h> #include <winuser.h>
#include <string.h>
#include <strsafe.h> #include <strsafe.h>
#pragma comment(lib, "Shell32.lib") #pragma comment(lib, "Shell32.lib")
#include <shellapi.h> #include <shellapi.h>
@ -43,33 +44,48 @@
#pragma comment(lib, "Gdi32.lib") #pragma comment(lib, "Gdi32.lib")
#include <Windows.h> #include <Windows.h>
#include "WinToast/wintoastlib.h"
using namespace WinToastLib;
#define APPWM_ICON_CLICK (WM_APP + 1) #define APPWM_ICON_CLICK (WM_APP + 1)
#define APPWM_SHOW_CONTEXT_MENU (WM_APP + 2) #define APPWM_SHOW_CONTEXT_MENU (WM_APP + 2)
#define APPWM_UPDATE_TRAY_ICON (WM_APP + 3) #define APPWM_UPDATE_TRAY_ICON (WM_APP + 3)
#define APPWM_SHOW_NOTIFICATION (WM_APP + 4)
const wchar_t *const ui_winclass = L"EspansoUI"; const wchar_t *const ui_winclass = L"EspansoUI";
typedef struct { typedef struct
{
UIOptions options; UIOptions options;
NOTIFYICONDATA nid; NOTIFYICONDATA nid;
HICON g_icons[MAX_ICON_COUNT]; HICON g_icons[MAX_ICON_COUNT];
wchar_t notification_icon_path[MAX_FILE_PATH];
// Rust interop // Rust interop
void *rust_instance; void *rust_instance;
EventCallback event_callback; EventCallback event_callback;
} UIVariables; } UIVariables;
// Needed to detect when Explorer crashes // Needed to detect when Explorer crashes
UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated"); UINT WM_TASKBARCREATED = RegisterWindowMessage(L"TaskbarCreated");
// Notification handler using: https://mohabouje.github.io/WinToast/
class EspansoNotificationHandler : public IWinToastHandler
{
public:
void toastActivated() const {}
void toastActivated(int actionIndex) const {}
void toastDismissed(WinToastDismissalReason state) const {}
void toastFailed() const {}
};
/* /*
* Message handler procedure for the window * Message handler procedure for the window
*/ */
LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp) LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, LPARAM lp)
{ {
UIEvent event = {}; UIEvent event = {};
UIVariables * variables = reinterpret_cast<UIVariables*>(GetWindowLongPtrW(window, GWLP_USERDATA)); UIVariables *variables = reinterpret_cast<UIVariables *>(GetWindowLongPtrW(window, GWLP_USERDATA));
switch (msg) switch (msg)
{ {
@ -125,20 +141,35 @@ LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, L
*/ */
break; break;
} }
case APPWM_UPDATE_TRAY_ICON: // Request to update the tray icon case APPWM_UPDATE_TRAY_ICON: // Request to update the tray icon
{ {
int32_t index = (int32_t) lp; int32_t index = (int32_t)lp;
if (index >= variables->options.icon_paths_count) { if (index >= variables->options.icon_paths_count)
{
break; break;
} }
variables->nid.hIcon = variables->g_icons[index]; variables->nid.hIcon = variables->g_icons[index];
if (variables->options.show_icon) { if (variables->options.show_icon)
{
Shell_NotifyIcon(NIM_MODIFY, &variables->nid); Shell_NotifyIcon(NIM_MODIFY, &variables->nid);
} }
break; break;
} }
case APPWM_SHOW_NOTIFICATION:
{
std::unique_ptr<wchar_t> message(reinterpret_cast<wchar_t *>(lp));
std::cout << "hello" << variables->notification_icon_path << std::endl;
WinToastTemplate templ = WinToastTemplate(WinToastTemplate::ImageAndText02);
templ.setImagePath(variables->notification_icon_path);
templ.setTextField(L"Espanso", WinToastTemplate::FirstLine);
templ.setTextField(message.get(), WinToastTemplate::SecondLine);
WinToast::instance()->showToast(templ, new EspansoNotificationHandler());
break;
}
case APPWM_ICON_CLICK: // Click on the tray icon case APPWM_ICON_CLICK: // Click on the tray icon
{ {
switch (lp) switch (lp)
@ -165,7 +196,8 @@ LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, L
} }
} }
void * ui_initialize(void *_self, UIOptions _options) { void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
{
HWND window = NULL; HWND window = NULL;
SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE); SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
@ -176,7 +208,7 @@ void * ui_initialize(void *_self, UIOptions _options) {
WNDCLASSEX uiwndclass = { WNDCLASSEX uiwndclass = {
sizeof(WNDCLASSEX), // cbSize: Size of this structure sizeof(WNDCLASSEX), // cbSize: Size of this structure
0, // style: Class styles 0, // style: Class styles
ui_window_procedure, // lpfnWndProc: Pointer to the window procedure ui_window_procedure, // lpfnWndProc: Pointer to the window procedure
0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure 0, // cbClsExtra: Number of extra bytes to allocate following the window-class structure
0, // cbWndExtra: The number of extra bytes to allocate following the window instance. 0, // cbWndExtra: The number of extra bytes to allocate following the window instance.
GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class. GetModuleHandle(0), // hInstance: A handle to the instance that contains the window procedure for the class.
@ -192,25 +224,26 @@ void * ui_initialize(void *_self, UIOptions _options) {
{ {
// Initialize the service window // Initialize the service window
window = CreateWindowEx( window = CreateWindowEx(
0, // dwExStyle: The extended window style of the window being created. 0, // dwExStyle: The extended window style of the window being created.
ui_winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass ui_winclass, // lpClassName: A null-terminated string or a class atom created by a previous call to the RegisterClass
L"Espanso UI Window", // lpWindowName: The window name. L"Espanso UI Window", // lpWindowName: The window name.
WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created. WS_OVERLAPPEDWINDOW, // dwStyle: The style of the window being created.
CW_USEDEFAULT, // X: The initial horizontal position of the window. CW_USEDEFAULT, // X: The initial horizontal position of the window.
CW_USEDEFAULT, // Y: The initial vertical position of the window. CW_USEDEFAULT, // Y: The initial vertical position of the window.
100, // nWidth: The width, in device units, of the window. 100, // nWidth: The width, in device units, of the window.
100, // nHeight: The height, in device units, of the window. 100, // nHeight: The height, in device units, of the window.
NULL, // hWndParent: handle to the parent or owner window of the window being created. NULL, // hWndParent: handle to the parent or owner window of the window being created.
NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style. NULL, // hMenu: A handle to a menu, or specifies a child-window identifier, depending on the window style.
GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window. GetModuleHandle(0), // hInstance: A handle to the instance of the module to be associated with the window.
NULL // lpParam: Pointer to a value to be passed to the window NULL // lpParam: Pointer to a value to be passed to the window
); );
if (window) if (window)
{ {
UIVariables * variables = new UIVariables(); UIVariables *variables = new UIVariables();
variables->options = _options; variables->options = _options;
variables->rust_instance = _self; variables->rust_instance = _self;
wcscpy(variables->notification_icon_path, _options.notification_icon_path);
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables)); SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<::LONG_PTR>(variables));
// Load the tray icons // Load the tray icons
@ -241,18 +274,38 @@ void * ui_initialize(void *_self, UIOptions _options) {
Shell_NotifyIcon(NIM_ADD, &variables->nid); Shell_NotifyIcon(NIM_ADD, &variables->nid);
} }
} }
else
{
*error_code = -2;
return nullptr;
}
} }
else
{
*error_code = -1;
return nullptr;
}
// Initialize the notification handler
WinToast::instance()->setAppName(L"Espanso");
const auto aumi = WinToast::configureAUMI(L"federico.terzi", L"Espanso", L"Espanso", L"1.0.0");
WinToast::instance()->setAppUserModelId(aumi);
if (!WinToast::instance()->initialize())
{
*error_code = -3;
return nullptr;
}
return window; return window;
} }
int32_t ui_eventloop(void * window, EventCallback _callback) int32_t ui_eventloop(void *window, EventCallback _callback)
{ {
if (window) if (window)
{ {
UIVariables * variables = reinterpret_cast<UIVariables*>(GetWindowLongPtrW((HWND) window, GWLP_USERDATA)); UIVariables *variables = reinterpret_cast<UIVariables *>(GetWindowLongPtrW((HWND)window, GWLP_USERDATA));
variables->event_callback = _callback; variables->event_callback = _callback;
// Enter the Event loop // Enter the Event loop
MSG msg; MSG msg;
while (GetMessage(&msg, 0, 0, 0)) while (GetMessage(&msg, 0, 0, 0))
@ -262,15 +315,30 @@ int32_t ui_eventloop(void * window, EventCallback _callback)
return 1; return 1;
} }
int32_t ui_destroy(void * window) { int32_t ui_destroy(void *window)
if (window) { {
return DestroyWindow((HWND) window); if (window)
{
return DestroyWindow((HWND)window);
}
return -1;
}
void ui_update_tray_icon(void *window, int32_t index)
{
if (window)
{
PostMessage((HWND)window, APPWM_UPDATE_TRAY_ICON, 0, index);
} }
} }
void ui_update_tray_icon(void * window, int32_t index) int32_t ui_show_notification(void *window, wchar_t *message)
{ {
if (window) { if (window)
PostMessage((HWND) window, APPWM_UPDATE_TRAY_ICON, 0, index); {
wchar_t *message_copy = _wcsdup(message);
PostMessage((HWND)window, APPWM_SHOW_NOTIFICATION, 0, (LPARAM)message_copy);
return 0;
} }
return -1;
} }

View File

@ -35,6 +35,7 @@ typedef struct {
wchar_t icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH]; wchar_t icon_paths[MAX_ICON_COUNT][MAX_FILE_PATH];
int32_t icon_paths_count; int32_t icon_paths_count;
wchar_t notification_icon_path[MAX_FILE_PATH];
} UIOptions; } UIOptions;
typedef struct { typedef struct {
@ -44,7 +45,7 @@ typedef struct {
typedef void (*EventCallback)(void * self, UIEvent data); typedef void (*EventCallback)(void * self, UIEvent data);
// Initialize the hidden UI window, the tray icon and returns the window handle. // Initialize the hidden UI window, the tray icon and returns the window handle.
extern "C" void * ui_initialize(void * self, UIOptions options); extern "C" void * ui_initialize(void * self, UIOptions options, int32_t * error_code);
// Run the event loop. Blocking call. // Run the event loop. Blocking call.
extern "C" int32_t ui_eventloop(void * window, EventCallback callback); extern "C" int32_t ui_eventloop(void * window, EventCallback callback);
@ -56,4 +57,7 @@ extern "C" int32_t ui_destroy(void * window);
// the icon within the UIOptions.icon_paths array. // the icon within the UIOptions.icon_paths array.
extern "C" void ui_update_tray_icon(void * window, int32_t index); extern "C" void ui_update_tray_icon(void * window, int32_t index);
// Show a native Windows 10 notification
extern "C" int32_t ui_show_notification(void * window, wchar_t * message);
#endif //ESPANSO_UI_H #endif //ESPANSO_UI_H

View File

@ -1,4 +1,4 @@
use espanso_detect::event::InputEvent; use espanso_detect::event::{InputEvent, Status};
fn main() { fn main() {
println!("Hello, world!z"); println!("Hello, world!z");
@ -17,6 +17,7 @@ fn main() {
let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions { let (remote, mut eventloop) = espanso_ui::win32::create(espanso_ui::win32::Win32UIOptions {
show_icon: true, show_icon: true,
icon_paths: &icon_paths, icon_paths: &icon_paths,
notification_icon_path: r"C:\Users\Freddy\Insync\Development\Espanso\Images\icongreensmall.png".to_string(),
}); });
std::thread::spawn(move || { std::thread::spawn(move || {
@ -27,8 +28,9 @@ fn main() {
match event { match event {
InputEvent::Mouse(_) => {} InputEvent::Mouse(_) => {}
InputEvent::Keyboard(evt) => { InputEvent::Keyboard(evt) => {
if evt.key == espanso_detect::event::Key::Shift { 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!");
} }
} }
} }