diff --git a/.github/workflows/photino.Native.Development.yml b/.github/workflows/photino.Native.Development.yml new file mode 100644 index 0000000..db245c3 --- /dev/null +++ b/.github/workflows/photino.Native.Development.yml @@ -0,0 +1,262 @@ +name: Build Photino.Native + +on: + push: + branches: [ master ] + workflow_dispatch: + +env: + buildConfiguration: Release + MAJOR: 4 + MINOR: 0 + # Note: PATCH below is set per-job in "PublishPackage" using github.run_number + +jobs: + Publish_Nuspec: + name: Publish .nuspec + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Upload .nuspec artifact + uses: actions/upload-artifact@v4 + with: + name: .nuspec + path: ./Photino.Native/Photino.Native.nuspec + + Build_Win_x64: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + + - name: Restore (Windows x64) + run: nuget restore Photino.Native.sln -NonInteractive + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Build (Windows x64) + run: > + msbuild ./Photino.Native/Photino.Native.vcxproj + /p:Configuration=${{ env.buildConfiguration }} + /p:Platform=x64 + + - name: Upload Windows x64 libs + uses: actions/upload-artifact@v4 + with: + name: photino.Native-Windows-x64 + path: ./Photino.Native/x64/${{ env.buildConfiguration }} + + Build_Win_Arm64: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + + - name: Restore (Windows ARM64) + run: nuget restore Photino.Native.sln -NonInteractive + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Build (Windows ARM64) + run: > + msbuild ./Photino.Native/Photino.Native.vcxproj + /p:Configuration=${{ env.buildConfiguration }} + /p:Platform=ARM64 + + - name: Upload Windows ARM64 libs + uses: actions/upload-artifact@v4 + with: + name: photino.Native-Windows-arm64 + path: ./Photino.Native/arm64/${{ env.buildConfiguration }} + + Build_Linux_x64: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + + - name: Build (Linux x64) + shell: bash + run: | + mkdir -p ./Photino.Native/x64/${{ env.buildConfiguration }} + make linux-x64 + mv ./lib/x64/Photino.Native.so ./Photino.Native/x64/${{ env.buildConfiguration }}/Photino.Native.so + + - name: Upload Linux x64 libs + uses: actions/upload-artifact@v4 + with: + name: photino.Native-Linux-x64 + path: ./Photino.Native/x64/${{ env.buildConfiguration }} + + # If/when you want to build Linux ARM64 on native hardware or via cross, uncomment and adapt: + # Build_Linux_Arm64: + # runs-on: ubuntu-24.04 + # steps: + # - uses: actions/checkout@v4 + # - name: Build (Linux ARM64) + # shell: bash + # run: | + # mkdir -p ./Photino.Native/arm64/${{ env.buildConfiguration }} + # make linux-dev + # mv ./lib/dev/Photino.Native.so ./Photino.Native/arm64/${{ env.buildConfiguration }}/Photino.Native.so + # - name: Upload Linux ARM64 libs + # uses: actions/upload-artifact@v4 + # with: + # name: LinuxArm64Artifact + # path: ./Photino.Native/arm64/${{ env.buildConfiguration }} + + Build_Mac_Universal: + runs-on: macos-latest + steps: + - uses: actions/checkout@v4 + + - name: Build (macOS universal) + shell: bash + run: | + mkdir -p ./Photino.Native/x64/${{ env.buildConfiguration }} + mkdir -p ./Photino.Native/arm64/${{ env.buildConfiguration }} + make mac-universal + cp ./lib/x64/Photino.Native.dylib ./Photino.Native/x64/${{ env.buildConfiguration }}/Photino.Native.dylib + cp ./lib/x64/Photino.Native.dylib ./Photino.Native/arm64/${{ env.buildConfiguration }}/Photino.Native.dylib + + - name: Upload macOS x64 dylib + uses: actions/upload-artifact@v4 + with: + name: photino.Native-macOS-x64 + path: ./Photino.Native/x64/${{ env.buildConfiguration }}/Photino.Native.dylib + + - name: Upload macOS ARM64 dylib + uses: actions/upload-artifact@v4 + with: + name: photino.Native-macOS-arm64 + path: ./Photino.Native/arm64/${{ env.buildConfiguration }}/Photino.Native.dylib + +# Copy_Manual_Arm64_Releases: +# runs-on: ubuntu-24.04 +# steps: +# - uses: actions/checkout@v4 +# +# - name: Copy ARM64 native assets +# shell: bash +# run: | +# mkdir -p ./Photino.Native/arm64/${{ env.buildConfiguration }} +# mv ./manual-arm-release/linux-arm64/Photino.Native.so ./Photino.Native/arm64/${{ env.buildConfiguration }}/Photino.Native.so +# +# - name: Upload Linux ARM64 libs +# uses: actions/upload-artifact@v4 +# with: +# name: LinuxArm64Artifact +# path: ./Photino.Native/arm64/${{ env.buildConfiguration }} + +# PublishPackage: +# runs-on: windows-latest +# needs: +# - Publish_Nuspec +# - Build_Win_x64 +# - Build_Win_Arm64 +# - Build_Linux_x64 +# - Build_Mac_Universal +# - Copy_Manual_Arm64_Releases +# steps: +# - name: Prepare workspace directory +# run: mkdir $env:GITHUB_WORKSPACE\pkg +# +# - name: Download NuspecArtifact +# uses: actions/download-artifact@v4 +# with: +# name: NuspecArtifact +# path: ${{ github.workspace }}\pkg +# +# - name: Download WindowsIntelArtifact +# uses: actions/download-artifact@v4 +# with: +# name: WindowsIntelArtifact +# path: ${{ github.workspace }}\pkg\x64 +# +# - name: Download WindowsArm64Artifact +# uses: actions/download-artifact@v4 +# with: +# name: WindowsArm64Artifact +# path: ${{ github.workspace }}\pkg\arm64 +# +# - name: Download LinuxIntelArtifact +# uses: actions/download-artifact@v4 +# with: +# name: LinuxIntelArtifact +# path: ${{ github.workspace }}\pkg\x64 +# +# - name: Download LinuxArm64Artifact +# uses: actions/download-artifact@v4 +# with: +# name: LinuxArm64Artifact +# path: ${{ github.workspace }}\pkg\arm64 +# +# - name: Download MacOsIntelArtifact +# uses: actions/download-artifact@v4 +# with: +# name: MacOsIntelArtifact +# path: ${{ github.workspace }}\pkg\x64 +# +# - name: Download MacOsArm64Artifact +# uses: actions/download-artifact@v4 +# with: +# name: MacOsArm64Artifact +# path: ${{ github.workspace }}\pkg\arm64 +# +# - name: List downloaded files (root) +# shell: pwsh +# run: Get-ChildItem -Recurse $env:GITHUB_WORKSPACE\pkg | Format-Table -AutoSize +# +# - name: List x64 +# shell: pwsh +# run: Get-ChildItem -Recurse $env:GITHUB_WORKSPACE\pkg\x64 | Format-Table -AutoSize +# +# - name: List arm64 +# shell: pwsh +# run: Get-ChildItem -Recurse $env:GITHUB_WORKSPACE\pkg\arm64 | Format-Table -AutoSize +# +# - name: Setup NuGet +# uses: NuGet/setup-nuget@v2 +# +# - name: Compute version +# id: ver +# shell: pwsh +# run: | +# $patch = "${{ github.run_number }}" +# $version = "${{ env.MAJOR }}.${{ env.MINOR }}.$patch" +# "patch=$patch" | Out-File -FilePath $env:GITHUB_OUTPUT -Append +# "version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append +# Write-Host "Version: $version" +# +# - name: Pack nupkg +# shell: pwsh +# working-directory: ${{ github.workspace }}\pkg +# run: | +# nuget pack Photino.Native.nuspec ` +# -Version "${{ steps.ver.outputs.version }}" ` +# -NonInteractive ` +# -Properties "version=${{ steps.ver.outputs.version }}" +# +# - name: Upload nupkg as artifact (optional) +# uses: actions/upload-artifact@v4 +# with: +# name: Photino.Native.nupkg +# path: ${{ github.workspace }}\pkg\**\*.nupkg +# +# - name: Push to nuget.org +# if: ${{ github.ref == 'refs/heads/master' }} +# shell: pwsh +# working-directory: ${{ github.workspace }}\pkg +# env: +# NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} +# run: | +# nuget push **\*.nupkg ` +# -Source "https://api.nuget.org/v3/index.json" ` +# -ApiKey "$env:NUGET_API_KEY" ` +# -NonInteractive \ No newline at end of file diff --git a/Photino.Native/Dependencies/wintoastlib.cpp b/Photino.Native/Dependencies/wintoastlib.cpp index 726950a..4317740 100644 --- a/Photino.Native/Dependencies/wintoastlib.cpp +++ b/Photino.Native/Dependencies/wintoastlib.cpp @@ -1,4 +1,7 @@ -/* * Copyright (C) 2016-2019 Mohammed Boujemaoui +/** + * MIT License + * + * Copyright (C) 2016-2023 WinToast v1.3.0 - Mohammed Boujemaoui * * 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 @@ -16,86 +19,103 @@ * 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. - */ +*/ #include "wintoastlib.h" + #include #include #include #include +#include -#pragma comment(lib,"shlwapi") -#pragma comment(lib,"user32") +#pragma comment(lib, "shlwapi") +#pragma comment(lib, "user32") + +#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\" +#define DEFAULT_LINK_FORMAT L".lnk" +#define STATUS_SUCCESS (0x00000000) #ifdef NDEBUG - #define DEBUG_MSG(str) do { } while ( false ) +static bool DebugOutputEnabled = false; #else - #define DEBUG_MSG(str) do { std::wcout << str << std::endl; } while( false ) +static bool DebugOutputEnabled = true; #endif -#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\" -#define DEFAULT_LINK_FORMAT L".lnk" -#define STATUS_SUCCESS (0x00000000) - +#define DEBUG_MSG(str) \ + do { \ + if (DebugOutputEnabled) { \ + std::wcout << str << std::endl; \ + } \ + } while (false) // Quickstart: Handling toast activations from Win32 apps in Windows 10 // https://blogs.msdn.microsoft.com/tiles_and_toasts/2015/10/16/quickstart-handling-toast-activations-from-win32-apps-in-windows-10/ using namespace WinToastLib; + +void WinToastLib::setDebugOutputEnabled(bool enabled) { + DebugOutputEnabled = enabled; +} + namespace DllImporter { // Function load a function from library template - HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function &func) { - if (!library) { - return E_INVALIDARG; - } + HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function& func) { + if (!library) { + return E_INVALIDARG; + } func = reinterpret_cast(GetProcAddress(library, name)); return (func != nullptr) ? S_OK : E_FAIL; } - typedef HRESULT(FAR STDAPICALLTYPE *f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); - typedef HRESULT(FAR STDAPICALLTYPE *f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); - typedef HRESULT(FAR STDAPICALLTYPE *f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, _COM_Outptr_ void ** factory); - typedef HRESULT(FAR STDAPICALLTYPE *f_WindowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, _Out_ HSTRING_HEADER * hstringHeader, _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING * string); - typedef PCWSTR(FAR STDAPICALLTYPE *f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_opt_ UINT32 *length); - typedef HRESULT(FAR STDAPICALLTYPE *f_WindowsDeleteString)(_In_opt_ HSTRING string); + typedef HRESULT(FAR STDAPICALLTYPE* f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID); + typedef HRESULT(FAR STDAPICALLTYPE* f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch); + typedef HRESULT(FAR STDAPICALLTYPE* f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, + _COM_Outptr_ void** factory); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, + _Out_ HSTRING_HEADER* hstringHeader, + _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING* string); + typedef PCWSTR(FAR STDAPICALLTYPE* f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_opt_ UINT32* length); + typedef HRESULT(FAR STDAPICALLTYPE* f_WindowsDeleteString)(_In_opt_ HSTRING string); - static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; - static f_PropVariantToString PropVariantToString; - static f_RoGetActivationFactory RoGetActivationFactory; - static f_WindowsCreateStringReference WindowsCreateStringReference; - static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer; - static f_WindowsDeleteString WindowsDeleteString; + static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID; + static f_PropVariantToString PropVariantToString; + static f_RoGetActivationFactory RoGetActivationFactory; + static f_WindowsCreateStringReference WindowsCreateStringReference; + static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer; + static f_WindowsDeleteString WindowsDeleteString; - - template - _Check_return_ __inline HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) { + template + __inline _Check_return_ HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) { return RoGetActivationFactory(activatableClassId, IID_INS_ARGS(factory)); } - template + template inline HRESULT Wrap_GetActivationFactory(_In_ HSTRING activatableClassId, _Inout_ Details::ComPtrRef factory) noexcept { return _1_GetActivationFactory(activatableClassId, factory.ReleaseAndGetAddressOf()); } inline HRESULT initialize() { HINSTANCE LibShell32 = LoadLibraryW(L"SHELL32.DLL"); - HRESULT hr = loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); + HRESULT hr = + loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID); if (SUCCEEDED(hr)) { HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL"); - hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString); + hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString); if (SUCCEEDED(hr)) { HINSTANCE LibComBase = LoadLibraryW(L"COMBASE.DLL"); - const bool succeded = SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory)) - && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference)) - && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer)) - && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString)); - return succeded ? S_OK : E_FAIL; + bool const succeded = + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer)) && + SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString)); + return succeded ? S_OK : E_FAIL; } } return hr; } -} +} // namespace DllImporter class WinToastStringWrapper { public: @@ -106,8 +126,9 @@ public: } } - WinToastStringWrapper(_In_ const std::wstring &stringRef) noexcept { - HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring); + WinToastStringWrapper(_In_ std::wstring const& stringRef) noexcept { + HRESULT hr = + DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring); if (FAILED(hr)) { RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr); } @@ -120,10 +141,10 @@ public: inline HSTRING Get() const noexcept { return _hstring; } + private: HSTRING _hstring; HSTRING_HEADER _header; - }; class InternalDateTime : public IReference { @@ -131,7 +152,7 @@ public: static INT64 Now() { FILETIME now; GetSystemTimeAsFileTime(&now); - return ((((INT64)now.dwHighDateTime) << 32) | now.dwLowDateTime); + return ((((INT64) now.dwHighDateTime) << 32) | now.dwLowDateTime); } InternalDateTime(DateTime dateTime) : _dateTime(dateTime) {} @@ -146,7 +167,7 @@ public: return _dateTime.UniversalTime; } - HRESULT STDMETHODCALLTYPE get_Value(DateTime *dateTime) { + HRESULT STDMETHODCALLTYPE get_Value(DateTime* dateTime) { *dateTime = _dateTime; return S_OK; } @@ -195,14 +216,14 @@ namespace Util { if (hMod) { RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion"); if (fxPtr != nullptr) { - RTL_OSVERSIONINFOW rovi = { 0 }; + RTL_OSVERSIONINFOW rovi = {0}; rovi.dwOSVersionInfoSize = sizeof(rovi); if (STATUS_SUCCESS == fxPtr(&rovi)) { return rovi; } } } - RTL_OSVERSIONINFOW rovi = { 0 }; + RTL_OSVERSIONINFOW rovi = {0}; return rovi; } @@ -212,47 +233,56 @@ namespace Util { return (written > 0) ? S_OK : E_FAIL; } - inline HRESULT defaultShellLinksDirectory(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { DWORD written = GetEnvironmentVariableW(L"APPDATA", path, nSize); - HRESULT hr = written > 0 ? S_OK : E_INVALIDARG; + HRESULT hr = written > 0 ? S_OK : E_INVALIDARG; if (SUCCEEDED(hr)) { errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH); - hr = (result == 0) ? S_OK : E_INVALIDARG; + hr = (result == 0) ? S_OK : E_INVALIDARG; DEBUG_MSG("Default shell link path: " << path); } return hr; } - inline HRESULT defaultShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { + inline HRESULT defaultShellLinkPath(_In_ std::wstring const& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) { HRESULT hr = defaultShellLinksDirectory(path, nSize); if (SUCCEEDED(hr)) { const std::wstring appLink(appname + DEFAULT_LINK_FORMAT); errno_t result = wcscat_s(path, nSize, appLink.c_str()); - hr = (result == 0) ? S_OK : E_INVALIDARG; + hr = (result == 0) ? S_OK : E_INVALIDARG; DEBUG_MSG("Default shell link file path: " << path); } return hr; } + inline std::wstring parentDirectory(WCHAR* path, DWORD size) { + size_t lastSeparator = 0; + for (size_t i = 0; i < size; i++) { + if (path[i] == L'\\' || path[i] == L'/') { + lastSeparator = i; + } + } + return {path, lastSeparator}; + } - inline PCWSTR AsString(ComPtr &xmlDocument) { + inline PCWSTR AsString(_In_ ComPtr& xmlDocument) { HSTRING xml; ComPtr ser; HRESULT hr = xmlDocument.As(&ser); - hr = ser->GetXml(&xml); - if (SUCCEEDED(hr)) + hr = ser->GetXml(&xml); + if (SUCCEEDED(hr)) { return DllImporter::WindowsGetStringRawBuffer(xml, nullptr); + } return nullptr; } - inline PCWSTR AsString(HSTRING hstring) { + inline PCWSTR AsString(_In_ HSTRING hstring) { return DllImporter::WindowsGetStringRawBuffer(hstring, nullptr); } - inline HRESULT setNodeStringValue(const std::wstring& string, IXmlNode *node, IXmlDocument *xml) { + inline HRESULT setNodeStringValue(_In_ std::wstring const& string, _Out_opt_ IXmlNode* node, _Out_ IXmlDocument* xml) { ComPtr textNode; - HRESULT hr = xml->CreateTextNode( WinToastStringWrapper(string).Get(), &textNode); + HRESULT hr = xml->CreateTextNode(WinToastStringWrapper(string).Get(), &textNode); if (SUCCEEDED(hr)) { ComPtr stringNode; hr = textNode.As(&stringNode); @@ -264,58 +294,114 @@ namespace Util { return hr; } - inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, _In_ INT64 expirationTime) { - EventRegistrationToken activatedToken, dismissedToken, failedToken; + template + inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, + _In_ INT64 expirationTime, _Out_ EventRegistrationToken& activatedToken, + _Out_ EventRegistrationToken& dismissedToken, _Out_ EventRegistrationToken& failedToken, + _In_ FunctorT&& markAsReadyForDeletionFunc) { HRESULT hr = notification->add_Activated( - Callback < Implements < RuntimeClassFlags, - ITypedEventHandler> >( - [eventHandler](IToastNotification*, IInspectable* inspectable) + Callback, ITypedEventHandler>>( + [eventHandler, markAsReadyForDeletionFunc](IToastNotification* notify, IInspectable* inspectable) { - IToastActivatedEventArgs *activatedEventArgs; - HRESULT hr = inspectable->QueryInterface(&activatedEventArgs); + ComPtr activatedEventArgs; + HRESULT hr = inspectable->QueryInterface(activatedEventArgs.GetAddressOf()); if (SUCCEEDED(hr)) { HSTRING argumentsHandle; hr = activatedEventArgs->get_Arguments(&argumentsHandle); if (SUCCEEDED(hr)) { PCWSTR arguments = Util::AsString(argumentsHandle); + + if(wcscmp(arguments, L"action=reply") == 0) + { + ComPtr inputBoxActivatedEventArgs; + HRESULT hr2 = inspectable->QueryInterface(inputBoxActivatedEventArgs.GetAddressOf()); + + if(SUCCEEDED(hr2)) + { + ComPtr replyHandle; + inputBoxActivatedEventArgs->get_UserInput(&replyHandle); + + ComPtr<__FIMap_2_HSTRING_IInspectable> replyMap; + hr = replyHandle.As(&replyMap); + + if(SUCCEEDED(hr)) + { + IInspectable* propertySet; + hr = replyMap.Get()->Lookup(WinToastStringWrapper(L"textBox").Get(), &propertySet); + if (SUCCEEDED(hr)) + { + ComPtr propertyValue; + hr = propertySet->QueryInterface(IID_PPV_ARGS(&propertyValue)); + + if (SUCCEEDED(hr)) + { + // Successfully queried IPropertyValue, now extract the value + HSTRING userInput; + hr = propertyValue->GetString(&userInput); + + // Convert the HSTRING to a wide string + PCWSTR strValue = AsString(userInput); + + if (SUCCEEDED(hr)) + { + eventHandler->toastActivated(std::wstring(strValue)); + return S_OK; + } + } + } + } + } + } + if (arguments && *arguments) { eventHandler->toastActivated(static_cast(wcstol(arguments, nullptr, 10))); + DllImporter::WindowsDeleteString(argumentsHandle); + markAsReadyForDeletionFunc(); return S_OK; } + DllImporter::WindowsDeleteString(argumentsHandle); } } eventHandler->toastActivated(); + markAsReadyForDeletionFunc(); return S_OK; - }).Get(), &activatedToken); + }) + .Get(), + &activatedToken); if (SUCCEEDED(hr)) { - hr = notification->add_Dismissed(Callback < Implements < RuntimeClassFlags, - ITypedEventHandler> >( - [eventHandler, expirationTime](IToastNotification*, IToastDismissedEventArgs* e) - { - ToastDismissalReason reason; - if (SUCCEEDED(e->get_Reason(&reason))) - { - if (reason == ToastDismissalReason_UserCanceled && expirationTime && InternalDateTime::Now() >= expirationTime) - reason = ToastDismissalReason_TimedOut; - eventHandler->toastDismissed(static_cast(reason)); - } - return S_OK; - }).Get(), &dismissedToken); + hr = notification->add_Dismissed( + Callback, ITypedEventHandler>>( + [eventHandler, expirationTime, markAsReadyForDeletionFunc](IToastNotification* notify, IToastDismissedEventArgs* e) { + ToastDismissalReason reason; + if (SUCCEEDED(e->get_Reason(&reason))) { + if (reason == ToastDismissalReason_UserCanceled && expirationTime && + InternalDateTime::Now() >= expirationTime) { + reason = ToastDismissalReason_TimedOut; + } + eventHandler->toastDismissed(static_cast(reason)); + } + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &dismissedToken); if (SUCCEEDED(hr)) { - hr = notification->add_Failed(Callback < Implements < RuntimeClassFlags, - ITypedEventHandler> >( - [eventHandler](IToastNotification*, IToastFailedEventArgs*) - { - eventHandler->toastFailed(); - return S_OK; - }).Get(), &failedToken); + hr = notification->add_Failed( + Callback, ITypedEventHandler>>( + [eventHandler, markAsReadyForDeletionFunc](IToastNotification* notify, IToastFailedEventArgs* e) { + eventHandler->toastFailed(); + markAsReadyForDeletionFunc(); + return S_OK; + }) + .Get(), + &failedToken); } } return hr; } - inline HRESULT addAttribute(_In_ IXmlDocument *xml, const std::wstring &name, IXmlNamedNodeMap *attributeMap) { + inline HRESULT addAttribute(_In_ IXmlDocument* xml, std::wstring const& name, IXmlNamedNodeMap* attributeMap) { ComPtr srcAttribute; HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute); if (SUCCEEDED(hr)) { @@ -329,7 +415,8 @@ namespace Util { return hr; } - inline HRESULT createElement(_In_ IXmlDocument *xml, _In_ const std::wstring& root_node, _In_ const std::wstring& element_name, _In_ const std::vector& attribute_names) { + inline HRESULT createElement(_In_ IXmlDocument* xml, _In_ std::wstring const& root_node, _In_ std::wstring const& element_name, + _In_ std::vector const& attribute_names) { ComPtr rootList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList); if (SUCCEEDED(hr)) { @@ -348,7 +435,7 @@ namespace Util { ComPtr attributes; hr = audioNode->get_Attributes(&attributes); if (SUCCEEDED(hr)) { - for (const auto& it : attribute_names) { + for (auto const& it : attribute_names) { hr = addAttribute(xml, it, attributes.Get()); } } @@ -359,34 +446,32 @@ namespace Util { } return hr; } -} +} // namespace Util WinToast* WinToast::instance() { - static WinToast instance; + thread_local static WinToast instance; return &instance; } -WinToast::WinToast() : - _isInitialized(false), - _hasCoInitialized(false) -{ - if (!isCompatible()) { - DEBUG_MSG(L"Warning: Your system is not compatible with this library "); - } +WinToast::WinToast() : _isInitialized(false), _hasCoInitialized(false) { + if (!isCompatible()) { + DEBUG_MSG(L"Warning: Your system is not compatible with this library "); + } } WinToast::~WinToast() { + clear(); + if (_hasCoInitialized) { CoUninitialize(); } } -void WinToast::setAppName(_In_ const std::wstring& appName) { +void WinToast::setAppName(_In_ std::wstring const& appName) { _appName = appName; } - -void WinToast::setAppUserModelId(_In_ const std::wstring& aumi) { +void WinToast::setAppUserModelId(_In_ std::wstring const& aumi) { _aumi = aumi; DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str()); } @@ -396,24 +481,23 @@ void WinToast::setShortcutPolicy(_In_ ShortcutPolicy shortcutPolicy) { } bool WinToast::isCompatible() { - DllImporter::initialize(); - return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr) - || (DllImporter::PropVariantToString == nullptr) - || (DllImporter::RoGetActivationFactory == nullptr) - || (DllImporter::WindowsCreateStringReference == nullptr) - || (DllImporter::WindowsDeleteString == nullptr)); + DllImporter::initialize(); + return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr) || (DllImporter::PropVariantToString == nullptr) || + (DllImporter::RoGetActivationFactory == nullptr) || (DllImporter::WindowsCreateStringReference == nullptr) || + (DllImporter::WindowsDeleteString == nullptr)); } bool WinToastLib::WinToast::isSupportingModernFeatures() { constexpr auto MinimumSupportedVersion = 6; return Util::getRealOSVersion().dwMajorVersion > MinimumSupportedVersion; - } -std::wstring WinToast::configureAUMI(_In_ const std::wstring &companyName, - _In_ const std::wstring &productName, - _In_ const std::wstring &subProduct, - _In_ const std::wstring &versionInformation) -{ + +bool WinToastLib::WinToast::isWin10AnniversaryOrHigher() { + return Util::getRealOSVersion().dwBuildNumber >= 14393; +} + +std::wstring WinToast::configureAUMI(_In_ std::wstring const& companyName, _In_ std::wstring const& productName, + _In_ std::wstring const& subProduct, _In_ std::wstring const& versionInformation) { std::wstring aumi = companyName; aumi += L"." + productName; if (subProduct.length() > 0) { @@ -429,19 +513,19 @@ std::wstring WinToast::configureAUMI(_In_ const std::wstring &companyName, return aumi; } -const std::wstring& WinToast::strerror(WinToastError error) { +std::wstring const& WinToast::strerror(WinToastError error) { static const std::unordered_map Labels = { - {WinToastError::NoError, L"No error. The process was executed correctly"}, - {WinToastError::NotInitialized, L"The library has not been initialized"}, - {WinToastError::SystemNotSupported, L"The OS does not support WinToast"}, - {WinToastError::ShellLinkNotCreated, L"The library was not able to create a Shell Link for the app"}, - {WinToastError::InvalidAppUserModelID, L"The AUMI is not a valid one"}, - {WinToastError::InvalidParameters, L"The parameters used to configure the library are not valid normally because an invalid AUMI or App Name"}, - {WinToastError::NotDisplayed, L"The toast was created correctly but WinToast was not able to display the toast"}, - {WinToastError::UnknownError, L"Unknown error"} + {WinToastError::NoError, L"No error. The process was executed correctly" }, + {WinToastError::NotInitialized, L"The library has not been initialized" }, + {WinToastError::SystemNotSupported, L"The OS does not support WinToast" }, + {WinToastError::ShellLinkNotCreated, L"The library was not able to create a Shell Link for the app" }, + {WinToastError::InvalidAppUserModelID, L"The AUMI is not a valid one" }, + {WinToastError::InvalidParameters, L"Invalid parameters, please double-check the AUMI or App Name" }, + {WinToastError::NotDisplayed, L"The toast was created correctly but WinToast was not able to display the toast"}, + {WinToastError::UnknownError, L"Unknown error" } }; - const auto iter = Labels.find(error); + auto const iter = Labels.find(error); assert(iter != Labels.end()); return iter->second; } @@ -463,8 +547,7 @@ enum WinToast::ShortcutResult WinToast::createShortcut() { if (FAILED(initHr) && initHr != S_FALSE) { DEBUG_MSG(L"Error on COM library initialization!"); return SHORTCUT_COM_INIT_FAILURE; - } - else { + } else { _hasCoInitialized = true; } } @@ -472,8 +555,9 @@ enum WinToast::ShortcutResult WinToast::createShortcut() { bool wasChanged; HRESULT hr = validateShellLinkHelper(wasChanged); - if (SUCCEEDED(hr)) + if (SUCCEEDED(hr)) { return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED; + } hr = createShellLinkHelper(); return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED; @@ -489,7 +573,6 @@ bool WinToast::initialize(_Out_opt_ WinToastError* error) { return false; } - if (_aumi.empty() || _appName.empty()) { setError(error, WinToastError::InvalidParameters); DEBUG_MSG(L"Error while initializing, did you set up a valid AUMI and App name?"); @@ -518,17 +601,16 @@ bool WinToast::isInitialized() const { return _isInitialized; } -const std::wstring& WinToast::appName() const { +std::wstring const& WinToast::appName() const { return _appName; } -const std::wstring& WinToast::appUserModelId() const { +std::wstring const& WinToast::appUserModelId() const { return _aumi; } - -HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { - WCHAR path[MAX_PATH] = { L'\0' }; +HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { + WCHAR path[MAX_PATH] = {L'\0'}; Util::defaultShellLinkPath(_appName, path); // Check if the file exist DWORD attr = GetFileAttributesW(path); @@ -558,7 +640,7 @@ HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar); if (SUCCEEDED(hr)) { WCHAR AUMI[MAX_PATH]; - hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH); + hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH); wasChanged = false; if (FAILED(hr) || _aumi != AUMI) { if (_shortcutPolicy == SHORTCUT_POLICY_REQUIRE_CREATE) { @@ -589,17 +671,16 @@ HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) { return hr; } - - -HRESULT WinToast::createShellLinkHelper() { +HRESULT WinToast::createShellLinkHelper() { if (_shortcutPolicy != SHORTCUT_POLICY_REQUIRE_CREATE) { - return E_FAIL; + return E_FAIL; } - WCHAR exePath[MAX_PATH]{L'\0'}; - WCHAR slPath[MAX_PATH]{L'\0'}; + WCHAR exePath[MAX_PATH]{L'\0'}; + WCHAR slPath[MAX_PATH]{L'\0'}; Util::defaultShellLinkPath(_appName, slPath); Util::defaultExecutablePath(exePath); + std::wstring exeDir = Util::parentDirectory(exePath, sizeof(exePath) / sizeof(exePath[0])); ComPtr shellLink; HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink)); if (SUCCEEDED(hr)) { @@ -607,7 +688,7 @@ HRESULT WinToast::createShellLinkHelper() { if (SUCCEEDED(hr)) { hr = shellLink->SetArguments(L""); if (SUCCEEDED(hr)) { - hr = shellLink->SetWorkingDirectory(exePath); + hr = shellLink->SetWorkingDirectory(exeDir.c_str()); if (SUCCEEDED(hr)) { ComPtr propertyStore; hr = shellLink.As(&propertyStore); @@ -636,7 +717,8 @@ HRESULT WinToast::createShellLinkHelper() { return hr; } -INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error) { +INT64 WinToast::showToast(_In_ WinToastTemplate const& toast, _In_ IWinToastHandler* eventHandler, _Out_ WinToastError* error) { + std::shared_ptr handler(eventHandler); setError(error, WinToastError::NoError); INT64 id = -1; if (!isInitialized()) { @@ -651,16 +733,21 @@ INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHan } ComPtr notificationManager; - HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + HRESULT hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); if (SUCCEEDED(hr)) { ComPtr notifier; hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); if (SUCCEEDED(hr)) { ComPtr notificationFactory; - hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); + hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory); if (SUCCEEDED(hr)) { - ComPtr xmlDocument; - HRESULT hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument); + ComPtr xmlDocument; + hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument); + if (SUCCEEDED(hr) && toast.isToastGeneric()) { + hr = setBindToastGenericHelper(xmlDocument.Get()); + } if (SUCCEEDED(hr)) { for (UINT32 i = 0, fieldsCount = static_cast(toast.textFieldsCount()); i < fieldsCount && SUCCEEDED(hr); i++) { hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i); @@ -668,11 +755,9 @@ INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHan // Modern feature are supported Windows > Windows 10 if (SUCCEEDED(hr) && isSupportingModernFeatures()) { - // Note that we do this *after* using toast.textFieldsCount() to // iterate/fill the template's text fields, since we're adding yet another text field. - if (SUCCEEDED(hr) - && !toast.attributionText().empty()) { + if (SUCCEEDED(hr) && !toast.attributionText().empty()) { hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText()); } @@ -684,12 +769,17 @@ INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHan if (SUCCEEDED(hr)) { hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default) - ? hr : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption()); + ? hr + : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption()); } if (SUCCEEDED(hr) && toast.duration() != WinToastTemplate::Duration::System) { hr = addDurationHelper(xmlDocument.Get(), - (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long"); + (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long"); + } + + if(SUCCEEDED(hr) && toast.isInput()) { + hr = addInputHelper(xmlDocument.Get()); } if (SUCCEEDED(hr)) { @@ -701,7 +791,14 @@ INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHan } if (SUCCEEDED(hr)) { - hr = toast.hasImage() ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath()) : hr; + bool isWin10AnniversaryOrAbove = WinToast::isWin10AnniversaryOrHigher(); + bool isCircleCropHint = isWin10AnniversaryOrAbove ? toast.isCropHintCircle() : false; + hr = toast.hasImage() + ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath(), toast.isToastGeneric(), isCircleCropHint) + : hr; + if (SUCCEEDED(hr) && isWin10AnniversaryOrAbove && toast.hasHeroImage()) { + hr = setHeroImageHelper(xmlDocument.Get(), toast.heroImagePath(), toast.isInlineHeroImage()); + } if (SUCCEEDED(hr)) { ComPtr notification; hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification); @@ -710,22 +807,25 @@ INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHan if (relativeExpiration > 0) { InternalDateTime expirationDateTime(relativeExpiration); expiration = expirationDateTime; - hr = notification->put_ExpirationTime(&expirationDateTime); + hr = notification->put_ExpirationTime(&expirationDateTime); } - if (SUCCEEDED(hr)) { - hr = Util::setEventHandlers(notification.Get(), std::shared_ptr(handler), expiration); + EventRegistrationToken activatedToken, dismissedToken, failedToken; + + GUID guid; + HRESULT hrGuid = CoCreateGuid(&guid); + id = guid.Data1; + if (SUCCEEDED(hr) && SUCCEEDED(hrGuid)) { + hr = Util::setEventHandlers(notification.Get(), handler, expiration, activatedToken, dismissedToken, + failedToken, [this, id]() { markAsReadyForDeletion(id); }); if (FAILED(hr)) { setError(error, WinToastError::InvalidHandler); } } if (SUCCEEDED(hr)) { - GUID guid; - hr = CoCreateGuid(&guid); if (SUCCEEDED(hr)) { - id = guid.Data1; - _buffer[id] = notification; + _buffer.emplace(id, NotifyData(notification, activatedToken, dismissedToken, failedToken)); DEBUG_MSG("xml: " << Util::AsString(xmlDocument)); hr = notifier->Show(notification.Get()); if (FAILED(hr)) { @@ -743,15 +843,34 @@ INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHan return FAILED(hr) ? -1 : id; } -ComPtr WinToast::notifier(_In_ bool* succeded) const { - ComPtr notificationManager; - ComPtr notifier; - HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); - if (SUCCEEDED(hr)) { - hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); - } - *succeded = SUCCEEDED(hr); - return notifier; +ComPtr WinToast::notifier(_In_ bool* succeded) const { + ComPtr notificationManager; + ComPtr notifier; + HRESULT hr = DllImporter::Wrap_GetActivationFactory( + WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager); + if (SUCCEEDED(hr)) { + hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier); + } + *succeded = SUCCEEDED(hr); + return notifier; +} + +void WinToast::markAsReadyForDeletion(_In_ INT64 id) { + // Flush the buffer by removing all the toasts that are ready for deletion + for (auto it = _buffer.begin(); it != _buffer.end();) { + if (it->second.isReadyForDeletion()) { + it->second.RemoveTokens(); + it = _buffer.erase(it); + } else { + ++it; + } + } + + // Mark the toast as ready for deletion (if it exists) so that it will be removed from the buffer in the next iteration + auto const iter = _buffer.find(id); + if (iter != _buffer.end()) { + _buffer[id].markAsReadyForDeletion(); + } } bool WinToast::hideToast(_In_ INT64 id) { @@ -760,28 +879,43 @@ bool WinToast::hideToast(_In_ INT64 id) { return false; } - if (_buffer.find(id) != _buffer.end()) { - auto succeded = false; - auto notify = notifier(&succeded); - if (succeded) { - auto result = notify->Hide(_buffer[id].Get()); - _buffer.erase(id); - return SUCCEEDED(result); - } - } - return false; + auto iter = _buffer.find(id); + if (iter == _buffer.end()) { + return false; + } + + auto succeded = false; + auto notify = notifier(&succeded); + if (!succeded) { + return false; + } + + auto& notifyData = iter->second; + auto result = notify->Hide(notifyData.notification()); + if (FAILED(result)) { + DEBUG_MSG("Error when hiding the toast. Error code: " << result); + return false; + } + + notifyData.RemoveTokens(); + _buffer.erase(iter); + return SUCCEEDED(result); } void WinToast::clear() { auto succeded = false; - auto notify = notifier(&succeded); - if (succeded) { - auto end = _buffer.end(); - for (auto it = _buffer.begin(); it != end; ++it) { - notify->Hide(it->second.Get()); - } - _buffer.clear(); - } + auto notify = notifier(&succeded); + if (!succeded) { + return; + } + + auto safeCopy = _buffer; + for (auto& data : safeCopy) { + auto& notifyData = data.second; + notify->Hide(notifyData.notification()); + notifyData.RemoveTokens(); + } + _buffer.clear(); } // @@ -791,8 +925,8 @@ void WinToast::clear() { // NOTE: This will add a new text field, so be aware when iterating over // the toast's text fields or getting a count of them. // -HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text) { - Util::createElement(xml, L"binding", L"text", { L"placement" }); +HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text) { + Util::createElement(xml, L"binding", L"text", {L"placement"}); ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); if (SUCCEEDED(hr)) { @@ -825,7 +959,7 @@ HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ con return hr; } -HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration) { +HRESULT WinToast::addDurationHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& duration) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); if (SUCCEEDED(hr)) { @@ -838,8 +972,7 @@ HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstr ComPtr toastElement; hr = toastNode.As(&toastElement); if (SUCCEEDED(hr)) { - hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), - WinToastStringWrapper(duration).Get()); + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(duration).Get()); } } } @@ -847,7 +980,7 @@ HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstr return hr; } -HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& scenario) { +HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& scenario) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList); if (SUCCEEDED(hr)) { @@ -860,8 +993,7 @@ HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ const std::wstr ComPtr toastElement; hr = toastNode.As(&toastElement); if (SUCCEEDED(hr)) { - hr = toastElement->SetAttribute(WinToastStringWrapper(L"scenario").Get(), - WinToastStringWrapper(scenario).Get()); + hr = toastElement->SetAttribute(WinToastStringWrapper(L"scenario").Get(), WinToastStringWrapper(scenario).Get()); } } } @@ -869,7 +1001,61 @@ HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ const std::wstr return hr; } -HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos) { +HRESULT WinToast::addInputHelper(_In_ IXmlDocument* xml) +{ + std::vector attrbs; + attrbs.push_back(L"id"); + attrbs.push_back(L"type"); + attrbs.push_back(L"placeHolderContent"); + + std::vector attrbs2; + attrbs2.push_back(L"content"); + attrbs2.push_back(L"arguments"); + + Util::createElement(xml, L"toast", L"actions", {}); + + Util::createElement(xml, L"actions", L"input",attrbs); + Util::createElement(xml, L"actions", L"action",attrbs2); + + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"input").Get(), &nodeList); + if (SUCCEEDED(hr)) + { + ComPtr inputNode; + hr = nodeList->Item(0, &inputNode); + if (SUCCEEDED(hr)) + { + ComPtr toastElement; + hr = inputNode.As(&toastElement); + if(SUCCEEDED(hr)){ + toastElement->SetAttribute(WinToastStringWrapper(L"id").Get(), WinToastStringWrapper(L"textBox").Get()); + toastElement->SetAttribute(WinToastStringWrapper(L"type").Get(), WinToastStringWrapper(L"text").Get()); + hr = toastElement->SetAttribute(WinToastStringWrapper(L"placeHolderContent").Get(), WinToastStringWrapper(L"...").Get()); + } + } + } + + ComPtr nodeList2; + hr = xml->GetElementsByTagName(WinToastStringWrapper(L"action").Get(), &nodeList2); + if (SUCCEEDED(hr)) + { + ComPtr actionNode; + hr = nodeList2->Item(0, &actionNode); + if (SUCCEEDED(hr)) + { + ComPtr actionElement; + hr = actionNode.As(&actionElement); + if(SUCCEEDED(hr)){ + actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(L"Reply").Get()); + actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(L"action=reply").Get()); + actionElement->SetAttribute(WinToastStringWrapper(L"hint-inputId").Get(), WinToastStringWrapper(L"textBox").Get()); + } + } + } + return hr; +} + +HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text, _In_ UINT32 pos) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList); if (SUCCEEDED(hr)) { @@ -882,19 +1068,49 @@ HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wst return hr; } +HRESULT WinToast::setBindToastGenericHelper(_In_ IXmlDocument* xml) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"binding").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr toastNode; + hr = nodeList->Item(0, &toastNode); + if (SUCCEEDED(hr)) { + ComPtr toastElement; + hr = toastNode.As(&toastElement); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get()); + } + } + } + } + return hr; +} -HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path) { +HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isToastGeneric, + _In_ bool isCropHintCircle) { assert(path.size() < MAX_PATH); wchar_t imagePath[MAX_PATH] = L"file:///"; - HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str()); + HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str()); if (SUCCEEDED(hr)) { ComPtr nodeList; HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"image").Get(), &nodeList); if (SUCCEEDED(hr)) { ComPtr node; hr = nodeList->Item(0, &node); - if (SUCCEEDED(hr)) { + + ComPtr imageElement; + HRESULT hrImage = node.As(&imageElement); + if (SUCCEEDED(hr) && SUCCEEDED(hrImage) && isToastGeneric) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"placement").Get(), WinToastStringWrapper(L"appLogoOverride").Get()); + if (SUCCEEDED(hr) && isCropHintCircle) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"hint-crop").Get(), WinToastStringWrapper(L"circle").Get()); + } + } + if (SUCCEEDED(hr)) { ComPtr attributes; hr = node->get_Attributes(&attributes); if (SUCCEEDED(hr)) { @@ -910,11 +1126,18 @@ HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::ws return hr; } -HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option) { +HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, + _In_opt_ WinToastTemplate::AudioOption option) { std::vector attrs; - if (!path.empty()) attrs.push_back(L"src"); - if (option == WinToastTemplate::AudioOption::Loop) attrs.push_back(L"loop"); - if (option == WinToastTemplate::AudioOption::Silent) attrs.push_back(L"silent"); + if (!path.empty()) { + attrs.push_back(L"src"); + } + if (option == WinToastTemplate::AudioOption::Loop) { + attrs.push_back(L"loop"); + } + if (option == WinToastTemplate::AudioOption::Silent) { + attrs.push_back(L"silent"); + } Util::createElement(xml, L"toast", L"audio", attrs); ComPtr nodeList; @@ -938,19 +1161,19 @@ HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::ws if (SUCCEEDED(hr)) { switch (option) { - case WinToastTemplate::AudioOption::Loop: - hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode); - if (SUCCEEDED(hr)) { - hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); - } - break; - case WinToastTemplate::AudioOption::Silent: - hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode); - if (SUCCEEDED(hr)) { - hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); - } - default: - break; + case WinToastTemplate::AudioOption::Loop: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + break; + case WinToastTemplate::AudioOption::Silent: + hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode); + if (SUCCEEDED(hr)) { + hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml); + } + default: + break; } } } @@ -959,9 +1182,9 @@ HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::ws return hr; } -HRESULT WinToast::addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& content, _In_ const std::wstring& arguments) { - ComPtr nodeList; - HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList); +HRESULT WinToast::addActionHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& content, _In_ std::wstring const& arguments) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList); if (SUCCEEDED(hr)) { UINT32 length; hr = nodeList->get_Length(&length); @@ -979,10 +1202,14 @@ HRESULT WinToast::addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstrin if (SUCCEEDED(hr)) { ComPtr toastElement; hr = toastNode.As(&toastElement); - if (SUCCEEDED(hr)) - hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get()); - if (SUCCEEDED(hr)) - hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(L"long").Get()); + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), + WinToastStringWrapper(L"ToastGeneric").Get()); + } + if (SUCCEEDED(hr)) { + hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), + WinToastStringWrapper(L"long").Get()); + } if (SUCCEEDED(hr)) { ComPtr actionsElement; hr = xml->CreateElement(WinToastStringWrapper(L"actions").Get(), &actionsElement); @@ -1001,10 +1228,12 @@ HRESULT WinToast::addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstrin if (SUCCEEDED(hr)) { ComPtr actionElement; hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement); - if (SUCCEEDED(hr)) + if (SUCCEEDED(hr)) { hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get()); - if (SUCCEEDED(hr)) + } + if (SUCCEEDED(hr)) { hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get()); + } if (SUCCEEDED(hr)) { ComPtr actionNode; hr = actionElement.As(&actionNode); @@ -1019,6 +1248,40 @@ HRESULT WinToast::addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstrin return hr; } +HRESULT WinToast::setHeroImageHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isInlineImage) { + ComPtr nodeList; + HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"binding").Get(), &nodeList); + if (SUCCEEDED(hr)) { + UINT32 length; + hr = nodeList->get_Length(&length); + if (SUCCEEDED(hr)) { + ComPtr bindingNode; + if (length > 0) { + hr = nodeList->Item(0, &bindingNode); + } + if (SUCCEEDED(hr)) { + ComPtr imageElement; + hr = xml->CreateElement(WinToastStringWrapper(L"image").Get(), &imageElement); + if (SUCCEEDED(hr) && isInlineImage == false) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"placement").Get(), WinToastStringWrapper(L"hero").Get()); + } + if (SUCCEEDED(hr)) { + hr = imageElement->SetAttribute(WinToastStringWrapper(L"src").Get(), WinToastStringWrapper(path).Get()); + } + if (SUCCEEDED(hr)) { + ComPtr actionNode; + hr = imageElement.As(&actionNode); + if (SUCCEEDED(hr)) { + ComPtr appendedChild; + hr = bindingNode->AppendChild(actionNode.Get(), &appendedChild); + } + } + } + } + } + return hr; +} + void WinToast::setError(_Out_opt_ WinToastError* error, _In_ WinToastError value) { if (error) { *error = value; @@ -1026,58 +1289,67 @@ void WinToast::setError(_Out_opt_ WinToastError* error, _In_ WinToastError value } WinToastTemplate::WinToastTemplate(_In_ WinToastTemplateType type) : _type(type) { - static constexpr std::size_t TextFieldsCount[] = { 1, 2, 2, 3, 1, 2, 2, 3}; - _textFields = std::vector(TextFieldsCount[type], L""); + constexpr static std::size_t TextFieldsCount[] = {1, 2, 2, 3, 1, 2, 2, 3}; + _textFields = std::vector(TextFieldsCount[type], L""); } WinToastTemplate::~WinToastTemplate() { _textFields.clear(); } -void WinToastTemplate::setTextField(_In_ const std::wstring& txt, _In_ WinToastTemplate::TextField pos) { - const auto position = static_cast(pos); - assert(position < _textFields.size()); +void WinToastTemplate::setTextField(_In_ std::wstring const& txt, _In_ WinToastTemplate::TextField pos) { + auto const position = static_cast(pos); + if (position >= _textFields.size()) { + DEBUG_MSG("The selected template type supports only " << _textFields.size() << " text lines"); + return; + } _textFields[position] = txt; } -void WinToastTemplate::setImagePath(_In_ const std::wstring& imgPath) { +void WinToastTemplate::setImagePath(_In_ std::wstring const& imgPath, _In_ CropHint cropHint) { _imagePath = imgPath; + _cropHint = cropHint; } -void WinToastTemplate::setAudioPath(_In_ const std::wstring& audioPath) { +void WinToastTemplate::setHeroImagePath(_In_ std::wstring const& imgPath, _In_ bool inlineImage) { + _heroImagePath = imgPath; + _inlineHeroImage = inlineImage; +} + +void WinToastTemplate::setAudioPath(_In_ std::wstring const& audioPath) { _audioPath = audioPath; } void WinToastTemplate::setAudioPath(_In_ AudioSystemFile file) { static const std::unordered_map Files = { - {AudioSystemFile::DefaultSound, L"ms-winsoundevent:Notification.Default"}, - {AudioSystemFile::IM, L"ms-winsoundevent:Notification.IM"}, - {AudioSystemFile::Mail, L"ms-winsoundevent:Notification.Mail"}, - {AudioSystemFile::Reminder, L"ms-winsoundevent:Notification.Reminder"}, - {AudioSystemFile::SMS, L"ms-winsoundevent:Notification.SMS"}, - {AudioSystemFile::Alarm, L"ms-winsoundevent:Notification.Looping.Alarm"}, - {AudioSystemFile::Alarm2, L"ms-winsoundevent:Notification.Looping.Alarm2"}, - {AudioSystemFile::Alarm3, L"ms-winsoundevent:Notification.Looping.Alarm3"}, - {AudioSystemFile::Alarm4, L"ms-winsoundevent:Notification.Looping.Alarm4"}, - {AudioSystemFile::Alarm5, L"ms-winsoundevent:Notification.Looping.Alarm5"}, - {AudioSystemFile::Alarm6, L"ms-winsoundevent:Notification.Looping.Alarm6"}, - {AudioSystemFile::Alarm7, L"ms-winsoundevent:Notification.Looping.Alarm7"}, - {AudioSystemFile::Alarm8, L"ms-winsoundevent:Notification.Looping.Alarm8"}, - {AudioSystemFile::Alarm9, L"ms-winsoundevent:Notification.Looping.Alarm9"}, - {AudioSystemFile::Alarm10, L"ms-winsoundevent:Notification.Looping.Alarm10"}, - {AudioSystemFile::Call, L"ms-winsoundevent:Notification.Looping.Call"}, - {AudioSystemFile::Call1, L"ms-winsoundevent:Notification.Looping.Call1"}, - {AudioSystemFile::Call2, L"ms-winsoundevent:Notification.Looping.Call2"}, - {AudioSystemFile::Call3, L"ms-winsoundevent:Notification.Looping.Call3"}, - {AudioSystemFile::Call4, L"ms-winsoundevent:Notification.Looping.Call4"}, - {AudioSystemFile::Call5, L"ms-winsoundevent:Notification.Looping.Call5"}, - {AudioSystemFile::Call6, L"ms-winsoundevent:Notification.Looping.Call6"}, - {AudioSystemFile::Call7, L"ms-winsoundevent:Notification.Looping.Call7"}, - {AudioSystemFile::Call8, L"ms-winsoundevent:Notification.Looping.Call8"}, - {AudioSystemFile::Call9, L"ms-winsoundevent:Notification.Looping.Call9"}, - {AudioSystemFile::Call10, L"ms-winsoundevent:Notification.Looping.Call10"}, + {AudioSystemFile::DefaultSound, L"ms-winsoundevent:Notification.Default" }, + {AudioSystemFile::IM, L"ms-winsoundevent:Notification.IM" }, + {AudioSystemFile::Mail, L"ms-winsoundevent:Notification.Mail" }, + {AudioSystemFile::Reminder, L"ms-winsoundevent:Notification.Reminder" }, + {AudioSystemFile::SMS, L"ms-winsoundevent:Notification.SMS" }, + {AudioSystemFile::Alarm, L"ms-winsoundevent:Notification.Looping.Alarm" }, + {AudioSystemFile::Alarm2, L"ms-winsoundevent:Notification.Looping.Alarm2" }, + {AudioSystemFile::Alarm3, L"ms-winsoundevent:Notification.Looping.Alarm3" }, + {AudioSystemFile::Alarm4, L"ms-winsoundevent:Notification.Looping.Alarm4" }, + {AudioSystemFile::Alarm5, L"ms-winsoundevent:Notification.Looping.Alarm5" }, + {AudioSystemFile::Alarm6, L"ms-winsoundevent:Notification.Looping.Alarm6" }, + {AudioSystemFile::Alarm7, L"ms-winsoundevent:Notification.Looping.Alarm7" }, + {AudioSystemFile::Alarm8, L"ms-winsoundevent:Notification.Looping.Alarm8" }, + {AudioSystemFile::Alarm9, L"ms-winsoundevent:Notification.Looping.Alarm9" }, + {AudioSystemFile::Alarm10, L"ms-winsoundevent:Notification.Looping.Alarm10"}, + {AudioSystemFile::Call, L"ms-winsoundevent:Notification.Looping.Call" }, + {AudioSystemFile::Call1, L"ms-winsoundevent:Notification.Looping.Call1" }, + {AudioSystemFile::Call2, L"ms-winsoundevent:Notification.Looping.Call2" }, + {AudioSystemFile::Call3, L"ms-winsoundevent:Notification.Looping.Call3" }, + {AudioSystemFile::Call4, L"ms-winsoundevent:Notification.Looping.Call4" }, + {AudioSystemFile::Call5, L"ms-winsoundevent:Notification.Looping.Call5" }, + {AudioSystemFile::Call6, L"ms-winsoundevent:Notification.Looping.Call6" }, + {AudioSystemFile::Call7, L"ms-winsoundevent:Notification.Looping.Call7" }, + {AudioSystemFile::Call8, L"ms-winsoundevent:Notification.Looping.Call8" }, + {AudioSystemFile::Call9, L"ms-winsoundevent:Notification.Looping.Call9" }, + {AudioSystemFile::Call10, L"ms-winsoundevent:Notification.Looping.Call10" }, }; - const auto iter = Files.find(file); + auto const iter = Files.find(file); assert(iter != Files.end()); _audioPath = iter->second; } @@ -1086,15 +1358,15 @@ void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOp _audioOption = audioOption; } -void WinToastTemplate::setFirstLine(_In_ const std::wstring &text) { +void WinToastTemplate::setFirstLine(_In_ std::wstring const& text) { setTextField(text, WinToastTemplate::FirstLine); } -void WinToastTemplate::setSecondLine(_In_ const std::wstring &text) { +void WinToastTemplate::setSecondLine(_In_ std::wstring const& text) { setTextField(text, WinToastTemplate::SecondLine); } -void WinToastTemplate::setThirdLine(_In_ const std::wstring &text) { +void WinToastTemplate::setThirdLine(_In_ std::wstring const& text) { setTextField(text, WinToastTemplate::ThirdLine); } @@ -1106,21 +1378,34 @@ void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) { _expiration = millisecondsFromNow; } -void WinToastLib::WinToastTemplate::setScenario(Scenario scenario) { +void WinToastLib::WinToastTemplate::setScenario(_In_ Scenario scenario) { switch (scenario) { - case Scenario::Default: _scenario = L"Default"; break; - case Scenario::Alarm: _scenario = L"Alarm"; break; - case Scenario::IncomingCall: _scenario = L"IncomingCall"; break; - case Scenario::Reminder: _scenario = L"Reminder"; break; + case Scenario::Default: + _scenario = L"Default"; + break; + case Scenario::Alarm: + _scenario = L"Alarm"; + break; + case Scenario::IncomingCall: + _scenario = L"IncomingCall"; + break; + case Scenario::Reminder: + _scenario = L"Reminder"; + break; } } -void WinToastTemplate::setAttributionText(_In_ const std::wstring& attributionText) { +void WinToastTemplate::setAttributionText(_In_ std::wstring const& attributionText) { _attributionText = attributionText; } -void WinToastTemplate::addAction(_In_ const std::wstring & label) { - _actions.push_back(label); +void WinToastTemplate::addAction(_In_ std::wstring const& label) { + _actions.push_back(label); +} + +void WinToastTemplate::addInput() +{ + _hasInput = true; } std::size_t WinToastTemplate::textFieldsCount() const { @@ -1132,37 +1417,45 @@ std::size_t WinToastTemplate::actionsCount() const { } bool WinToastTemplate::hasImage() const { - return _type < WinToastTemplateType::Text01; + return _type < WinToastTemplateType::Text01; } -const std::vector& WinToastTemplate::textFields() const { +bool WinToastTemplate::hasHeroImage() const { + return hasImage() && !_heroImagePath.empty(); +} + +std::vector const& WinToastTemplate::textFields() const { return _textFields; } -const std::wstring& WinToastTemplate::textField(_In_ TextField pos) const { - const auto position = static_cast(pos); +std::wstring const& WinToastTemplate::textField(_In_ TextField pos) const { + auto const position = static_cast(pos); assert(position < _textFields.size()); return _textFields[position]; } -const std::wstring& WinToastTemplate::actionLabel(_In_ std::size_t position) const { +std::wstring const& WinToastTemplate::actionLabel(_In_ std::size_t position) const { assert(position < _actions.size()); return _actions[position]; } -const std::wstring& WinToastTemplate::imagePath() const { +std::wstring const& WinToastTemplate::imagePath() const { return _imagePath; } -const std::wstring& WinToastTemplate::audioPath() const { +std::wstring const& WinToastTemplate::heroImagePath() const { + return _heroImagePath; +} + +std::wstring const& WinToastTemplate::audioPath() const { return _audioPath; } -const std::wstring& WinToastTemplate::attributionText() const { +std::wstring const& WinToastTemplate::attributionText() const { return _attributionText; } -const std::wstring& WinToastLib::WinToastTemplate::scenario() const { +std::wstring const& WinToastLib::WinToastTemplate::scenario() const { return _scenario; } @@ -1181,3 +1474,19 @@ WinToastTemplate::AudioOption WinToastTemplate::audioOption() const { WinToastTemplate::Duration WinToastTemplate::duration() const { return _duration; } + +bool WinToastTemplate::isToastGeneric() const { + return hasHeroImage() || _cropHint == WinToastTemplate::Circle; +} + +bool WinToastTemplate::isInlineHeroImage() const { + return _inlineHeroImage; +} + +bool WinToastTemplate::isCropHintCircle() const { + return _cropHint == CropHint::Circle; +} + +bool WinToastTemplate::isInput() const{ + return _hasInput; +} diff --git a/Photino.Native/Dependencies/wintoastlib.h b/Photino.Native/Dependencies/wintoastlib.h index fab5727..475c5b3 100644 --- a/Photino.Native/Dependencies/wintoastlib.h +++ b/Photino.Native/Dependencies/wintoastlib.h @@ -1,4 +1,7 @@ -/* * Copyright (C) 2016-2019 Mohammed Boujemaoui +/** + * MIT License + * + * Copyright (C) 2016-2023 WinToast v1.3.0 - Mohammed Boujemaoui * * 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 @@ -20,6 +23,7 @@ #ifndef WINTOASTLIB_H #define WINTOASTLIB_H + #include #include #include @@ -38,27 +42,32 @@ #include #include #include +#include + 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 { + void setDebugOutputEnabled(bool enabled); + class IWinToastHandler { public: enum WinToastDismissalReason { - UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, + UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, - TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut + TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut }; - virtual ~IWinToastHandler() = default; - virtual void toastActivated() const = 0; - virtual void toastActivated(int actionIndex) const = 0; + + virtual ~IWinToastHandler() = default; + virtual void toastActivated() const = 0; + virtual void toastActivated(int actionIndex) const = 0; + virtual void toastActivated(std::wstring response) const = 0; virtual void toastDismissed(WinToastDismissalReason state) const = 0; - virtual void toastFailed() const = 0; + virtual void toastFailed() const = 0; }; class WinToastTemplate { @@ -67,15 +76,16 @@ namespace WinToastLib { 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, + Text01 = ToastTemplateType::ToastTemplateType_ToastText01, + Text02 = ToastTemplateType::ToastTemplateType_ToastText02, + Text03 = ToastTemplateType::ToastTemplateType_ToastText03, + Text04 = ToastTemplateType::ToastTemplateType_ToastText04 }; enum AudioSystemFile { @@ -107,49 +117,67 @@ namespace WinToastLib { Call10, }; + enum CropHint { + Square, + Circle, + }; 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 setFirstLine(_In_ std::wstring const& text); + void setSecondLine(_In_ std::wstring const& text); + void setThirdLine(_In_ std::wstring const& text); + void setTextField(_In_ std::wstring const& txt, _In_ TextField pos); + void setAttributionText(_In_ std::wstring const& attributionText); + void setImagePath(_In_ std::wstring const& imgPath, _In_ CropHint cropHint = CropHint::Square); + void setHeroImagePath(_In_ std::wstring const& imgPath, _In_ bool inlineImage = false); void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio); - void setAudioPath(_In_ const std::wstring& audioPath); + void setAudioPath(_In_ std::wstring const& audioPath); void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption); void setDuration(_In_ Duration duration); void setExpiration(_In_ INT64 millisecondsFromNow); void setScenario(_In_ Scenario scenario); - void addAction(_In_ const std::wstring& label); + void addAction(_In_ std::wstring const& label); + void addInput(); std::size_t textFieldsCount() const; std::size_t actionsCount() const; bool hasImage() const; - const std::vector& 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; - const std::wstring& scenario() const; + bool hasHeroImage() const; + std::vector const& textFields() const; + std::wstring const& textField(_In_ TextField pos) const; + std::wstring const& actionLabel(_In_ std::size_t pos) const; + std::wstring const& imagePath() const; + std::wstring const& heroImagePath() const; + std::wstring const& audioPath() const; + std::wstring const& attributionText() const; + std::wstring const& scenario() const; INT64 expiration() const; WinToastTemplateType type() const; WinToastTemplate::AudioOption audioOption() const; Duration duration() const; + bool isToastGeneric() const; + bool isInlineHeroImage() const; + bool isCropHintCircle() const; + bool isInput() const; + private: - std::vector _textFields{}; - std::vector _actions{}; - std::wstring _imagePath{}; - std::wstring _audioPath{}; - std::wstring _attributionText{}; - std::wstring _scenario{L"Default"}; - INT64 _expiration{0}; - AudioOption _audioOption{WinToastTemplate::AudioOption::Default}; - WinToastTemplateType _type{WinToastTemplateType::Text01}; - Duration _duration{Duration::System}; + bool _hasInput{false}; + + std::vector _textFields{}; + std::vector _actions{}; + std::wstring _imagePath{}; + std::wstring _heroImagePath{}; + bool _inlineHeroImage{false}; + std::wstring _audioPath{}; + std::wstring _attributionText{}; + std::wstring _scenario{L"Default"}; + INT64 _expiration{0}; + AudioOption _audioOption{WinToastTemplate::AudioOption::Default}; + WinToastTemplateType _type{WinToastTemplateType::Text01}; + Duration _duration{Duration::System}; + CropHint _cropHint{CropHint::Square}; }; class WinToast { @@ -167,14 +195,14 @@ namespace WinToastLib { }; enum ShortcutResult { - SHORTCUT_UNCHANGED = 0, + 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 + SHORTCUT_INCOMPATIBLE_OS = -2, + SHORTCUT_COM_INIT_FAILURE = -3, + SHORTCUT_CREATE_FAILED = -4 }; enum ShortcutPolicy { @@ -182,8 +210,7 @@ namespace WinToastLib { SHORTCUT_POLICY_IGNORE = 0, /* Require a shortcut with matching AUMI, don't create or modify an existing one. */ SHORTCUT_POLICY_REQUIRE_NO_CREATE = 1, - /* Require a shortcut with matching AUMI, create if missing, modify if not matching. - * This is the default. */ + /* Require a shortcut with matching AUMI, create if missing, modify if not matching. This is the default. */ SHORTCUT_POLICY_REQUIRE_CREATE = 2, }; @@ -192,43 +219,100 @@ namespace WinToastLib { 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); + static bool isWin10AnniversaryOrHigher(); + static std::wstring configureAUMI(_In_ std::wstring const& companyName, _In_ std::wstring const& productName, + _In_ std::wstring const& subProduct = std::wstring(), + _In_ std::wstring const& versionInformation = std::wstring()); + static std::wstring const& strerror(_In_ WinToastError error); virtual bool initialize(_Out_opt_ WinToastError* error = nullptr); virtual bool isInitialized() const; virtual bool hideToast(_In_ INT64 id); - virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_opt_ WinToastError* error = nullptr); + virtual INT64 showToast(_In_ WinToastTemplate const& toast, _In_ IWinToastHandler* eventHandler, + _Out_opt_ 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); + std::wstring const& appName() const; + std::wstring const& appUserModelId() const; + void setAppUserModelId(_In_ std::wstring const& aumi); + void setAppName(_In_ std::wstring const& appName); void setShortcutPolicy(_In_ ShortcutPolicy policy); protected: - bool _isInitialized{false}; - bool _hasCoInitialized{false}; - ShortcutPolicy _shortcutPolicy{SHORTCUT_POLICY_REQUIRE_CREATE}; - std::wstring _appName{}; - std::wstring _aumi{}; - std::map> _buffer{}; + struct NotifyData { + NotifyData(){}; + NotifyData(_In_ ComPtr notify, _In_ EventRegistrationToken activatedToken, + _In_ EventRegistrationToken dismissedToken, _In_ EventRegistrationToken failedToken) : + _notify(notify), _activatedToken(activatedToken), _dismissedToken(dismissedToken), _failedToken(failedToken) {} + ~NotifyData() { + RemoveTokens(); + } + + void RemoveTokens() { + if (!_readyForDeletion) { + return; + } + + if (_previouslyTokenRemoved) { + return; + } + + if (!_notify.Get()) { + return; + } + + _notify->remove_Activated(_activatedToken); + _notify->remove_Dismissed(_dismissedToken); + _notify->remove_Failed(_failedToken); + _previouslyTokenRemoved = true; + } + + void markAsReadyForDeletion() { + _readyForDeletion = true; + } + + bool isReadyForDeletion() const { + return _readyForDeletion; + } + + IToastNotification* notification() { + return _notify.Get(); + } + + private: + ComPtr _notify{nullptr}; + EventRegistrationToken _activatedToken{}; + EventRegistrationToken _dismissedToken{}; + EventRegistrationToken _failedToken{}; + bool _readyForDeletion{false}; + bool _previouslyTokenRemoved{false}; + }; + + bool _isInitialized{false}; + bool _hasCoInitialized{false}; + ShortcutPolicy _shortcutPolicy{SHORTCUT_POLICY_REQUIRE_CREATE}; + std::wstring _appName{}; + std::wstring _aumi{}; + std::map _buffer{}; + + void markAsReadyForDeletion(_In_ INT64 id); 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); - HRESULT addScenarioHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& scenario); + HRESULT setImageFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isToastGeneric, bool isCropHintCircle); + HRESULT setHeroImageHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, _In_ bool isInlineImage); + HRESULT setBindToastGenericHelper(_In_ IXmlDocument* xml); + HRESULT + setAudioFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& path, + _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); + HRESULT setTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text, _In_ UINT32 pos); + HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& text); + HRESULT addActionHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& action, _In_ std::wstring const& arguments); + HRESULT addDurationHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& duration); + HRESULT addScenarioHelper(_In_ IXmlDocument* xml, _In_ std::wstring const& scenario); + HRESULT addInputHelper(_In_ IXmlDocument* xml); ComPtr notifier(_In_ bool* succeded) const; void setError(_Out_opt_ WinToastError* error, _In_ WinToastError value); }; -} +} // namespace WinToastLib #endif // WINTOASTLIB_H diff --git a/Photino.Native/Exports.cpp b/Photino.Native/Exports.cpp index 2fbd7c2..536e319 100644 --- a/Photino.Native/Exports.cpp +++ b/Photino.Native/Exports.cpp @@ -1,4 +1,5 @@ #include "Photino.Dialog.h" +#include "Photino.Notification.h" #include "Photino.h" #ifdef _WIN32 @@ -56,6 +57,11 @@ extern "C" instance->ClearBrowserAutoFill(); } + EXPORTED void Photino_Focus(Photino* instance) + { + instance->Focus(); + } + EXPORTED void Photino_Close(Photino* instance) { instance->Close(); @@ -266,6 +272,16 @@ extern "C" instance->SetZoom(zoom); } + EXPORTED void Photino_StartDragging(Photino* instance) + { + instance->StartDragging(); + } + + EXPORTED void Photino_StartResizing(Photino* instance, PhotinoWindowHitTestCode hitTestCode) + { + instance->StartResizing(hitTestCode); + } + EXPORTED void Photino_SetFlash(Photino* instance, bool state) { instance->SetFlash(state); @@ -280,11 +296,6 @@ extern "C" { instance->ClearProgress(); } - - EXPORTED void Photino_ShowNotification(Photino* instance, AutoString title, AutoString body) - { - instance->ShowNotification(title, body); - } EXPORTED void Photino_WaitForExit(Photino* instance) { @@ -349,4 +360,54 @@ extern "C" { instance->Invoke(callback); } + + EXPORTED PhotinoNotification* PhotinoNotification_ctor(Photino* window) + { + return new PhotinoNotification(window); + } + + EXPORTED void PhotinoNotification_dtor(PhotinoNotification* instance) + { + delete instance; + } + + EXPORTED void PhotinoNotification_AddText(PhotinoNotification* instance, AutoString text) + { + instance->AddText(text); + } + + EXPORTED void PhotinoNotification_AddAction(PhotinoNotification* instance, AutoString action) + { + instance->AddAction(action); + } + + EXPORTED void PhotinoNotification_SetImagePath(PhotinoNotification* instance, AutoString imagePath) + { + instance->SetImagePath(imagePath); + } + + EXPORTED void PhotinoNotification_SetType(PhotinoNotification* instance, PhotinoNotificationType type) + { + instance->SetType(type); + } + + EXPORTED void PhotinoNotification_Show(PhotinoNotification* instance) + { + instance->Show(); + } + + EXPORTED void PhotinoNotification_SetActionCallback(PhotinoNotification* instance, ActionCallback callback) + { + instance->SetActionCallback(callback); + } + + EXPORTED void PhotinoNotification_SetActivatedCallback(PhotinoNotification* instance, ActivatedCallback callback) + { + instance->SetActivatedCallback(callback); + } + + EXPORTED void PhotinoNotification_SetDismissedCallback(PhotinoNotification* instance, DismissedCallback callback) + { + instance->SetDismissedCallback(callback); + } } diff --git a/Photino.Native/Photino.Linux.cpp b/Photino.Native/Photino.Linux.cpp index fff9257..cef0207 100644 --- a/Photino.Native/Photino.Linux.cpp +++ b/Photino.Native/Photino.Linux.cpp @@ -333,6 +333,11 @@ void Photino::ClearBrowserAutoFill() // TODO } +void Photino::Focus() +{ + gtk_window_present(GTK_WINDOW(_window)); +} + void Photino::Close() { gtk_window_close(GTK_WINDOW(_window)); diff --git a/Photino.Native/Photino.Mac.mm b/Photino.Native/Photino.Mac.mm index d4a958d..8dad585 100644 --- a/Photino.Native/Photino.Mac.mm +++ b/Photino.Native/Photino.Mac.mm @@ -344,6 +344,12 @@ void Photino::ClearBrowserAutoFill() //TODO } +void Photino::Focus() +{ + [NSApp activateIgnoringOtherApps:YES]; + [_window makeKeyAndOrderFront:nil]; +} + void Photino::Close() { if (_chromeless) diff --git a/Photino.Native/Photino.Native.vcxproj b/Photino.Native/Photino.Native.vcxproj index ed0b25d..229365a 100644 --- a/Photino.Native/Photino.Native.vcxproj +++ b/Photino.Native/Photino.Native.vcxproj @@ -172,8 +172,9 @@ COPY .\$(OutDir)WebView2Loader.dll ..\Photino.Test\bin\Debug\net8.0\ - - + COPY .\$(OutDir)Photino.Native.dll ..\Photino.Test\bin\Debug\net8.0\ +COPY .\$(OutDir)Photino.Native.pdb ..\Photino.Test\bin\Debug\net8.0\ +COPY .\$(OutDir)WebView2Loader.dll ..\Photino.Test\bin\Debug\net8.0\ @@ -248,6 +249,8 @@ COPY .\$(OutDir)WebView2Loader.dll ..\Photino.Test\bin\ARM64\Debug\net8.0\ + + @@ -260,6 +263,7 @@ COPY .\$(OutDir)WebView2Loader.dll ..\Photino.Test\bin\ARM64\Debug\net8.0\ + diff --git a/Photino.Native/Photino.Native.vcxproj.filters b/Photino.Native/Photino.Native.vcxproj.filters index 7bdab04..347c9bc 100644 --- a/Photino.Native/Photino.Native.vcxproj.filters +++ b/Photino.Native/Photino.Native.vcxproj.filters @@ -36,6 +36,12 @@ Source Files + + Source Files + + + Source Files + @@ -105,6 +111,9 @@ Header Files + + Header Files + diff --git a/Photino.Native/Photino.Notification.h b/Photino.Native/Photino.Notification.h new file mode 100644 index 0000000..a5fe4fa --- /dev/null +++ b/Photino.Native/Photino.Notification.h @@ -0,0 +1,91 @@ +#pragma once + +#ifndef PHOTINO_NOTIFICATION_H +#define PHOTINO_NOTIFICATION_H + +#include "Photino.h" +#include + +#ifdef _WIN32 +class PhotinoWinToastHandler; +#endif + +enum class PhotinoNotificationType { + ImageAndText01, + ImageAndText02, + ImageAndText03, + ImageAndText04, + Text01, + Text02, + Text03, + Text04, +}; + +enum class PhotinoNotificationDismissalReason { + UserCanceled, + ApplicationHidden, + TimedOut, +}; + +typedef void (*ActionCallback)(int actionIndex); +typedef void (*ActivatedCallback)(); +typedef void (*DismissedCallback)(PhotinoNotificationDismissalReason reason); + +class PhotinoNotification +{ +private: + PhotinoNotificationType _type; + + ActionCallback _actionCallback; + ActivatedCallback _activatedCallback; + DismissedCallback _dismissedCallback; + + std::vector _textLines; + std::vector _actionNames; + AutoString _imagePath; + +public: + PhotinoNotification(Photino* window); + +#ifdef _WIN32 + ~PhotinoNotification(); +#endif + + std::vector GetActions() const; + + void AddText(AutoString text); + void AddAction(AutoString action); + void SetImagePath(AutoString imagePath); + void SetType(PhotinoNotificationType type); + void Show(); + + void SetActionCallback(ActionCallback callback) { _actionCallback = callback; } + void SetActivatedCallback(ActivatedCallback callback) { _activatedCallback = callback; } + void SetDismissedCallback(DismissedCallback callback) { _dismissedCallback = callback; } + + void InvokeAction(int actionIndex) const + { + if (_actionCallback) + return _actionCallback(actionIndex); + } + + void InvokeActivated() const + { + if (_activatedCallback) + return _activatedCallback(); + } + + void InvokeDismissed(PhotinoNotificationDismissalReason reason) const + { + if (_dismissedCallback) + return _dismissedCallback(reason); + } + +protected: +#ifdef _WIN32 + Photino* _window{}; + PhotinoWinToastHandler* _handler{}; +#endif +}; + +#endif \ No newline at end of file diff --git a/Photino.Native/Photino.Windows.Notification.cpp b/Photino.Native/Photino.Windows.Notification.cpp new file mode 100644 index 0000000..7dc998a --- /dev/null +++ b/Photino.Native/Photino.Windows.Notification.cpp @@ -0,0 +1,114 @@ +#include "Photino.Notification.h" + +#ifdef _WIN32 +#include "Photino.Windows.ToastHandler.h" +#endif + +WinToastLib::WinToastTemplate _toast; + +PhotinoNotification::PhotinoNotification(Photino* window) +{ + _window = window; + _handler = new PhotinoWinToastHandler(_window, this); +} + +PhotinoNotification::~PhotinoNotification() +{ + delete _handler; +} + +std::vector PhotinoNotification::GetActions() const +{ + return _actionNames; +} + +void PhotinoNotification::SetType(PhotinoNotificationType type) +{ + _type = type; + + WinToastLib::WinToastTemplate::WinToastTemplateType winToastType; + + switch (_type) + { + case PhotinoNotificationType::ImageAndText01: + winToastType = WinToastLib::WinToastTemplate::ImageAndText01; + break; + + case PhotinoNotificationType::ImageAndText02: + winToastType = WinToastLib::WinToastTemplate::ImageAndText02; + break; + + case PhotinoNotificationType::ImageAndText03: + winToastType = WinToastLib::WinToastTemplate::ImageAndText03; + break; + + case PhotinoNotificationType::ImageAndText04: + winToastType = WinToastLib::WinToastTemplate::ImageAndText04; + break; + + case PhotinoNotificationType::Text01: + winToastType = WinToastLib::WinToastTemplate::Text01; + break; + + case PhotinoNotificationType::Text02: + winToastType = WinToastLib::WinToastTemplate::Text02; + break; + + case PhotinoNotificationType::Text03: + winToastType = WinToastLib::WinToastTemplate::Text03; + break; + + case PhotinoNotificationType::Text04: + winToastType = WinToastLib::WinToastTemplate::Text04; + break; + + default: + winToastType = WinToastLib::WinToastTemplate::Text01; + break; + } + + _toast = WinToastLib::WinToastTemplate(winToastType); +} + +void PhotinoNotification::AddText(AutoString text) +{ + AutoString conv = _window->ToUTF16String(text); + + _textLines.push_back(conv); + + switch (_textLines.size()) + { + case 1: + _toast.setTextField(conv, WinToastLib::WinToastTemplate::FirstLine); + break; + + case 2: + _toast.setTextField(conv, WinToastLib::WinToastTemplate::SecondLine); + break; + + case 3: + _toast.setTextField(conv, WinToastLib::WinToastTemplate::ThirdLine); + break; + + case 4: + _toast.setAttributionText(conv); + break; + } +} + +void PhotinoNotification::AddAction(AutoString action) +{ + _actionNames.push_back(action); + _toast.addAction(_window->ToUTF16String(action)); +} + +void PhotinoNotification::SetImagePath(AutoString imagePath) +{ + _imagePath = imagePath; + _toast.setImagePath(_window->ToUTF16String(imagePath)); +} + +void PhotinoNotification::Show() +{ + WinToastLib::WinToast::instance()->showToast(_toast, _handler); +} \ No newline at end of file diff --git a/Photino.Native/Photino.Windows.ToastHandler.cpp b/Photino.Native/Photino.Windows.ToastHandler.cpp new file mode 100644 index 0000000..97e11a8 --- /dev/null +++ b/Photino.Native/Photino.Windows.ToastHandler.cpp @@ -0,0 +1,47 @@ +#ifdef _WIN32 +#include "Photino.Windows.ToastHandler.h" +#include "Photino.Notification.h" +using namespace WinToastLib; + +void PhotinoWinToastHandler::toastActivated() const +{ + ShowWindow(this->_window->getHwnd(), SW_SHOW); + ShowWindow(this->_window->getHwnd(), SW_RESTORE); + SetForegroundWindow(this->_window->getHwnd()); + + _notification->InvokeActivated(); +} + +void PhotinoWinToastHandler::toastActivated(int actionIndex) const +{ + _notification->InvokeAction(actionIndex); +} + +void PhotinoWinToastHandler::toastActivated(std::wstring response) const +{ + // Already implemented above +} + +void PhotinoWinToastHandler::toastDismissed(WinToastDismissalReason state) const +{ + switch (state) + { + case WinToastDismissalReason::UserCanceled: + _notification->InvokeDismissed(PhotinoNotificationDismissalReason::UserCanceled); + break; + + case WinToastDismissalReason::ApplicationHidden: + _notification->InvokeDismissed(PhotinoNotificationDismissalReason::ApplicationHidden); + break; + + case WinToastDismissalReason::TimedOut: + _notification->InvokeDismissed(PhotinoNotificationDismissalReason::TimedOut); + break; + } +} + +void PhotinoWinToastHandler::toastFailed() const +{ + // Not implemented +} +#endif \ No newline at end of file diff --git a/Photino.Native/Photino.Windows.ToastHandler.h b/Photino.Native/Photino.Windows.ToastHandler.h index 8dbd6c9..07d1d57 100644 --- a/Photino.Native/Photino.Windows.ToastHandler.h +++ b/Photino.Native/Photino.Windows.ToastHandler.h @@ -1,42 +1,34 @@ +#ifdef _WIN32 #ifndef TOASTHANDLER_H #define TOASTHANDLER_H + #include "Photino.h" +#include #include "Dependencies/wintoastlib.h" #include using namespace WinToastLib; -class WinToastHandler : public IWinToastHandler +class PhotinoNotification; + +class PhotinoWinToastHandler : public WinToastLib::IWinToastHandler { private: - Photino* _window; + Photino* _window{}; + PhotinoNotification* _notification{}; public: - WinToastHandler(Photino* window) + explicit PhotinoWinToastHandler(Photino* window, PhotinoNotification* notification) { - this->_window = window; + _window = window; + _notification = notification; } - void toastActivated() const - { - ShowWindow(this->_window->getHwnd(), SW_SHOW); // Make the window visible if it was hidden - ShowWindow(this->_window->getHwnd(), SW_RESTORE); // Next, restore it if it was minimized - SetForegroundWindow(this->_window->getHwnd()); // Finally, activate the window - } - - void toastActivated(int actionIndex) const - { - // - } - - void toastDismissed(WinToastDismissalReason state) const - { - // - } - - void toastFailed() const - { - // - } + void toastActivated() const override; + void toastActivated(int actionIndex) const override; + void toastActivated(std::wstring response) const override; + void toastDismissed(WinToastDismissalReason state) const override; + void toastFailed() const override; }; +#endif #endif \ No newline at end of file diff --git a/Photino.Native/Photino.Windows.cpp b/Photino.Native/Photino.Windows.cpp index c1b895c..73226a5 100644 --- a/Photino.Native/Photino.Windows.cpp +++ b/Photino.Native/Photino.Windows.cpp @@ -300,9 +300,7 @@ Photino::Photino(PhotinoInitParams* initParams) if (_notificationRegistrationId != NULL) WinToast::instance()->setAppUserModelId(_notificationRegistrationId); - this->_toastHandler = new WinToastHandler(this); WinToast::instance()->initialize(); - } _dialog = new PhotinoDialog(this); @@ -520,6 +518,13 @@ void Photino::Center() SetPosition(left, top); } +void Photino::Focus() +{ + ShowWindow(_hWnd, SW_SHOW); + ShowWindow(_hWnd, SW_RESTORE); + SetForegroundWindow(_hWnd); +} + void Photino::Close() { PostMessage(_hWnd, WM_CLOSE, NULL, NULL); @@ -857,6 +862,59 @@ void Photino::SetZoom(int zoom) //MessageBox(nullptr, msg, L"Setter", MB_OK); } + +void Photino::StartDragging() +{ + ReleaseCapture(); + SendMessage(_hWnd, WM_NCLBUTTONDOWN, HTCAPTION, 0); +} + +void Photino::StartResizing(PhotinoWindowHitTestCode hitTestCode) +{ + int winHitTestCode = 0; + + switch (hitTestCode) + { + case PhotinoWindowHitTestCode::Left: + winHitTestCode = HTLEFT; + break; + + case PhotinoWindowHitTestCode::Right: + winHitTestCode = HTRIGHT; + break; + + case PhotinoWindowHitTestCode::Top: + winHitTestCode = HTTOP; + break; + + case PhotinoWindowHitTestCode::Bottom: + winHitTestCode = HTBOTTOM; + break; + + case PhotinoWindowHitTestCode::TopLeft: + winHitTestCode = HTTOPLEFT; + break; + + case PhotinoWindowHitTestCode::TopRight: + winHitTestCode = HTTOPRIGHT; + break; + + case PhotinoWindowHitTestCode::BottomLeft: + winHitTestCode = HTBOTTOMLEFT; + break; + + case PhotinoWindowHitTestCode::BottomRight: + winHitTestCode = HTBOTTOMRIGHT; + break; + } + + if (winHitTestCode != 0) + { + ReleaseCapture(); + SendMessage(_hWnd, WM_NCLBUTTONDOWN, winHitTestCode, 0); + } +} + void Photino::SetFlash(bool state) { FLASHWINFO f = { sizeof(FLASHWINFO) }; @@ -921,23 +979,6 @@ void Photino::ClearProgress() } } - - -void Photino::ShowNotification(AutoString title, AutoString body) -{ - title = ToUTF16String(title); - body = ToUTF16String(body); - if (_notificationsEnabled && WinToast::isCompatible()) - { - WinToastTemplate toast = WinToastTemplate(WinToastTemplate::ImageAndText02); - toast.setTextField(title, WinToastTemplate::FirstLine); - toast.setTextField(body, WinToastTemplate::SecondLine); - if (this->_iconFileName != NULL) - toast.setImagePath(this->_iconFileName); - WinToast::instance()->showToast(toast, _toastHandler); - } -} - void Photino::WaitForExit() { messageLoopRootWindowHandle = _hWnd; diff --git a/Photino.Native/Photino.h b/Photino.Native/Photino.h index 222865d..40bb4ac 100644 --- a/Photino.Native/Photino.h +++ b/Photino.Native/Photino.h @@ -6,6 +6,7 @@ #include typedef wchar_t *AutoString; class WinToastHandler; +class PhotinoNotification; #else // AutoString for macOS/Linux typedef char *AutoString; @@ -40,6 +41,18 @@ struct Monitor double scale; }; +enum class PhotinoWindowHitTestCode +{ + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +}; + enum class PhotinoWindowProgressState { Error, @@ -61,6 +74,7 @@ typedef bool (*ClosingCallback)(); typedef void (*FocusInCallback)(); typedef void (*FocusOutCallback)(); +class PhotinoNotification; class PhotinoDialog; class Photino; @@ -87,7 +101,7 @@ struct PhotinoInitParams MovedCallback *MovedHandler; WebMessageReceivedCallback *WebMessageReceivedHandler; AutoString CustomSchemeNames[16]; - WebResourceRequestedCallback *CustomSchemeHandler; + WebResourceRequestedCallback* CustomSchemeHandler; int Left; int Top; @@ -103,6 +117,7 @@ struct PhotinoInitParams bool Chromeless; bool Transparent; bool ContextMenuEnabled; + bool ZoomEnabled; bool DevToolsEnabled; bool FullScreen; bool Maximized; @@ -252,6 +267,7 @@ public: void Center(); void ClearBrowserAutoFill(); + void Focus(); void Close(); void GetTransparentEnabled(bool *enabled); @@ -302,7 +318,10 @@ public: void SetProgress(ULONGLONG current, ULONGLONG total, PhotinoWindowProgressState state); void ClearProgress(); - void ShowNotification(AutoString title, AutoString message); + // Window manipulation + void StartDragging(); + void StartResizing(PhotinoWindowHitTestCode hitTest); + void WaitForExit(); // Callbacks diff --git a/Photino.Test/Photino.Test.csproj b/Photino.Test/Photino.Test.csproj index 46ac456..a25b3bd 100644 --- a/Photino.Test/Photino.Test.csproj +++ b/Photino.Test/Photino.Test.csproj @@ -30,16 +30,23 @@ - + + - + + + + + + PreserveNewest + Always @@ -51,7 +58,7 @@ - + diff --git a/Photino.Test/Program.cs b/Photino.Test/Program.cs index 9140104..5ac9006 100644 --- a/Photino.Test/Program.cs +++ b/Photino.Test/Program.cs @@ -99,7 +99,7 @@ namespace Photino.NET //.Offset(new Point(150, 150)) //.Offset(250, 250) .SetNotificationRegistrationId("8FDF1B15-3408-47A6-8EF5-2B0676B76277") //Replaces the window title when registering toast notifications - .SetNotificationsEnabled(false) + .SetNotificationsEnabled(true) //Browser settings //.SetContextMenuEnabled(false) @@ -132,6 +132,7 @@ namespace Photino.NET .SetLogVerbosity(_logEvents ? 2 : 0); + mainWindow.WaitForClose(); Console.WriteLine("Done Blocking!"); @@ -412,7 +413,21 @@ namespace Photino.NET } else if (string.Compare(message, "toastNotification", true) == 0) { - currentWindow.SendNotification("Toast Title", " Toast message! 🤖"); + currentWindow + .CreateNotification(PhotinoNotificationType.ToastImageAndText04) + .AddText("Hello World!") + .AddText("Lorem ipsum dolor sit amet") + .AddText("Some third text!") + .SetImagePath($"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}\\testImage.jpg") + .AddAction("Launch", (notification) => + { + Log(notification, "Launch notification action clicked!"); + }) + .AddAction("Ignore", (notification) => + { + Log(notification, "Ignore notification action clicked!"); + }) + .Show(); } else if (string.Compare(message, "showOpenFile", true) == 0) { @@ -515,6 +530,42 @@ namespace Photino.NET { currentWindow.ClearProgress(); } + else if (string.Compare(message, "startDragging", true) == 0) + { + currentWindow.StartDragging(); + } + else if (string.Compare(message, "startResizing-left", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.Left); + } + else if (string.Compare(message, "startResizing-topLeft", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.TopLeft); + } + else if (string.Compare(message, "startResizing-top", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.Top); + } + else if (string.Compare(message, "startResizing-topRight", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.TopRight); + } + else if (string.Compare(message, "startResizing-right", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.Right); + } + else if (string.Compare(message, "startResizing-bottomRight", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.BottomRight); + } + else if (string.Compare(message, "startResizing-bottom", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.Bottom); + } + else if (string.Compare(message, "startResizing-bottomLeft", true) == 0) + { + currentWindow.StartResizing(PhotinoWindowHitTestCode.BottomLeft); + } else throw new Exception($"Unknown message '{message}'"); } @@ -526,7 +577,23 @@ namespace Photino.NET private static void WindowCreated(object sender, EventArgs e) { + var window = (PhotinoWindow)sender; Log(sender, "WindowCreated Callback Fired."); + + window + .CreateNotification(PhotinoNotificationType.ToastImageAndText04) + .AddText("Hello World!") + .AddText("Lorem ipsum dolor sit amet") + .AddText("Some third text!") + .AddAction("Launch", (notification) => + { + Log(notification, "Launch notification action clicked!"); + }) + .AddAction("Ignore", (notification) => + { + Log(notification, "Ignore notification action clicked!"); + }) + .Show(); } private static void WindowLocationChanged(object sender, Point location) diff --git a/Photino.Test/testImage.jpg b/Photino.Test/testImage.jpg new file mode 100644 index 0000000..bc9676c Binary files /dev/null and b/Photino.Test/testImage.jpg differ diff --git a/Photino.Test/wwwroot/main.html b/Photino.Test/wwwroot/main.html index dcb7ea5..e28a505 100644 --- a/Photino.Test/wwwroot/main.html +++ b/Photino.Test/wwwroot/main.html @@ -144,6 +144,14 @@ window.external.sendMessage('setMaxSize'); } + function StartDragging() { + window.external.sendMessage('startDragging'); + } + + function StartResizing(hitTest) { + window.external.sendMessage('startResizing-' + hitTest); + } + function SetProgress(state) { window.external.sendMessage('setProgress-' + state + '-' + document.getElementById('progress-range').value); } @@ -300,6 +308,7 @@ +
Size
@@ -371,5 +380,80 @@ + + + + + + + + + + +