diff --git a/.gitignore b/.gitignore
index 5ca6fdd..5c784bd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,7 +1,8 @@
.idea
.vs
.stfolder
+builds
+.stignore
+.DS_Store
CMakeSettings.json
-cmake-build-debug
-cmake-build-release
CMakeLists.txt.user
\ No newline at end of file
diff --git a/CMakeLists.txt b/CMakeLists.txt
index d489009..68a1faa 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,28 +1,19 @@
cmake_minimum_required(VERSION 3.14)
project(Strategr)
-
set(CMAKE_CXX_STANDARD 17)
set(Boost_USE_STATIC_LIBS ON)
-message("platform ${CMAKE_GENERATOR_PLATFORM}")
-
find_package(Boost COMPONENTS filesystem REQUIRED)
find_package(catch2 REQUIRED)
find_package(Qt5 COMPONENTS Widgets Network Test REQUIRED)
-find_library(utf8Proc_LIBRARY_PATH
- libutf8proc.a
- utf8proc_static.lib
- utf8proc
- HINTS $ENV{LIBPATH})
+find_library(utf8Proc_LIBRARY_PATH libutf8proc.a utf8proc.lib utf8proc)
if (APPLE)
find_package(Qt5 COMPONENTS MacExtras REQUIRED)
endif ()
-message(${utf8Proc_LIBRARY_PATH})
-
include_directories(.)
include_directories(models)
include_directories(models/apple)
@@ -88,7 +79,11 @@ set(MODELS ${MODELS_PLATFORM_FILES}
models/utility.h
models/mousehandler.cpp
models/mousehandler.h
- models/mousehandleroperations.h models/selection.cpp models/selection.h models/event.cpp models/event.h)
+ models/mousehandleroperations.h
+ models/selection.cpp
+ models/selection.h
+ models/event.cpp
+ models/event.h)
set(MODELS_TESTS
models/tests/activity_tests.cpp
@@ -182,7 +177,11 @@ set(UI
ui/searchboxwidget.cpp
ui/searchboxwidget.h
ui/dynamicpalette.cpp
- ui/dynamicpalette.h ui/slotboardcircleswidget.cpp ui/slotboardcircleswidget.h)
+ ui/dynamicpalette.h
+ ui/slotboardcircleswidget.cpp
+ ui/slotboardcircleswidget.h
+ ui/iconwidget.cpp
+ ui/iconwidget.h)
set(UTILITY
${version_file}
@@ -191,8 +190,6 @@ set(UTILITY
utility/applicationsettings.h
utility/utils.cpp
utility/utils.h
- utility/notifierimplementation.cpp
- utility/notifierimplementation.h
utility/notifierbackend.h
utility/notifierbackend.cpp
utility/fontutils.cpp
@@ -248,7 +245,18 @@ if (APPLE)
deployment/Strategr.icns
deployment/Strategy.icns)
- SET(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Strategr.app/Contents/Frameworks)
+ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Strategr.app/Contents/Frameworks)
+endif ()
+
+if (WIN32)
+ set(PLATFORM_FILES
+ deployment/Strategr.rc
+ third-party/wintoast/wintoastlib.h
+ third-party/wintoast/wintoastlib.cpp)
+
+ set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Strategr)
+ set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Strategr)
+ set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}/Strategr)
endif ()
add_executable(models_tests
@@ -263,7 +271,6 @@ target_link_libraries(models_tests
${MODELS_PLATFORM_LIBRARIES})
option(COVERAGE "Generate code coverage" OFF)
-message("Compiler: ${CMAKE_CXX_COMPILER_ID}")
if (COVERAGE)
if ("${CMAKE_CXX_COMPILER_ID}" MATCHES Clang)
@@ -284,8 +291,7 @@ add_library(StrategrCore SHARED ${MODELS})
set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS OFF)
-#add_definitions(-DUTF8PROC_STATIC -DWIN32_LEAN_AND_MEAN)
-target_compile_definitions(StrategrCore PUBLIC UTF8PROC_STATIC WIN32_LEAN_AND_MEAN)
+target_compile_definitions(StrategrCore PUBLIC UTF8PROC_STATIC)
target_link_libraries(StrategrCore
${MODELS_LIBRARIES}
@@ -338,16 +344,38 @@ target_link_libraries(Strategr
${PLATFORM_LIBRARIES})
if (APPLE)
- if (CMAKE_BUILD_TYPE MATCHES Release)
+ if (CMAKE_BUILD_TYPE MATCHES Rel)
add_custom_command(TARGET Strategr
POST_BUILD
COMMAND ${CMAKE_SOURCE_DIR}/scripts/macos_deploy.sh ${CMAKE_CURRENT_BINARY_DIR} ${VERSION})
- endif (CMAKE_BUILD_TYPE MATCHES Release)
+ endif (CMAKE_BUILD_TYPE MATCHES Rel)
set_target_properties(updater PROPERTIES
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/Strategr.app/Contents/MacOS")
endif ()
+if (WIN32)
+ if (CMAKE_BUILD_TYPE MATCHES Rel)
+ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/deployment/Strategr_x86.iss.in"
+ "${CMAKE_CURRENT_SOURCE_DIR}/deployment/Strategr_x86.iss")
+
+ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/deployment/Strategr_x64.iss.in"
+ "${CMAKE_CURRENT_SOURCE_DIR}/deployment/Strategr_x64.iss")
+
+ set_target_properties(Strategr PROPERTIES WIN32_EXECUTABLE ON)
+
+ add_custom_command(TARGET Strategr
+ POST_BUILD
+ COMMAND ${Qt5_DIR}/../../../bin/windeployqt.exe --no-translations ${CMAKE_RUNTIME_OUTPUT_DIRECTORY})
+
+ endif (CMAKE_BUILD_TYPE MATCHES Rel)
+
+ file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/deployment/Strategr.ico"
+ DESTINATION "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
+ file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/deployment/Strategy.ico"
+ DESTINATION "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}")
+endif ()
+
# Set a custom plist file for the app bundle
set_target_properties(Strategr PROPERTIES
MACOSX_BUNDLE TRUE
diff --git a/deployment/Strategr.entitlements b/deployment/Strategr.entitlements
new file mode 100644
index 0000000..f7e0ccb
--- /dev/null
+++ b/deployment/Strategr.entitlements
@@ -0,0 +1,8 @@
+
+
+
+
+ com.apple.security.personal-information.calendars
+
+
+
diff --git a/deployment/Strategr.ico b/deployment/Strategr.ico
new file mode 100644
index 0000000..6a2a29d
Binary files /dev/null and b/deployment/Strategr.ico differ
diff --git a/deployment/Strategr.rc b/deployment/Strategr.rc
new file mode 100644
index 0000000..e8f7f8e
--- /dev/null
+++ b/deployment/Strategr.rc
@@ -0,0 +1 @@
+IDI_ICON1 ICON "Strategr.ico"
\ No newline at end of file
diff --git a/deployment/Strategr_x64.iss b/deployment/Strategr_x64.iss
new file mode 100644
index 0000000..28aa559
--- /dev/null
+++ b/deployment/Strategr_x64.iss
@@ -0,0 +1,47 @@
+[Setup]
+AppName=Strategr
+AppVersion=0.0.9
+DefaultDirName={pf64}/Strategr
+DefaultGroupName=Strategr
+OutputBaseFilename=Strategr-x64-v0.0.9
+OutputDir=..\builds\Windows\x64-Release\Installer
+ChangesAssociations=yes
+
+[Files]
+Source: "..\builds\Windows\x64-Release\Strategr\*"; DestDir: {app}; Flags: recursesubdirs;
+
+[Run]
+Filename: "{app}\vc_redist.x64.exe"; StatusMsg: "Installing Visual Studio C++ Runtime.."; Parameters: "/quiet"; Check: VC2019RedistNeedsInstall; Flags: waituntilterminated
+Filename: "{app}\Strategr.exe"; Description: "Launch Strategr"; Flags: postinstall nowait skipifsilent
+
+[Icons]
+Name: "{group}\Strategr"; Filename: "{app}\Strategr.exe"; WorkingDir: "{app}"
+Name: "{group}\Uninstall Strategr"; Filename: "{uninstallexe}"
+
+[Registry]
+Root: HKLM; Subkey: "Software\Classes\.stg"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletevalue
+Root: HKLM; Subkey: "Software\Classes\Strategy"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletekey
+Root: HKLM; Subkey: "Software\Classes\Strategy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\Strategy.ico"
+Root: HKLM; Subkey: "Software\Classes\Strategy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\Strategr.exe"" ""%1"""
+
+[Code]
+function VC2019RedistNeedsInstall: Boolean;
+var
+ Version: String;
+begin
+ if (RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', Version)) then
+ begin
+ // Is the installed version at least 14.24?
+ Log('VC Redist Version check : found ' + Version);
+ Result := (CompareStr(Version, 'v14.24.28127.04')<0);
+ end
+ else
+ begin
+ // Not even an old version installed
+ Result := True;
+ end;
+ if (Result) then
+ begin
+ ExtractTemporaryFile('vc_redist.x64.exe');
+ end;
+end;
diff --git a/deployment/Strategr_x64.iss.in b/deployment/Strategr_x64.iss.in
new file mode 100644
index 0000000..f7a0152
--- /dev/null
+++ b/deployment/Strategr_x64.iss.in
@@ -0,0 +1,47 @@
+[Setup]
+AppName=Strategr
+AppVersion=@VERSION_SHORT@
+DefaultDirName={pf64}/Strategr
+DefaultGroupName=Strategr
+OutputBaseFilename=Strategr-x64-v@VERSION@
+OutputDir=..\builds\Windows\x64-Release\Installer
+ChangesAssociations=yes
+
+[Files]
+Source: "..\builds\Windows\x64-Release\Strategr\*"; DestDir: {app}; Flags: recursesubdirs;
+
+[Run]
+Filename: "{app}\vc_redist.x64.exe"; StatusMsg: "Installing Visual Studio C++ Runtime.."; Parameters: "/quiet"; Check: VC2019RedistNeedsInstall; Flags: waituntilterminated
+Filename: "{app}\Strategr.exe"; Description: "Launch Strategr"; Flags: postinstall nowait skipifsilent
+
+[Icons]
+Name: "{group}\Strategr"; Filename: "{app}\Strategr.exe"; WorkingDir: "{app}"
+Name: "{group}\Uninstall Strategr"; Filename: "{uninstallexe}"
+
+[Registry]
+Root: HKLM; Subkey: "Software\Classes\.stg"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletevalue
+Root: HKLM; Subkey: "Software\Classes\Strategy"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletekey
+Root: HKLM; Subkey: "Software\Classes\Strategy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\Strategy.ico"
+Root: HKLM; Subkey: "Software\Classes\Strategy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\Strategr.exe"" ""%1"""
+
+[Code]
+function VC2019RedistNeedsInstall: Boolean;
+var
+ Version: String;
+begin
+ if (RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64', 'Version', Version)) then
+ begin
+ // Is the installed version at least 14.24?
+ Log('VC Redist Version check : found ' + Version);
+ Result := (CompareStr(Version, 'v14.24.28127.04')<0);
+ end
+ else
+ begin
+ // Not even an old version installed
+ Result := True;
+ end;
+ if (Result) then
+ begin
+ ExtractTemporaryFile('vc_redist.x64.exe');
+ end;
+end;
diff --git a/deployment/Strategr_x86.iss b/deployment/Strategr_x86.iss
new file mode 100644
index 0000000..bf38244
--- /dev/null
+++ b/deployment/Strategr_x86.iss
@@ -0,0 +1,47 @@
+[Setup]
+AppName=Strategr
+AppVersion=0.0.9
+DefaultDirName={pf}/Strategr
+DefaultGroupName=Strategr
+OutputBaseFilename=Strategr-x86-v0.0.9
+OutputDir=..\builds\Windows\x86-Release\Installer
+ChangesAssociations=yes
+
+[Files]
+Source: "..\builds\Windows\x86-Release\Strategr\*"; DestDir: {app}; Flags: recursesubdirs;
+
+[Run]
+Filename: "{app}\vc_redist.x86.exe"; StatusMsg: "Installing Visual Studio C++ Runtime.."; Parameters: "/quiet"; Check: VC2019RedistNeedsInstall; Flags: waituntilterminated
+Filename: "{app}\Strategr.exe"; Description: "Launch Strategr"; Flags: postinstall nowait skipifsilent
+
+[Icons]
+Name: "{group}\Strategr"; Filename: "{app}\Strategr.exe"; WorkingDir: "{app}"
+Name: "{group}\Uninstall Strategr"; Filename: "{uninstallexe}"
+
+[Registry]
+Root: HKLM; Subkey: "Software\Classes\.stg"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletevalue
+Root: HKLM; Subkey: "Software\Classes\Strategy"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletekey
+Root: HKLM; Subkey: "Software\Classes\Strategy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\Strategy.ico"
+Root: HKLM; Subkey: "Software\Classes\Strategy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\Strategr.exe"" ""%1"""
+
+[Code]
+function VC2019RedistNeedsInstall: Boolean;
+var
+ Version: String;
+begin
+ if (RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86', 'Version', Version)) then
+ begin
+ // Is the installed version at least 14.24?
+ Log('VC Redist Version check : found ' + Version);
+ Result := (CompareStr(Version, 'v14.24.28127.04')<0);
+ end
+ else
+ begin
+ // Not even an old version installed
+ Result := True;
+ end;
+ if (Result) then
+ begin
+ ExtractTemporaryFile('vc_redist.x86.exe');
+ end;
+end;
diff --git a/deployment/Strategr_x86.iss.in b/deployment/Strategr_x86.iss.in
new file mode 100644
index 0000000..8faddf9
--- /dev/null
+++ b/deployment/Strategr_x86.iss.in
@@ -0,0 +1,47 @@
+[Setup]
+AppName=Strategr
+AppVersion=@VERSION_SHORT@
+DefaultDirName={pf}/Strategr
+DefaultGroupName=Strategr
+OutputBaseFilename=Strategr-x86-v@VERSION@
+OutputDir=..\builds\Windows\x86-Release\Installer
+ChangesAssociations=yes
+
+[Files]
+Source: "..\builds\Windows\x86-Release\Strategr\*"; DestDir: {app}; Flags: recursesubdirs;
+
+[Run]
+Filename: "{app}\vc_redist.x86.exe"; StatusMsg: "Installing Visual Studio C++ Runtime.."; Parameters: "/quiet"; Check: VC2019RedistNeedsInstall; Flags: waituntilterminated
+Filename: "{app}\Strategr.exe"; Description: "Launch Strategr"; Flags: postinstall nowait skipifsilent
+
+[Icons]
+Name: "{group}\Strategr"; Filename: "{app}\Strategr.exe"; WorkingDir: "{app}"
+Name: "{group}\Uninstall Strategr"; Filename: "{uninstallexe}"
+
+[Registry]
+Root: HKLM; Subkey: "Software\Classes\.stg"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletevalue
+Root: HKLM; Subkey: "Software\Classes\Strategy"; ValueType: string; ValueName: ""; ValueData: "Strategy"; Flags: uninsdeletekey
+Root: HKLM; Subkey: "Software\Classes\Strategy\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\Strategy.ico"
+Root: HKLM; Subkey: "Software\Classes\Strategy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\Strategr.exe"" ""%1"""
+
+[Code]
+function VC2019RedistNeedsInstall: Boolean;
+var
+ Version: String;
+begin
+ if (RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x86', 'Version', Version)) then
+ begin
+ // Is the installed version at least 14.24?
+ Log('VC Redist Version check : found ' + Version);
+ Result := (CompareStr(Version, 'v14.24.28127.04')<0);
+ end
+ else
+ begin
+ // Not even an old version installed
+ Result := True;
+ end;
+ if (Result) then
+ begin
+ ExtractTemporaryFile('vc_redist.x86.exe');
+ end;
+end;
diff --git a/deployment/Strategy.ico b/deployment/Strategy.ico
new file mode 100644
index 0000000..eee373d
Binary files /dev/null and b/deployment/Strategy.ico differ
diff --git a/deployment/package.dmg b/deployment/package.dmg
new file mode 100644
index 0000000..1f4500d
Binary files /dev/null and b/deployment/package.dmg differ
diff --git a/models/mousehandler.cpp b/models/mousehandler.cpp
index 1d45eee..42a260a 100644
--- a/models/mousehandler.cpp
+++ b/models/mousehandler.cpp
@@ -48,6 +48,7 @@ void stg::mouse_handler::mouse_move(const stg::mouse_event &event) {
current_slot_index = get_slot_index(event);
current_session_index = get_session_index(current_slot_index);
current_mouse_zone = get_mouse_zone(current_session_index, event.position);
+
current_key_modifiers = event.modifiers;
if (current_operaion->type() != none) {
@@ -210,9 +211,6 @@ stg::mouse_handler::range stg::mouse_handler::get_session_range(index_t session_
auto top = static_cast(strategy.sessions().relative_begin_time(session) * px_in_time());
auto height = static_cast(session.duration() * px_in_time());
-// std::cout << "slot_index: " << slot_index << "\n";
-// std::cout << "session_index: " << session_index << "\n";
-
return range{top, top + height};
}
@@ -230,7 +228,8 @@ stg::mouse_handler::mouse_zone stg::mouse_handler::get_mouse_zone(int session_in
session_range.bottom
};
-// std::cout << "pos: " << event.position << "\n";
+// std::cout << "pos: " << mouse_pos << "\n";
+// std::cout << "slot_height: " << get_slot_height() << "\n";
// std::cout << "session_range: " << session_range << "\n";
// std::cout << "top_stretch_zone: " << top_stretch_zone << "\n";
// std::cout << "bottom_stretch_zone: " << bottom_stretch_zone << "\n";
diff --git a/models/notifier.cpp b/models/notifier.cpp
index aa53c02..6ada2a0 100644
--- a/models/notifier.cpp
+++ b/models/notifier.cpp
@@ -11,21 +11,17 @@
#include "strategy.h"
#include "time_utils.h"
-void stg::notifier::set_strategy(const stg::strategy *new_strategy) {
- strategy = new_strategy;
- strategy->add_on_change_callback(this,
- &stg::notifier::schedule_notifications);
-}
+stg::notifier::notifier(const stg::strategy &strategy) : strategy(strategy) {
+ strategy.add_on_change_callback(this,
+ &stg::notifier::schedule);
-stg::notifier::notifier(const stg::strategy *strategy) : strategy(strategy) {
- strategy->add_on_change_callback(this,
- &stg::notifier::schedule_notifications);
+ schedule();
}
void stg::notifier::start_watching() {
is_watching = true;
- // teardown() -> schedule();
+ // schedule();
}
void stg::notifier::stop_watching() {
@@ -34,65 +30,61 @@ void stg::notifier::stop_watching() {
// teardown();
}
-void stg::notifier::on_schedule_notifications(const scheduler_t &callback) {
- scheduler = callback;
-}
-
-void stg::notifier::on_reset_notifications(const resetter_t &callback) {
- resetter = callback;
-}
-
-stg::notifier::~notifier() {
-}
-
-void stg::notifier::schedule_notifications() {
- std::cout << "schedule notifications" << std::endl;
+void stg::notifier::schedule() {
+// std::cout << "scheduled notifications: \n";
- scheduled_notifications_t notifications;
- for (auto it = strategy->sessions().begin();
- it != strategy->sessions().end();
- ++it) {
+ std::vector notifications;
+ for (auto it = strategy.sessions().begin(); it != strategy.sessions().end(); ++it) {
const auto &session = *it;
auto next_it = std::next(it);
- if (next_it == strategy->sessions().end()) {
- notifications.push_back(notification(session,
- notification::type::prepare_strategy_end));
- notifications.push_back(notification(session,
- notification::type::strategy_end));
+ if (session.activity) {
+ notifications.emplace_back(session,
+ notification::type::prepare_start);
+ notifications.emplace_back(session,
+ notification::type::start);
+
+ if (next_it != strategy.sessions().end() && !next_it->activity) {
+ notifications.emplace_back(session,
+ notification::type::prepare_end);
+ notifications.emplace_back(session,
+ notification::type::end);
+ }
}
- if (!session.activity) {
- return;
+ if (next_it == strategy.sessions().end()) {
+ notifications.emplace_back(session,
+ notification::type::prepare_strategy_end);
+ notifications.emplace_back(session,
+ notification::type::strategy_end);
}
+ }
- notifications.push_back(notification(session,
- notification::type::prepare_start));
- notifications.push_back(notification(session,
- notification::type::start));
+ remove_stale(notifications);
+// for (auto &n : notifications) {
+// std::cout << n << "\n";
+// }
- if (next_it != strategy->sessions().end() && !next_it->activity) {
- notifications.push_back(notification(session,
- notification::type::prepare_end));
- notifications.push_back(notification(session,
- notification::type::end));
- }
- }
+ if (on_delete_notifications)
+ on_delete_notifications(scheduled_identifiers());
-}
+ _scheduled_notifications = notifications;
-void stg::notifier::reset_notifications() {
- if (resetter) {
- resetter();
- }
+ if (on_schedule_notifications)
+ on_schedule_notifications(_scheduled_notifications);
}
-void stg::notifier::on_send_notification(const sender_t &callback) {
- sender = callback;
+void stg::notifier::remove_stale(std::vector ¬ifications) {
+ auto current_seconds = stg::time_utils::current_seconds();
+ notifications.erase(std::remove_if(notifications.begin(),
+ notifications.end(),
+ [current_seconds](const stg::notification ¬ification) {
+ return notification.delivery_time < current_seconds;
+ }), notifications.end());
}
-std::string stg::notifier::make_string_uuid() {
+std::string stg::notification::make_string_uuid() {
auto uuid = boost::uuids::random_generator()();
std::stringstream sstream;
@@ -101,7 +93,7 @@ std::string stg::notifier::make_string_uuid() {
return sstream.str();
}
-const stg::notifier::scheduled_notifications_t
+const std::vector
&stg::notifier::scheduled_notifications() const {
return _scheduled_notifications;
}
@@ -126,10 +118,58 @@ stg::notifier::seconds stg::notifier::prepare_delivery_seconds(stg::notifier::mi
return minutes_time * 60 - prepare_seconds_interval;
}
-std::string stg::notifier::notification::make_title(const session &session, type type) {
+void stg::notifier::send_now_if_needed(seconds polling_seconds_interval) {
+// std::cout << "send notification, if needed => \n";
+ auto current_time = time_utils::current_seconds();
+
+ if (last_poll_time && current_time - last_poll_time > 4 * polling_seconds_interval) {
+// std::cout << "system time changed\n";
+
+ // If time difference between two calls of this function was too big,
+ // this probably means that the system time had changed, we need to reschedule.
+ schedule();
+ }
+
+ last_poll_time = current_time;
+
+ if (_scheduled_notifications.empty() ||
+ current_time < _scheduled_notifications.front().delivery_time)
+ return;
+
+// std::cout << "current_time: " << current_time << "\n";
+// std::cout << "delivery_time: " << next_notification.delivery_time << "\n";
+
+ // We have to send only last notification for which delivery time is less than the current.
+ // The first is guaranteed to be so, we need to check the others:
+
+ auto next_notification_it = _scheduled_notifications.begin();
+ for (auto it = std::next(_scheduled_notifications.begin());
+ it != _scheduled_notifications.end();
+ ++it) {
+ auto ¬ification = *it;
+
+ if (current_time < notification.delivery_time) {
+ next_notification_it = std::prev(it);
+ break;
+ }
+ }
+
+ auto &next_notification = *next_notification_it;
+
+ if (on_send_notiifcation)
+ on_send_notiifcation(next_notification);
+
+// std::cout << "notification sent: " << next_notification << "\n";
+
+ // Remove sent and stale notifications
+ _scheduled_notifications.erase(_scheduled_notifications.begin(),
+ std::next(next_notification_it));
+}
+
+std::string stg::notification::make_title(const session &session, type type) {
if (type == type::prepare_strategy_end ||
type == type::strategy_end) {
- return "end of a strategy";
+ return "End Of A Strategy";
}
if (!session.activity) {
@@ -138,50 +178,68 @@ std::string stg::notifier::notification::make_title(const session &session, type
return session.activity->name()
+ " ("
- + stg::time_utils::human_time_for_minutes(session.duration())
+ + stg::time_utils::human_string_from_minutes(session.duration())
+ ")";
}
-stg::notifier::seconds stg::notifier::notification::make_delivery_time(const session &session, type type) {
+stg::notification::seconds stg::notification::make_delivery_time(const session &session, type type) {
switch (type) {
case type::prepare_start:
- return prepare_delivery_seconds(session.begin_time());
+ return notifier::prepare_delivery_seconds(session.begin_time());
case type::start:
- return immediate_delivery_seconds(session.begin_time());
+ return notifier::immediate_delivery_seconds(session.begin_time());
case type::prepare_end:
case type::prepare_strategy_end:
- return prepare_delivery_seconds(session.end_time());
+ return notifier::prepare_delivery_seconds(session.end_time());
case type::end:
case type::strategy_end:
- return immediate_delivery_seconds(session.end_time());
+ return notifier::immediate_delivery_seconds(session.end_time());
default:
return 0;
}
}
-std::string stg::notifier::notification::make_sub_title(const session &session, type type) {
+std::string stg::notification::make_sub_title(const session &session, type type) {
switch (type) {
case type::prepare_start:
- return "coming up in "
- + stg::time_utils::human_time_for_minutes(prepare_seconds_interval / 60);
+ return "Coming up in "
+ + time_utils::human_string_from_minutes(notifier::prepare_seconds_interval / 60);
case type::start:
- return "starts right now";
+ return "Starts right now";
case type::prepare_end:
- return "ends in "
- + stg::time_utils::human_time_for_minutes(prepare_seconds_interval / 60);
+ return "Ends in "
+ + time_utils::human_string_from_minutes(notifier::prepare_seconds_interval / 60);
case type::end:
- return "ends right now";
+ return "Ends right now";
case type::prepare_strategy_end:
- return "strategy ends in "
- + stg::time_utils::human_time_for_minutes(prepare_seconds_interval / 60);
+ return "Strategy ends in "
+ + time_utils::human_string_from_minutes(notifier::prepare_seconds_interval / 60);
case type::strategy_end:
- return "strategy ends right now";
+ return "Strategy ends right now";
default:
- return 0;
+ return "";
}
}
-stg::notifier::notification::notification(const session &session, type type) :
+stg::notification::notification(const session &session, type type) :
title(make_title(session, type)),
- sub_title(make_sub_title(session, type)),
+ message(make_sub_title(session, type)),
delivery_time(make_delivery_time(session, type)) {}
+
+std::ostream &stg::operator<<(std::ostream &os, const stg::notification ¬ification) {
+ os << "notification: [ ";
+ os << "id: \"" << notification.identifier << "\", ";
+ os << "title: \"" << notification.title << "\", ";
+ os << "message: \"" << notification.message << "\", ";
+ os << "delivery_time: \"" << time_utils::string_from_seconds(notification.delivery_time) << "\"";
+ os << "]";
+
+ return os;
+}
+
+bool stg::operator==(const stg::notification &lhs, const stg::notification &rhs) {
+ // Two notifications are considered equal if all properties other than id are equal,
+ return lhs.title == rhs.title &&
+ lhs.message == rhs.message &&
+ lhs.delivery_time == rhs.delivery_time;
+}
diff --git a/models/notifier.h b/models/notifier.h
index 6e88b9d..cad47b7 100644
--- a/models/notifier.h
+++ b/models/notifier.h
@@ -7,84 +7,82 @@
#include
#include
+#include
#include "strategy.h"
-#include "timer.h"
namespace stg {
class strategy;
-
- class notifier {
- public:
+ struct notification {
using seconds = unsigned int;
using minutes = unsigned int;
- struct notification {
- enum class type {
- prepare_start,
- start,
- prepare_end,
- end,
- prepare_strategy_end,
- strategy_end
- };
+ enum class type {
+ prepare_start,
+ start,
+ prepare_end,
+ end,
+ prepare_strategy_end,
+ strategy_end
+ };
- notification(const session &session, type type);
+ notification(const session &session, type type);
- const std::string identifier = make_string_uuid();
+ std::string identifier = make_string_uuid();
- const std::string title;
- const std::string sub_title;
+ std::string title;
+ std::string message;
- const seconds delivery_time;
+ seconds delivery_time;
- private:
- static std::string make_title(const session &session, type type);
- static std::string make_sub_title(const session &session, type type);
- static seconds make_delivery_time(const session &session, type type);
- };
+ private:
+ static std::string make_string_uuid();
+ static std::string make_title(const session &session, type type);
+ static std::string make_sub_title(const session &session, type type);
+ static seconds make_delivery_time(const session &session, type type);
+
+ friend bool operator==(const notification &lhs, const notification &rhs);
+
+ friend std::ostream &operator<<(std::ostream &os, const notification ¬ification);
+ };
+
+ class notifier {
+ public:
+ using seconds = notification::seconds;
+ using minutes = notification::minutes;
- using scheduled_notifications_t = std::vector;
- using scheduler_t = std::function;
- using resetter_t = std::function;
+ using scheduler_t = std::function &)>;
+ using resetter_t = std::function &)>;
using sender_t = std::function;
- explicit notifier(const strategy *strategy);
- ~notifier();
+ static seconds immediate_delivery_seconds(minutes minutes_time);
+ static seconds prepare_delivery_seconds(minutes minutes_time);
+
+ scheduler_t on_schedule_notifications = nullptr;
+ resetter_t on_delete_notifications = nullptr;
+ sender_t on_send_notiifcation = nullptr;
- void set_strategy(const strategy *strategy);
+ explicit notifier(const strategy &strategy);
void start_watching();
void stop_watching();
+ void schedule();
- void on_send_notification(const sender_t &callback);
- void on_schedule_notifications(const scheduler_t &callback);
- void on_reset_notifications(const resetter_t &callback);
+ void send_now_if_needed(seconds polling_seconds_interval);
- const scheduled_notifications_t &scheduled_notifications() const;
+ const std::vector &scheduled_notifications() const;
std::vector scheduled_identifiers() const;
static const seconds prepare_seconds_interval = 5 * 60;
static const seconds immediate_seconds_interval = 20;
-
private:
- const strategy *strategy;
+ static void remove_stale(std::vector ¬ifications);
+ const strategy &strategy;
+ seconds last_poll_time = 0;
bool is_watching = false;
- scheduler_t scheduler;
- resetter_t resetter;
- sender_t sender;
-
- scheduled_notifications_t _scheduled_notifications;
-
- void schedule_notifications();
- void reset_notifications();
-
- static seconds immediate_delivery_seconds(minutes minutes_time);
- static seconds prepare_delivery_seconds(minutes minutes_time);
-
- static std::string make_string_uuid();
+ std::vector _scheduled_notifications;
};
}
diff --git a/models/privatelist.h b/models/privatelist.h
index 436518e..10352a8 100644
--- a/models/privatelist.h
+++ b/models/privatelist.h
@@ -23,8 +23,8 @@ namespace stg {
explicit private_list(data_t data = {})
: _data(std::move(data)) {}
- void reset_with(data_t data = {}) {
- _data = data;
+ virtual void reset_with(data_t data) {
+ _data = std::move(data);
}
const data_t &data() const {
diff --git a/models/strategy.cpp b/models/strategy.cpp
index a179f21..fb6e07c 100644
--- a/models/strategy.cpp
+++ b/models/strategy.cpp
@@ -301,7 +301,7 @@ const stg::session *stg::strategy::get_current_session() const {
return nullptr;
}
-const stg::session *stg::strategy::get_active_session() const {
+const stg::session *stg::strategy::active_session() const {
auto current_session = this->get_current_session();
if (!current_session || !current_session->activity) {
return nullptr;
@@ -371,4 +371,12 @@ void stg::strategy::reorder_activities_by_usage() {
_activities.on_change_event();
commit_to_history();
-}
\ No newline at end of file
+}
+
+bool stg::strategy::is_dragging() {
+ return current_drag_operation != nullptr;
+}
+
+bool stg::strategy::is_resizing() {
+ return current_resize_operation != nullptr;
+}
diff --git a/models/strategy.h b/models/strategy.h
index 4720009..a30fa76 100644
--- a/models/strategy.h
+++ b/models/strategy.h
@@ -63,32 +63,32 @@ namespace stg {
time_t end_time() const;
duration_t duration() const;
- const session *get_active_session() const;
+ const session *active_session() const;
const session *upcoming_session() const;
+ /* Operations on activities */
void add_activity(const activity &activity);
void delete_activity(activity_index activity_index);
- void edit_activity(activity_index activity_index,
- const activity &new_activity);
-
+ void edit_activity(activity_index activity_index, const activity &new_activity);
void drag_activity(activity_index from_index, activity_index to_index);
+ void reorder_activities_by_usage();
+ /* Operations on slots */
void place_activity(activity_index activity_index,
const std::vector &time_slot_indices);
void make_empty_at(const std::vector &time_slot_indices);
+ void shift_below_time_slot(time_slot_index_t from_index, int length);
+ bool is_resizing();
void begin_resizing();
void fill_time_slots(time_slot_index_t from_index, time_slot_index_t till_index);
void end_resizing();
+ bool is_dragging();
void begin_dragging(session_index_t session_index);
void drag_session(session_index_t session_index, int distance);
void end_dragging();
- void shift_below_time_slot(time_slot_index_t from_index, int length);
-
- void reorder_activities_by_usage();
-
void commit_to_history();
void undo();
void redo();
diff --git a/models/time_utils.cpp b/models/time_utils.cpp
index 8d430a1..732d6df 100644
--- a/models/time_utils.cpp
+++ b/models/time_utils.cpp
@@ -40,7 +40,7 @@ namespace stg {
return clock_now - clock_start_of_today;
}
- std::string time_utils::human_time_for_minutes(time_utils::minutes minutes) {
+ std::string time_utils::human_string_from_minutes(time_utils::minutes minutes) {
if (minutes < 1) {
return "Less than 1 min";
}
@@ -74,4 +74,20 @@ namespace stg {
return result;
}
+
+ std::string time_utils::string_from_seconds(time_utils::minutes total_seconds) {
+ auto hours = total_seconds / 3600;
+ auto minutes = (total_seconds - 3600 * hours) / 60;
+ auto seconds = total_seconds - 3600 * hours - 60 * minutes;
+
+ std::string result = std::to_string(hours) + " h";
+
+ if (minutes)
+ result += " " + std::to_string(minutes) + " m";
+
+ if (seconds)
+ result += " " + std::to_string(seconds) + "s";
+
+ return result;
+ }
};
diff --git a/models/time_utils.h b/models/time_utils.h
index 3c81192..5c67ce3 100644
--- a/models/time_utils.h
+++ b/models/time_utils.h
@@ -10,8 +10,7 @@
#include
namespace stg {
- class time_utils {
- public:
+ namespace time_utils {
using seconds = unsigned;
using minutes = unsigned;
@@ -22,12 +21,13 @@ namespace stg {
constexpr static auto day_components_from_timestamp = std::localtime;
constexpr static auto timestamp_from_day_components = std::mktime;
- static timestamp start_of_a_day_from_timestamp(timestamp timestamp);
- static duration current_day_duration();
- static minutes current_minutes();
- static seconds current_seconds();
+ timestamp start_of_a_day_from_timestamp(timestamp timestamp);
+ duration current_day_duration();
+ minutes current_minutes();
+ seconds current_seconds();
- static std::string human_time_for_minutes(minutes minutes);
+ std::string string_from_seconds(minutes total_seconds);
+ std::string human_string_from_minutes(minutes minutes);
};
}
diff --git a/models/timeslotsstate.cpp b/models/timeslotsstate.cpp
index 4718e0a..3909460 100644
--- a/models/timeslotsstate.cpp
+++ b/models/timeslotsstate.cpp
@@ -67,12 +67,16 @@ stg::time_slots_state::time_slots_state(time_t start_time,
}
stg::time_slots_state::time_slots_state(std::vector from_vector) {
- auto first_slot = from_vector.front();
+ assert(!from_vector.empty() && "Can't create time slots from empty vector");
+ _data = std::move(from_vector);
+ reset_times();
+}
+
+void stg::time_slots_state::reset_times() {
+ auto first_slot = _data.front();
_begin_time = first_slot.begin_time;
_slot_duration = first_slot.duration;
-
- _data = std::move(from_vector);
}
void stg::time_slots_state::fill_slots(index_t from_index, index_t till_index) {
@@ -243,4 +247,9 @@ bool stg::time_slots_state::previous_slot_empty(index_t index) const {
}
}
+void stg::time_slots_state::reset_with(data_t raw_data) {
+ time_slots_state_base::reset_with(raw_data);
+ reset_times();
+}
+
diff --git a/models/timeslotsstate.h b/models/timeslotsstate.h
index 77453ff..52c395b 100644
--- a/models/timeslotsstate.h
+++ b/models/timeslotsstate.h
@@ -75,6 +75,9 @@ namespace stg {
std::string class_print_name() const override;
const time_slot &at(index_t index);
+
+ void reset_with(data_t raw_data) override;
+
private:
friend strategy;
@@ -83,6 +86,8 @@ namespace stg {
time_t slot_begin_time(time_t global_begin_time, index_t slot_index);
void update_begin_times();
+
+ void reset_times();
};
}
diff --git a/models/utility.h b/models/utility.h
index 3909880..d1af841 100644
--- a/models/utility.h
+++ b/models/utility.h
@@ -6,19 +6,30 @@
#define STRATEGR_UTILITY_H
#include
+#include
#include
namespace stg::text {
- inline std::string utf8_fold_case(const std::string &str) {
- auto *lowered = (char *) utf8proc_NFKC_Casefold((utf8proc_uint8_t *) str.c_str());
- return std::string(lowered);
- }
+ inline std::string utf8_fold_case(const std::string &str) {
+ auto *lowered = (char *) utf8proc_NFKC_Casefold((utf8proc_uint8_t *) str.c_str());
+ return std::string(lowered);
+ }
+
+ inline bool utf8_is_equal_case_insensitive(const std::string &str1, const std::string &str2) {
+ char *lower1 = (char *) utf8proc_NFKC_Casefold((utf8proc_uint8_t *) str1.c_str());
+ char *lower2 = (char *) utf8proc_NFKC_Casefold((utf8proc_uint8_t *) str2.c_str());
- inline bool utf8_is_equal_case_insensitive(const std::string &str1, const std::string &str2) {
- char *lower1 = (char *) utf8proc_NFKC_Casefold((utf8proc_uint8_t *) str1.c_str());
- char *lower2 = (char *) utf8proc_NFKC_Casefold((utf8proc_uint8_t *) str2.c_str());
+ return strcmp(lower1, lower2) == 0;
+ }
+
+ inline std::wstring wstring_from_utf8_string(const std::string &str) {
+ std::wstring_convert> converter;
+ return converter.from_bytes(str);
+ }
- return strcmp(lower1, lower2) == 0;
- }
+ inline std::string string_from_utf8_wstring(const std::wstring &wstr) {
+ std::wstring_convert> converter;
+ return converter.to_bytes(wstr);
}
+}
#endif //STRATEGR_UTILITY_H
diff --git a/resources/FontAwesome.ttf b/resources/FontAwesome.ttf
new file mode 100644
index 0000000..35acda2
Binary files /dev/null and b/resources/FontAwesome.ttf differ
diff --git a/scripts/macos_deploy.sh b/scripts/macos_deploy.sh
index 3d4c778..6c34b03 100755
--- a/scripts/macos_deploy.sh
+++ b/scripts/macos_deploy.sh
@@ -2,7 +2,7 @@
build_path=$1
version=$2
app_name="Strategr"
-dmg_path="$build_path/$app_name v$version.dmg"
+dmg_path="$build_path/$app_name-v$version.dmg"
dmg_template_path="$build_path/../../../deployment/package.dmg"
#entitlements_path="$build_path/../../../deployment/Strategr.entitlements"
dmg_source_path="./DMGContainer"
diff --git a/scripts/release_latest.sh b/scripts/release_latest.sh
index 97e7da8..a74ab0a 100755
--- a/scripts/release_latest.sh
+++ b/scripts/release_latest.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
scripts/release.sh "$(scripts/repo_name.sh)" "$(git describe)" -- \
- "./builds/macOS/Release/Strategr $(git describe).dmg" \
+ "./builds/macOS/Release/Strategr-$(git describe).dmg" \
"./builds/macOS/Release/macOS_update.zip"
\ No newline at end of file
diff --git a/third-party/wintoast/wintoastlib.cpp b/third-party/wintoast/wintoastlib.cpp
new file mode 100644
index 0000000..0895ff7
--- /dev/null
+++ b/third-party/wintoast/wintoastlib.cpp
@@ -0,0 +1,1129 @@
+/* * Copyright (C) 2016-2019 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
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "wintoastlib.h"
+#include
+#include
+#include
+#include
+
+#pragma comment(lib,"shlwapi")
+#pragma comment(lib,"user32")
+
+#ifdef NDEBUG
+ #define DEBUG_MSG(str) do { } while ( false )
+#else
+ #define DEBUG_MSG(str) do { std::wcout << str << std::endl; } while( false )
+#endif
+
+#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\"
+#define DEFAULT_LINK_FORMAT L".lnk"
+#define STATUS_SUCCESS (0x00000000)
+
+
+// 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;
+namespace DllImporter {
+
+ // Function load a function from library
+ template
+ 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_ 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;
+
+
+ template
+ _Check_return_ __inline HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) {
+ return RoGetActivationFactory(activatableClassId, IID_INS_ARGS(factory));
+ }
+
+ 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);
+ if (SUCCEEDED(hr)) {
+ HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL");
+ 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;
+ }
+ }
+ return hr;
+ }
+}
+
+class WinToastStringWrapper {
+public:
+ WinToastStringWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) noexcept {
+ HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef, length, &_header, &_hstring);
+ if (!SUCCEEDED(hr)) {
+ RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr);
+ }
+ }
+
+ WinToastStringWrapper(_In_ const std::wstring &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);
+ }
+ }
+
+ ~WinToastStringWrapper() {
+ DllImporter::WindowsDeleteString(_hstring);
+ }
+
+ inline HSTRING Get() const noexcept {
+ return _hstring;
+ }
+private:
+ HSTRING _hstring;
+ HSTRING_HEADER _header;
+
+};
+
+class InternalDateTime : public IReference {
+public:
+ static INT64 Now() {
+ FILETIME now;
+ GetSystemTimeAsFileTime(&now);
+ return ((((INT64)now.dwHighDateTime) << 32) | now.dwLowDateTime);
+ }
+
+ InternalDateTime(DateTime dateTime) : _dateTime(dateTime) {}
+
+ InternalDateTime(INT64 millisecondsFromNow) {
+ _dateTime.UniversalTime = Now() + millisecondsFromNow * 10000;
+ }
+
+ virtual ~InternalDateTime() = default;
+
+ operator INT64() {
+ return _dateTime.UniversalTime;
+ }
+
+ HRESULT STDMETHODCALLTYPE get_Value(DateTime *dateTime) {
+ *dateTime = _dateTime;
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) {
+ if (!ppvObject) {
+ return E_POINTER;
+ }
+ if (riid == __uuidof(IUnknown) || riid == __uuidof(IReference)) {
+ *ppvObject = static_cast(static_cast*>(this));
+ return S_OK;
+ }
+ return E_NOINTERFACE;
+ }
+
+ ULONG STDMETHODCALLTYPE Release() {
+ return 1;
+ }
+
+ ULONG STDMETHODCALLTYPE AddRef() {
+ return 2;
+ }
+
+ HRESULT STDMETHODCALLTYPE GetIids(ULONG*, IID**) {
+ return E_NOTIMPL;
+ }
+
+ HRESULT STDMETHODCALLTYPE GetRuntimeClassName(HSTRING*) {
+ return E_NOTIMPL;
+ }
+
+ HRESULT STDMETHODCALLTYPE GetTrustLevel(TrustLevel*) {
+ return E_NOTIMPL;
+ }
+
+protected:
+ DateTime _dateTime;
+};
+
+namespace Util {
+
+ typedef LONG NTSTATUS, *PNTSTATUS;
+ typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
+ inline RTL_OSVERSIONINFOW getRealOSVersion() {
+ HMODULE hMod = ::GetModuleHandleW(L"ntdll.dll");
+ if (hMod) {
+ RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion");
+ if (fxPtr != nullptr) {
+ RTL_OSVERSIONINFOW rovi = { 0 };
+ rovi.dwOSVersionInfoSize = sizeof(rovi);
+ if (STATUS_SUCCESS == fxPtr(&rovi)) {
+ return rovi;
+ }
+ }
+ }
+ RTL_OSVERSIONINFOW rovi = { 0 };
+ return rovi;
+ }
+
+ inline HRESULT defaultExecutablePath(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
+ DWORD written = GetModuleFileNameExW(GetCurrentProcess(), nullptr, path, nSize);
+ DEBUG_MSG("Default executable path: " << path);
+ 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;
+ if (SUCCEEDED(hr)) {
+ errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH);
+ 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) {
+ 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;
+ DEBUG_MSG("Default shell link file path: " << path);
+ }
+ return hr;
+ }
+
+
+ inline PCWSTR AsString(ComPtr &xmlDocument) {
+ HSTRING xml;
+ ComPtr ser;
+ HRESULT hr = xmlDocument.As(&ser);
+ hr = ser->GetXml(&xml);
+ if (SUCCEEDED(hr))
+ return DllImporter::WindowsGetStringRawBuffer(xml, nullptr);
+ return nullptr;
+ }
+
+ inline PCWSTR AsString(HSTRING hstring) {
+ return DllImporter::WindowsGetStringRawBuffer(hstring, nullptr);
+ }
+
+ inline HRESULT setNodeStringValue(const std::wstring& string, IXmlNode *node, IXmlDocument *xml) {
+ ComPtr textNode;
+ HRESULT hr = xml->CreateTextNode( WinToastStringWrapper(string).Get(), &textNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr stringNode;
+ hr = textNode.As(&stringNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr appendedChild;
+ hr = node->AppendChild(stringNode.Get(), &appendedChild);
+ }
+ }
+ return hr;
+ }
+
+ inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, _In_ INT64 expirationTime) {
+ EventRegistrationToken activatedToken, dismissedToken, failedToken;
+ HRESULT hr = notification->add_Activated(
+ Callback < Implements < RuntimeClassFlags,
+ ITypedEventHandler> >(
+ [eventHandler](IToastNotification*, IInspectable* inspectable)
+ {
+ IToastActivatedEventArgs *activatedEventArgs;
+ HRESULT hr = inspectable->QueryInterface(&activatedEventArgs);
+ if (SUCCEEDED(hr)) {
+ HSTRING argumentsHandle;
+ hr = activatedEventArgs->get_Arguments(&argumentsHandle);
+ if (SUCCEEDED(hr)) {
+ PCWSTR arguments = Util::AsString(argumentsHandle);
+ if (arguments && *arguments) {
+ eventHandler->toastActivated(static_cast(wcstol(arguments, nullptr, 10)));
+ return S_OK;
+ }
+ }
+ }
+ eventHandler->toastActivated();
+ return S_OK;
+ }).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);
+ if (SUCCEEDED(hr)) {
+ hr = notification->add_Failed(Callback < Implements < RuntimeClassFlags,
+ ITypedEventHandler> >(
+ [eventHandler](IToastNotification*, IToastFailedEventArgs*)
+ {
+ eventHandler->toastFailed();
+ return S_OK;
+ }).Get(), &failedToken);
+ }
+ }
+ return hr;
+ }
+
+ inline HRESULT addAttribute(_In_ IXmlDocument *xml, const std::wstring &name, IXmlNamedNodeMap *attributeMap) {
+ ComPtr srcAttribute;
+ HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute);
+ if (SUCCEEDED(hr)) {
+ ComPtr node;
+ hr = srcAttribute.As(&node);
+ if (SUCCEEDED(hr)) {
+ ComPtr pNode;
+ hr = attributeMap->SetNamedItem(node.Get(), &pNode);
+ }
+ }
+ 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) {
+ ComPtr rootList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList);
+ if (SUCCEEDED(hr)) {
+ ComPtr root;
+ hr = rootList->Item(0, &root);
+ if (SUCCEEDED(hr)) {
+ ComPtr audioElement;
+ hr = xml->CreateElement(WinToastStringWrapper(element_name).Get(), &audioElement);
+ if (SUCCEEDED(hr)) {
+ ComPtr audioNodeTmp;
+ hr = audioElement.As(&audioNodeTmp);
+ if (SUCCEEDED(hr)) {
+ ComPtr audioNode;
+ hr = root->AppendChild(audioNodeTmp.Get(), &audioNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr attributes;
+ hr = audioNode->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ for (const auto& it : attribute_names) {
+ hr = addAttribute(xml, it, attributes.Get());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return hr;
+ }
+}
+
+WinToast* WinToast::instance() {
+ 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() {
+ if (_hasCoInitialized) {
+ CoUninitialize();
+ }
+}
+
+void WinToast::setAppName(_In_ const std::wstring& appName) {
+ _appName = appName;
+}
+
+
+void WinToast::setAppUserModelId(_In_ const std::wstring& aumi) {
+ _aumi = aumi;
+ DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str());
+}
+
+bool WinToast::isCompatible() {
+ 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)
+{
+ std::wstring aumi = companyName;
+ aumi += L"." + productName;
+ if (subProduct.length() > 0) {
+ aumi += L"." + subProduct;
+ if (versionInformation.length() > 0) {
+ aumi += L"." + versionInformation;
+ }
+ }
+
+ if (aumi.length() > SCHAR_MAX) {
+ DEBUG_MSG("Error: max size allowed for AUMI: 128 characters.");
+ }
+ return aumi;
+}
+
+const std::wstring& 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"}
+ };
+
+ const auto iter = Labels.find(error);
+ assert(iter != Labels.end());
+ return iter->second;
+}
+
+enum WinToast::ShortcutResult WinToast::createShortcut() {
+ if (_aumi.empty() || _appName.empty()) {
+ DEBUG_MSG(L"Error: App User Model Id or Appname is empty!");
+ return SHORTCUT_MISSING_PARAMETERS;
+ }
+
+ if (!isCompatible()) {
+ DEBUG_MSG(L"Your OS is not compatible with this library! =(");
+ return SHORTCUT_INCOMPATIBLE_OS;
+ }
+
+ if (!_hasCoInitialized) {
+ HRESULT initHr = CoInitializeEx(nullptr, COINIT::COINIT_MULTITHREADED);
+ if (initHr != RPC_E_CHANGED_MODE) {
+ if (FAILED(initHr) && initHr != S_FALSE) {
+ DEBUG_MSG(L"Error on COM library initialization!");
+ return SHORTCUT_COM_INIT_FAILURE;
+ }
+ else {
+ _hasCoInitialized = true;
+ }
+ }
+ }
+
+ bool wasChanged;
+ HRESULT hr = validateShellLinkHelper(wasChanged);
+ if (SUCCEEDED(hr))
+ return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED;
+
+ hr = createShellLinkHelper();
+ return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED;
+}
+
+bool WinToast::initialize(_Out_ WinToastError* error) {
+ _isInitialized = false;
+ setError(error, WinToastError::NoError);
+
+ if (!isCompatible()) {
+ setError(error, WinToastError::SystemNotSupported);
+ DEBUG_MSG(L"Error: system not supported.");
+ 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?");
+ return false;
+ }
+
+ if (createShortcut() < 0) {
+ setError(error, WinToastError::ShellLinkNotCreated);
+ DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =(");
+ return false;
+ }
+
+ if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) {
+ setError(error, WinToastError::InvalidAppUserModelID);
+ DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =(");
+ return false;
+ }
+
+ _isInitialized = true;
+ return _isInitialized;
+}
+
+bool WinToast::isInitialized() const {
+ return _isInitialized;
+}
+
+const std::wstring& WinToast::appName() const {
+ return _appName;
+}
+
+const std::wstring& WinToast::appUserModelId() const {
+ return _aumi;
+}
+
+
+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);
+ if (attr >= 0xFFFFFFF) {
+ DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path);
+ return E_FAIL;
+ }
+
+ // Let's load the file as shell link to validate.
+ // - Create a shell link
+ // - Create a persistant file
+ // - Load the path as data for the persistant file
+ // - Read the property AUMI and validate with the current
+ // - Review if AUMI is equal.
+ ComPtr shellLink;
+ HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink));
+ if (SUCCEEDED(hr)) {
+ ComPtr persistFile;
+ hr = shellLink.As(&persistFile);
+ if (SUCCEEDED(hr)) {
+ hr = persistFile->Load(path, STGM_READWRITE);
+ if (SUCCEEDED(hr)) {
+ ComPtr propertyStore;
+ hr = shellLink.As(&propertyStore);
+ if (SUCCEEDED(hr)) {
+ PROPVARIANT appIdPropVar;
+ hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ WCHAR AUMI[MAX_PATH];
+ hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH);
+ wasChanged = false;
+ if (FAILED(hr) || _aumi != AUMI) {
+ // AUMI Changed for the same app, let's update the current value! =)
+ wasChanged = true;
+ PropVariantClear(&appIdPropVar);
+ hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->Commit();
+ if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) {
+ hr = persistFile->Save(path, TRUE);
+ }
+ }
+ }
+ }
+ PropVariantClear(&appIdPropVar);
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+
+
+HRESULT WinToast::createShellLinkHelper() {
+ WCHAR exePath[MAX_PATH]{L'\0'};
+ WCHAR slPath[MAX_PATH]{L'\0'};
+ Util::defaultShellLinkPath(_appName, slPath);
+ Util::defaultExecutablePath(exePath);
+ ComPtr shellLink;
+ HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink));
+ if (SUCCEEDED(hr)) {
+ hr = shellLink->SetPath(exePath);
+ if (SUCCEEDED(hr)) {
+ hr = shellLink->SetArguments(L"");
+ if (SUCCEEDED(hr)) {
+ hr = shellLink->SetWorkingDirectory(exePath);
+ if (SUCCEEDED(hr)) {
+ ComPtr propertyStore;
+ hr = shellLink.As(&propertyStore);
+ if (SUCCEEDED(hr)) {
+ PROPVARIANT appIdPropVar;
+ hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->Commit();
+ if (SUCCEEDED(hr)) {
+ ComPtr persistFile;
+ hr = shellLink.As(&persistFile);
+ if (SUCCEEDED(hr)) {
+ hr = persistFile->Save(slPath, TRUE);
+ }
+ }
+ }
+ PropVariantClear(&appIdPropVar);
+ }
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error) {
+ setError(error, WinToastError::NoError);
+ INT64 id = -1;
+ if (!isInitialized()) {
+ setError(error, WinToastError::NotInitialized);
+ DEBUG_MSG("Error when launching the toast. WinToast is not initialized.");
+ return id;
+ }
+ if (!handler) {
+ setError(error, WinToastError::InvalidHandler);
+ DEBUG_MSG("Error when launching the toast. Handler cannot be nullptr.");
+ return id;
+ }
+
+ ComPtr notificationManager;
+ 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);
+ if (SUCCEEDED(hr)) {
+ ComPtr xmlDocument;
+ HRESULT hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument);
+ if (SUCCEEDED(hr)) {
+ for (std::size_t i = 0, fieldsCount = toast.textFieldsCount(); i < fieldsCount && SUCCEEDED(hr); i++) {
+ hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i);
+ }
+
+ // 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()) {
+ hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText());
+ }
+
+ std::array buf;
+ for (std::size_t i = 0, actionsCount = toast.actionsCount(); i < actionsCount && SUCCEEDED(hr); i++) {
+ _snwprintf_s(buf.data(), buf.size(), _TRUNCATE, L"%zd", i);
+ hr = addActionHelper(xmlDocument.Get(), toast.actionLabel(i), buf.data());
+ }
+
+ if (SUCCEEDED(hr)) {
+ hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default)
+ ? 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");
+ }
+
+ } else {
+ DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version");
+ }
+
+ if (SUCCEEDED(hr)) {
+ hr = toast.hasImage() ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath()) : hr;
+ if (SUCCEEDED(hr)) {
+ ComPtr notification;
+ hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification);
+ if (SUCCEEDED(hr)) {
+ INT64 expiration = 0, relativeExpiration = toast.expiration();
+ if (relativeExpiration > 0) {
+ InternalDateTime expirationDateTime(relativeExpiration);
+ expiration = expirationDateTime;
+ hr = notification->put_ExpirationTime(&expirationDateTime);
+ }
+
+ if (SUCCEEDED(hr)) {
+ hr = Util::setEventHandlers(notification.Get(), std::shared_ptr(handler), expiration);
+ if (FAILED(hr)) {
+ setError(error, WinToastError::InvalidHandler);
+ }
+ }
+
+ if (SUCCEEDED(hr)) {
+ GUID guid;
+ hr = CoCreateGuid(&guid);
+ if (SUCCEEDED(hr)) {
+ id = guid.Data1;
+ _buffer[id] = notification;
+ DEBUG_MSG("xml: " << Util::AsString(xmlDocument));
+ hr = notifier->Show(notification.Get());
+ if (FAILED(hr)) {
+ setError(error, WinToastError::NotDisplayed);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ 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;
+}
+
+bool WinToast::hideToast(_In_ INT64 id) {
+ if (!isInitialized()) {
+ DEBUG_MSG("Error when hiding the toast. WinToast is not initialized.");
+ 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;
+}
+
+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();
+ }
+}
+
+//
+// Available as of Windows 10 Anniversary Update
+// Ref: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts
+//
+// 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" });
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ UINT32 nodeListLength;
+ hr = nodeList->get_Length(&nodeListLength);
+ if (SUCCEEDED(hr)) {
+ for (UINT32 i = 0; i < nodeListLength; i++) {
+ ComPtr textNode;
+ hr = nodeList->Item(i, &textNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr attributes;
+ hr = textNode->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ ComPtr editedNode;
+ if (SUCCEEDED(hr)) {
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"placement").Get(), &editedNode);
+ if (FAILED(hr) || !editedNode) {
+ continue;
+ }
+ hr = Util::setNodeStringValue(L"attribution", editedNode.Get(), xml);
+ if (SUCCEEDED(hr)) {
+ return setTextFieldHelper(xml, text, i);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").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"duration").Get(),
+ WinToastStringWrapper(duration).Get());
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ ComPtr node;
+ hr = nodeList->Item(pos, &node);
+ if (SUCCEEDED(hr)) {
+ hr = Util::setNodeStringValue(text, node.Get(), xml);
+ }
+ }
+ return hr;
+}
+
+
+HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path) {
+ assert(path.size() < MAX_PATH);
+
+ wchar_t imagePath[MAX_PATH] = L"file:///";
+ 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 attributes;
+ hr = node->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ ComPtr editedNode;
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode);
+ if (SUCCEEDED(hr)) {
+ Util::setNodeStringValue(imagePath, editedNode.Get(), xml);
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& 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");
+ Util::createElement(xml, L"toast", L"audio", attrs);
+
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"audio").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ ComPtr node;
+ hr = nodeList->Item(0, &node);
+ if (SUCCEEDED(hr)) {
+ ComPtr attributes;
+ hr = node->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ ComPtr editedNode;
+ if (!path.empty()) {
+ if (SUCCEEDED(hr)) {
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode);
+ if (SUCCEEDED(hr)) {
+ hr = Util::setNodeStringValue(path, editedNode.Get(), xml);
+ }
+ }
+ }
+
+ 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;
+ }
+ }
+ }
+ }
+ }
+ 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);
+ if (SUCCEEDED(hr)) {
+ UINT32 length;
+ hr = nodeList->get_Length(&length);
+ if (SUCCEEDED(hr)) {
+ ComPtr actionsNode;
+ if (length > 0) {
+ hr = nodeList->Item(0, &actionsNode);
+ } else {
+ hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ 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());
+ 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);
+ if (SUCCEEDED(hr)) {
+ hr = actionsElement.As(&actionsNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr appendedChild;
+ hr = toastNode->AppendChild(actionsNode.Get(), &appendedChild);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ if (SUCCEEDED(hr)) {
+ ComPtr actionElement;
+ hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement);
+ if (SUCCEEDED(hr))
+ hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get());
+ if (SUCCEEDED(hr))
+ hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get());
+ if (SUCCEEDED(hr)) {
+ ComPtr actionNode;
+ hr = actionElement.As(&actionNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr appendedChild;
+ hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild);
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+void WinToast::setError(_Out_ WinToastError* error, _In_ WinToastError value) {
+ if (error) {
+ *error = 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"");
+}
+
+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());
+ _textFields[position] = txt;
+}
+
+void WinToastTemplate::setImagePath(_In_ const std::wstring& imgPath) {
+ _imagePath = imgPath;
+}
+
+void WinToastTemplate::setAudioPath(_In_ const std::wstring& 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"},
+ };
+ const auto iter = Files.find(file);
+ assert(iter != Files.end());
+ _audioPath = iter->second;
+}
+
+void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOption) {
+ _audioOption = audioOption;
+}
+
+void WinToastTemplate::setFirstLine(const std::wstring &text) {
+ setTextField(text, WinToastTemplate::FirstLine);
+}
+
+void WinToastTemplate::setSecondLine(const std::wstring &text) {
+ setTextField(text, WinToastTemplate::SecondLine);
+}
+
+void WinToastTemplate::setThirdLine(const std::wstring &text) {
+ setTextField(text, WinToastTemplate::ThirdLine);
+}
+
+void WinToastTemplate::setDuration(_In_ Duration duration) {
+ _duration = duration;
+}
+
+void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) {
+ _expiration = millisecondsFromNow;
+}
+
+void WinToastTemplate::setAttributionText(_In_ const std::wstring& attributionText) {
+ _attributionText = attributionText;
+}
+
+void WinToastTemplate::addAction(_In_ const std::wstring & label) {
+ _actions.push_back(label);
+}
+
+std::size_t WinToastTemplate::textFieldsCount() const {
+ return _textFields.size();
+}
+
+std::size_t WinToastTemplate::actionsCount() const {
+ return _actions.size();
+}
+
+bool WinToastTemplate::hasImage() const {
+ return _type < WinToastTemplateType::Text01;
+}
+
+const std::vector& WinToastTemplate::textFields() const {
+ return _textFields;
+}
+
+const std::wstring& WinToastTemplate::textField(_In_ TextField pos) const {
+ const auto position = static_cast(pos);
+ assert(position < _textFields.size());
+ return _textFields[position];
+}
+
+const std::wstring& WinToastTemplate::actionLabel(_In_ std::size_t position) const {
+ assert(position < _actions.size());
+ return _actions[position];
+}
+
+const std::wstring& WinToastTemplate::imagePath() const {
+ return _imagePath;
+}
+
+const std::wstring& WinToastTemplate::audioPath() const {
+ return _audioPath;
+}
+
+const std::wstring& WinToastTemplate::attributionText() const {
+ return _attributionText;
+}
+
+INT64 WinToastTemplate::expiration() const {
+ return _expiration;
+}
+
+WinToastTemplate::WinToastTemplateType WinToastTemplate::type() const {
+ return _type;
+}
+
+WinToastTemplate::AudioOption WinToastTemplate::audioOption() const {
+ return _audioOption;
+}
+
+WinToastTemplate::Duration WinToastTemplate::duration() const {
+ return _duration;
+}
diff --git a/third-party/wintoast/wintoastlib.h b/third-party/wintoast/wintoastlib.h
new file mode 100644
index 0000000..68b1cb1
--- /dev/null
+++ b/third-party/wintoast/wintoastlib.h
@@ -0,0 +1,217 @@
+/* * Copyright (C) 2016-2019 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
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef WINTOASTLIB_H
+#define WINTOASTLIB_H
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include