diff --git a/CMakeLists.txt b/CMakeLists.txt index 6b6cc3d..cfa44e1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,17 +1,15 @@ -project( snoretoast ) cmake_minimum_required(VERSION 3.4) +project( snoretoast VERSION 0.5.99) -add_definitions(-DUNICODE -D_UNICODE -D__WRL_CLASSIC_COM_STRICT__ -DWIN32_LEAN_AND_MEAN) +include(GenerateExportHeader) set(CMAKE_CXX_STANDARD 17) +set(CMAKE_INCLUDE_CURRENT_DIR ON) -#link runtime static -if(MSVC) - foreach(_bt DEBUG RELEASE RELWITHDEBINFO) - string(REPLACE "/MD" "/MT" CMAKE_CXX_FLAGS_${_bt} ${CMAKE_CXX_FLAGS_${_bt}}) - endforeach(_bt DEBUG RELEASE RELWITHDEBINFO) -endif(MSVC) +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/bin) add_subdirectory(data) add_subdirectory(src) +add_subdirectory(examples) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 0000000..449f82a --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(qt-lib) diff --git a/examples/qt/CMakeLists.txt b/examples/qt-lib/CMakeLists.txt similarity index 51% copy from examples/qt/CMakeLists.txt copy to examples/qt-lib/CMakeLists.txt index 8e35319..605509d 100644 --- a/examples/qt/CMakeLists.txt +++ b/examples/qt-lib/CMakeLists.txt @@ -1,13 +1,15 @@ cmake_minimum_required(VERSION 3.0.0) -project(socktest VERSION 0.1 LANGUAGES CXX) +project(SnoreToastQtExample VERSION 0.1 LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) find_package(Qt5Core) +find_package(Qt5Gui) find_package(Qt5Network) add_executable(${PROJECT_NAME} "main.cpp") -target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network) +target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Gui libsnoretoast) diff --git a/examples/qt-lib/main.cpp b/examples/qt-lib/main.cpp new file mode 100644 index 0000000..2d28cf0 --- /dev/null +++ b/examples/qt-lib/main.cpp @@ -0,0 +1,52 @@ +/* + SnoreToast is capable to invoke Windows 8 toast notifications. + Copyright (C) 2019 Hannah von Reth + + SnoreToast is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SnoreToast 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with SnoreToast. If not, see . +*/ + +#include +#include +#include + +#include + +#include "../../src/snoretoasts.h" +#include "../../src/snoretoastactions.h" +#include "../../src/linkhelper.h" + +namespace { +const auto appId = + QStringLiteral("Snore.DesktopToasts%1.QtLib").arg(SnoreToasts::version()).toStdWString(); +} + +int main(int argc, char *argv[]) +{ + QGuiApplication a(argc, argv); + qDebug() << SUCCEEDED(LinkHelper::tryCreateShortcut( + std::filesystem::path(L"SnoreToast") / SnoreToasts::version() / L"SnoreToastQtLib", + appId)); + QTimer::singleShot(0, &a, [&] { + SnoreToasts toast(appId); + // app.setPipeName(pipe); + // app.setApplication(app.applicationDirPath().); + toast.displayToast(L"Test", L"Message", L"", true); + std::wcout << "Result" << SnoreToastActions::getActionString(toast.userAction()) + << std::endl; + a.quit(); + }); + return a.exec(); +} + +#include "main.moc" diff --git a/examples/qt/CMakeLists.txt b/examples/qt/CMakeLists.txt index 8e35319..2076f20 100644 --- a/examples/qt/CMakeLists.txt +++ b/examples/qt/CMakeLists.txt @@ -1,13 +1,14 @@ cmake_minimum_required(VERSION 3.0.0) -project(socktest VERSION 0.1 LANGUAGES CXX) +project(SnoreToastQtExample VERSION 0.1 LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 17) set(CMAKE_INCLUDE_CURRENT_DIR ON) set(CMAKE_AUTOMOC ON) find_package(Qt5Core) find_package(Qt5Network) add_executable(${PROJECT_NAME} "main.cpp") target_link_libraries(${PROJECT_NAME} Qt5::Core Qt5::Network) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index adc9388..3bdbcca 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,8 +1,16 @@ -add_library(libsnoretoast STATIC snoretoasts.cpp toasteventhandler.cpp linkhelper.cpp utils.cpp) +add_library(libsnoretoast SHARED snoretoasts.cpp toasteventhandler.cpp linkhelper.cpp utils.cpp) +target_link_libraries(libsnoretoast PUBLIC runtimeobject shlwapi) +target_compile_definitions(libsnoretoast PRIVATE UNICODE _UNICODE __WRL_CLASSIC_COM_STRICT__ WIN32_LEAN_AND_MEAN NOMINMAX) +target_compile_definitions(libsnoretoast PUBLIC __WRL_CLASSIC_COM_STRICT__) +target_include_directories(libsnoretoast PUBLIC $) + +generate_export_header(libsnoretoast) add_executable(SnoreToast WIN32 main.cpp ${SNORE_TOAST_DEPS}) -target_link_libraries(SnoreToast runtimeobject shlwapi libsnoretoast) +target_link_libraries(SnoreToast libsnoretoast) +target_compile_definitions(SnoreToast PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX) +target_compile_definitions(SnoreToast PRIVATE SNORETOAST_CALLBACK_UUID="{383803B6-AFDA-4220-BFC3-0DBF810106BF}") -install(TARGETS SnoreToast RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) +install(TARGETS libsnoretoast SnoreToast RUNTIME DESTINATION bin LIBRARY DESTINATION lib ARCHIVE DESTINATION lib) install(FILES snoretoastactions.h DESTINATION includes/snoretoast) diff --git a/src/linkhelper.cpp b/src/linkhelper.cpp index 7daf36f..4cd1161 100644 --- a/src/linkhelper.cpp +++ b/src/linkhelper.cpp @@ -1,146 +1,148 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see . */ #include "linkhelper.h" #include "toasteventhandler.h" #include "utils.h" #include #include #include #include // compat with older sdk #ifndef INIT_PKEY_AppUserModel_ToastActivatorCLSID EXTERN_C const PROPERTYKEY DECLSPEC_SELECTANY PKEY_AppUserModel_ToastActivatorCLSID = { { 0x9F4C2855, 0x9F79, 0x4B39, { 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 } }, 26 }; #define INIT_PKEY_AppUserModel_ToastActivatorCLSID \ { \ { 0x9F4C2855, 0x9F79, 0x4B39, 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3 }, 26 \ } #endif //#ifndef INIT_PKEY_AppUserModel_ToastActivatorCLSID HRESULT LinkHelper::tryCreateShortcut(const std::filesystem::path &shortcutPath, const std::filesystem::path &exePath, - const std::wstring &appID) + const std::wstring &appID, const std::wstring &callbackUUID) { if (!std::filesystem::path(shortcutPath).is_relative()) { std::wcerr << L"The shortcut path must be relative" << std::endl; return S_FALSE; } - const std::filesystem::path path = - (startmenuPath() / L"SnoreToast" / SnoreToasts::version() / shortcutPath) - .replace_extension(L".lnk"); - + const std::filesystem::path path = (startmenuPath() / shortcutPath).replace_extension(L".lnk"); if (std::filesystem::exists(path)) { tLog << L"Path: " << path << L" already exists, skip creation of shortcut"; return S_OK; } if (!std::filesystem::exists(path.parent_path()) && !std::filesystem::create_directories(path.parent_path())) { tLog << L"Failed to create dir: " << path.parent_path(); return S_FALSE; } - return installShortcut(path, exePath, appID); + return installShortcut(path, exePath, appID, callbackUUID); } -HRESULT LinkHelper::tryCreateShortcut(const std::wstring &appID) +HRESULT LinkHelper::tryCreateShortcut(const std::filesystem::path &shortcutPath, + const std::wstring &appID, const std::wstring &callbackUUID) { - return tryCreateShortcut(L"SnoreToast", Utils::selfLocate().c_str(), appID); + return tryCreateShortcut(shortcutPath, Utils::selfLocate(), appID, callbackUUID); } // Install the shortcut HRESULT LinkHelper::installShortcut(const std::filesystem::path &shortcutPath, - const std::filesystem::path &exePath, const std::wstring &appID) + const std::filesystem::path &exePath, const std::wstring &appID, + const std::wstring &callbackUUID) { + HRESULT hr = S_OK; std::wcout << L"Installing shortcut: " << shortcutPath << L" " << exePath << L" " << appID << std::endl; - tLog << L"Installing shortcut: " << shortcutPath << L" " << exePath << L" " << appID; - /** - * Add CToastNotificationActivationCallback to registry - * Required to use the CToastNotificationActivationCallback for buttons and textbox - * interactions. windows.ui.notifications does not support user interaction from cpp - */ - ; - const std::wstring locPath = Utils::selfLocate().wstring(); - HRESULT hr = HRESULT_FROM_WIN32(::RegSetKeyValueW( - HKEY_CURRENT_USER, L"SOFTWARE\\Classes\\CLSID\\" TOAST_UUID L"\\LocalServer32", nullptr, - REG_SZ, locPath.c_str(), static_cast(locPath.size() * sizeof(wchar_t)))); + tLog << L"Installing shortcut: " << shortcutPath << L" " << exePath << L" " << appID << L" " + << callbackUUID; + if (!callbackUUID.empty()) { + /** + * Add CToastNotificationActivationCallback to registry + * Required to use the CToastNotificationActivationCallback for buttons and textbox + * interactions. windows.ui.notifications does not support user interaction from cpp + */ + const std::wstring locPath = Utils::selfLocate().wstring(); + std::wstringstream url; + url << L"SOFTWARE\\Classes\\CLSID\\" << callbackUUID << L"\\LocalServer32"; + hr = HRESULT_FROM_WIN32(::RegSetKeyValueW( + HKEY_CURRENT_USER, url.str().c_str(), nullptr, REG_SZ, locPath.c_str(), + static_cast(locPath.size() * sizeof(wchar_t)))); + } if (SUCCEEDED(hr)) { ComPtr shellLink; hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); if (SUCCEEDED(hr)) { hr = shellLink->SetPath(exePath.c_str()); if (SUCCEEDED(hr)) { hr = shellLink->SetArguments(L""); if (SUCCEEDED(hr)) { ComPtr propertyStore; hr = shellLink.As(&propertyStore); if (SUCCEEDED(hr)) { PROPVARIANT appIdPropVar; hr = InitPropVariantFromString(appID.c_str(), &appIdPropVar); if (SUCCEEDED(hr)) { hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar); + PropVariantClear(&appIdPropVar); + } + } + if (SUCCEEDED(hr) && !callbackUUID.empty()) { + PROPVARIANT toastActivatorPropVar = {}; + toastActivatorPropVar.vt = VT_CLSID; + CLSIDFromString(callbackUUID.c_str(), toastActivatorPropVar.puuid); + hr = propertyStore->SetValue(PKEY_AppUserModel_ToastActivatorCLSID, + toastActivatorPropVar); + } + if (SUCCEEDED(hr)) { + hr = propertyStore->Commit(); + if (SUCCEEDED(hr)) { + ComPtr persistFile; + hr = shellLink.As(&persistFile); if (SUCCEEDED(hr)) { - PropVariantClear(&appIdPropVar); - PROPVARIANT toastActivatorPropVar; - toastActivatorPropVar.vt = VT_CLSID; - toastActivatorPropVar.puuid = const_cast( - &__uuidof(CToastNotificationActivationCallback)); - - hr = propertyStore->SetValue(PKEY_AppUserModel_ToastActivatorCLSID, - toastActivatorPropVar); - if (SUCCEEDED(hr)) { - hr = propertyStore->Commit(); - if (SUCCEEDED(hr)) { - ComPtr persistFile; - hr = shellLink.As(&persistFile); - if (SUCCEEDED(hr)) { - hr = persistFile->Save(shortcutPath.c_str(), TRUE); - } - } - } + hr = persistFile->Save(shortcutPath.c_str(), TRUE); } } } } } } } if (FAILED(hr)) { std::wcerr << "Failed to install shortcut " << shortcutPath << " error: " << _com_error(hr).ErrorMessage() << std::endl; } return hr; } std::filesystem::path LinkHelper::startmenuPath() { wchar_t buffer[MAX_PATH]; std::wstringstream path; if (GetEnvironmentVariable(L"APPDATA", buffer, MAX_PATH) > 0) { path << buffer << L"\\Microsoft\\Windows\\Start Menu\\Programs\\"; } return path.str(); } diff --git a/src/linkhelper.h b/src/linkhelper.h index 991f84f..32d49b4 100644 --- a/src/linkhelper.h +++ b/src/linkhelper.h @@ -1,35 +1,40 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see . */ #pragma once #include "snoretoasts.h" -class LinkHelper +class LIBSNORETOAST_EXPORT LinkHelper { public: static HRESULT tryCreateShortcut(const std::filesystem::path &shortcutPath, const std::filesystem::path &exePath, - const std::wstring &appID); - static HRESULT tryCreateShortcut(const std::wstring &appID); + const std::wstring &appID, + const std::wstring &callbackUUID = {}); + + static HRESULT tryCreateShortcut(const std::filesystem::path &shortcutPath, + const std::wstring &appID, + const std::wstring &callbackUUID = {}); private: static HRESULT installShortcut(const std::filesystem::path &shortcutPath, - const std::filesystem::path &exePath, const std::wstring &appID); + const std::filesystem::path &exePath, const std::wstring &appID, + const std::wstring &callbackUUID = {}); static std::filesystem::path startmenuPath(); }; diff --git a/src/main.cpp b/src/main.cpp index b923327..c553e26 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,324 +1,326 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see . */ #include "snoretoasts.h" #include "toasteventhandler.h" -#include "utils.h" + +#include "snoretoastactioncenterintegration.h" #include "linkhelper.h" #include #include #include #include #include #include #include #include using namespace Windows::Foundation; void help(const std::wstring &error) { if (!error.empty()) { std::wcerr << error << std::endl; } else { std::wcerr << L"Welcome to SnoreToast " << SnoreToasts::version() << "." << std::endl << L"A command line application which is capable of creating Windows Toast " L"notifications." << std::endl; } std::wcerr << std::endl << L"---- Usage ----" << std::endl << L"SnoreToast [Options]" << std::endl << std::endl << L"---- Options ----" << std::endl << L"[-t] \t| Displayed on the first line of the toast." << std::endl << L"[-m] <message string>\t| Displayed on the remaining lines, wrapped." << std::endl << L"[-b] <button1;button2 string>| Displayed on the bottom line, can list multiple " L"buttons separated by ;" << std::endl << L"[-tb]\t\t\t| Displayed a textbox on the bottom line, only if buttons are not " L"presented." << std::endl << L"[-p] <image URI>\t| Display toast with an image, local files only." << std::endl << L"[-w] \t\t\t| Wait for toast to expire or activate." << std::endl << L"[-id] <id>\t\t| sets the id for a notification to be able to close it later." << std::endl << L"[-s] <sound URI> \t| Sets the sound of the notifications, for possible values see " L"http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx." << std::endl << L"[-silent] \t\t| Don't play a sound file when showing the notifications." << std::endl << L"[-appID] <App.ID>\t| Don't create a shortcut but use the provided app id." << std::endl << L"[-pipeName] <\\.\\pipe\\pipeName\\>\t| Provide a name pipe which is used for " L"callbacks." << std::endl << L"[-application] <C:\\foo.exe>\t| Provide a application that might be started if " L"the pipe does not exist." << std::endl << L"-close <id>\t\t| Closes a currently displayed notification, in order to be able " L"to close a notification the parameter -w must be used to create the notification." << std::endl << std::endl << L"-install <name> <application> <appID>| Creates a shortcut <name> in the start " L"menu which point to the executable <application>, appID used for the " L"notifications." << std::endl << std::endl << L"-v \t\t\t| Print the version and copying information." << std::endl << L"-h\t\t\t| Print these instructions. Same as no args." << std::endl << L"Exit Status\t: Exit Code" << std::endl << L"Failed\t\t: " << static_cast<int>(SnoreToastActions::Actions::Error) << std::endl << std::endl << "Success\t\t: " << static_cast<int>(SnoreToastActions::Actions::Clicked) << std::endl << "Hidden\t\t: " << static_cast<int>(SnoreToastActions::Actions::Hidden) << std::endl << "Dismissed\t: " << static_cast<int>(SnoreToastActions::Actions::Dismissed) << std::endl << "TimedOut\t: " << static_cast<int>(SnoreToastActions::Actions::Timedout) << std::endl << "ButtonPressed\t: " << static_cast<int>(SnoreToastActions::Actions::ButtonClicked) << std::endl << "TextEntered\t: " << static_cast<int>(SnoreToastActions::Actions::TextEntered) << std::endl << std::endl << L"---- Image Notes ----" << std::endl << L"Images must be .png with:" << std::endl << L"\tmaximum dimensions of 1024x1024" << std::endl << L"\tsize <= 200kb" << std::endl << L"These limitations are due to the Toast notification system." << std::endl; } void version() { std::wcerr << L"SnoreToast version " << SnoreToasts::version() << std::endl << L"Copyright (C) 2019 Hannah von Reth <vonreth@kde.org>" << std::endl << L"SnoreToast is free software: you can redistribute it and/or modify" << std::endl << L"it under the terms of the GNU Lesser General Public License as published by" << std::endl << L"the Free Software Foundation, either version 3 of the License, or" << std::endl << L"(at your option) any later version." << std::endl; } SnoreToastActions::Actions parse(std::vector<wchar_t *> args) { HRESULT hr = S_OK; std::wstring appID; std::filesystem::path pipe; std::filesystem::path application; std::wstring title; std::wstring body; std::filesystem::path image; std::wstring id; std::wstring sound(L"Notification.Default"); std::wstring buttons; bool silent = false; bool wait = false; bool closeNotify = false; bool isTextBoxEnabled = false; auto nextArg = [&](std::vector<wchar_t *>::const_iterator &it, const std::wstring &helpText) -> std::wstring { if (it != args.cend()) { return *it++; } else { help(helpText); exit(static_cast<int>(SnoreToastActions::Actions::Error)); } }; auto it = args.begin() + 1; while (it != args.end()) { std::wstring arg(nextArg(it, L"")); std::transform(arg.begin(), arg.end(), arg.begin(), [](int i) -> int { return ::tolower(i); }); if (arg == L"-m") { body = nextArg(it, L"Missing argument to -m.\n" L"Supply argument as -m \"message string\""); } else if (arg == L"-t") { title = nextArg(it, L"Missing argument to -t.\n" L"Supply argument as -t \"bold title string\""); } else if (arg == L"-p") { std::wstring path = nextArg(it, L"Missing argument to -p." L"Supply argument as -p \"image path\""); if (path.substr(0, 8) != L"file:///") { image = L"file:///"; path = _wfullpath(nullptr, path.c_str(), MAX_PATH); } image.append(path); } else if (arg == L"-w") { wait = true; } else if (arg == L"-s") { sound = nextArg(it, L"Missing argument to -s.\n" L"Supply argument as -s \"sound name\""); } else if (arg == L"-id") { id = nextArg(it, L"Missing argument to -id.\n" L"Supply argument as -id \"id\""); } else if (arg == L"-silent") { silent = true; } else if (arg == L"-appid") { appID = nextArg(it, L"Missing argument to -appID.\n" L"Supply argument as -appID \"Your.APP.ID\""); } else if (arg == L"-pipename") { pipe = nextArg(it, L"Missing argument to -pipeName.\n" L"Supply argument as -pipeName \"\\.\\pipe\\foo\\\""); } else if (arg == L"-application") { application = nextArg(it, L"Missing argument to -pipeName.\n" L"Supply argument as -applicatzion \"C:\\foo.exe\""); } else if (arg == L"-b") { buttons = nextArg(it, L"Missing argument to -b.\n" L"Supply argument for buttons as -b \"button1;button2\""); } else if (arg == L"-tb") { isTextBoxEnabled = true; } else if (arg == L"-install") { const std::wstring shortcut( nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\"")); const std::wstring exe( nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\"")); appID = nextArg(it, L"Missing argument to -install.\n" L"Supply argument as -install \"path to your shortcut\" \"path to the " L"application the shortcut should point to\" \"App.ID\""); - return SUCCEEDED(LinkHelper::tryCreateShortcut(shortcut, exe, appID)) + return SUCCEEDED(LinkHelper::tryCreateShortcut( + shortcut, exe, appID, SnoreToastActionCenterIntegration::uuid())) ? SnoreToastActions::Actions::Clicked : SnoreToastActions::Actions::Error; } else if (arg == L"-close") { id = nextArg(it, L"Missing agument to -close" L"Supply argument as -close \"id\""); closeNotify = true; } else if (arg == L"-v") { version(); return SnoreToastActions::Actions::Clicked; } else if (arg == L"-h") { help(L""); return SnoreToastActions::Actions::Clicked; } else { std::wstringstream ws; ws << L"Unknown argument: " << arg << std::endl; help(ws.str()); return SnoreToastActions::Actions::Error; } } if (closeNotify) { if (!id.empty()) { SnoreToasts app(appID); app.setId(id); if (app.closeNotification()) { return SnoreToastActions::Actions::Clicked; } } else { help(L"Close only works if an -id id was provided."); } } else { hr = (title.length() > 0 && body.length() > 0) ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { if (appID.empty()) { std::wstringstream _appID; _appID << L"Snore.DesktopToasts." << SnoreToasts::version(); appID = _appID.str(); - hr = LinkHelper::tryCreateShortcut(appID); + hr = LinkHelper::tryCreateShortcut(std::filesystem::path(L"SnoreToast") + / SnoreToasts::version() / L"SnoreToast", + appID, + SnoreToastActionCenterIntegration::uuid()); } if (SUCCEEDED(hr)) { if (isTextBoxEnabled) { if (pipe.empty()) { std::wcerr << L"TextBox notifications only work if a pipe for the result " L"was provided" << std::endl; return SnoreToastActions::Actions::Error; }; } SnoreToasts app(appID); app.setPipeName(pipe); app.setApplication(application); app.setSilent(silent); app.setSound(sound); app.setId(id); app.setButtons(buttons); app.setTextBoxEnabled(isTextBoxEnabled); app.displayToast(title, body, image, wait); return app.userAction(); } } else { help(L""); return SnoreToastActions::Actions::Clicked; } } return SnoreToastActions::Actions::Error; } SnoreToastActions::Actions handleEmbedded() { - Utils::registerActivator(); - CToastNotificationActivationCallback::waitForActivation(); - Utils::unregisterActivator(); + SnoreToasts::waitForCallbackActivation(); return SnoreToastActions::Actions::Clicked; } int WINAPI wWinMain(HINSTANCE, HINSTANCE, wchar_t *, int) { if (AttachConsole(ATTACH_PARENT_PROCESS)) { FILE *dummy; _wfreopen_s(&dummy, L"CONOUT$", L"w", stdout); setvbuf(stdout, nullptr, _IONBF, 0); _wfreopen_s(&dummy, L"CONOUT$", L"w", stderr); setvbuf(stderr, nullptr, _IONBF, 0); std::ios::sync_with_stdio(); } const auto commandLine = GetCommandLineW(); - tLog << commandLine; int argc; wchar_t **argv = CommandLineToArgvW(commandLine, &argc); SnoreToastActions::Actions action = SnoreToastActions::Actions::Clicked; HRESULT hr = Initialize(RO_INIT_MULTITHREADED); if (SUCCEEDED(hr)) { if (std::wstring(commandLine).find(L"-Embedding") != std::wstring::npos) { action = handleEmbedded(); } else { action = parse(std::vector<wchar_t *>(argv, argv + argc)); } Uninitialize(); } return static_cast<int>(action); } diff --git a/src/snoretoastactioncenterintegration.h b/src/snoretoastactioncenterintegration.h new file mode 100644 index 0000000..43edc3f --- /dev/null +++ b/src/snoretoastactioncenterintegration.h @@ -0,0 +1,79 @@ +/* + SnoreToast is capable to invoke Windows 8 toast notifications. + Copyright (C) 2019 Hannah von Reth <vonreth@kde.org> + + SnoreToast is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + SnoreToast 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. +*/ +#pragma once + +#include <algorithm> +#include <ntverp.h> +#include <sstream> +#include <wrl.h> + +typedef struct NOTIFICATION_USER_INPUT_DATA +{ + LPCWSTR Key; + LPCWSTR Value; +} NOTIFICATION_USER_INPUT_DATA; + +MIDL_INTERFACE("53E31837-6600-4A81-9395-75CFFE746F94") +INotificationActivationCallback : public IUnknown +{ +public: + virtual HRESULT STDMETHODCALLTYPE Activate( + __RPC__in_string LPCWSTR appUserModelId, __RPC__in_opt_string LPCWSTR invokedArgs, + __RPC__in_ecount_full_opt(count) const NOTIFICATION_USER_INPUT_DATA *data, + ULONG count) = 0; +}; + +// The COM server which implements the callback notifcation from Action Center +class DECLSPEC_UUID(SNORETOAST_CALLBACK_UUID) SnoreToastActionCenterIntegration + : public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, + INotificationActivationCallback> +{ +public: + static std::wstring uuid() + { + static std::wstring _uuid = [] { + std::wstringstream out; + out << SNORETOAST_CALLBACK_UUID; + return out.str(); + }(); + return _uuid; + } + + SnoreToastActionCenterIntegration() {} + virtual HRESULT STDMETHODCALLTYPE Activate(__RPC__in_string LPCWSTR appUserModelId, + __RPC__in_opt_string LPCWSTR invokedArgs, + __RPC__in_ecount_full_opt(count) + const NOTIFICATION_USER_INPUT_DATA *data, + ULONG count) override + { + if (invokedArgs == nullptr) { + return S_OK; + } + std::wstringstream msg; + for (ULONG i = 0; i < count; ++i) { + std::wstring tmp = data[i].Value; + // printing \r to stdcout is kind of problematic :D + std::replace(tmp.begin(), tmp.end(), L'\r', L'\n'); + msg << tmp; + } + return SnoreToasts::backgroundCallback(appUserModelId, invokedArgs, msg.str()); + } +}; + +CoCreatableClass(SnoreToastActionCenterIntegration); diff --git a/src/snoretoastactions.h b/src/snoretoastactions.h index ed3108d..22640f8 100644 --- a/src/snoretoastactions.h +++ b/src/snoretoastactions.h @@ -1,58 +1,56 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth <vonreth@kde.org> SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #pragma once #include <string_view> #include <vector> class SnoreToastActions { public: enum class Actions { Clicked, Hidden, Dismissed, Timedout, ButtonClicked, TextEntered, Error = -1 }; static constexpr std::wstring_view getActionString(const Actions &a) { return ActionStrings[static_cast<int>(a)]; } - static SnoreToastActions::Actions getAction(const std::wstring_view &s) + static inline SnoreToastActions::Actions getAction(const std::wstring_view &s) { - int i = 0; - for (const auto &sv : ActionStrings) { - if (sv.compare(s) == 0) { + for (unsigned int i = 0; i <= sizeof(ActionStrings); ++i) { + if (ActionStrings[i].compare(s) == 0) { return static_cast<SnoreToastActions::Actions>(i); } - ++i; } return SnoreToastActions::Actions::Error; } private: static constexpr std::wstring_view ActionStrings[] = { L"clicked", L"hidden", L"dismissed", L"timedout", L"buttonClicked", L"textEntered", }; }; diff --git a/src/snoretoasts.cpp b/src/snoretoasts.cpp index 853ebac..e302f4c 100644 --- a/src/snoretoasts.cpp +++ b/src/snoretoasts.cpp @@ -1,578 +1,666 @@ -#include "snoretoasts.h" -#include "snoretoasts.h" -#include "snoretoasts.h" /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth <vonreth@kde.org> SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #include "snoretoasts.h" #include "toasteventhandler.h" #include "linkhelper.h" #include "utils.h" +#include "stringreferencewrapper.h" #include <sstream> #include <iostream> using namespace Microsoft::WRL; +using namespace ABI::Windows::UI; using namespace ABI::Windows::UI::Notifications; using namespace ABI::Windows::Data::Xml::Dom; using namespace Windows::Foundation; -SnoreToasts::SnoreToasts(const std::wstring &appID) - : m_appID(appID), m_id(std::to_wstring(GetCurrentProcessId())) +class SnoreToastsPrivate +{ +public: + SnoreToastsPrivate(SnoreToasts *parent, const std::wstring &appID) + : m_parent(parent), m_appID(appID), m_id(std::to_wstring(GetCurrentProcessId())) + { + } + SnoreToasts *m_parent; + + std::wstring m_appID; + std::filesystem::path m_pipeName; + std::filesystem::path m_application; + + std::wstring m_title; + std::wstring m_body; + std::filesystem::path m_image; + std::wstring m_sound = L"Notification.Default"; + std::wstring m_id; + std::wstring m_buttons; + bool m_silent = false; + bool m_wait = false; + bool m_textbox = false; + + SnoreToastActions::Actions m_action = SnoreToastActions::Actions::Clicked; + + ComPtr<IXmlDocument> m_toastXml; + ComPtr<Notifications::IToastNotificationManagerStatics> m_toastManager; + ComPtr<Notifications::IToastNotifier> m_notifier; + ComPtr<Notifications::IToastNotification> m_notification; + + ComPtr<ToastEventHandler> m_eventHanlder; + + static HANDLE ctoastEvent() + { + static HANDLE _event = [] { + std::wstringstream eventName; + eventName << L"ToastActivationEvent" << GetCurrentProcessId(); + return CreateEvent(nullptr, true, false, eventName.str().c_str()); + }(); + return _event; + } +}; + +SnoreToasts::SnoreToasts(const std::wstring &appID) : d(new SnoreToastsPrivate(this, appID)) { Utils::registerActivator(); } SnoreToasts::~SnoreToasts() { Utils::unregisterActivator(); + delete d; } void SnoreToasts::displayToast(const std::wstring &title, const std::wstring &body, const std::filesystem::path &image, bool wait) { HRESULT hr = S_OK; - m_title = title; - m_body = body; - m_image = image; - m_wait = wait; + d->m_title = title; + d->m_body = body; + d->m_image = image; + d->m_wait = wait; hr = GetActivationFactory( StringReferenceWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) .Get(), - &m_toastManager); + &d->m_toastManager); + if (!SUCCEEDED(hr)) { + std::wcerr << L"SnoreToasts: Failed to register com Factory, please make sure you " + L"correctly initialised with RO_INIT_MULTITHREADED" + << std::endl; + d->m_action = SnoreToastActions::Actions::Error; + return; + } + + if (!d->m_image.empty()) { + hr = d->m_toastManager->GetTemplateContent(ToastTemplateType_ToastImageAndText02, + &d->m_toastXml); + } else { + hr = d->m_toastManager->GetTemplateContent(ToastTemplateType_ToastText02, &d->m_toastXml); + } + if (SUCCEEDED(hr)) { - if (!m_image.empty()) { - hr = m_toastManager->GetTemplateContent(ToastTemplateType_ToastImageAndText02, - &m_toastXml); - } else { - hr = m_toastManager->GetTemplateContent(ToastTemplateType_ToastText02, &m_toastXml); - } + ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeList> rootList; + hr = d->m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"toast").Get(), &rootList); if (SUCCEEDED(hr)) { - ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeList> rootList; - hr = m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"toast").Get(), - &rootList); + ComPtr<IXmlNode> root; + hr = rootList->Item(0, &root); + ComPtr<IXmlNamedNodeMap> rootAttributes; + hr = root->get_Attributes(&rootAttributes); if (SUCCEEDED(hr)) { - ComPtr<IXmlNode> root; - hr = rootList->Item(0, &root); - - ComPtr<IXmlNamedNodeMap> rootAttributes; - hr = root->get_Attributes(&rootAttributes); - if (SUCCEEDED(hr)) { - const auto data = formatAction(SnoreToastActions::Actions::Clicked); - hr = addAttribute(L"launch", rootAttributes.Get(), data); - } - // Adding buttons - if (!m_buttons.empty()) { - setButtons(root); - } else if (m_textbox) { - setTextBox(root); - } - + const auto data = formatAction(SnoreToastActions::Actions::Clicked); + hr = addAttribute(L"launch", rootAttributes.Get(), data); + } + // Adding buttons + if (!d->m_buttons.empty()) { + setButtons(root); + } else if (d->m_textbox) { + setTextBox(root); + } + if (SUCCEEDED(hr)) { + ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> audioElement; + hr = d->m_toastXml->CreateElement(StringReferenceWrapper(L"audio").Get(), + &audioElement); if (SUCCEEDED(hr)) { - - ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> audioElement; - hr = m_toastXml->CreateElement(StringReferenceWrapper(L"audio").Get(), - &audioElement); + ComPtr<IXmlNode> audioNodeTmp; + hr = audioElement.As(&audioNodeTmp); if (SUCCEEDED(hr)) { - ComPtr<IXmlNode> audioNodeTmp; - hr = audioElement.As(&audioNodeTmp); + ComPtr<IXmlNode> audioNode; + hr = root->AppendChild(audioNodeTmp.Get(), &audioNode); if (SUCCEEDED(hr)) { - ComPtr<IXmlNode> audioNode; - hr = root->AppendChild(audioNodeTmp.Get(), &audioNode); - if (SUCCEEDED(hr)) { - ComPtr<IXmlNamedNodeMap> attributes; + ComPtr<IXmlNamedNodeMap> attributes; - hr = audioNode->get_Attributes(&attributes); + hr = audioNode->get_Attributes(&attributes); + if (SUCCEEDED(hr)) { + hr = addAttribute(L"src", attributes.Get()); if (SUCCEEDED(hr)) { - hr = addAttribute(L"src", attributes.Get()); - if (SUCCEEDED(hr)) { - addAttribute(L"silent", attributes.Get()); - } + addAttribute(L"silent", attributes.Get()); } } } } } } } } // printXML(); if (SUCCEEDED(hr)) { - if (!m_image.empty()) { + if (!d->m_image.empty()) { hr = setImage(); } if (SUCCEEDED(hr)) { hr = setSound(); if (SUCCEEDED(hr)) { hr = setTextValues(); if (SUCCEEDED(hr)) { printXML(); hr = createToast(); } } } } - m_action = + d->m_action = SUCCEEDED(hr) ? SnoreToastActions::Actions::Clicked : SnoreToastActions::Actions::Error; } SnoreToastActions::Actions SnoreToasts::userAction() { - if (m_eventHanlder.Get()) { - HANDLE event = m_eventHanlder.Get()->event(); + if (d->m_eventHanlder.Get()) { + HANDLE event = d->m_eventHanlder.Get()->event(); WaitForSingleObject(event, INFINITE); - m_action = m_eventHanlder.Get()->userAction(); - if (m_action == SnoreToastActions::Actions::Hidden) { - m_notifier->Hide(m_notification.Get()); + d->m_action = d->m_eventHanlder.Get()->userAction(); + if (d->m_action == SnoreToastActions::Actions::Hidden) { + d->m_notifier->Hide(d->m_notification.Get()); tLog << L"The application hid the toast using ToastNotifier.hide()"; } CloseHandle(event); } - return m_action; + return d->m_action; } bool SnoreToasts::closeNotification() { std::wstringstream eventName; - eventName << L"ToastEvent" << m_id; + eventName << L"ToastEvent" << d->m_id; HANDLE event = OpenEventW(EVENT_ALL_ACCESS, FALSE, eventName.str().c_str()); if (event) { SetEvent(event); return true; } - tLog << "Notification " << m_id << " does not exist"; + tLog << "Notification " << d->m_id << " does not exist"; return false; } void SnoreToasts::setSound(const std::wstring &soundFile) { - m_sound = soundFile; + d->m_sound = soundFile; } void SnoreToasts::setSilent(bool silent) { - m_silent = silent; + d->m_silent = silent; } void SnoreToasts::setId(const std::wstring &id) { if (!id.empty()) { - m_id = id; + d->m_id = id; } } std::wstring SnoreToasts::id() const { - return m_id; + return d->m_id; } void SnoreToasts::setButtons(const std::wstring &buttons) { - m_buttons = buttons; + d->m_buttons = buttons; } void SnoreToasts::setTextBoxEnabled(bool textBoxEnabled) { - m_textbox = textBoxEnabled; + d->m_textbox = textBoxEnabled; } // Set the value of the "src" attribute of the "image" node HRESULT SnoreToasts::setImage() { HRESULT hr = S_OK; ComPtr<IXmlNodeList> nodeList; - hr = m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"image").Get(), &nodeList); + hr = d->m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"image").Get(), &nodeList); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> imageNode; hr = nodeList->Item(0, &imageNode); if (SUCCEEDED(hr)) { ComPtr<IXmlNamedNodeMap> attributes; hr = imageNode->get_Attributes(&attributes); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> srcAttribute; hr = attributes->GetNamedItem(StringReferenceWrapper(L"src").Get(), &srcAttribute); if (SUCCEEDED(hr)) { - hr = setNodeValueString(StringReferenceWrapper(m_image).Get(), + hr = setNodeValueString(StringReferenceWrapper(d->m_image).Get(), srcAttribute.Get()); } } } } return hr; } HRESULT SnoreToasts::setSound() { HRESULT hr = S_OK; ComPtr<IXmlNodeList> nodeList; - hr = m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"audio").Get(), &nodeList); + hr = d->m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"audio").Get(), &nodeList); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> audioNode; hr = nodeList->Item(0, &audioNode); if (SUCCEEDED(hr)) { ComPtr<IXmlNamedNodeMap> attributes; hr = audioNode->get_Attributes(&attributes); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> srcAttribute; hr = attributes->GetNamedItem(StringReferenceWrapper(L"src").Get(), &srcAttribute); if (SUCCEEDED(hr)) { std::wstring sound; - if (m_sound.find(L"ms-winsoundevent:") == std::wstring::npos) { + if (d->m_sound.find(L"ms-winsoundevent:") == std::wstring::npos) { sound = L"ms-winsoundevent:"; - sound.append(m_sound); + sound.append(d->m_sound); } else { - sound = m_sound; + sound = d->m_sound; } hr = setNodeValueString(StringReferenceWrapper(sound).Get(), srcAttribute.Get()); if (SUCCEEDED(hr)) { hr = attributes->GetNamedItem(StringReferenceWrapper(L"silent").Get(), &srcAttribute); if (SUCCEEDED(hr)) { hr = setNodeValueString( - StringReferenceWrapper(m_silent ? L"true" : L"false").Get(), + StringReferenceWrapper(d->m_silent ? L"true" : L"false").Get(), srcAttribute.Get()); } } } } } } return hr; } // Set the values of each of the text nodes HRESULT SnoreToasts::setTextValues() { HRESULT hr = S_OK; if (SUCCEEDED(hr)) { ComPtr<IXmlNodeList> nodeList; - hr = m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"text").Get(), &nodeList); + hr = d->m_toastXml->GetElementsByTagName(StringReferenceWrapper(L"text").Get(), &nodeList); if (SUCCEEDED(hr)) { // create the title ComPtr<IXmlNode> textNode; hr = nodeList->Item(0, &textNode); if (SUCCEEDED(hr)) { - hr = setNodeValueString(StringReferenceWrapper(m_title).Get(), textNode.Get()); + hr = setNodeValueString(StringReferenceWrapper(d->m_title).Get(), textNode.Get()); if (SUCCEEDED(hr)) { hr = nodeList->Item(1, &textNode); if (SUCCEEDED(hr)) { - hr = setNodeValueString(StringReferenceWrapper(m_body).Get(), + hr = setNodeValueString(StringReferenceWrapper(d->m_body).Get(), textNode.Get()); } } } } } return hr; } HRESULT SnoreToasts::setButtons(ComPtr<IXmlNode> root) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> actionsElement; HRESULT hr = - m_toastXml->CreateElement(StringReferenceWrapper(L"actions").Get(), &actionsElement); + d->m_toastXml->CreateElement(StringReferenceWrapper(L"actions").Get(), &actionsElement); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionsNodeTmp; hr = actionsElement.As(&actionsNodeTmp); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionsNode; root->AppendChild(actionsNodeTmp.Get(), &actionsNode); std::wstring buttonText; - std::wstringstream wss(m_buttons); + std::wstringstream wss(d->m_buttons); while (std::getline(wss, buttonText, L';')) { hr &= createNewActionButton(actionsNode, buttonText); } } } return hr; } HRESULT SnoreToasts::setTextBox(ComPtr<IXmlNode> root) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> actionsElement; HRESULT hr = - m_toastXml->CreateElement(StringReferenceWrapper(L"actions").Get(), &actionsElement); + d->m_toastXml->CreateElement(StringReferenceWrapper(L"actions").Get(), &actionsElement); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionsNodeTmp; hr = actionsElement.As(&actionsNodeTmp); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionsNode; root->AppendChild(actionsNodeTmp.Get(), &actionsNode); ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> inputElement; - HRESULT hr = m_toastXml->CreateElement(StringReferenceWrapper(L"input").Get(), - &inputElement); + HRESULT hr = d->m_toastXml->CreateElement(StringReferenceWrapper(L"input").Get(), + &inputElement); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> inputNodeTmp; hr = inputElement.As(&inputNodeTmp); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> inputNode; actionsNode->AppendChild(inputNodeTmp.Get(), &inputNode); ComPtr<IXmlNamedNodeMap> inputAttributes; hr = inputNode->get_Attributes(&inputAttributes); if (SUCCEEDED(hr)) { hr &= addAttribute(L"id", inputAttributes.Get(), L"textBox"); hr &= addAttribute(L"type", inputAttributes.Get(), L"text"); hr &= addAttribute(L"placeHolderContent", inputAttributes.Get(), L"Type a reply"); } } ComPtr<IXmlElement> actionElement; - HRESULT hr = m_toastXml->CreateElement(StringReferenceWrapper(L"action").Get(), - &actionElement); + HRESULT hr = d->m_toastXml->CreateElement(StringReferenceWrapper(L"action").Get(), + &actionElement); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionNodeTmp; hr = actionElement.As(&actionNodeTmp); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionNode; actionsNode->AppendChild(actionNodeTmp.Get(), &actionNode); ComPtr<IXmlNamedNodeMap> actionAttributes; hr = actionNode->get_Attributes(&actionAttributes); if (SUCCEEDED(hr)) { hr &= addAttribute(L"content", actionAttributes.Get(), L"Send"); const auto data = formatAction(SnoreToastActions::Actions::ButtonClicked); hr &= addAttribute(L"arguments", actionAttributes.Get(), data); hr &= addAttribute(L"hint-inputId", actionAttributes.Get(), L"textBox"); } } } } } } return hr; } HRESULT SnoreToasts::setEventHandler(ComPtr<IToastNotification> toast) { // Register the event handlers EventRegistrationToken activatedToken, dismissedToken, failedToken; ComPtr<ToastEventHandler> eventHandler(new ToastEventHandler(*this)); HRESULT hr = toast->add_Activated(eventHandler.Get(), &activatedToken); if (SUCCEEDED(hr)) { hr = toast->add_Dismissed(eventHandler.Get(), &dismissedToken); if (SUCCEEDED(hr)) { hr = toast->add_Failed(eventHandler.Get(), &failedToken); if (SUCCEEDED(hr)) { - m_eventHanlder = eventHandler; + d->m_eventHanlder = eventHandler; } } } return hr; } HRESULT SnoreToasts::setNodeValueString(const HSTRING &inputString, IXmlNode *node) { ComPtr<IXmlText> inputText; - HRESULT hr = m_toastXml->CreateTextNode(inputString, &inputText); + HRESULT hr = d->m_toastXml->CreateTextNode(inputString, &inputText); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> inputTextNode; hr = inputText.As(&inputTextNode); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> pAppendedChild; hr = node->AppendChild(inputTextNode.Get(), &pAppendedChild); } } return hr; } HRESULT SnoreToasts::addAttribute(const std::wstring &name, IXmlNamedNodeMap *attributeMap) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlAttribute> srcAttribute; - HRESULT hr = m_toastXml->CreateAttribute(StringReferenceWrapper(name).Get(), &srcAttribute); + HRESULT hr = d->m_toastXml->CreateAttribute(StringReferenceWrapper(name).Get(), &srcAttribute); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> node; hr = srcAttribute.As(&node); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> pNode; hr = attributeMap->SetNamedItem(node.Get(), &pNode); } } return hr; } HRESULT SnoreToasts::addAttribute(const std::wstring &name, IXmlNamedNodeMap *attributeMap, const std::wstring &value) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlAttribute> srcAttribute; - HRESULT hr = m_toastXml->CreateAttribute(StringReferenceWrapper(name).Get(), &srcAttribute); + HRESULT hr = d->m_toastXml->CreateAttribute(StringReferenceWrapper(name).Get(), &srcAttribute); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> node; hr = srcAttribute.As(&node); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> pNode; hr = attributeMap->SetNamedItem(node.Get(), &pNode); hr = setNodeValueString(StringReferenceWrapper(value).Get(), node.Get()); } } return hr; } HRESULT SnoreToasts::createNewActionButton(ComPtr<IXmlNode> actionsNode, const std::wstring &value) { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlElement> actionElement; - HRESULT hr = m_toastXml->CreateElement(StringReferenceWrapper(L"action").Get(), &actionElement); + HRESULT hr = + d->m_toastXml->CreateElement(StringReferenceWrapper(L"action").Get(), &actionElement); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionNodeTmp; hr = actionElement.As(&actionNodeTmp); if (SUCCEEDED(hr)) { ComPtr<IXmlNode> actionNode; actionsNode->AppendChild(actionNodeTmp.Get(), &actionNode); ComPtr<IXmlNamedNodeMap> actionAttributes; hr = actionNode->get_Attributes(&actionAttributes); if (SUCCEEDED(hr)) { hr &= addAttribute(L"content", actionAttributes.Get(), value); const auto data = formatAction(SnoreToastActions::Actions::ButtonClicked, { { L"button", value } }); hr &= addAttribute(L"arguments", actionAttributes.Get(), data); hr &= addAttribute(L"activationType", actionAttributes.Get(), L"foreground"); } } } return hr; } void SnoreToasts::printXML() { ComPtr<ABI::Windows::Data::Xml::Dom::IXmlNodeSerializer> s; - ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> ss(m_toastXml); + ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> ss(d->m_toastXml); ss.As(&s); HSTRING string; s->GetXml(&string); PCWSTR str = WindowsGetStringRawBuffer(string, nullptr); tLog << L"------------------------\n\t\t\t" << str << L"\n\t\t" << L"------------------------"; } std::filesystem::path SnoreToasts::pipeName() const { - return m_pipeName; + return d->m_pipeName; } void SnoreToasts::setPipeName(const std::filesystem::path &pipeName) { - m_pipeName = pipeName; + d->m_pipeName = pipeName; } std::filesystem::path SnoreToasts::application() const { - return m_application; + return d->m_application; } void SnoreToasts::setApplication(const std::filesystem::path &application) { - m_application = application; + d->m_application = application; } std::wstring SnoreToasts::formatAction( const SnoreToastActions::Actions &action, const std::vector<std::pair<std::wstring_view, std::wstring_view>> &extraData) const { - const auto pipe = m_pipeName.wstring(); - const auto application = m_application.wstring(); + const auto pipe = d->m_pipeName.wstring(); + const auto application = d->m_application.wstring(); std::vector<std::pair<std::wstring_view, std::wstring_view>> data = { { L"action", SnoreToastActions::getActionString(action) }, - { L"notificationId", std::wstring_view(m_id) }, + { L"notificationId", std::wstring_view(d->m_id) }, { L"pipe", std::wstring_view(pipe) }, { L"application", std::wstring_view(application) } }; data.insert(data.end(), extraData.cbegin(), extraData.cend()); return Utils::formatData(data); } // Create and display the toast HRESULT SnoreToasts::createToast() { - HRESULT hr = m_toastManager->CreateToastNotifierWithId(StringReferenceWrapper(m_appID).Get(), - &m_notifier); + HRESULT hr = d->m_toastManager->CreateToastNotifierWithId( + StringReferenceWrapper(d->m_appID).Get(), &d->m_notifier); if (SUCCEEDED(hr)) { if (SUCCEEDED(hr)) { ComPtr<IToastNotificationFactory> factory; hr = GetActivationFactory( StringReferenceWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification) .Get(), &factory); if (SUCCEEDED(hr)) { - hr = factory->CreateToastNotification(m_toastXml.Get(), &m_notification); + hr = factory->CreateToastNotification(d->m_toastXml.Get(), &d->m_notification); if (SUCCEEDED(hr)) { - if (m_wait) { + if (d->m_wait) { NotificationSetting setting = NotificationSetting_Enabled; - m_notifier->get_Setting(&setting); + d->m_notifier->get_Setting(&setting); if (setting == NotificationSetting_Enabled) { - hr = setEventHandler(m_notification); + hr = setEventHandler(d->m_notification); } else { std::wcerr << L"Notifications are disabled" << std::endl << L"Reason: "; switch (setting) { case NotificationSetting_DisabledForApplication: std::wcerr << L"DisabledForApplication" << std::endl; break; case NotificationSetting_DisabledForUser: std::wcerr << L"DisabledForUser" << std::endl; break; case NotificationSetting_DisabledByGroupPolicy: std::wcerr << L"DisabledByGroupPolicy" << std::endl; break; case NotificationSetting_DisabledByManifest: std::wcerr << L"DisabledByManifest" << std::endl; break; } std::wcerr << L"Please make sure that the app id is set correctly." << std::endl; std::wcerr << L"Command Line: " << GetCommandLineW() << std::endl; } } if (SUCCEEDED(hr)) { - hr = m_notifier->Show(m_notification.Get()); + hr = d->m_notifier->Show(d->m_notification.Get()); } } } } } return hr; } std::wstring SnoreToasts::version() { // if there are changes to the callback mechanism we need to change the uuid for the activator // TOAST_UUID return L"0.5.99"; -} \ No newline at end of file +} + +HRESULT SnoreToasts::backgroundCallback(const std::wstring &appUserModelId, + const std::wstring &invokedArgs, const std::wstring &msg) +{ + tLog << "CToastNotificationActivationCallback::Activate: " << appUserModelId << " : " + << invokedArgs << " : " << msg; + const auto dataMap = Utils::splitData(invokedArgs); + const auto action = SnoreToastActions::getAction(dataMap.at(L"action")); + std::wstring dataString; + if (action == SnoreToastActions::Actions::TextEntered) { + std::wstringstream sMsg; + sMsg << invokedArgs << L"text=" << msg; + ; + dataString = sMsg.str(); + } else { + dataString = invokedArgs; + } + const auto pipe = dataMap.find(L"pipe"); + if (pipe != dataMap.cend()) { + if (!Utils::writePipe(pipe->second, dataString)) { + const auto app = dataMap.find(L"application"); + if (app != dataMap.cend()) { + if (Utils::startProcess(app->second)) { + Utils::writePipe(pipe->second, dataString, true); + } + } + } + } + + tLog << dataString; + if (!SetEvent(SnoreToastsPrivate::ctoastEvent())) { + tLog << "SetEvent failed" << GetLastError(); + return S_FALSE; + } + return S_OK; +} +void SnoreToasts::waitForCallbackActivation() +{ + Utils::registerActivator(); + WaitForSingleObject(SnoreToastsPrivate::ctoastEvent(), INFINITE); + Utils::unregisterActivator(); +} diff --git a/src/snoretoasts.h b/src/snoretoasts.h index e56bdea..ecb155b 100644 --- a/src/snoretoasts.h +++ b/src/snoretoasts.h @@ -1,123 +1,103 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth <vonreth@kde.org> SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #pragma once -#include "stringreferencewrapper.h" #include "snoretoastactions.h" +#include "libsnoretoast_export.h" #include <sdkddkver.h> // Windows Header Files: #include <windows.h> #include <sal.h> #include <psapi.h> #include <strsafe.h> #include <objbase.h> #include <shobjidl.h> #include <functiondiscoverykeys.h> #include <guiddef.h> #include <shlguid.h> #include <wrl/client.h> #include <wrl/implements.h> #include <windows.ui.notifications.h> #include <filesystem> #include <string> #include <vector> using namespace Microsoft::WRL; using namespace ABI::Windows::Data::Xml::Dom; -class ToastEventHandler; - -class SnoreToasts +class LIBSNORETOAST_EXPORT SnoreToasts { public: static std::wstring version(); + static void waitForCallbackActivation(); + static HRESULT backgroundCallback(const std::wstring &appUserModelId, + const std::wstring &invokedArgs, const std::wstring &msg); SnoreToasts(const std::wstring &appID); ~SnoreToasts(); void displayToast(const std::wstring &title, const std::wstring &body, const std::filesystem::path &image, bool wait); SnoreToastActions::Actions userAction(); bool closeNotification(); void setSound(const std::wstring &soundFile); void setSilent(bool silent); void setId(const std::wstring &id); std::wstring id() const; void setButtons(const std::wstring &buttons); void setTextBoxEnabled(bool textBoxEnabled); std::filesystem::path pipeName() const; void setPipeName(const std::filesystem::path &pipeName); std::filesystem::path application() const; void setApplication(const std::filesystem::path &application); std::wstring formatAction(const SnoreToastActions::Actions &action, const std::vector<std::pair<std::wstring_view, std::wstring_view>> &extraData = {}) const; private: HRESULT createToast(); HRESULT setImage(); HRESULT setSound(); HRESULT setTextValues(); HRESULT setButtons(ComPtr<IXmlNode> root); HRESULT setTextBox(ComPtr<IXmlNode> root); HRESULT setEventHandler( Microsoft::WRL::ComPtr<ABI::Windows::UI::Notifications::IToastNotification> toast); HRESULT setNodeValueString(const HSTRING &onputString, ABI::Windows::Data::Xml::Dom::IXmlNode *node); HRESULT addAttribute(const std::wstring &name, ABI::Windows::Data::Xml::Dom::IXmlNamedNodeMap *attributeMap); HRESULT addAttribute(const std::wstring &name, ABI::Windows::Data::Xml::Dom::IXmlNamedNodeMap *attributeMap, const std::wstring &value); HRESULT createNewActionButton(ComPtr<IXmlNode> actionsNode, const std::wstring &value); void printXML(); - std::wstring m_appID; - std::filesystem::path m_pipeName; - std::filesystem::path m_application; - - std::wstring m_title; - std::wstring m_body; - std::filesystem::path m_image; - std::wstring m_sound = L"Notification.Default"; - std::wstring m_id; - std::wstring m_buttons; - bool m_silent; - bool m_wait; - bool m_textbox; - - SnoreToastActions::Actions m_action = SnoreToastActions::Actions::Clicked; - - Microsoft::WRL::ComPtr<ABI::Windows::Data::Xml::Dom::IXmlDocument> m_toastXml; - Microsoft::WRL::ComPtr<ABI::Windows::UI::Notifications::IToastNotificationManagerStatics> - m_toastManager; - Microsoft::WRL::ComPtr<ABI::Windows::UI::Notifications::IToastNotifier> m_notifier; - Microsoft::WRL::ComPtr<ABI::Windows::UI::Notifications::IToastNotification> m_notification; - - Microsoft::WRL::ComPtr<ToastEventHandler> m_eventHanlder; + friend class SnoreToastsPrivate; + SnoreToastsPrivate *d; }; diff --git a/src/stringreferencewrapper.h b/src/stringreferencewrapper.h index 76f29cc..adbf44d 100644 --- a/src/stringreferencewrapper.h +++ b/src/stringreferencewrapper.h @@ -1,69 +1,68 @@ -#include "snoretoasts.h" #include <string> #include <winstring.h> #include <hstring.h> class StringReferenceWrapper { public: // Constructor which takes an existing string buffer and its length as the parameters. // It fills an HSTRING_HEADER struct with the parameter. // Warning: The caller must ensure the lifetime of the buffer outlives this // object as it does not make a copy of the wide string memory. StringReferenceWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) throw() { HRESULT hr = WindowsCreateStringReference(stringRef, length, &_header, &_hstring); if (FAILED(hr)) { RaiseException(static_cast<DWORD>(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); } } StringReferenceWrapper(const std::wstring &stringRef) throw() : m_data(stringRef) { HRESULT hr = WindowsCreateStringReference( m_data.c_str(), static_cast<UINT32>(m_data.length()), &_header, &_hstring); if (FAILED(hr)) { RaiseException(static_cast<DWORD>(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); } } ~StringReferenceWrapper() { WindowsDeleteString(_hstring); } template<size_t N> StringReferenceWrapper(_In_reads_(N) wchar_t const (&stringRef)[N]) throw() { UINT32 length = N - 1; HRESULT hr = WindowsCreateStringReference(stringRef, length, &_header, &_hstring); if (FAILED(hr)) { RaiseException(static_cast<DWORD>(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); } } template<size_t _> StringReferenceWrapper(_In_reads_(_) wchar_t (&stringRef)[_]) throw() { UINT32 length; HRESULT hr = SizeTToUInt32(wcslen(stringRef), &length); if (FAILED(hr)) { RaiseException(static_cast<DWORD>(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); } WindowsCreateStringReference(stringRef, length, &_header, &_hstring); } HSTRING Get() const throw() { return _hstring; } private: HSTRING _hstring; HSTRING_HEADER _header; std::wstring m_data; }; diff --git a/src/toasteventhandler.cpp b/src/toasteventhandler.cpp index 0de7456..d45d92f 100644 --- a/src/toasteventhandler.cpp +++ b/src/toasteventhandler.cpp @@ -1,189 +1,126 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth <vonreth@kde.org> SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #include "snoretoasts.h" #include "toasteventhandler.h" #include "utils.h" #include <sstream> #include <iostream> #include <wchar.h> #include <algorithm> #include <assert.h> using namespace ABI::Windows::UI::Notifications; ToastEventHandler::ToastEventHandler(const SnoreToasts &toast) : m_ref(1), m_userAction(SnoreToastActions::Actions::Hidden), m_toast(toast) { std::wstringstream eventName; eventName << L"ToastEvent" << m_toast.id(); m_event = CreateEventW(nullptr, true, false, eventName.str().c_str()); } ToastEventHandler::~ToastEventHandler() { CloseHandle(m_event); } HANDLE ToastEventHandler::event() { return m_event; } SnoreToastActions::Actions &ToastEventHandler::userAction() { return m_userAction; } // DesktopToastActivatedEventHandler IFACEMETHODIMP ToastEventHandler::Invoke(_In_ IToastNotification * /*sender*/, _In_ IInspectable *args) { IToastActivatedEventArgs *buttonReply = nullptr; args->QueryInterface(&buttonReply); if (buttonReply == nullptr) { std::wcerr << L"args is not a IToastActivatedEventArgs" << std::endl; } else { HSTRING args; buttonReply->get_Arguments(&args); std::wstring data = WindowsGetStringRawBuffer(args, nullptr); tLog << data; const auto dataMap = Utils::splitData(data); const auto action = SnoreToastActions::getAction(dataMap.at(L"action")); assert(dataMap.at(L"notificationId") == m_toast.id()); if (action == SnoreToastActions::Actions::TextEntered) { // The text is only passed to the named pipe tLog << L"The user entered a text."; m_userAction = SnoreToastActions::Actions::TextEntered; } else if (action == SnoreToastActions::Actions::Clicked) { tLog << L"The user clicked on the toast."; m_userAction = SnoreToastActions::Actions::Clicked; } else { tLog << L"The user clicked on a toast button."; std::wcout << dataMap.at(L"button") << std::endl; m_userAction = SnoreToastActions::Actions::ButtonClicked; } } SetEvent(m_event); return S_OK; } // DesktopToastDismissedEventHandler IFACEMETHODIMP ToastEventHandler::Invoke(_In_ IToastNotification * /* sender */, _In_ IToastDismissedEventArgs *e) { ToastDismissalReason tdr; HRESULT hr = e->get_Reason(&tdr); if (SUCCEEDED(hr)) { switch (tdr) { case ToastDismissalReason_ApplicationHidden: tLog << L"The application hid the toast using ToastNotifier.hide()"; m_userAction = SnoreToastActions::Actions::Hidden; break; case ToastDismissalReason_UserCanceled: tLog << L"The user dismissed this toast"; m_userAction = SnoreToastActions::Actions::Dismissed; break; case ToastDismissalReason_TimedOut: tLog << L"The toast has timed out"; m_userAction = SnoreToastActions::Actions::Timedout; break; } } if (!m_toast.pipeName().empty()) { Utils::writePipe(m_toast.pipeName(), m_toast.formatAction(m_userAction)); } SetEvent(m_event); return S_OK; } // DesktopToastFailedEventHandler IFACEMETHODIMP ToastEventHandler::Invoke(_In_ IToastNotification * /* sender */, _In_ IToastFailedEventArgs * /* e */) { std::wcerr << L"The toast encountered an error." << std::endl; std::wcerr << L"Please make sure that the app id is set correctly." << std::endl; std::wcerr << L"Command Line: " << GetCommandLineW() << std::endl; m_userAction = SnoreToastActions::Actions::Error; SetEvent(m_event); return S_OK; } - -CToastNotificationActivationCallback::CToastNotificationActivationCallback() {} - -HANDLE CToastNotificationActivationCallback::event() -{ - static HANDLE _event = [] { - std::wstringstream eventName; - eventName << L"ToastActivationEvent" << GetCurrentProcessId(); - return CreateEvent(nullptr, true, false, eventName.str().c_str()); - }(); - return _event; -} - -HRESULT CToastNotificationActivationCallback::Activate(LPCWSTR appUserModelId, LPCWSTR invokedArgs, - const NOTIFICATION_USER_INPUT_DATA *data, - ULONG count) -{ - if (invokedArgs == nullptr) { - return S_OK; - } - tLog << "CToastNotificationActivationCallback::Activate: " << appUserModelId << " : " - << invokedArgs << " : " << data; - const auto dataMap = Utils::splitData(invokedArgs); - const auto action = SnoreToastActions::getAction(dataMap.at(L"action")); - std::wstring dataString; - if (action == SnoreToastActions::Actions::TextEntered) { - assert(count); - std::wstringstream sMsg; - sMsg << invokedArgs << L"text="; - for (ULONG i = 0; i < count; ++i) { - std::wstring tmp = data[i].Value; - // printing \r to stdcout is kind of problematic :D - std::replace(tmp.begin(), tmp.end(), L'\r', L'\n'); - sMsg << tmp; - } - dataString = sMsg.str(); - } else { - dataString = invokedArgs; - } - const auto pipe = dataMap.find(L"pipe"); - if (pipe != dataMap.cend()) { - if (!Utils::writePipe(pipe->second, dataString)) { - const auto app = dataMap.find(L"application"); - if (app != dataMap.cend()) { - if (Utils::startProcess(app->second)) { - Utils::writePipe(pipe->second, dataString, true); - } - } - } - } - - tLog << dataString; - if (!SetEvent(event())) { - tLog << "SetEvent failed" << GetLastError(); - return S_FALSE; - } - return S_OK; -} - -void CToastNotificationActivationCallback::waitForActivation() -{ - WaitForSingleObject(event(), INFINITE); -} \ No newline at end of file diff --git a/src/toasteventhandler.h b/src/toasteventhandler.h index 28400d4..e7e4b09 100644 --- a/src/toasteventhandler.h +++ b/src/toasteventhandler.h @@ -1,140 +1,96 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2013-2019 Hannah von Reth <vonreth@kde.org> SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #pragma once #include "snoretoasts.h" -#include "wrl.h" - -#define TOAST_UUID "{383803B6-AFDA-4220-BFC3-0DBF810106BF}" typedef ABI::Windows::Foundation::ITypedEventHandler< ABI::Windows::UI::Notifications::ToastNotification *, ::IInspectable *> DesktopToastActivatedEventHandler; typedef ABI::Windows::Foundation::ITypedEventHandler< ABI::Windows::UI::Notifications::ToastNotification *, ABI::Windows::UI::Notifications::ToastDismissedEventArgs *> DesktopToastDismissedEventHandler; typedef ABI::Windows::Foundation::ITypedEventHandler< ABI::Windows::UI::Notifications::ToastNotification *, ABI::Windows::UI::Notifications::ToastFailedEventArgs *> DesktopToastFailedEventHandler; -// Define INotificationActivationCallback for older versions of the Windows SDK -#include <ntverp.h> - -typedef struct NOTIFICATION_USER_INPUT_DATA -{ - LPCWSTR Key; - LPCWSTR Value; -} NOTIFICATION_USER_INPUT_DATA; - -MIDL_INTERFACE("53E31837-6600-4A81-9395-75CFFE746F94") -INotificationActivationCallback : public IUnknown -{ -public: - virtual HRESULT STDMETHODCALLTYPE Activate( - __RPC__in_string LPCWSTR appUserModelId, __RPC__in_opt_string LPCWSTR invokedArgs, - __RPC__in_ecount_full_opt(count) const NOTIFICATION_USER_INPUT_DATA *data, - ULONG count) = 0; -}; - -// The COM server which implements the callback notifcation from Action Center -class DECLSPEC_UUID(TOAST_UUID) CToastNotificationActivationCallback - : public Microsoft::WRL::RuntimeClass< - Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, - INotificationActivationCallback> -{ -public: - static void waitForActivation(); - - CToastNotificationActivationCallback(); - virtual HRESULT STDMETHODCALLTYPE Activate(__RPC__in_string LPCWSTR appUserModelId, - __RPC__in_opt_string LPCWSTR invokedArgs, - __RPC__in_ecount_full_opt(count) - const NOTIFICATION_USER_INPUT_DATA *data, - ULONG count) override; - -private: - static HANDLE event(); -}; - -CoCreatableClass(CToastNotificationActivationCallback); - class ToastEventHandler : public Microsoft::WRL::Implements<DesktopToastActivatedEventHandler, DesktopToastDismissedEventHandler, DesktopToastFailedEventHandler> { public: explicit ToastEventHandler::ToastEventHandler(const SnoreToasts &toast); ~ToastEventHandler(); HANDLE event(); SnoreToastActions::Actions &userAction(); // DesktopToastActivatedEventHandler IFACEMETHODIMP Invoke(_In_ ABI::Windows::UI::Notifications::IToastNotification *sender, _In_ IInspectable *args); // DesktopToastDismissedEventHandler IFACEMETHODIMP Invoke(_In_ ABI::Windows::UI::Notifications::IToastNotification *sender, _In_ ABI::Windows::UI::Notifications::IToastDismissedEventArgs *e); // DesktopToastFailedEventHandler IFACEMETHODIMP Invoke(_In_ ABI::Windows::UI::Notifications::IToastNotification *sender, _In_ ABI::Windows::UI::Notifications::IToastFailedEventArgs *e); // IUnknown IFACEMETHODIMP_(ULONG) AddRef() { return InterlockedIncrement(&m_ref); } IFACEMETHODIMP_(ULONG) Release() { ULONG l = InterlockedDecrement(&m_ref); if (l == 0) { delete this; } return l; } IFACEMETHODIMP QueryInterface(_In_ REFIID riid, _COM_Outptr_ void **ppv) { if (IsEqualIID(riid, IID_IUnknown)) { *ppv = static_cast<IUnknown *>(static_cast<DesktopToastActivatedEventHandler *>(this)); } else if (IsEqualIID(riid, __uuidof(DesktopToastActivatedEventHandler))) { *ppv = static_cast<DesktopToastActivatedEventHandler *>(this); } else if (IsEqualIID(riid, __uuidof(DesktopToastDismissedEventHandler))) { *ppv = static_cast<DesktopToastDismissedEventHandler *>(this); } else if (IsEqualIID(riid, __uuidof(DesktopToastFailedEventHandler))) { *ppv = static_cast<DesktopToastFailedEventHandler *>(this); } else { *ppv = nullptr; } if (*ppv) { reinterpret_cast<IUnknown *>(*ppv)->AddRef(); return S_OK; } return E_NOINTERFACE; } private: ULONG m_ref; SnoreToastActions::Actions m_userAction; HANDLE m_event; const SnoreToasts &m_toast; }; diff --git a/src/utils.cpp b/src/utils.cpp index 1ef9e21..60a45a5 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -1,154 +1,155 @@ /* SnoreToast is capable to invoke Windows 8 toast notifications. Copyright (C) 2019 Hannah von Reth <vonreth@kde.org> SnoreToast is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. SnoreToast 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with SnoreToast. If not, see <http://www.gnu.org/licenses/>. */ #include "utils.h" #include "snoretoasts.h" #include <wrl/client.h> #include <wrl/implements.h> #include <wrl/module.h> using namespace Microsoft::WRL; namespace { bool s_registered = false; } + namespace Utils { bool registerActivator() { if (!s_registered) { s_registered = true; Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::Create([] {}); Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule().IncrementObjectCount(); return SUCCEEDED( Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule().RegisterObjects()); } return true; } void unregisterActivator() { if (s_registered) { s_registered = false; Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule().UnregisterObjects(); Microsoft::WRL::Module<Microsoft::WRL::OutOfProc>::GetModule().DecrementObjectCount(); } } std::unordered_map<std::wstring_view, std::wstring_view> splitData(const std::wstring_view &data) { std::unordered_map<std::wstring_view, std::wstring_view> out; size_t start = 0; for (size_t end = data.find(L";", start); end != std::wstring::npos; start = end + 1, end = data.find(L";", start)) { if (start == end) { end = data.size(); } const std::wstring_view tmp(data.data() + start, end - start); const auto pos = tmp.find(L"="); out[tmp.substr(0, pos)] = tmp.substr(pos + 1); // tLog << L"'" << tmp.substr(0, pos) << L"' = '" << tmp.substr(pos + 1) << L"'"; } return out; } const std::filesystem::path &selfLocate() { static const std::filesystem::path path = [] { std::wstring buf; size_t size; do { buf.resize(buf.size() + 1024); size = GetModuleFileNameW(nullptr, const_cast<wchar_t *>(buf.data()), static_cast<DWORD>(buf.size())); } while (GetLastError() == ERROR_INSUFFICIENT_BUFFER); buf.resize(size); return buf; }(); return path; } bool writePipe(const std::filesystem::path &pipe, const std::wstring &data, bool wait) { HANDLE hPipe = CreateFile(pipe.wstring().c_str(), GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); if (hPipe != INVALID_HANDLE_VALUE) { DWORD written; const DWORD toWrite = static_cast<DWORD>(data.size() * sizeof(wchar_t)); WriteFile(hPipe, data.c_str(), toWrite, &written, nullptr); const bool success = written == toWrite; tLog << (success ? L"Wrote: " : L"Failed to write: ") << data << " to " << pipe; WriteFile(hPipe, nullptr, sizeof(wchar_t), &written, nullptr); CloseHandle(hPipe); return success; } else if (wait) { // wait and retry Sleep(20000); return writePipe(pipe, data); } tLog << L"Failed to open pipe: " << pipe << L" data: " << data; return false; } bool startProcess(const std::filesystem::path &app) { STARTUPINFO info = {}; info.cb = sizeof(info); PROCESS_INFORMATION pInfo = {}; const auto application = app.wstring(); if (!CreateProcess(const_cast<wchar_t *>(application.c_str()), const_cast<wchar_t *>(application.c_str()), nullptr, nullptr, false, DETACHED_PROCESS | INHERIT_PARENT_AFFINITY | CREATE_NO_WINDOW, nullptr, nullptr, &info, &pInfo)) { tLog << L"Failed to start: " << app; return false; } WaitForInputIdle(pInfo.hProcess, INFINITE); DWORD status; GetExitCodeProcess(pInfo.hProcess, &status); CloseHandle(pInfo.hProcess); CloseHandle(pInfo.hThread); tLog << L"Started: " << app << L" Status: " << (status == STILL_ACTIVE ? L"STILL_ACTIVE" : std::to_wstring(status)); return status == STILL_ACTIVE; } std::wstring formatData(const std::vector<std::pair<std::wstring_view, std::wstring_view>> &data) { std::wstringstream out; for (const auto &p : data) { if (!p.second.empty()) { out << p.first << L"=" << p.second << L";"; } } return out.str(); } } ToastLog::ToastLog() { *this << Utils::selfLocate() << L" v" << SnoreToasts::version() << L"\n\t"; } ToastLog::~ToastLog() { m_log << L"\n"; OutputDebugStringW(m_log.str().c_str()); }