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 +using namespace Microsoft::WRL; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Windows::Foundation; + + +namespace WinToastLib { + + class IWinToastHandler { + public: + enum WinToastDismissalReason { + UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, + ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, + TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut + }; + virtual ~IWinToastHandler() = default; + virtual void toastActivated() const = 0; + virtual void toastActivated(int actionIndex) const = 0; + virtual void toastDismissed(WinToastDismissalReason state) const = 0; + virtual void toastFailed() const = 0; + }; + + class WinToastTemplate { + public: + enum Duration { System, Short, Long }; + enum AudioOption { Default = 0, Silent, Loop }; + enum TextField { FirstLine = 0, SecondLine, ThirdLine }; + enum WinToastTemplateType { + ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01, + ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02, + ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03, + ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04, + Text01 = ToastTemplateType::ToastTemplateType_ToastText01, + Text02 = ToastTemplateType::ToastTemplateType_ToastText02, + Text03 = ToastTemplateType::ToastTemplateType_ToastText03, + Text04 = ToastTemplateType::ToastTemplateType_ToastText04, + }; + + enum AudioSystemFile { + DefaultSound, + IM, + Mail, + Reminder, + SMS, + Alarm, + Alarm2, + Alarm3, + Alarm4, + Alarm5, + Alarm6, + Alarm7, + Alarm8, + Alarm9, + Alarm10, + Call, + Call1, + Call2, + Call3, + Call4, + Call5, + Call6, + Call7, + Call8, + Call9, + Call10, + }; + + + WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02); + ~WinToastTemplate(); + + void setFirstLine(_In_ const std::wstring& text); + void setSecondLine(_In_ const std::wstring& text); + void setThirdLine(_In_ const std::wstring& text); + void setTextField(_In_ const std::wstring& txt, _In_ TextField pos); + void setAttributionText(_In_ const std::wstring & attributionText); + void setImagePath(_In_ const std::wstring& imgPath); + void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio); + void setAudioPath(_In_ const std::wstring& audioPath); + void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption); + void setDuration(_In_ Duration duration); + void setExpiration(_In_ INT64 millisecondsFromNow); + void addAction(_In_ const std::wstring& label); + + std::size_t textFieldsCount() const; + std::size_t actionsCount() const; + bool hasImage() const; + const std::vector& textFields() const; + const std::wstring& textField(_In_ TextField pos) const; + const std::wstring& actionLabel(_In_ std::size_t pos) const; + const std::wstring& imagePath() const; + const std::wstring& audioPath() const; + const std::wstring& attributionText() const; + INT64 expiration() const; + WinToastTemplateType type() const; + WinToastTemplate::AudioOption audioOption() const; + Duration duration() const; + private: + std::vector _textFields{}; + std::vector _actions{}; + std::wstring _imagePath{}; + std::wstring _audioPath{}; + std::wstring _attributionText{}; + INT64 _expiration{0}; + AudioOption _audioOption{WinToastTemplate::AudioOption::Default}; + WinToastTemplateType _type{WinToastTemplateType::Text01}; + Duration _duration{Duration::System}; + }; + + class WinToast { + public: + enum WinToastError { + NoError = 0, + NotInitialized, + SystemNotSupported, + ShellLinkNotCreated, + InvalidAppUserModelID, + InvalidParameters, + InvalidHandler, + NotDisplayed, + UnknownError + }; + + enum ShortcutResult { + SHORTCUT_UNCHANGED = 0, + SHORTCUT_WAS_CHANGED = 1, + SHORTCUT_WAS_CREATED = 2, + + SHORTCUT_MISSING_PARAMETERS = -1, + SHORTCUT_INCOMPATIBLE_OS = -2, + SHORTCUT_COM_INIT_FAILURE = -3, + SHORTCUT_CREATE_FAILED = -4 + }; + + WinToast(void); + virtual ~WinToast(); + static WinToast* instance(); + static bool isCompatible(); + static bool isSupportingModernFeatures(); + static std::wstring configureAUMI(_In_ const std::wstring& companyName, + _In_ const std::wstring& productName, + _In_ const std::wstring& subProduct = std::wstring(), + _In_ const std::wstring& versionInformation = std::wstring()); + static const std::wstring& strerror(_In_ WinToastError error); + virtual bool initialize(_Out_ WinToastError* error = nullptr); + virtual bool isInitialized() const; + virtual bool hideToast(_In_ INT64 id); + virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error = nullptr); + virtual void clear(); + virtual enum ShortcutResult createShortcut(); + + const std::wstring& appName() const; + const std::wstring& appUserModelId() const; + void setAppUserModelId(_In_ const std::wstring& aumi); + void setAppName(_In_ const std::wstring& appName); + + protected: + bool _isInitialized{false}; + bool _hasCoInitialized{false}; + std::wstring _appName{}; + std::wstring _aumi{}; + std::map> _buffer{}; + + HRESULT validateShellLinkHelper(_Out_ bool& wasChanged); + HRESULT createShellLinkHelper(); + HRESULT setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path); + HRESULT setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); + HRESULT setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos); + HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text); + HRESULT addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& action, _In_ const std::wstring& arguments); + HRESULT addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration); + ComPtr notifier(_In_ bool* succeded) const; + void setError(_Out_ WinToastError* error, _In_ WinToastError value); + }; +} +#endif // WINTOASTLIB_H diff --git a/ui/aboutwindow.cpp b/ui/aboutwindow.cpp index 3755276..ef1ade2 100644 --- a/ui/aboutwindow.cpp +++ b/ui/aboutwindow.cpp @@ -2,8 +2,6 @@ // Created by Dmitry Khrykin on 2019-08-12. // -#include - #include #include #include @@ -14,7 +12,6 @@ #include "colorprovider.h" #include "applicationsettings.h" - class HyperLinkLabel : public ColoredLabel { public: using ColoredLabel::ColoredLabel; @@ -32,7 +29,7 @@ class HyperLinkLabel : public ColoredLabel { inactive, clicked, hovered - } state; + } state = inactive; void updateColor() { auto color = dynamicColor(); @@ -82,7 +79,6 @@ class HyperLinkLabel : public ColoredLabel { } }; - AboutWindow::AboutWindow(QWidget *parent) : QDialog(parent) { auto mainLayout = new QVBoxLayout(); setLayout(mainLayout); diff --git a/ui/activitylistwidget.cpp b/ui/activitylistwidget.cpp index 2eef65d..5be2671 100644 --- a/ui/activitylistwidget.cpp +++ b/ui/activitylistwidget.cpp @@ -81,7 +81,7 @@ void ActivityListWidget::layoutChildWidgets() { " }"); scrollArea->setWidget(listWidget); - searchBox = new SearchBoxWidget("Search activities"); + searchBox = new SearchBoxWidget("Find..."); connect(searchBox, &SearchBoxWidget::textEdited, [=](const QString &text) { @@ -100,8 +100,7 @@ void ActivityListWidget::layoutChildWidgets() { getBackAction->setEnabled(true); }); - auto buttonName = navbar->rightButton() ? "Add" : "+"; - emptyListNotice = new ColoredLabel(QString("Click \"%1\" to add activities").arg(buttonName)); + emptyListNotice = new ColoredLabel("Click \"+\" to add activities"); emptyListNotice->setDynamicColor(&ColorProvider::secondaryTextColor); emptyListNotice->setAlignment(Qt::AlignCenter); emptyListNotice->setFontHeight(14); @@ -143,10 +142,10 @@ void ActivityListWidget::setupNavbar() { layout()->addWidget(navbar); navbar->setTitle("Activities"); - navbar->setLeftButton("Back", + navbar->setLeftButton(IconWidget::backCode(), this, &ActivityListWidget::getBack); - navbar->setRightButton("Add", + navbar->setRightButton(IconWidget::addCode(), this, &ActivityListWidget::showNewActivityMenu); } @@ -237,9 +236,6 @@ void ActivityListWidget::showNewActivityMenu() { } void ActivityListWidget::reloadStrategy() { - strategy.activities() - .add_on_change_callback(this, &ActivityListWidget::updateUI); - updateUI(); } diff --git a/ui/activitywidget.cpp b/ui/activitywidget.cpp index 1d771a4..040ce76 100644 --- a/ui/activitywidget.cpp +++ b/ui/activitywidget.cpp @@ -12,6 +12,7 @@ #include "colorutils.h" #include "applicationsettings.h" #include "mainwindow.h" +#include "mainscene.h" ActivityWidget::ActivityWidget(stg::activity *activity, QWidget *parent) diff --git a/ui/application.cpp b/ui/application.cpp index 7c9023e..033848e 100644 --- a/ui/application.cpp +++ b/ui/application.cpp @@ -3,9 +3,14 @@ // #include +#include #include "application.h" +#ifdef Q_OS_WIN +#include "Windows.h" +#endif + QStringList Application::openedFiles = QStringList(); SelfUpdater Application::_updater = SelfUpdater(); @@ -16,13 +21,6 @@ UpdateDialog *Application::updateDialog = nullptr; Application::Application(int &argc, char **argv) : QApplication(argc, argv) { - QTimer::singleShot(0, [=]() { - if (!launchedByOpenEvent) { - auto window = MainWindow::createLastOpened(); - window->show(); - } - }); - Application::setAttribute(Qt::AA_EnableHighDpiScaling); Application::setAttribute(Qt::AA_UseHighDpiPixmaps); @@ -34,6 +32,27 @@ Application::Application(int &argc, char **argv) #ifdef Q_OS_MAC setupCocoaDelegate(); #endif + + if (argc == 2) { + launchedByOpenEvent = true; + auto filePath = QString(argv[1]); + + auto window = new MainWindow(filePath); + window->show(); + } + + if (!launchedByOpenEvent) { +#ifdef Q_OS_MAC + QTimer::singleShot(100, [=]() { +#endif + if (!launchedByOpenEvent) { + auto window = MainWindow::createLastOpened(); + window->show(); + } +#ifdef Q_OS_MAC + }); +#endif + } } bool Application::event(QEvent *event) { @@ -41,10 +60,12 @@ bool Application::event(QEvent *event) { launchedByOpenEvent = true; auto openEvent = dynamic_cast(event); + auto filePath = openEvent->file(); - auto window = new MainWindow(); - window->loadFile(openEvent->file(), MainWindow::loadInCurrentWindow); - window->show(); + if (!Application::fileIsOpened(filePath)) { + auto window = new MainWindow(filePath); + window->show(); + } } return QApplication::event(event); @@ -56,6 +77,17 @@ void Application::setupFonts() { if (QFontDatabase::addApplicationFont(":/fonts/ionicons.ttf") < 0) qWarning() << "Ionicons cannot be loaded!"; + +#ifdef Q_OS_WIN + if (QFontDatabase().families().contains("Segoe UI")) { + auto font = QFont("Segoe UI"); + font.setPixelSize(12); + setFont(font); + _isGreaterThanWin8 = true; + } else { + _isGreaterThanWin8 = false; + } +#endif } const SelfUpdater &Application::updater() { @@ -77,3 +109,16 @@ void Application::markFileClosed(const QString &filePath) { bool Application::fileIsOpened(const QString &filePath) { return openedFiles.indexOf(filePath) >= 0; } + +void Application::clearRecentFiles() { +#ifdef Q_OS_MAC + Application::clearCocoaRecentFiles(); +#endif +} + + +#ifdef Q_OS_WIN + +bool Application::_isGreaterThanWin8 = false; + +#endif \ No newline at end of file diff --git a/ui/application.h b/ui/application.h index 3f0bdcc..7e677f9 100644 --- a/ui/application.h +++ b/ui/application.h @@ -13,6 +13,7 @@ #include "aboutwindow.h" #include "updatedialog.h" #include "mainwindow.h" +#include "selfupdater.h" #ifdef Q_OS_MAC Q_FORWARD_DECLARE_OBJC_CLASS(CocoaDelegate); @@ -37,24 +38,37 @@ class Application : public QApplication { static const SelfUpdater &updater(); static void registerOpenedFile(const QString &filePath); + static void clearRecentFiles(); static void markFileClosed(const QString &filePath); static bool fileIsOpened(const QString &filePath); -private: - static void setupFonts(); +#ifdef Q_OS_WIN + static bool isGreaterThanWin8() { + return _isGreaterThanWin8; + } +#endif +private: static SelfUpdater _updater; + static void setupFonts(); + bool launchedByOpenEvent = false; #ifdef Q_OS_MAC static CocoaDelegate *cocoaDelegate; static void registerCocoaRecentFile(const QString &filePath); + static void clearCocoaRecentFiles(); + void setupCocoaDelegate(); void releaseCocoaDelegate(); #endif +#ifdef Q_OS_WIN + static bool _isGreaterThanWin8; +#endif + bool event(QEvent *event) override; }; diff --git a/ui/application.mm b/ui/application.mm index b459ab0..1bb1190 100644 --- a/ui/application.mm +++ b/ui/application.mm @@ -16,6 +16,7 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center macosx(10.14)); + (void)registerRecentFile:(NSString *)filePath; ++ (void)clearRecentFiles; @end @@ -34,6 +35,10 @@ + (void)registerRecentFile:(NSString *)filePath { noteNewRecentDocumentURL:[NSURL fileURLWithPath:filePath]]; } ++ (void)clearRecentFiles { + [[NSDocumentController sharedDocumentController] clearRecentDocuments:nil]; +} + @end CocoaDelegate *Application::cocoaDelegate = nullptr; @@ -42,6 +47,10 @@ + (void)registerRecentFile:(NSString *)filePath { [CocoaDelegate registerRecentFile:filePath.toNSString()]; } +void Application::clearCocoaRecentFiles() { + [CocoaDelegate clearRecentFiles]; +} + void Application::setupCocoaDelegate() { cocoaDelegate = [[CocoaDelegate alloc] init]; @@ -50,6 +59,7 @@ + (void)registerRecentFile:(NSString *)filePath { } } + void Application::releaseCocoaDelegate() { [cocoaDelegate release]; }; diff --git a/ui/applicationicon.cpp b/ui/applicationicon.cpp index e24f7fa..7979bf1 100644 --- a/ui/applicationicon.cpp +++ b/ui/applicationicon.cpp @@ -2,10 +2,22 @@ // Created by Dmitry Khrykin on 2019-08-12. // +#include + #include "applicationicon.h" -#ifndef Q_OS_MAC +#if !defined(Q_OS_MAC) && !defined(Q_OS_WIN) QPixmap ApplicationIcon::defaultIcon() { - return QPixmap(); + return QApplication::windowIcon().pixmap(QSize(size, size)); } #endif + +#ifdef Q_OS_WIN +#include + +QPixmap ApplicationIcon::defaultIcon() { + auto icon = QIcon(QCoreApplication::applicationDirPath() + "\\Strategr.ico"); + return icon.pixmap(QSize(size, size)); +} +#endif + diff --git a/ui/applicationmenu.cpp b/ui/applicationmenu.cpp index 087574b..b9c2017 100644 --- a/ui/applicationmenu.cpp +++ b/ui/applicationmenu.cpp @@ -11,10 +11,12 @@ #include "mainwindow.h" #include "aboutwindow.h" #include "updatedialog.h" +#include "mainscene.h" #ifdef Q_OS_MAC #include "macoscalendarexporter.h" +#include "mainscene.h" #endif @@ -153,7 +155,9 @@ void ApplicationMenu::addExportToCalendarAction() const { if (result == MacOSCalendarExporter::Response::Export) { QSettings().setValue("calendarExportOptions", options); - QSettings().setValue("exportCalendarTitle", QString::fromStdString(calendarTitle)); + + if (!calendarTitle.empty()) + QSettings().setValue("exportCalendarTitle", QString::fromStdString(calendarTitle)); MacOSCalendarExporter::exportStrategy(window->strategy, options, date, calendarTitle); } diff --git a/ui/applicationmenu.h b/ui/applicationmenu.h index cf141f0..f4de07b 100644 --- a/ui/applicationmenu.h +++ b/ui/applicationmenu.h @@ -1,4 +1,3 @@ -// // Created by Dmitry Khrykin on 2019-07-08. // diff --git a/ui/colorprovider.cpp b/ui/colorprovider.cpp index e40994d..5dae1ff 100644 --- a/ui/colorprovider.cpp +++ b/ui/colorprovider.cpp @@ -58,13 +58,13 @@ QColor ColorProvider::highlightedTextColor() { QColor ColorProvider::secondaryTextColor() { return ColorUtils::overlayWithAlpha( QApplication::palette().color(QPalette::Text), - 0.5 * ColorUtils::shadesAlphaFactor(2)); + 0.5 * ColorUtils::shadesAlphaFactor(4, 0.95)); } QColor ColorProvider::tertiaryTextColor() { return ColorUtils::overlayWithAlpha( QApplication::palette().color(QPalette::Text), - 0.35 * ColorUtils::shadesAlphaFactor(0)); + 0.30 * ColorUtils::shadesAlphaFactor(0)); } QColor ColorProvider::textColorJustLighter() { diff --git a/ui/currentsessionwidget.cpp b/ui/currentsessionwidget.cpp index b29bdb1..c6b0bfc 100644 --- a/ui/currentsessionwidget.cpp +++ b/ui/currentsessionwidget.cpp @@ -47,7 +47,7 @@ CurrentSessionWidget::CurrentSessionWidget(stg::strategy &strategy, QWidget *par activityLabel->setFontHeight(ApplicationSettings::sessionFontSize - 1); activityLabel->setContentsMargins(0, 0, 0, 3); activityLabel->customRenderer = [&](QPainter *painter, const QString &) { - auto activeSession = strategy.get_active_session(); + auto activeSession = strategy.active_session(); if (!activeSession) { return; } @@ -188,7 +188,7 @@ void CurrentSessionWidget::mouseReleaseEvent(QMouseEvent *) { } void CurrentSessionWidget::reloadStrategy() { - updateUIWithSession(strategy.get_active_session()); + updateUIWithSession(strategy.active_session()); } void CurrentSessionWidget::slideAndHide(const std::function &onFinishedCallback) { @@ -208,7 +208,7 @@ void CurrentSessionWidget::slideAndShow(const std::function &onFinishedC isVisible = true; QTimer::singleShot(ApplicationSettings::currentSessionShowDelay, [=]() { - if (strategy.get_active_session()) { + if (strategy.active_session()) { SlidingAnimator::showWidget(this); } else { isVisible = false; @@ -246,7 +246,7 @@ void CurrentSessionWidget::updateUIWithSession(const stg::session *activeSession } QString CurrentSessionWidget::makeActivitySessionTitle() const { - auto activitySession = strategy.get_active_session(); + auto activitySession = strategy.active_session(); return humanTimeForMinutes(activitySession->duration()) + " " + " +#include + +#include "iconwidget.h" + +IconWidget::IconWidget(const QString &code, QWidget *parent) + : _code(code), QWidget(parent) {} + +void IconWidget::paintEvent(QPaintEvent *) { + auto painter = QPainter(this); + auto iconRect = QRect(0, 0, width(), height()); + + auto font = QFont("FontAwesome"); + font.setPixelSize(14); + + auto color = _color; + if (isPressed) + color.setAlphaF(0.6); + + painter.setFont(font); + painter.setPen(color); + + painter.setRenderHint(QPainter::Antialiasing); + painter.drawText(iconRect, Qt::AlignCenter, _code); +} + +const QString &IconWidget::code() const { + return _code; +} + +void IconWidget::setCode(const QString &code) { + _code = code; + update(); +} + +void IconWidget::setColor(const QColor &color) { + _color = color; + update(); +} + +void IconWidget::mousePressEvent(QMouseEvent *) { + isPressed = true; + update(); +} + +void IconWidget::mouseReleaseEvent(QMouseEvent *) { + isPressed = false; + update(); + + emit clicked(); +} + +QString IconWidget::backCode() { + return QStringLiteral(u"\uf053"); +} + +QString IconWidget::addCode() { + return QStringLiteral(u"\uf067"); +} diff --git a/ui/iconwidget.h b/ui/iconwidget.h new file mode 100644 index 0000000..96a9622 --- /dev/null +++ b/ui/iconwidget.h @@ -0,0 +1,38 @@ +// +// Created by Dmitry Khrykin on 2020-02-08. +// + +#ifndef STRATEGR_ICONWIDGET_H +#define STRATEGR_ICONWIDGET_H + +#include + +class IconWidget : public QWidget { +Q_OBJECT +public: + static QString backCode(); + static QString addCode(); + + explicit IconWidget(const QString &code = "", QWidget *parent = nullptr); + const QString &code() const; + void setCode(const QString &code); + +signals: + void clicked(); +private: + QString _code = ""; + +public: + void setColor(const QColor &color); +private: + bool isPressed = false; + QColor _color; + + void mousePressEvent(QMouseEvent *) override; + void mouseReleaseEvent(QMouseEvent *) override; + + void paintEvent(QPaintEvent *) override; +}; + + +#endif //STRATEGR_ICONWIDGET_H diff --git a/ui/macoswindow.mm b/ui/macoswindow.mm index 5c46a55..06a9ce4 100644 --- a/ui/macoswindow.mm +++ b/ui/macoswindow.mm @@ -14,6 +14,7 @@ #include "macoswindow.h" #include "mainwindow.h" +#include "mainscene.h" const NSString *ToolbarItemActivitiesIdentifier = @"Activities"; const NSString *ToolbarItemStrategyTitleIdentifier = @"Title:Strategy"; diff --git a/ui/mainscene.cpp b/ui/mainscene.cpp index 26a7e64..8bc79c9 100644 --- a/ui/mainscene.cpp +++ b/ui/mainscene.cpp @@ -63,7 +63,6 @@ void MainScene::clearSelection() { sessionsMainWidget->clearSelection(); } - void MainScene::showNewActivityMenu() { activitiesWidget->showNewActivityMenu(); } @@ -71,3 +70,7 @@ void MainScene::showNewActivityMenu() { void MainScene::reorderActivitiesByUsage() { strategy.reorder_activities_by_usage(); } + +stg::selection &MainScene::selection() { + return findChildren().first()->selection; +} diff --git a/ui/mainscene.h b/ui/mainscene.h index 9b6fcc4..a843593 100644 --- a/ui/mainscene.h +++ b/ui/mainscene.h @@ -21,13 +21,11 @@ Q_OBJECT void showSessions(); void focusOnCurrentTime(); - void showStrategySettings(); + void showStrategySettings(); void showNewActivityMenu(); - stg::selection &selection() { - return findChildren().first()->selection; - } + stg::selection &selection(); void clearSelection(); void reloadStrategy(); diff --git a/ui/mainwindow.cpp b/ui/mainwindow.cpp index 853e192..24acaf8 100644 --- a/ui/mainwindow.cpp +++ b/ui/mainwindow.cpp @@ -1,35 +1,46 @@ // // Created by Dmitry Khrykin on 2019-07-08. // + #include +#include + #include "mainwindow.h" #include "application.h" #include "alert.h" #include "macoswindow.h" +#include "mainscene.h" +#include "applicationmenu.h" + +MainWindow::MainWindow(const QString &filePath, QWidget *parent) + : strategy(fsIOManager.openFromPathOrDefault(filePath)), + QMainWindow(parent) { + setup(); +} MainWindow::MainWindow(QWidget *parent) - : QMainWindow(parent), - strategy(*fsIOManager.openDefault()) { + : strategy(fsIOManager.openDefault()), + QMainWindow(parent) { setup(); } MainWindow::MainWindow(FileSystemIOManager existingFsIOManager, QWidget *parent) : QMainWindow(parent), fsIOManager(std::move(existingFsIOManager)), - strategy(*fsIOManager.openLastOrDefault()) { + strategy(fsIOManager.openLastOrDefault()) { fsIOManager.setWindow(this); - if (!fsIOManager.fileInfo().filePath().isEmpty()) { - Application::registerOpenedFile(fsIOManager.fileInfo().filePath()); - } - setup(); } void MainWindow::setup() { WindowGeometryManager::setInitialGeometry(this); + if (!fsIOManager.fileInfo().filePath().isEmpty()) { + Application::registerOpenedFile(fsIOManager.fileInfo().filePath()); + } + #ifdef Q_OS_MAC MacOSWindow::setup(this); #endif @@ -94,6 +105,7 @@ void MainWindow::saveCurrentStrategyAsDefault() { void MainWindow::clearRecentFilesList() { FileSystemIOManager::clearRecent(); menu()->updateRecentFilesActions(); + Application::clearRecentFiles(); } void MainWindow::openRecentFile() { @@ -112,10 +124,8 @@ void MainWindow::setIsSaved(bool isSaved) { fsIOManager.setIsSaved(isSaved); auto strategyTitle = fsIOManager.fileInfo().baseName(); - - if (strategyTitle.isEmpty()) { + if (strategyTitle.isEmpty()) strategyTitle = "Untitled"; - } setWindowModified(!isSaved); setWindowTitle(strategyTitle + "[*]"); @@ -198,4 +208,4 @@ MainWindow *MainWindow::createLastOpened() { void MainWindow::reloadStrategy() { scene()->reloadStrategy(); -} +} \ No newline at end of file diff --git a/ui/mainwindow.h b/ui/mainwindow.h index e2182c1..a7f2245 100644 --- a/ui/mainwindow.h +++ b/ui/mainwindow.h @@ -6,22 +6,22 @@ #define UI_MAINWINDOW_H #include -#include -#include -#include #include "strategy.h" #include "filesystemiomanager.h" #include "windowgeometrymanager.h" -#include "mainscene.h" -#include "applicationmenu.h" -#include "macoswindow.h" + +class MainScene; +class ApplicationMenu; + +#ifdef Q_OS_MAC +class MacOSWindow; +#endif class MainWindow : public QMainWindow { Q_OBJECT public: - static const bool loadInCurrentWindow = false; - + explicit MainWindow(const QString &filePath, QWidget *parent = nullptr); explicit MainWindow(QWidget *parent = nullptr); static MainWindow *createLastOpened(); diff --git a/ui/navbar.cpp b/ui/navbar.cpp index c3d90f6..3e7c7c9 100644 --- a/ui/navbar.cpp +++ b/ui/navbar.cpp @@ -1,9 +1,10 @@ -#include "navbar.h" #include #include #include #include #include + +#include "navbar.h" #include "applicationsettings.h" #ifdef HIDE_NAVBAR @@ -14,11 +15,11 @@ Navbar::Navbar(QWidget *parent) : QWidget(parent) { void Navbar::setTitle(const QString &title) {} -QPushButton *Navbar::leftButton() const { +QWidget *Navbar::leftButton() const { return nullptr; } -QPushButton *Navbar::rightButton() const { +QWidget *Navbar::rightButton() const { return nullptr; } @@ -41,11 +42,11 @@ void Navbar::setTitle(const QString &title) { _titleLabel->setText(title); } -QPushButton *Navbar::leftButton() const { +QWidget *Navbar::leftButton() const { return _leftButton; } -QPushButton *Navbar::rightButton() const { +QWidget *Navbar::rightButton() const { return _rightButton; } @@ -58,6 +59,8 @@ void Navbar::paintEvent(QPaintEvent *) { painter.setPen(Qt::NoPen); painter.setBrush(baseColor()); painter.drawRect(0, 0, width(), height()); +// painter.setBrush(borderColor()); +// painter.drawRect(0, height() - 1, width(), 1); } void Navbar::setupWidgets() { @@ -70,41 +73,26 @@ void Navbar::setupLayout() { setLayout(new QHBoxLayout()); layout()->setSpacing(0); layout()->setContentsMargins(0, 0, 0, 0); - setFixedHeight(45); + setFixedHeight(40); } void Navbar::setupLeftButton() { - _leftButton = new QPushButton(""); - _leftButton->setProperty("navButton", true); - _leftButton->setFixedWidth(70); - _leftButton->setStyleSheet("[navButton] {" - "background: transparent;" - "font-size: 14px;" - "color: #357EDD;" - "}" - "[navButton]:hover {" - "color: #00449E;" - "}"); + _leftButton = new IconWidget("", this); + _leftButton->setFixedWidth(64); + _leftButton->setColor("#094FD1"); +} + +void Navbar::setupRightButton() { + _rightButton = new IconWidget("", this); + _rightButton->setFixedWidth(64); + _rightButton->setColor("#094FD1"); } void Navbar::setupTitleLabel() { _titleLabel = new QLabel(_title); _titleLabel->setAlignment(Qt::AlignCenter); - _titleLabel->setStyleSheet("font-size: 18px;"); -} - -void Navbar::setupRightButton() { - _rightButton = new QPushButton(""); - _rightButton->setProperty("navButton", true); - _rightButton->setFixedWidth(70); - _rightButton->setStyleSheet("[navButton] {" - "background: transparent;" - "font-size: 14px;" - "color: #357EDD;" - "}" - "[navButton]:hover {" - "color: #00449E;" - "}"); + _titleLabel->setStyleSheet("font-weight: bold;" + "font-size: 14px;"); } void Navbar::addWidgetsToLayout() { diff --git a/ui/navbar.h b/ui/navbar.h index 8d7094f..362f042 100644 --- a/ui/navbar.h +++ b/ui/navbar.h @@ -4,7 +4,9 @@ #include #include #include + #include "colorprovider.h" +#include "iconwidget.h" #define DEBUG_NAVBAR false @@ -19,8 +21,8 @@ Q_OBJECT void setTitle(const QString &title); - QPushButton *leftButton() const; - QPushButton *rightButton() const; + QWidget *leftButton() const; + QWidget *rightButton() const; QLabel *titleLabel() const; #ifdef HIDE_NAVBAR @@ -48,8 +50,8 @@ Q_OBJECT const QString &text, const typename QtPrivate::FunctionPointer::Object *receiver, Method slot) { - _leftButton->setText(text); - QObject::connect(_leftButton, &QPushButton::clicked, receiver, slot); + _leftButton->setCode(text); + QObject::connect(_leftButton, &IconWidget::clicked, receiver, slot); } template @@ -57,15 +59,15 @@ Q_OBJECT const QString &text, const typename QtPrivate::FunctionPointer::Object *receiver, Method slot) { - _rightButton->setText(text); - QObject::connect(_rightButton, &QPushButton::clicked, receiver, slot); + _rightButton->setCode(text); + QObject::connect(_rightButton, &IconWidget::clicked, receiver, slot); } private: QString _title; - QPushButton *_leftButton = nullptr; - QPushButton *_rightButton = nullptr; + IconWidget *_leftButton = nullptr; + IconWidget *_rightButton = nullptr; QLabel *_titleLabel = nullptr; QWidget *navbar = nullptr; diff --git a/ui/sessionsmainwidget.cpp b/ui/sessionsmainwidget.cpp index 1336514..b7bba55 100644 --- a/ui/sessionsmainwidget.cpp +++ b/ui/sessionsmainwidget.cpp @@ -2,15 +2,17 @@ // Created by Dmitry Khrykin on 2019-07-10. // +#include + #include #include #include "sessionsmainwidget.h" -#include "notifierimplementation.h" #include "overviewwidget.h" #include "strategysettingswidget.h" #include "currentsessionwidget.h" #include "slotboardwidget.h" +#include "notifierbackend.h" SessionsMainWidget::SessionsMainWidget(stg::strategy &strategy, QWidget *parent) @@ -23,7 +25,22 @@ SessionsMainWidget::SessionsMainWidget(stg::strategy &strategy, layoutChildWidgets(); - notifier = new NotifierImplementation(&strategy, this); + notifier.on_send_notiifcation = [this](const stg::notification ¬ification) { + std::cout << "send notification: " << notification << "\n"; + notifierBackend.sendMessage( + QString::fromStdString(notification.title), + QString::fromStdString(notification.message)); + }; + + notifierTimer = new QTimer(this); + notifierTimer->setInterval(ApplicationSettings::notifierTimerMillisecondsInterval); + connect(notifierTimer, + &QTimer::timeout, + std::bind(&stg::notifier::send_now_if_needed, + ¬ifier, + ApplicationSettings::notifierTimerMillisecondsInterval / 1000)); + + notifierTimer->start(); } void SessionsMainWidget::toggleStrategySettingsOpen() { @@ -86,17 +103,17 @@ void SessionsMainWidget::reloadStrategy() { overviewWidget->reloadStrategy(); currentSessionWidget->reloadStrategy(); - notifier->setStrategy(&strategy); + notifier.schedule(); } void SessionsMainWidget::clearSelection() { slotBoard->clearSelection(); } - void SessionsMainWidget::updateTimerDependants() { overviewWidget->update(); - currentSessionWidget->reloadSessionIfNeeded(); + if (!strategy.is_dragging() && !strategy.is_resizing()) + currentSessionWidget->reloadSessionIfNeeded(); } void SessionsMainWidget::paintEvent(QPaintEvent *paintEvent) { diff --git a/ui/sessionsmainwidget.h b/ui/sessionsmainwidget.h index be74019..b848f8a 100644 --- a/ui/sessionsmainwidget.h +++ b/ui/sessionsmainwidget.h @@ -13,6 +13,8 @@ #include "strategy.h" #include "colorprovider.h" #include "selectionwidget.h" +#include "notifier.h" +#include "notifierbackend.h" class NotifierImplementation; class StrategySettingsWidget; @@ -20,6 +22,7 @@ class CurrentSessionWidget; class QScrollArea; class SlotBoardWidget; class OverviewWidget; +class QTimer; class SessionsMainWidget : public QWidget, public ColorProvider { Q_OBJECT @@ -36,17 +39,20 @@ Q_OBJECT QScrollArea *slotBoardScrollArea() const; private: stg::strategy &strategy; + stg::notifier notifier{strategy}; + + NotifierBackend notifierBackend; - NotifierImplementation *notifier = nullptr; StrategySettingsWidget *strategySettingsWidget = nullptr; CurrentSessionWidget *currentSessionWidget = nullptr; QScrollArea *_slotBoardScrollArea = nullptr; SlotBoardWidget *slotBoard = nullptr; OverviewWidget *overviewWidget = nullptr; + QTimer *notifierTimer = nullptr; + void layoutChildWidgets(); void updateTimerDependants(); - void paintEvent(QPaintEvent *paintEvent) override; void updateOverviewWidget() const; diff --git a/ui/slotboardwidget.cpp b/ui/slotboardwidget.cpp index 35515c4..6932d9e 100644 --- a/ui/slotboardwidget.cpp +++ b/ui/slotboardwidget.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "slotboardwidget.h" #include "utils.h" @@ -51,7 +52,6 @@ void SlotBoardWidget::layoutChildWidgets(QHBoxLayout * mainLayout) { slotRuler = new SlotRuler(makeLabelsForStrategy(), slotsWidget->slotHeight()); - _slotsLayout = new QVBoxLayout(); _slotsLayout->setSpacing(0); _slotsLayout->addWidget(slotsWidget); @@ -99,12 +99,14 @@ QVector SlotBoardWidget::makeLabelsForStrategy() { QVector labels; for (auto &timeSlot : strategy.time_slots()) { - auto label = QStringForMinutes(timeSlot.begin_time); - labels.append(TimeLabel{label, timeSlot.begin_time}); + auto labelText = QStringForMinutes(timeSlot.begin_time); + auto label = TimeLabel{labelText, timeSlot.begin_time}; + + labels.append(label); } - auto endTimeLabel = QStringForMinutes(strategy.end_time()); - labels.append(TimeLabel{endTimeLabel, strategy.end_time()}); + auto endTimeText = QStringForMinutes(strategy.end_time()); + labels.append(TimeLabel{endTimeText, strategy.end_time()}); return labels; } diff --git a/ui/slotboardwidget.h b/ui/slotboardwidget.h index fe88738..802b87c 100644 --- a/ui/slotboardwidget.h +++ b/ui/slotboardwidget.h @@ -41,7 +41,6 @@ Q_OBJECT SlotRuler *slotRuler = nullptr; QVBoxLayout *_slotsLayout = nullptr; - QTimer *currentTimeTimer = nullptr; CurrentTimeMarkerWidget *currentTimeMarkerWidget = nullptr; diff --git a/ui/slotruler.cpp b/ui/slotruler.cpp index 6d02d1d..cc1262c 100644 --- a/ui/slotruler.cpp +++ b/ui/slotruler.cpp @@ -6,10 +6,10 @@ #include "applicationsettings.h" #include "utils.h" -SlotRuler::SlotRuler(QVector labels, +SlotRuler::SlotRuler(const QVector &labels, int cellHeight, QWidget *parent) - : _labels(std::move(labels)), + : _labels(labels), _cellHeight(cellHeight), QWidget(parent) { auto *layout = new QVBoxLayout(); @@ -17,21 +17,23 @@ SlotRuler::SlotRuler(QVector labels, layout->setSpacing(0); setLayout(layout); - setFixedWidth(calculateLabelWidth() + ApplicationSettings::defaultPadding); + auto fontWidth = calculateLabelWidth() + ApplicationSettings::defaultPadding; + auto width = std::max(fontWidth, 40); + setFixedWidth(width); updateList(); } int SlotRuler::calculateLabelWidth() const { auto font = QFont(); - font.setPixelSize(10); + font.setPixelSize(bigFontHeight); font.setBold(true); return QFontMetrics(font) .horizontalAdvance(QStringForMinutes(0)); } -QVector SlotRuler::labels() const { +const QVector &SlotRuler::labels() const { return _labels; } @@ -67,7 +69,7 @@ QVBoxLayout *SlotRuler::listLayout() { void SlotRuler::reuseItemAtIndex(int index, ColoredLabel *itemWidget) { itemWidget->setText(labels()[index].label); itemWidget->setFixedHeight(cellHeight()); - itemWidget->setFontHeight(isIntegerHourAtIndex(index) ? 10 : 9); + itemWidget->setFontHeight(isIntegerHourAtIndex(index) ? bigFontHeight : smallFontHeight); itemWidget->setDynamicColor(labelColorGetterAtIndex(index)); } @@ -83,15 +85,17 @@ ColoredLabel *SlotRuler::makeNewItemAtIndex(int index) { label->setAlignment(Qt::AlignCenter); label->setFixedHeight(cellHeight()); label->setBold(true); - label->setFontHeight(isIntegerHourAtIndex(index) ? 10 : 9); + label->setFontHeight(isIntegerHourAtIndex(index) ? bigFontHeight : smallFontHeight); label->setDynamicColor(labelColorGetterAtIndex(index)); label->setText(labels()[index].label); + return label; } bool SlotRuler::isIntegerHourAtIndex(int index) const { auto &timeLabel = labels()[index]; auto isIntegerHour = timeLabel.time % 60 == 0; + return isIntegerHour; } diff --git a/ui/slotruler.h b/ui/slotruler.h index 6683bb4..e7bf299 100644 --- a/ui/slotruler.h +++ b/ui/slotruler.h @@ -15,16 +15,15 @@ class SlotRuler : public QWidget, public ColorProvider { Q_OBJECT public: - explicit SlotRuler(QVector labels, + explicit SlotRuler(const QVector &labels, int cellHeight, QWidget *parent = nullptr); - QVector labels() const; + const QVector &labels() const; void setLabels(const QVector &labels); int cellHeight() const; void setCellHeight(int cellHeight); - private: using ColorGetter = QColor (*)(); QVector _labels; @@ -42,6 +41,14 @@ Q_OBJECT ColoredLabel *makeNewItemAtIndex(int index) override; void paintEvent(QPaintEvent *) override; + +//#ifdef Q_OS_WIN +// static const int bigFontHeight = 12; +// static const int smallFontHeight = 10; +//#else + static const int bigFontHeight = 10; + static const int smallFontHeight = 9; +//#endif }; #endif // SLOTRULER_H diff --git a/ui/slotsmousehandler.h b/ui/slotsmousehandler.h index a0db9ad..4da7cd5 100644 --- a/ui/slotsmousehandler.h +++ b/ui/slotsmousehandler.h @@ -32,9 +32,9 @@ Q_OBJECT stg::mouse_handler handler{strategy(), selection(), - [this] { return slotHeight(); }, - [this] { return slotsWidget->geometry(); }, - [this] { return viewportRect(); }}; + std::bind(&SlotsMouseHandler::slotHeight, this), + std::bind(&QWidget::geometry, slotsWidget), + std::bind(&SlotsMouseHandler::viewportRect, this)}; stg::strategy &strategy(); stg::selection &selection(); diff --git a/ui/slotswidget.cpp b/ui/slotswidget.cpp index 4cbbab2..913b548 100644 --- a/ui/slotswidget.cpp +++ b/ui/slotswidget.cpp @@ -1,18 +1,16 @@ #include #include #include -#include #include #include #include -#include #include "slotswidget.h" -#include "third-party/stacklayout.h" #include "mainwindow.h" +#include "mainscene.h" #include "utils.h" #include "slotsmousehandler.h" - +#include "third-party/stacklayout.h" SlotsWidget::SlotsWidget(stg::strategy &strategy, QWidget *parent) : strategy(strategy), @@ -122,9 +120,6 @@ void SlotsWidget::selectAllSlots() { } void SlotsWidget::reloadStrategy() { - strategy.sessions() - .add_on_change_callback(this, &SlotsWidget::updateUI); - updateList(); } diff --git a/utility/applicationsettings.h b/utility/applicationsettings.h index acb2c6f..31a4269 100644 --- a/utility/applicationsettings.h +++ b/utility/applicationsettings.h @@ -17,7 +17,7 @@ namespace ApplicationSettings { const auto sessionFontSize = 14; const auto currentTimeTimerSecondsInterval = 1; - const auto notifierTimerTimeInterval = 15 * 1000; + const auto notifierTimerMillisecondsInterval = 15 * 1000; const auto currentSessionShowDelay = 500; const auto rowBorderColor = "#ccc"; diff --git a/utility/filesystemiomanager.cpp b/utility/filesystemiomanager.cpp index de43900..af10464 100644 --- a/utility/filesystemiomanager.cpp +++ b/utility/filesystemiomanager.cpp @@ -8,7 +8,8 @@ #include #include "filesystemiomanager.h" -#include "ui/mainwindow.h" +#include "mainwindow.h" +#include "applicationmenu.h" #include "alert.h" FileSystemIOManager::FileSystemIOManager(QWidget *parent) : window(parent) {} @@ -41,10 +42,13 @@ void FileSystemIOManager::saveAs(const stg::strategy &strategy) { QSettings().setValue(Settings::lastOpenedDirectoryKey, saveAsFilepath); if (saveAsFilepath.isEmpty()) { + _isSaved = false; return; } filepath = saveAsFilepath; + _isSaved = true; + QSettings().setValue(Settings::lastOpenedDirectoryKey, fileInfo().absolutePath()); write(strategy); @@ -121,7 +125,7 @@ void FileSystemIOManager::setIsSaved(bool isSaved) { _isSaved = isSaved; } -std::unique_ptr +stg::strategy FileSystemIOManager::openDefault() { if (defaultStrategyIsSet()) { auto defaultStrategyString = QSettings() @@ -134,15 +138,15 @@ FileSystemIOManager::openDefault() { resetFilepath(); if (!defaultStrategy) { - return std::make_unique(); + return stg::strategy(); } - return defaultStrategy; + return *defaultStrategy; } resetFilepath(); - return std::make_unique(); + return stg::strategy(); } bool FileSystemIOManager::defaultStrategyIsSet() { @@ -272,15 +276,24 @@ void FileSystemIOManager::setWindow(QWidget *newWindow) { window = newWindow; } -std::unique_ptr FileSystemIOManager::openLastOrDefault() { +stg::strategy FileSystemIOManager::openLastOrDefault() { auto lastOpenedPath = FileSystemIOManager::lastOpenedFilePath(); if (lastOpenedPath) { auto lastOpened = read(*lastOpenedPath); - if (lastOpened) { - return lastOpened; - } + if (lastOpened) + return *lastOpened; + } return openDefault(); } +stg::strategy FileSystemIOManager::openFromPathOrDefault(const QString &path) { + auto strategy = read(path); + + if (!strategy) + return openDefault(); + + return *strategy; +} + diff --git a/utility/filesystemiomanager.h b/utility/filesystemiomanager.h index 3d74d6d..4b48871 100644 --- a/utility/filesystemiomanager.h +++ b/utility/filesystemiomanager.h @@ -31,8 +31,9 @@ class FileSystemIOManager { std::unique_ptr read(const QString &readFilepath); static std::optional lastOpenedFilePath(); - std::unique_ptr openDefault(); - std::unique_ptr openLastOrDefault(); + stg::strategy openDefault(); + stg::strategy openLastOrDefault(); + stg::strategy openFromPathOrDefault(const QString &path); void resetFilepath(); bool askIfWantToDiscardOrLeaveCurrent(const stg::strategy &strategy); @@ -56,7 +57,7 @@ class FileSystemIOManager { QWidget *window; - static constexpr auto searchPattern = "stg::strategy files (*.stg)"; + static constexpr auto searchPattern = "Strategy files (*.stg)"; static QString destinationDir(); void write(const stg::strategy &strategy); diff --git a/utility/macoscalendarexporter.h b/utility/macoscalendarexporter.h index 70cdae5..6604c2b 100644 --- a/utility/macoscalendarexporter.h +++ b/utility/macoscalendarexporter.h @@ -42,11 +42,10 @@ class MacOSCalendarExporter { static MacOSCalendarExporter::OptionsWindowResult showOptionsAlert(Options initialOptions = defaultOptions, const std::string &initialCalendarName = ""); - static void - exportStrategy(const stg::strategy &strategy, - Options options, - time_t dateSecsFromEpoch, - const std::string &calendarTitle = nullptr); + static void exportStrategy(const stg::strategy &strategy, + Options options, + time_t dateSecsFromEpoch, + const std::string &calendarTitle = nullptr); private: static bool optionEnabled(Options optionsMask, Options setting); diff --git a/utility/macoscalendarexporter.mm b/utility/macoscalendarexporter.mm index 770782b..ae5a269 100644 --- a/utility/macoscalendarexporter.mm +++ b/utility/macoscalendarexporter.mm @@ -20,8 +20,7 @@ Options options, time_t dateSecsFromEpoch, const std::string &calTitle) { - - // Obj-C block seem to can't capture wrapping C++ function's object parameters, + // Obj-C block can't capture wrapping C++ function's object parameters, // so we copy it here std::string calendarTitle = calTitle; @autoreleasepool { diff --git a/utility/notifierbackend.cpp b/utility/notifierbackend.cpp index 10026e0..76b71f7 100644 --- a/utility/notifierbackend.cpp +++ b/utility/notifierbackend.cpp @@ -2,16 +2,89 @@ // Created by Dmitry Khrykin on 2019-07-13. // +#include + #include "notifierbackend.h" +#include "utility.h" +#include "applicationsettings.h" -#ifndef Q_OS_MAC -NotifierBackend::NotifierBackend(QSystemTrayIcon *trayIcon, QObject *parent) - : QObject(parent), trayIcon(trayIcon) { +#include +#include + +#ifdef Q_OS_WIN + +#include "third-party/wintoast/wintoastlib.h" + +class NotifierBackend::WintoastHandler : public WinToastLib::IWinToastHandler { +public: + void toastActivated() const override {} + + void toastActivated(int) const override {} + + void toastDismissed(WinToastDismissalReason state) const override {} + + void toastFailed() const override {} +}; + +#endif + +NotifierBackend::NotifierBackend() { +#ifdef Q_OS_WIN + using namespace WinToastLib; + + if (!WinToast::isCompatible()) { + std::cout << "Error, wintoast is not supported" << std::endl; + return; + } + + const auto appName = QCoreApplication::applicationName().toStdWString(); + const auto aumi = WinToast::configureAUMI(L"khrykin", appName, L"desktop", + stg::text::wstring_from_utf8_string(ApplicationSettings::shortVersion)); + WinToast::instance()->setAppName(appName); + WinToast::instance()->setAppUserModelId(aumi); + + if (!WinToast::instance()->initialize()) { + std::cout << "Error, could not initialize wintoast lib" << std::endl; + return; + } + + handler = new NotifierBackend::WintoastHandler(); + + winToastInitialised = true; +#endif } +#ifndef Q_OS_MAC void NotifierBackend::sendMessage(const QString &title, const QString &message) { - if (QSystemTrayIcon::supportsMessages()) { - trayIcon->showMessage(title, message, QIcon(), 10000); - } +#ifdef Q_OS_WIN + if (!winToastInitialised) { + fallbackSendMessage(title, message); + return; + } + + using namespace WinToastLib; + WinToastTemplate templ = WinToastTemplate(WinToastTemplate::Text02); + templ.setTextField(title.toStdWString(), WinToastTemplate::FirstLine); + templ.setTextField(message.toStdWString(), WinToastTemplate::SecondLine); + templ.setExpiration(60 * 1000); + + WinToast::WinToastError error; + const bool launched = WinToast::instance()->showToast(templ, handler, &error); + if (!launched) { + std::cout << "Error: Could not launch toast notification. Error: " + << error << std::endl; + } +#else + fallbackSendMessage(title, message); +#endif +} +#endif + +void NotifierBackend::fallbackSendMessage(const QString &title, const QString &message) { + if (QSystemTrayIcon::supportsMessages()) { + auto icon = QSystemTrayIcon(); + icon.showMessage(title, message, QIcon(), 10000); + } else { + std::cout << "Error: Can't send notification\n"; + } } -#endif \ No newline at end of file diff --git a/utility/notifierbackend.h b/utility/notifierbackend.h index a6bc1f4..42d7e8b 100644 --- a/utility/notifierbackend.h +++ b/utility/notifierbackend.h @@ -1,19 +1,21 @@ #ifndef NOTIFIERBACKEND_H #define NOTIFIERBACKEND_H -#include #include -#include -class NotifierBackend : QObject { -Q_OBJECT +class NotifierBackend { public: - explicit NotifierBackend(QSystemTrayIcon *trayIcon, - QObject *parent = nullptr); + NotifierBackend(); + void sendMessage(const QString& title, const QString& message); - void sendMessage(const QString &title, const QString &message); private: - QSystemTrayIcon *trayIcon; + static void fallbackSendMessage(const QString &title, const QString &message); + +#ifdef Q_OS_WIN + class WintoastHandler; + WintoastHandler *handler = nullptr; + bool winToastInitialised = false; +#endif }; diff --git a/utility/notifierbackend.mm b/utility/notifierbackend.mm index 7f6f0a2..5f1614e 100644 --- a/utility/notifierbackend.mm +++ b/utility/notifierbackend.mm @@ -5,10 +5,6 @@ #include "notifierbackend.h" -NotifierBackend::NotifierBackend(QSystemTrayIcon *trayIcon, QObject *parent) - : QObject(parent), trayIcon(nullptr) { -} - void NotifierBackend::sendMessage(const QString &title, const QString &message) { @autoreleasepool { if (@available(macOS 10.14, *)) { diff --git a/utility/notifierimplementation.cpp b/utility/notifierimplementation.cpp deleted file mode 100644 index affd7b3..0000000 --- a/utility/notifierimplementation.cpp +++ /dev/null @@ -1,131 +0,0 @@ -#include "notifierimplementation.h" -#include "utils.h" - -#include -#include -#include "applicationsettings.h" - - -NotifierImplementation::NotifierImplementation(stg::strategy *strategy, QObject *parent) - : strategy(strategy), QObject(parent) { - setupTrayIcon(); - - backend = new NotifierBackend(trayIcon, this); - - timer = new QTimer(this); - connect(timer, - &QTimer::timeout, - this, - &NotifierImplementation::timerTick); - - timer->start(ApplicationSettings::notifierTimerTimeInterval); - - timerTick(); -} - -void NotifierImplementation::setupTrayIcon() { - contextMenu = new QMenu(); - - trayIcon = new QSystemTrayIcon(this); - trayIcon->setContextMenu(contextMenu); - trayIcon->show(); -} - -NotifierImplementation::~NotifierImplementation() { - delete contextMenu; - delete trayIcon; - timer->stop(); -} - -void NotifierImplementation::timerTick() { - const auto newUpcomingSession = strategy->upcoming_session(); - auto strategyEndCountdown = - strategy->end_time() * 60 - currentSeconds(); - - if (!newUpcomingSession) { - if (strategyEndCountdown <= getReadyInterval) { - if (!nextIsTheEndOfStrategy) { - resetSents(); - - nextIsTheEndOfStrategy = true; - } - } else { - nextIsTheEndOfStrategy = false; - return; - } - } - - if (newUpcomingSession && *newUpcomingSession != upcomingSession) { - upcomingSession = *newUpcomingSession; - resetSents(); - } - - auto countdown = getCountdown(); - auto[prepareMessage, startMessage] = makeMessages(); - - if (countdown < startSentInterval && !startSent) { - sendStartMessage(startMessage); - return; - } - - if (countdown < getReadyInterval && !getReadySent) { - sendPrepareMessage(prepareMessage); - return; - } -} - -stg::session::time_t NotifierImplementation::getCountdown() const { - return nextIsTheEndOfStrategy - ? strategy->end_time() * 60 - currentSeconds() - : upcomingSession.begin_time() * 60 - currentSeconds(); -} - -void NotifierImplementation::sendStartMessage(const Message &message) { - backend->sendMessage(message.title, message.text); - startSent = true; - getReadySent = true; -} - - -void NotifierImplementation::sendPrepareMessage(const Message &message) { - backend->sendMessage(message.title, message.text); - getReadySent = true; -} - -void NotifierImplementation::resetSents() { - getReadySent = false; - startSent = false; -} - -void NotifierImplementation::setStrategy(stg::strategy *newStrategy) { - strategy = newStrategy; -} - -QString NotifierImplementation::titleForSession(const stg::session &activitySession) { - return QString::fromStdString(activitySession.activity->name()) - + " (" - + humanTimeForMinutes(activitySession.duration()) - + ")"; -} - -std::tuple NotifierImplementation::makeMessages() { - Message prepareMessage; - Message startMessage; - - if (nextIsTheEndOfStrategy) { - prepareMessage.title = "End Of A Strategy"; - startMessage.text = "Strategy ends right now"; - } else { - prepareMessage.title = titleForSession(upcomingSession); - startMessage.text = "Starts right now"; - } - - prepareMessage.text = - QString("Coming up in %1 minutes") - .arg(Settings::getReadyMinutes); - - startMessage.title = prepareMessage.title; - - return std::make_tuple(prepareMessage, startMessage); -} - diff --git a/utility/notifierimplementation.h b/utility/notifierimplementation.h deleted file mode 100644 index 42bbd82..0000000 --- a/utility/notifierimplementation.h +++ /dev/null @@ -1,64 +0,0 @@ -#ifndef NOTIFIER_H -#define NOTIFIER_H - -#include -#include -#include -#include -#include - -#include "strategy.h" -#include "notifierbackend.h" - -class NotifierImplementation : public QObject { -Q_OBJECT -public: - explicit NotifierImplementation(stg::strategy *strategy, - QObject *parent = nullptr); - ~NotifierImplementation() override; - void setStrategy(stg::strategy *newStrategy); - void timerTick(); - -signals: - -public slots: - -private: - struct Message { - QString title; - QString text; - }; - - struct Settings { - static const auto getReadyMinutes = 5; - static const auto startSeconds = 20; - }; - - const static auto getReadyInterval = Settings::getReadyMinutes * 60; - const static auto startSentInterval = Settings::startSeconds; - - stg::strategy *strategy; - stg::session upcomingSession; - - QTimer *timer = nullptr; - QSystemTrayIcon *trayIcon = nullptr; - QMenu *contextMenu = nullptr; - NotifierBackend *backend = nullptr; - - bool getReadySent = false; - bool startSent = false; - bool nextIsTheEndOfStrategy = false; - - QString titleForSession(const stg::session &activitySession); - void setupTrayIcon(); - - void sendPrepareMessage(const Message &message); - void sendStartMessage(const Message &message); - - std::tuple makeMessages(); - - void resetSents(); - stg::session::time_t getCountdown() const; -}; - -#endif // NOTIFIER_H