feat(ui): refactor windows notification system away from WinToast
This commit is contained in:
parent
f8b7300d31
commit
a8799c3d9a
|
@ -20,6 +20,8 @@ thiserror = "1.0.23"
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
widestring = "0.4.3"
|
widestring = "0.4.3"
|
||||||
lazycell = "1.3.0"
|
lazycell = "1.3.0"
|
||||||
|
winrt-notification = "0.3.1"
|
||||||
|
lazy_static = "1.4.0"
|
||||||
|
|
||||||
[target.'cfg(target_os="macos")'.dependencies]
|
[target.'cfg(target_os="macos")'.dependencies]
|
||||||
lazycell = "1.3.0"
|
lazycell = "1.3.0"
|
||||||
|
|
|
@ -24,10 +24,8 @@ 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")
|
|
||||||
.include("src/win32/json/json.hpp")
|
.include("src/win32/json/json.hpp")
|
||||||
.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");
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
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
|
@ -1,217 +0,0 @@
|
||||||
/* * 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
|
|
|
@ -22,6 +22,7 @@ use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ffi::{c_void, CString},
|
ffi::{c_void, CString},
|
||||||
os::raw::c_char,
|
os::raw::c_char,
|
||||||
|
path::PathBuf,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicPtr, Ordering},
|
atomic::{AtomicPtr, Ordering},
|
||||||
Arc,
|
Arc,
|
||||||
|
@ -29,6 +30,8 @@ use std::{
|
||||||
thread::ThreadId,
|
thread::ThreadId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod notification;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use lazycell::LazyCell;
|
use lazycell::LazyCell;
|
||||||
use log::{error, trace};
|
use log::{error, trace};
|
||||||
|
@ -51,8 +54,6 @@ 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)]
|
||||||
|
@ -76,7 +77,6 @@ extern "C" {
|
||||||
pub fn ui_destroy(window_handle: *const c_void) -> i32;
|
pub fn ui_destroy(window_handle: *const c_void) -> i32;
|
||||||
pub fn ui_exit(window_handle: *const c_void) -> i32;
|
pub fn ui_exit(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 fn ui_show_context_menu(window_handle: *const c_void, payload: *const c_char);
|
pub fn ui_show_context_menu(window_handle: *const c_void, payload: *const c_char);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,13 +102,12 @@ pub fn create(options: Win32UIOptions) -> Result<(Win32Remote, Win32EventLoop)>
|
||||||
icons.push(path.clone());
|
icons.push(path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let eventloop = Win32EventLoop::new(
|
let eventloop = Win32EventLoop::new(handle.clone(), icons, options.show_icon);
|
||||||
handle.clone(),
|
let remote = Win32Remote::new(
|
||||||
icons,
|
handle,
|
||||||
options.show_icon,
|
icon_indexes,
|
||||||
options.notification_icon_path,
|
PathBuf::from(options.notification_icon_path),
|
||||||
);
|
);
|
||||||
let remote = Win32Remote::new(handle, icon_indexes);
|
|
||||||
|
|
||||||
Ok((remote, eventloop))
|
Ok((remote, eventloop))
|
||||||
}
|
}
|
||||||
|
@ -118,7 +117,6 @@ pub struct Win32EventLoop {
|
||||||
|
|
||||||
show_icon: bool,
|
show_icon: bool,
|
||||||
icons: Vec<String>,
|
icons: Vec<String>,
|
||||||
notification_icon_path: String,
|
|
||||||
|
|
||||||
// Internal
|
// Internal
|
||||||
_event_callback: LazyCell<UIEventCallback>,
|
_event_callback: LazyCell<UIEventCallback>,
|
||||||
|
@ -126,17 +124,11 @@ pub struct Win32EventLoop {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Win32EventLoop {
|
impl Win32EventLoop {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(handle: Arc<AtomicPtr<c_void>>, icons: Vec<String>, show_icon: bool) -> Self {
|
||||||
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(),
|
||||||
}
|
}
|
||||||
|
@ -160,17 +152,10 @@ impl UIEventLoop for Win32EventLoop {
|
||||||
icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]);
|
icon_path[0..len].clone_from_slice(&wide_path.as_slice()[..len]);
|
||||||
}
|
}
|
||||||
|
|
||||||
let wide_notification_icon_path =
|
|
||||||
widestring::WideCString::from_str(&self.notification_icon_path)?;
|
|
||||||
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 mut error_code = 0;
|
let mut error_code = 0;
|
||||||
|
@ -190,12 +175,6 @@ impl UIEventLoop for Win32EventLoop {
|
||||||
)
|
)
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
-3 => Err(
|
|
||||||
Win32UIError::EventLoopInitError(
|
|
||||||
"Unable to initialize Win32EventLoop, initializing notifications".to_string(),
|
|
||||||
)
|
|
||||||
.into(),
|
|
||||||
),
|
|
||||||
_ => Err(
|
_ => Err(
|
||||||
Win32UIError::EventLoopInitError(
|
Win32UIError::EventLoopInitError(
|
||||||
"Unable to initialize Win32EventLoop, unknown error".to_string(),
|
"Unable to initialize Win32EventLoop, unknown error".to_string(),
|
||||||
|
@ -284,7 +263,12 @@ impl Win32Remote {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
handle: Arc<AtomicPtr<c_void>>,
|
handle: Arc<AtomicPtr<c_void>>,
|
||||||
icon_indexes: HashMap<TrayIcon, usize>,
|
icon_indexes: HashMap<TrayIcon, usize>,
|
||||||
|
notification_icon_path: PathBuf,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
if let Err(err) = notification::initialize_notification_thread(notification_icon_path) {
|
||||||
|
error!("unable to initialize notification thread: {}", err);
|
||||||
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
handle,
|
handle,
|
||||||
icon_indexes,
|
icon_indexes,
|
||||||
|
@ -308,21 +292,8 @@ impl UIRemote for Win32Remote {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn show_notification(&self, message: &str) {
|
fn show_notification(&self, message: &str) {
|
||||||
let handle = self.handle.load(Ordering::Acquire);
|
if let Err(err) = notification::show_notification(message) {
|
||||||
if handle.is_null() {
|
error!("Unable to show notification: {}", err);
|
||||||
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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,16 +43,12 @@
|
||||||
|
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
|
|
||||||
#include "WinToast/wintoastlib.h"
|
|
||||||
using namespace WinToastLib;
|
|
||||||
|
|
||||||
#include "json/json.hpp"
|
#include "json/json.hpp"
|
||||||
using json = nlohmann::json;
|
using json = nlohmann::json;
|
||||||
|
|
||||||
#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";
|
||||||
|
|
||||||
|
@ -61,7 +57,6 @@ 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;
|
||||||
|
@ -71,16 +66,6 @@ typedef struct
|
||||||
// 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
|
||||||
*/
|
*/
|
||||||
|
@ -154,19 +139,6 @@ LRESULT CALLBACK ui_window_procedure(HWND window, unsigned int msg, WPARAM wp, L
|
||||||
|
|
||||||
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)
|
||||||
|
@ -240,7 +212,6 @@ void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
|
||||||
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
|
||||||
|
@ -252,11 +223,11 @@ void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
|
||||||
// Hide the window
|
// Hide the window
|
||||||
ShowWindow(window, SW_HIDE);
|
ShowWindow(window, SW_HIDE);
|
||||||
|
|
||||||
// Setup the icon in the notification space
|
// Setup the icon in the tray space
|
||||||
SendMessage(window, WM_SETICON, ICON_BIG, (LPARAM)variables->g_icons[0]);
|
SendMessage(window, WM_SETICON, ICON_BIG, (LPARAM)variables->g_icons[0]);
|
||||||
SendMessage(window, WM_SETICON, ICON_SMALL, (LPARAM)variables->g_icons[0]);
|
SendMessage(window, WM_SETICON, ICON_SMALL, (LPARAM)variables->g_icons[0]);
|
||||||
|
|
||||||
// Notification
|
// Tray icon
|
||||||
variables->nid.cbSize = sizeof(variables->nid);
|
variables->nid.cbSize = sizeof(variables->nid);
|
||||||
variables->nid.hWnd = window;
|
variables->nid.hWnd = window;
|
||||||
variables->nid.uID = 1;
|
variables->nid.uID = 1;
|
||||||
|
@ -283,16 +254,6 @@ void *ui_initialize(void *_self, UIOptions _options, int32_t *error_code)
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the notification handler
|
|
||||||
WinToast::instance()->setAppName(L"Espanso");
|
|
||||||
const auto aumi = WinToast::configureAUMI(L"federico.terzi", L"Espanso", L"Core", L"1.0.0");
|
|
||||||
WinToast::instance()->setAppUserModelId(aumi);
|
|
||||||
if (!WinToast::instance()->initialize())
|
|
||||||
{
|
|
||||||
*error_code = -3;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return window;
|
return window;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,17 +298,6 @@ void ui_update_tray_icon(void *window, int32_t index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int32_t ui_show_notification(void *window, wchar_t *message)
|
|
||||||
{
|
|
||||||
if (window)
|
|
||||||
{
|
|
||||||
wchar_t *message_copy = _wcsdup(message);
|
|
||||||
PostMessage((HWND)window, APPWM_SHOW_NOTIFICATION, 0, (LPARAM)message_copy);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Menu related methods
|
// Menu related methods
|
||||||
|
|
||||||
void _insert_separator_menu(HMENU parent)
|
void _insert_separator_menu(HMENU parent)
|
||||||
|
|
|
@ -35,7 +35,6 @@ 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 {
|
||||||
|
@ -61,9 +60,6 @@ extern "C" void ui_exit(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);
|
|
||||||
|
|
||||||
// Display the context menu on the tray icon.
|
// Display the context menu on the tray icon.
|
||||||
// Payload is passed as JSON as given the complex structure, parsing
|
// Payload is passed as JSON as given the complex structure, parsing
|
||||||
// this manually would have been complex.
|
// this manually would have been complex.
|
||||||
|
|
102
espanso-ui/src/win32/notification.rs
Normal file
102
espanso-ui/src/win32/notification.rs
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/*
|
||||||
|
* 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::{path::{PathBuf}, sync::{Arc, Mutex, mpsc::{Sender, channel}}};
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow, bail};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
|
use log::{error, warn};
|
||||||
|
use std::process::Command;
|
||||||
|
use winrt_notification::{IconCrop, Toast};
|
||||||
|
use std::os::windows::process::CommandExt;
|
||||||
|
|
||||||
|
const ESPANSO_APP_USER_MODEL_ID: &str = "{5E3B6C0F-1A4D-45C4-8872-D8174702101A}";
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref SEND_CHANNEL: Arc<Mutex<Option<Sender<String>>>> = Arc::new(Mutex::new(None));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn initialize_notification_thread(notification_icon_path: PathBuf) -> Result<()> {
|
||||||
|
let (sender, receiver) = channel::<String>();
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut lock = SEND_CHANNEL.lock().map_err(|e| anyhow!("failed to define shared notification sender: {}", e))?;
|
||||||
|
*lock = Some(sender);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::thread::Builder::new().name("notification-thread".to_string()).spawn(move || {
|
||||||
|
// First determine which AppUserModelID we can use
|
||||||
|
lazy_static! {
|
||||||
|
static ref APP_USER_MODEL_ID: &'static str = if is_espanso_app_user_model_id_set() {
|
||||||
|
ESPANSO_APP_USER_MODEL_ID
|
||||||
|
} else {
|
||||||
|
warn!("unable to find espanso AppUserModelID in the list of registered ones, falling back to Powershell");
|
||||||
|
Toast::POWERSHELL_APP_ID
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Ok(message) = receiver.recv() {
|
||||||
|
if let Err(err) = Toast::new(&APP_USER_MODEL_ID)
|
||||||
|
.icon(¬ification_icon_path, IconCrop::Square, "Espanso")
|
||||||
|
.title("Espanso")
|
||||||
|
.text1(&message)
|
||||||
|
.sound(None)
|
||||||
|
.show()
|
||||||
|
.map_err(|e| anyhow!("failed to show notification: {}", e)) {
|
||||||
|
error!("unable to show notification: {}", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_notification(msg: &str) -> Result<()> {
|
||||||
|
let mut lock = SEND_CHANNEL.lock().map_err(|e| anyhow!("unable to acquire notification send channel: {}", e))?;
|
||||||
|
match &mut *lock {
|
||||||
|
Some(sender) => {
|
||||||
|
sender.send(msg.to_string())?;
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
None => bail!("notification sender not available"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_espanso_app_user_model_id_set() -> bool {
|
||||||
|
match Command::new("powershell")
|
||||||
|
.args(&["-c", "get-startapps"])
|
||||||
|
.creation_flags(0x08000000)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
Ok(output) => {
|
||||||
|
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||||
|
// Check if espanso is present
|
||||||
|
output_str
|
||||||
|
.lines()
|
||||||
|
.any(|line| line.contains(ESPANSO_APP_USER_MODEL_ID))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!(
|
||||||
|
"unable to determine if AppUserModelID was registered: {}",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user