From 3e13e29af5abc7709651837a1dba8ea252a4ba6e Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 6 Sep 2025 22:43:27 -0500 Subject: [PATCH 01/10] Add overload for showing complex notifications --- Photino.Native/Exports.cpp | 5 ++ Photino.Native/Photino.Windows.cpp | 93 ++++++++++++++++++++++++++++++ Photino.Native/Photino.h | 25 ++++++++ Photino.Test/Photino.Test.csproj | 6 +- Photino.Test/Program.cs | 14 ++++- 5 files changed, 141 insertions(+), 2 deletions(-) diff --git a/Photino.Native/Exports.cpp b/Photino.Native/Exports.cpp index 98d4904..28a6106 100644 --- a/Photino.Native/Exports.cpp +++ b/Photino.Native/Exports.cpp @@ -265,6 +265,11 @@ extern "C" { instance->SetZoom(zoom); } + + EXPORTED void Photino_ShowNotificationEx(Photino* instance, AutoString firstLine, AutoString secondLine, AutoString thirdLine, AutoString attributionText, AutoString iconPath, PhotinoNotificationType type, AutoString button1, AutoString button2, AutoString button3, AutoString button4, AutoString button5) + { + instance->ShowNotification(firstLine, secondLine, thirdLine, attributionText, iconPath, type, button1, button2, button3, button4, button5); + } EXPORTED void Photino_ShowNotification(Photino* instance, AutoString title, AutoString body) { diff --git a/Photino.Native/Photino.Windows.cpp b/Photino.Native/Photino.Windows.cpp index 8e51e6f..24dcc87 100644 --- a/Photino.Native/Photino.Windows.cpp +++ b/Photino.Native/Photino.Windows.cpp @@ -857,7 +857,100 @@ void Photino::SetZoom(int zoom) //MessageBox(nullptr, msg, L"Setter", MB_OK); } +void Photino::ShowNotification( + AutoString firstLine, + AutoString secondLine, + AutoString thirdLine, + AutoString attributionText, + AutoString iconPath, + PhotinoNotificationType type, + AutoString button1, + AutoString button2, + AutoString button3, + AutoString button4, + AutoString button5) +{ + if (_notificationsEnabled && WinToast::isCompatible()) + { + WinToastTemplate::WinToastTemplateType toastType; + switch (type) + { + case PhotinoNotificationType::ImageAndText01: + toastType = WinToastTemplate::ImageAndText01; + break; + + case PhotinoNotificationType::ImageAndText02: + toastType = WinToastTemplate::ImageAndText02; + break; + + case PhotinoNotificationType::ImageAndText03: + toastType = WinToastTemplate::ImageAndText03; + break; + + case PhotinoNotificationType::ImageAndText04: + toastType = WinToastTemplate::ImageAndText04; + break; + + case PhotinoNotificationType::Text01: + toastType = WinToastTemplate::Text01; + break; + + case PhotinoNotificationType::Text02: + toastType = WinToastTemplate::Text02; + break; + + case PhotinoNotificationType::Text03: + toastType = WinToastTemplate::Text03; + break; + + case PhotinoNotificationType::Text04: + toastType = WinToastTemplate::Text04; + break; + + default: + toastType = WinToastTemplate::Text01; + break; + } + + WinToastTemplate toast = WinToastTemplate(toastType); + + if (firstLine != NULL) + toast.setFirstLine(ToUTF16String(firstLine)); + + if (secondLine != NULL) + toast.setSecondLine(ToUTF16String(secondLine)); + + if (thirdLine != NULL) + toast.setThirdLine(ToUTF16String(thirdLine)); + + if (attributionText != NULL) + toast.setAttributionText(ToUTF16String(attributionText)); + + if (iconPath != NULL) + toast.setImagePath(ToUTF16String(iconPath)); + else + toast.setImagePath(this->_iconFileName); + + if (button1 != NULL) + toast.addAction(ToUTF16String(button1)); + + if (button2 != NULL) + toast.addAction(ToUTF16String(button2)); + + if (button3 != NULL) + toast.addAction(ToUTF16String(button3)); + + if (button4 != NULL) + toast.addAction(ToUTF16String(button4)); + + if (button5 != NULL) + toast.addAction(ToUTF16String(button5)); + + // Show the toast + WinToast::instance()->showToast(toast, _toastHandler); + } +} void Photino::ShowNotification(AutoString title, AutoString body) { diff --git a/Photino.Native/Photino.h b/Photino.Native/Photino.h index cf44c5d..d48b226 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; @@ -56,6 +57,17 @@ typedef void (*FocusOutCallback)(); class PhotinoDialog; class Photino; +enum PhotinoNotificationType { + ImageAndText01, + ImageAndText02, + ImageAndText03, + ImageAndText04, + Text01, + Text02, + Text03, + Text04, +}; + struct PhotinoInitParams { AutoString StartString; @@ -291,6 +303,19 @@ public: void SetTopmost(bool topmost); void SetZoom(int zoom); + void ShowNotification( + AutoString firstLine, + AutoString secondLine, + AutoString thirdLine, + AutoString attributionText, + AutoString iconPath, + PhotinoNotificationType type, + AutoString button1, + AutoString button2, + AutoString button3, + AutoString button4, + AutoString button5); + void ShowNotification(AutoString title, AutoString message); void WaitForExit(); diff --git a/Photino.Test/Photino.Test.csproj b/Photino.Test/Photino.Test.csproj index bba84d9..999743f 100644 --- a/Photino.Test/Photino.Test.csproj +++ b/Photino.Test/Photino.Test.csproj @@ -35,7 +35,11 @@ - + + + + + diff --git a/Photino.Test/Program.cs b/Photino.Test/Program.cs index a8d83e5..1f568db 100644 --- a/Photino.Test/Program.cs +++ b/Photino.Test/Program.cs @@ -97,7 +97,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) @@ -130,6 +130,7 @@ namespace Photino.NET .SetLogVerbosity(_logEvents ? 2 : 0); + mainWindow.WaitForClose(); Console.WriteLine("Done Blocking!"); @@ -487,7 +488,18 @@ namespace Photino.NET private static void WindowCreated(object sender, EventArgs e) { + var window = (PhotinoWindow)sender; Log(sender, "WindowCreated Callback Fired."); + + window.SendNotification(builder => + { + builder + .AddText("Hello World!") + .AddText("Lorem ipsum dolor sit amet") + .AddText("Some third text!") + .AddButton("Launch") + .AddButton("Ignore"); + }); } private static void WindowLocationChanged(object sender, Point location) From 1b1ab615e77109b30afac2b5e6f5a94c9b3c59a4 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sun, 7 Sep 2025 18:54:25 -0500 Subject: [PATCH 02/10] Use setTextField --- Photino.Native/Photino.Windows.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Photino.Native/Photino.Windows.cpp b/Photino.Native/Photino.Windows.cpp index 24dcc87..470071c 100644 --- a/Photino.Native/Photino.Windows.cpp +++ b/Photino.Native/Photino.Windows.cpp @@ -916,13 +916,13 @@ void Photino::ShowNotification( WinToastTemplate toast = WinToastTemplate(toastType); if (firstLine != NULL) - toast.setFirstLine(ToUTF16String(firstLine)); + toast.setTextField(ToUTF16String(firstLine), WinToastTemplate::FirstLine); if (secondLine != NULL) - toast.setSecondLine(ToUTF16String(secondLine)); + toast.setTextField(ToUTF16String(secondLine), WinToastTemplate::SecondLine); if (thirdLine != NULL) - toast.setThirdLine(ToUTF16String(thirdLine)); + toast.setTextField(ToUTF16String(thirdLine), WinToastTemplate::ThirdLine); if (attributionText != NULL) toast.setAttributionText(ToUTF16String(attributionText)); From a114a41e18e86f2b1dcbac67bcaa05edd3cf43ae Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 17 Oct 2025 20:50:36 -0500 Subject: [PATCH 03/10] Add Notification type Main definition of the Notification type. Each notification can have callbacks registered to it, so that activation/dismissal isn't handled globally. This will allow for more complex handling of notifications. --- Photino.Native/Exports.cpp | 10 -- Photino.Native/Photino.Native.vcxproj | 2 + Photino.Native/Photino.Native.vcxproj.filters | 6 + Photino.Native/Photino.Notification.h | 91 ++++++++++++++ .../Photino.Windows.Notification.cpp | 98 +++++++++++++++ Photino.Native/Photino.Windows.ToastHandler.h | 29 ++++- Photino.Native/Photino.Windows.cpp | 112 ------------------ Photino.Native/Photino.h | 29 +---- 8 files changed, 225 insertions(+), 152 deletions(-) create mode 100644 Photino.Native/Photino.Notification.h create mode 100644 Photino.Native/Photino.Windows.Notification.cpp diff --git a/Photino.Native/Exports.cpp b/Photino.Native/Exports.cpp index 28a6106..27e3cb3 100644 --- a/Photino.Native/Exports.cpp +++ b/Photino.Native/Exports.cpp @@ -266,16 +266,6 @@ extern "C" instance->SetZoom(zoom); } - EXPORTED void Photino_ShowNotificationEx(Photino* instance, AutoString firstLine, AutoString secondLine, AutoString thirdLine, AutoString attributionText, AutoString iconPath, PhotinoNotificationType type, AutoString button1, AutoString button2, AutoString button3, AutoString button4, AutoString button5) - { - instance->ShowNotification(firstLine, secondLine, thirdLine, attributionText, iconPath, type, button1, button2, button3, button4, button5); - } - - EXPORTED void Photino_ShowNotification(Photino* instance, AutoString title, AutoString body) - { - instance->ShowNotification(title, body); - } - EXPORTED void Photino_WaitForExit(Photino* instance) { instance->WaitForExit(); diff --git a/Photino.Native/Photino.Native.vcxproj b/Photino.Native/Photino.Native.vcxproj index ed0b25d..6541c33 100644 --- a/Photino.Native/Photino.Native.vcxproj +++ b/Photino.Native/Photino.Native.vcxproj @@ -248,6 +248,7 @@ COPY .\$(OutDir)WebView2Loader.dll ..\Photino.Test\bin\ARM64\Debug\net8.0\ + @@ -260,6 +261,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..1974427 100644 --- a/Photino.Native/Photino.Native.vcxproj.filters +++ b/Photino.Native/Photino.Native.vcxproj.filters @@ -36,6 +36,9 @@ Source Files + + Source Files + @@ -105,6 +108,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..3c62d86 --- /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" + +#ifndef _WIN32 +#include "Dependencies/wintoastlib.h" +#endif + +enum class PhotinoNotificationType { + ImageAndText01, + ImageAndText02, + ImageAndText03, + ImageAndText04, + Text01, + Text02, + Text03, + Text04, +}; + +enum class PhotinoNotificationDismissalReason { + UserCanceled, + ApplicationHidden, + TimedOut, +}; + +typedef void (*ActionCallback)(AutoString action); +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: +#ifdef _WIN32 + PhotinoNotification(Photino* window); +#else + PhotinoNotification(); +#endif + ~PhotinoNotification(); + + 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(AutoString action) const + { + if (_actionCallback) + return _actionCallback(action); + } + + void InvokeActivated() const + { + if (_activatedCallback) + return _activatedCallback(); + } + + void InvokeDismissed(PhotinoNotificationDismissalReason reason) const + { + if (_dismissedCallback) + return _dismissedCallback(reason); + } + +protected: +#ifdef _WIN32 + Photino* _window; + WinToastHandler* _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..8e41819 --- /dev/null +++ b/Photino.Native/Photino.Windows.Notification.cpp @@ -0,0 +1,98 @@ +#include "Photino.Notification.h" +#include "Photino.Windows.ToastHandler.h" + +#include +#include "Dependencies/wintoastlib.h" + +WinToastLib::WinToastTemplate _toast; + +PhotinoNotification::PhotinoNotification(Photino* window) +{ + _window = window; + _handler = new WinToastHandler(this); +} + +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; + + case PhotinoNotificationType::ImageAndText02: + winToastType = WinToastLib::WinToastTemplate::ImageAndText02; + + case PhotinoNotificationType::ImageAndText03: + winToastType = WinToastLib::WinToastTemplate::ImageAndText03; + + case PhotinoNotificationType::ImageAndText04: + winToastType = WinToastLib::WinToastTemplate::ImageAndText04; + + case PhotinoNotificationType::Text01: + winToastType = WinToastLib::WinToastTemplate::Text01; + + case PhotinoNotificationType::Text02: + winToastType = WinToastLib::WinToastTemplate::Text02; + + case PhotinoNotificationType::Text03: + winToastType = WinToastLib::WinToastTemplate::Text03; + + case PhotinoNotificationType::Text04: + winToastType = WinToastLib::WinToastTemplate::Text04; + + default: + winToastType = WinToastLib::WinToastTemplate::Text01; + } + + _toast = WinToastLib::WinToastTemplate(winToastType); +} + +void PhotinoNotification::AddText(AutoString text) +{ + _textLines.push_back(text); + + switch (_textLines.size()) + { + case 1: + _toast.setTextField(_window->ToUTF16String(text), WinToastLib::WinToastTemplate::FirstLine); + break; + + case 2: + _toast.setTextField(_window->ToUTF16String(text), WinToastLib::WinToastTemplate::SecondLine); + break; + + case 3: + _toast.setTextField(_window->ToUTF16String(text), WinToastLib::WinToastTemplate::ThirdLine); + break; + + case 4: + _toast.setAttributionText(_window->ToUTF16String(text)); + 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.h b/Photino.Native/Photino.Windows.ToastHandler.h index 8dbd6c9..20fcbb1 100644 --- a/Photino.Native/Photino.Windows.ToastHandler.h +++ b/Photino.Native/Photino.Windows.ToastHandler.h @@ -1,6 +1,7 @@ #ifndef TOASTHANDLER_H #define TOASTHANDLER_H #include "Photino.h" +#include "Photino.Notification.h" #include "Dependencies/wintoastlib.h" #include @@ -10,11 +11,12 @@ class WinToastHandler : public IWinToastHandler { private: Photino* _window; + PhotinoNotification* _notification; public: - WinToastHandler(Photino* window) + WinToastHandler(PhotinoNotification* notification) { - this->_window = window; + this->_notification = notification; } void toastActivated() const @@ -22,16 +24,35 @@ public: 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 + + _notification->InvokeActivated(); } void toastActivated(int actionIndex) const { - // + std::vector actions = _notification->GetActions(); + + AutoString action = actions.at(actionIndex); + + _notification->InvokeAction(action); } void 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 toastFailed() const diff --git a/Photino.Native/Photino.Windows.cpp b/Photino.Native/Photino.Windows.cpp index 470071c..b1f5a20 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); @@ -857,116 +855,6 @@ void Photino::SetZoom(int zoom) //MessageBox(nullptr, msg, L"Setter", MB_OK); } -void Photino::ShowNotification( - AutoString firstLine, - AutoString secondLine, - AutoString thirdLine, - AutoString attributionText, - AutoString iconPath, - PhotinoNotificationType type, - AutoString button1, - AutoString button2, - AutoString button3, - AutoString button4, - AutoString button5) -{ - if (_notificationsEnabled && WinToast::isCompatible()) - { - WinToastTemplate::WinToastTemplateType toastType; - - switch (type) - { - case PhotinoNotificationType::ImageAndText01: - toastType = WinToastTemplate::ImageAndText01; - break; - - case PhotinoNotificationType::ImageAndText02: - toastType = WinToastTemplate::ImageAndText02; - break; - - case PhotinoNotificationType::ImageAndText03: - toastType = WinToastTemplate::ImageAndText03; - break; - - case PhotinoNotificationType::ImageAndText04: - toastType = WinToastTemplate::ImageAndText04; - break; - - case PhotinoNotificationType::Text01: - toastType = WinToastTemplate::Text01; - break; - - case PhotinoNotificationType::Text02: - toastType = WinToastTemplate::Text02; - break; - - case PhotinoNotificationType::Text03: - toastType = WinToastTemplate::Text03; - break; - - case PhotinoNotificationType::Text04: - toastType = WinToastTemplate::Text04; - break; - - default: - toastType = WinToastTemplate::Text01; - break; - } - - WinToastTemplate toast = WinToastTemplate(toastType); - - if (firstLine != NULL) - toast.setTextField(ToUTF16String(firstLine), WinToastTemplate::FirstLine); - - if (secondLine != NULL) - toast.setTextField(ToUTF16String(secondLine), WinToastTemplate::SecondLine); - - if (thirdLine != NULL) - toast.setTextField(ToUTF16String(thirdLine), WinToastTemplate::ThirdLine); - - if (attributionText != NULL) - toast.setAttributionText(ToUTF16String(attributionText)); - - if (iconPath != NULL) - toast.setImagePath(ToUTF16String(iconPath)); - else - toast.setImagePath(this->_iconFileName); - - if (button1 != NULL) - toast.addAction(ToUTF16String(button1)); - - if (button2 != NULL) - toast.addAction(ToUTF16String(button2)); - - if (button3 != NULL) - toast.addAction(ToUTF16String(button3)); - - if (button4 != NULL) - toast.addAction(ToUTF16String(button4)); - - if (button5 != NULL) - toast.addAction(ToUTF16String(button5)); - - // Show the toast - WinToast::instance()->showToast(toast, _toastHandler); - } -} - -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 d48b226..d7ea436 100644 --- a/Photino.Native/Photino.h +++ b/Photino.Native/Photino.h @@ -54,20 +54,10 @@ typedef bool (*ClosingCallback)(); typedef void (*FocusInCallback)(); typedef void (*FocusOutCallback)(); +class PhotinoNotification; class PhotinoDialog; class Photino; -enum PhotinoNotificationType { - ImageAndText01, - ImageAndText02, - ImageAndText03, - ImageAndText04, - Text01, - Text02, - Text03, - Text04, -}; - struct PhotinoInitParams { AutoString StartString; @@ -91,7 +81,7 @@ struct PhotinoInitParams MovedCallback *MovedHandler; WebMessageReceivedCallback *WebMessageReceivedHandler; AutoString CustomSchemeNames[16]; - WebResourceRequestedCallback *CustomSchemeHandler; + WebResourceRequestedCallback* CustomSchemeHandler; int Left; int Top; @@ -107,6 +97,7 @@ struct PhotinoInitParams bool Chromeless; bool Transparent; bool ContextMenuEnabled; + bool ZoomEnabled; bool DevToolsEnabled; bool FullScreen; bool Maximized; @@ -303,20 +294,6 @@ public: void SetTopmost(bool topmost); void SetZoom(int zoom); - void ShowNotification( - AutoString firstLine, - AutoString secondLine, - AutoString thirdLine, - AutoString attributionText, - AutoString iconPath, - PhotinoNotificationType type, - AutoString button1, - AutoString button2, - AutoString button3, - AutoString button4, - AutoString button5); - - void ShowNotification(AutoString title, AutoString message); void WaitForExit(); // Callbacks From 762d13f4a4c3df3b4762d05b2202ef7c696081c2 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 18 Oct 2025 17:59:29 -0500 Subject: [PATCH 04/10] Update WinToastLib, implement C#->C++ calls --- Photino.Native/Dependencies/wintoastlib.cpp | 859 ++++++++++++------ Photino.Native/Dependencies/wintoastlib.h | 218 +++-- Photino.Native/Exports.cpp | 51 ++ Photino.Native/Photino.Native.vcxproj | 6 +- Photino.Native/Photino.Native.vcxproj.filters | 3 + Photino.Native/Photino.Notification.h | 22 +- .../Photino.Windows.Notification.cpp | 34 +- .../Photino.Windows.ToastHandler.cpp | 47 + Photino.Native/Photino.Windows.ToastHandler.h | 63 +- Photino.Test/Photino.Test.csproj | 8 +- Photino.Test/Program.cs | 39 +- Photino.Test/testImage.jpg | Bin 0 -> 77876 bytes 12 files changed, 926 insertions(+), 424 deletions(-) create mode 100644 Photino.Native/Photino.Windows.ToastHandler.cpp create mode 100644 Photino.Test/testImage.jpg 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 27e3cb3..eaf839e 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 @@ -329,4 +330,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.Native.vcxproj b/Photino.Native/Photino.Native.vcxproj index 6541c33..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\ @@ -249,6 +250,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 1974427..347c9bc 100644 --- a/Photino.Native/Photino.Native.vcxproj.filters +++ b/Photino.Native/Photino.Native.vcxproj.filters @@ -39,6 +39,9 @@ Source Files + + Source Files + diff --git a/Photino.Native/Photino.Notification.h b/Photino.Native/Photino.Notification.h index 3c62d86..a5fe4fa 100644 --- a/Photino.Native/Photino.Notification.h +++ b/Photino.Native/Photino.Notification.h @@ -4,9 +4,10 @@ #define PHOTINO_NOTIFICATION_H #include "Photino.h" +#include -#ifndef _WIN32 -#include "Dependencies/wintoastlib.h" +#ifdef _WIN32 +class PhotinoWinToastHandler; #endif enum class PhotinoNotificationType { @@ -26,7 +27,7 @@ enum class PhotinoNotificationDismissalReason { TimedOut, }; -typedef void (*ActionCallback)(AutoString action); +typedef void (*ActionCallback)(int actionIndex); typedef void (*ActivatedCallback)(); typedef void (*DismissedCallback)(PhotinoNotificationDismissalReason reason); @@ -44,12 +45,11 @@ private: AutoString _imagePath; public: -#ifdef _WIN32 PhotinoNotification(Photino* window); -#else - PhotinoNotification(); -#endif + +#ifdef _WIN32 ~PhotinoNotification(); +#endif std::vector GetActions() const; @@ -63,10 +63,10 @@ public: void SetActivatedCallback(ActivatedCallback callback) { _activatedCallback = callback; } void SetDismissedCallback(DismissedCallback callback) { _dismissedCallback = callback; } - void InvokeAction(AutoString action) const + void InvokeAction(int actionIndex) const { if (_actionCallback) - return _actionCallback(action); + return _actionCallback(actionIndex); } void InvokeActivated() const @@ -83,8 +83,8 @@ public: protected: #ifdef _WIN32 - Photino* _window; - WinToastHandler* _handler; + Photino* _window{}; + PhotinoWinToastHandler* _handler{}; #endif }; diff --git a/Photino.Native/Photino.Windows.Notification.cpp b/Photino.Native/Photino.Windows.Notification.cpp index 8e41819..7dc998a 100644 --- a/Photino.Native/Photino.Windows.Notification.cpp +++ b/Photino.Native/Photino.Windows.Notification.cpp @@ -1,15 +1,20 @@ #include "Photino.Notification.h" -#include "Photino.Windows.ToastHandler.h" -#include -#include "Dependencies/wintoastlib.h" +#ifdef _WIN32 +#include "Photino.Windows.ToastHandler.h" +#endif WinToastLib::WinToastTemplate _toast; PhotinoNotification::PhotinoNotification(Photino* window) { _window = window; - _handler = new WinToastHandler(this); + _handler = new PhotinoWinToastHandler(_window, this); +} + +PhotinoNotification::~PhotinoNotification() +{ + delete _handler; } std::vector PhotinoNotification::GetActions() const @@ -27,30 +32,39 @@ void PhotinoNotification::SetType(PhotinoNotificationType 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); @@ -58,24 +72,26 @@ void PhotinoNotification::SetType(PhotinoNotificationType type) void PhotinoNotification::AddText(AutoString text) { - _textLines.push_back(text); + AutoString conv = _window->ToUTF16String(text); + + _textLines.push_back(conv); switch (_textLines.size()) { case 1: - _toast.setTextField(_window->ToUTF16String(text), WinToastLib::WinToastTemplate::FirstLine); + _toast.setTextField(conv, WinToastLib::WinToastTemplate::FirstLine); break; case 2: - _toast.setTextField(_window->ToUTF16String(text), WinToastLib::WinToastTemplate::SecondLine); + _toast.setTextField(conv, WinToastLib::WinToastTemplate::SecondLine); break; case 3: - _toast.setTextField(_window->ToUTF16String(text), WinToastLib::WinToastTemplate::ThirdLine); + _toast.setTextField(conv, WinToastLib::WinToastTemplate::ThirdLine); break; case 4: - _toast.setAttributionText(_window->ToUTF16String(text)); + _toast.setAttributionText(conv); break; } } 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 20fcbb1..07d1d57 100644 --- a/Photino.Native/Photino.Windows.ToastHandler.h +++ b/Photino.Native/Photino.Windows.ToastHandler.h @@ -1,63 +1,34 @@ +#ifdef _WIN32 #ifndef TOASTHANDLER_H #define TOASTHANDLER_H + #include "Photino.h" -#include "Photino.Notification.h" +#include #include "Dependencies/wintoastlib.h" #include using namespace WinToastLib; -class WinToastHandler : public IWinToastHandler +class PhotinoNotification; + +class PhotinoWinToastHandler : public WinToastLib::IWinToastHandler { private: - Photino* _window; - PhotinoNotification* _notification; + Photino* _window{}; + PhotinoNotification* _notification{}; public: - WinToastHandler(PhotinoNotification* notification) + explicit PhotinoWinToastHandler(Photino* window, PhotinoNotification* notification) { - this->_notification = notification; + _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 - - _notification->InvokeActivated(); - } - - void toastActivated(int actionIndex) const - { - std::vector actions = _notification->GetActions(); - - AutoString action = actions.at(actionIndex); - - _notification->InvokeAction(action); - } - - void 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 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.Test/Photino.Test.csproj b/Photino.Test/Photino.Test.csproj index 999743f..a9f963a 100644 --- a/Photino.Test/Photino.Test.csproj +++ b/Photino.Test/Photino.Test.csproj @@ -30,16 +30,16 @@ - + + - - + @@ -54,7 +54,7 @@ - + diff --git a/Photino.Test/Program.cs b/Photino.Test/Program.cs index 1f568db..cb870d3 100644 --- a/Photino.Test/Program.cs +++ b/Photino.Test/Program.cs @@ -411,7 +411,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("F:\\Repositories\\photino.Native\\Photino.Test\\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) { @@ -491,15 +505,20 @@ namespace Photino.NET var window = (PhotinoWindow)sender; Log(sender, "WindowCreated Callback Fired."); - window.SendNotification(builder => - { - builder - .AddText("Hello World!") - .AddText("Lorem ipsum dolor sit amet") - .AddText("Some third text!") - .AddButton("Launch") - .AddButton("Ignore"); - }); + 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 0000000000000000000000000000000000000000..bc9676cbc5752fccfd8cea35ae68b52cb196ce9c GIT binary patch literal 77876 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 K^7RZjpa0n+i5;x~ literal 0 HcmV?d00001 From 44b3ffb2b7e4b0460ea6ee48ed019b561d6bbe55 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 18 Oct 2025 18:06:29 -0500 Subject: [PATCH 05/10] Fix image test path --- Photino.Test/Photino.Test.csproj | 3 +++ Photino.Test/Program.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Photino.Test/Photino.Test.csproj b/Photino.Test/Photino.Test.csproj index a9f963a..04ec74b 100644 --- a/Photino.Test/Photino.Test.csproj +++ b/Photino.Test/Photino.Test.csproj @@ -43,6 +43,9 @@ + + PreserveNewest + Always diff --git a/Photino.Test/Program.cs b/Photino.Test/Program.cs index cb870d3..87e57d9 100644 --- a/Photino.Test/Program.cs +++ b/Photino.Test/Program.cs @@ -416,7 +416,7 @@ namespace Photino.NET .AddText("Hello World!") .AddText("Lorem ipsum dolor sit amet") .AddText("Some third text!") - .SetImagePath("F:\\Repositories\\photino.Native\\Photino.Test\\testImage.jpg") + .SetImagePath($"{Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)}\\testImage.jpg") .AddAction("Launch", (notification) => { Log(notification, "Launch notification action clicked!"); From a9f781b289d3c9f14c243ce621f9cc36143782dc Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Sat, 18 Oct 2025 20:54:35 -0500 Subject: [PATCH 06/10] Add functions for dragging and resizing windows --- Photino.Native/Exports.cpp | 10 ++++ Photino.Native/Photino.Windows.cpp | 52 ++++++++++++++++++ Photino.Native/Photino.h | 16 ++++++ Photino.Test/Photino.Test.csproj | 3 +- Photino.Test/Program.cs | 36 +++++++++++++ Photino.Test/wwwroot/main.html | 84 ++++++++++++++++++++++++++++++ 6 files changed, 200 insertions(+), 1 deletion(-) diff --git a/Photino.Native/Exports.cpp b/Photino.Native/Exports.cpp index 98d4904..470aa9a 100644 --- a/Photino.Native/Exports.cpp +++ b/Photino.Native/Exports.cpp @@ -265,6 +265,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_ShowNotification(Photino* instance, AutoString title, AutoString body) { diff --git a/Photino.Native/Photino.Windows.cpp b/Photino.Native/Photino.Windows.cpp index 8e51e6f..b1f3807 100644 --- a/Photino.Native/Photino.Windows.cpp +++ b/Photino.Native/Photino.Windows.cpp @@ -858,6 +858,58 @@ void Photino::SetZoom(int zoom) } +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::ShowNotification(AutoString title, AutoString body) { diff --git a/Photino.Native/Photino.h b/Photino.Native/Photino.h index cf44c5d..ea314c9 100644 --- a/Photino.Native/Photino.h +++ b/Photino.Native/Photino.h @@ -40,6 +40,18 @@ struct Monitor double scale; }; +enum class PhotinoWindowHitTestCode +{ + Left, + Right, + Top, + Bottom, + TopLeft, + TopRight, + BottomLeft, + BottomRight, +}; + typedef void (*ACTION)(); typedef void (*WebMessageReceivedCallback)(AutoString message); typedef void *(*WebResourceRequestedCallback)(AutoString url, int *outNumBytes, AutoString *outContentType); @@ -291,6 +303,10 @@ public: void SetTopmost(bool topmost); void SetZoom(int zoom); + // Window manipulation + void StartDragging(); + void StartResizing(PhotinoWindowHitTestCode hitTest); + void ShowNotification(AutoString title, AutoString message); void WaitForExit(); diff --git a/Photino.Test/Photino.Test.csproj b/Photino.Test/Photino.Test.csproj index bba84d9..c429ad4 100644 --- a/Photino.Test/Photino.Test.csproj +++ b/Photino.Test/Photino.Test.csproj @@ -35,7 +35,8 @@ - + + diff --git a/Photino.Test/Program.cs b/Photino.Test/Program.cs index a8d83e5..528cb0b 100644 --- a/Photino.Test/Program.cs +++ b/Photino.Test/Program.cs @@ -476,6 +476,42 @@ namespace Photino.NET { var result = currentWindow.ShowMessage("Title", "Testing... 🤖"); } + 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}'"); } diff --git a/Photino.Test/wwwroot/main.html b/Photino.Test/wwwroot/main.html index 0848b89..29a4c6a 100644 --- a/Photino.Test/wwwroot/main.html +++ b/Photino.Test/wwwroot/main.html @@ -138,6 +138,14 @@ window.external.sendMessage('setMaxSize'); } + function StartDragging() { + window.external.sendMessage('startDragging'); + } + + function StartResizing(hitTest) { + window.external.sendMessage('startResizing-' + hitTest); + } + (() => { // The width and height of the captured photo. We will set the // width to the value defined here, but the height will be @@ -278,6 +286,7 @@ +
Size
@@ -336,5 +345,80 @@ + + + + + + + + + + + From f59988bb554d4c98ac6bac0e0ac0c320f2b25bfd Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Wed, 22 Oct 2025 18:58:35 -0500 Subject: [PATCH 07/10] Add function to focus the window --- Photino.Native/Photino.Linux.cpp | 5 +++++ Photino.Native/Photino.Mac.mm | 6 ++++++ Photino.Native/Photino.Windows.cpp | 7 +++++++ Photino.Native/Photino.h | 1 + 4 files changed, 19 insertions(+) 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.Windows.cpp b/Photino.Native/Photino.Windows.cpp index 8e51e6f..1a0f344 100644 --- a/Photino.Native/Photino.Windows.cpp +++ b/Photino.Native/Photino.Windows.cpp @@ -520,6 +520,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); diff --git a/Photino.Native/Photino.h b/Photino.Native/Photino.h index cf44c5d..9960ca1 100644 --- a/Photino.Native/Photino.h +++ b/Photino.Native/Photino.h @@ -244,6 +244,7 @@ public: void Center(); void ClearBrowserAutoFill(); + void Focus(); void Close(); void GetTransparentEnabled(bool *enabled); From 0117fe56a03aebf75ac5167e82d32b09a8d6a715 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Wed, 22 Oct 2025 19:00:00 -0500 Subject: [PATCH 08/10] Add export --- Photino.Native/Exports.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Photino.Native/Exports.cpp b/Photino.Native/Exports.cpp index 98d4904..b3ffd87 100644 --- a/Photino.Native/Exports.cpp +++ b/Photino.Native/Exports.cpp @@ -56,6 +56,11 @@ extern "C" instance->ClearBrowserAutoFill(); } + EXPORTED void Photino_Focus(Photino* instance) + { + instance->Focus(); + } + EXPORTED void Photino_Close(Photino* instance) { instance->Close(); From eb9ff6eef2c2a796dfe735eba9007b2e0838ce8c Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 24 Oct 2025 17:12:45 -0500 Subject: [PATCH 09/10] Copy output to test artifact directory --- Photino.Native/Photino.Native.vcxproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Photino.Native/Photino.Native.vcxproj b/Photino.Native/Photino.Native.vcxproj index ed0b25d..8a6a31b 100644 --- a/Photino.Native/Photino.Native.vcxproj +++ b/Photino.Native/Photino.Native.vcxproj @@ -174,6 +174,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\ From 8d8d9166fb6df9a44723e863fe91d61cc8bd02c4 Mon Sep 17 00:00:00 2001 From: Pat Hartl Date: Fri, 24 Oct 2025 17:31:31 -0500 Subject: [PATCH 10/10] GitHub action --- .../workflows/photino.Native.Development.yml | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 .github/workflows/photino.Native.Development.yml 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