From 2fe47372d64a336aab18dd1db866ee5be1040a85 Mon Sep 17 00:00:00 2001 From: Anton Laurenau Date: Sun, 1 Mar 2020 13:04:44 +0300 Subject: [PATCH] Fixed toast notifications and image resource paths. --- Tomighty.Core/Duration.cs | 12 +- Tomighty.Core/PomodoroEngine.cs | 2 + Tomighty.Windows/App.config | 6 +- .../DesktopNotificationManagerCompat.cs | 398 ++++++++++++++++++ .../Notifications/MyNotificationActivator.cs | 23 + .../Notifications/NotificationsPresenter.cs | 23 +- Tomighty.Windows/Notifications/Toasts.cs | 6 +- Tomighty.Windows/Program.cs | 21 +- .../Properties/Resources.Designer.cs | 13 +- .../Properties/Settings.Designer.cs | 22 +- Tomighty.Windows/Timer/TimerWindow.cs | 4 + Tomighty.Windows/Tomighty.Windows.csproj | 19 +- Tomighty.Windows/packages.config | 3 +- dist.bat | 1 + setup.nsi | 30 ++ shortcut-properties.nsh | 130 ++++++ 16 files changed, 656 insertions(+), 57 deletions(-) create mode 100644 Tomighty.Windows/Notifications/DesktopNotificationManagerCompat.cs create mode 100644 Tomighty.Windows/Notifications/MyNotificationActivator.cs create mode 100644 shortcut-properties.nsh diff --git a/Tomighty.Core/Duration.cs b/Tomighty.Core/Duration.cs index 1bf6a58..5e47d1e 100644 --- a/Tomighty.Core/Duration.cs +++ b/Tomighty.Core/Duration.cs @@ -39,6 +39,16 @@ public string ToTimeString() } public static bool operator ==(Duration a, Duration b) => a.Seconds == b.Seconds; - public static bool operator !=(Duration a, Duration b) => a.Seconds != b.Seconds; + public static bool operator !=(Duration a, Duration b) => a.Seconds != b.Seconds; + + public override bool Equals(object obj) + { + return obj is Duration other && Seconds == other.Seconds; + } + + public override int GetHashCode() + { + return Seconds; + } } } \ No newline at end of file diff --git a/Tomighty.Core/PomodoroEngine.cs b/Tomighty.Core/PomodoroEngine.cs index 9f8574d..2c3c7b6 100644 --- a/Tomighty.Core/PomodoroEngine.cs +++ b/Tomighty.Core/PomodoroEngine.cs @@ -15,6 +15,7 @@ public class PomodoroEngine : IPomodoroEngine private readonly IUserPreferences userPreferences; private readonly IEventHub eventHub; private int _pomodoroCount; + public static PomodoroEngine pomodoroEngine; public PomodoroEngine(ITimer timer, IUserPreferences userPreferences, IEventHub eventHub) { @@ -23,6 +24,7 @@ public PomodoroEngine(ITimer timer, IUserPreferences userPreferences, IEventHub this.eventHub = eventHub; eventHub.Subscribe(OnTimerStopped); + pomodoroEngine = this; } public int PomodoroCount diff --git a/Tomighty.Windows/App.config b/Tomighty.Windows/App.config index 88fa402..ecdcf8a 100644 --- a/Tomighty.Windows/App.config +++ b/Tomighty.Windows/App.config @@ -1,6 +1,6 @@ - + - + - \ No newline at end of file + diff --git a/Tomighty.Windows/Notifications/DesktopNotificationManagerCompat.cs b/Tomighty.Windows/Notifications/DesktopNotificationManagerCompat.cs new file mode 100644 index 0000000..7357057 --- /dev/null +++ b/Tomighty.Windows/Notifications/DesktopNotificationManagerCompat.cs @@ -0,0 +1,398 @@ +// ****************************************************************** +// Copyright (c) Microsoft. All rights reserved. +// This code is licensed under the MIT License (MIT). +// THE CODE 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 CODE OR THE USE OR OTHER DEALINGS IN THE CODE. +// ****************************************************************** + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using Windows.UI.Notifications; + +namespace DesktopNotifications +{ + public class DesktopNotificationManagerCompat + { + public const string TOAST_ACTIVATED_LAUNCH_ARG = "-ToastActivated"; + + private static bool _registeredAumidAndComServer; + private static string _aumid; + private static bool _registeredActivator; + + /// + /// If not running under the Desktop Bridge, you must call this method to register your AUMID with the Compat library and to + /// register your COM CLSID and EXE in LocalServer32 registry. Feel free to call this regardless, and we will no-op if running + /// under Desktop Bridge. Call this upon application startup, before calling any other APIs. + /// + /// An AUMID that uniquely identifies your application. + public static void RegisterAumidAndComServer(string aumid) + where T : NotificationActivator + { + if (string.IsNullOrWhiteSpace(aumid)) + { + throw new ArgumentException("You must provide an AUMID.", nameof(aumid)); + } + + // If running as Desktop Bridge + if (DesktopBridgeHelpers.IsRunningAsUwp()) + { + // Clear the AUMID since Desktop Bridge doesn't use it, and then we're done. + // Desktop Bridge apps are registered with platform through their manifest. + // Their LocalServer32 key is also registered through their manifest. + _aumid = null; + _registeredAumidAndComServer = true; + return; + } + + _aumid = aumid; + + String exePath = Process.GetCurrentProcess().MainModule.FileName; + RegisterComServer(exePath); + + _registeredAumidAndComServer = true; + } + + private static void RegisterComServer(String exePath) + where T : NotificationActivator + { + // We register the EXE to start up when the notification is activated + string regString = String.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}\\LocalServer32", typeof(T).GUID); + var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(regString); + + // Include a flag so we know this was a toast activation and should wait for COM to process + // We also wrap EXE path in quotes for extra security + key.SetValue(null, '"' + exePath + '"' + " " + TOAST_ACTIVATED_LAUNCH_ARG); + } + + /// + /// Registers the activator type as a COM server client so that Windows can launch your activator. + /// + /// Your implementation of NotificationActivator. Must have GUID and ComVisible attributes on class. + public static void RegisterActivator() + where T : NotificationActivator + { + // Register type + var regService = new RegistrationServices(); + + regService.RegisterTypeForComClients( + typeof(T), + RegistrationClassContext.LocalServer, + RegistrationConnectionType.MultipleUse); + + _registeredActivator = true; + } + + /// + /// Creates a toast notifier. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// + /// + public static ToastNotifier CreateToastNotifier() + { + EnsureRegistered(); + + if (_aumid != null) + { + // Non-Desktop Bridge + return ToastNotificationManager.CreateToastNotifier(_aumid); + } + else + { + // Desktop Bridge + return ToastNotificationManager.CreateToastNotifier(); + } + } + + /// + /// Gets the object. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// + public static DesktopNotificationHistoryCompat History + { + get + { + EnsureRegistered(); + + return new DesktopNotificationHistoryCompat(_aumid); + } + } + + private static void EnsureRegistered() + { + // If not registered AUMID yet + if (!_registeredAumidAndComServer) + { + // Check if Desktop Bridge + if (DesktopBridgeHelpers.IsRunningAsUwp()) + { + // Implicitly registered, all good! + _registeredAumidAndComServer = true; + } + + else + { + // Otherwise, incorrect usage + throw new Exception("You must call RegisterAumidAndComServer first."); + } + } + + // If not registered activator yet + if (!_registeredActivator) + { + // Incorrect usage + throw new Exception("You must call RegisterActivator first."); + } + } + + /// + /// Gets a boolean representing whether http images can be used within toasts. This is true if running under Desktop Bridge. + /// + public static bool CanUseHttpImages { get { return DesktopBridgeHelpers.IsRunningAsUwp(); } } + + /// + /// Code from https://github.com/qmatteoq/DesktopBridgeHelpers/edit/master/DesktopBridge.Helpers/Helpers.cs + /// + private class DesktopBridgeHelpers + { + const long APPMODEL_ERROR_NO_PACKAGE = 15700L; + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder packageFullName); + + private static bool? _isRunningAsUwp; + public static bool IsRunningAsUwp() + { + if (_isRunningAsUwp == null) + { + if (IsWindows7OrLower) + { + _isRunningAsUwp = false; + } + else + { + int length = 0; + StringBuilder sb = new StringBuilder(0); + int result = GetCurrentPackageFullName(ref length, sb); + + sb = new StringBuilder(length); + result = GetCurrentPackageFullName(ref length, sb); + + _isRunningAsUwp = result != APPMODEL_ERROR_NO_PACKAGE; + } + } + + return _isRunningAsUwp.Value; + } + + private static bool IsWindows7OrLower + { + get + { + int versionMajor = Environment.OSVersion.Version.Major; + int versionMinor = Environment.OSVersion.Version.Minor; + double version = versionMajor + (double)versionMinor / 10; + return version <= 6.1; + } + } + } + } + + /// + /// Manages the toast notifications for an app including the ability the clear all toast history and removing individual toasts. + /// + public sealed class DesktopNotificationHistoryCompat + { + private string _aumid; + private ToastNotificationHistory _history; + + /// + /// Do not call this. Instead, call to obtain an instance. + /// + /// + internal DesktopNotificationHistoryCompat(string aumid) + { + _aumid = aumid; + _history = ToastNotificationManager.History; + } + + /// + /// Removes all notifications sent by this app from action center. + /// + public void Clear() + { + if (_aumid != null) + { + _history.Clear(_aumid); + } + else + { + _history.Clear(); + } + } + + /// + /// Gets all notifications sent by this app that are currently still in Action Center. + /// + /// A collection of toasts. + public IReadOnlyList GetHistory() + { + return _aumid != null ? _history.GetHistory(_aumid) : _history.GetHistory(); + } + + /// + /// Removes an individual toast, with the specified tag label, from action center. + /// + /// The tag label of the toast notification to be removed. + public void Remove(string tag) + { + if (_aumid != null) + { + _history.Remove(tag, string.Empty, _aumid); + } + else + { + _history.Remove(tag); + } + } + + /// + /// Removes a toast notification from the action using the notification's tag and group labels. + /// + /// The tag label of the toast notification to be removed. + /// The group label of the toast notification to be removed. + public void Remove(string tag, string group) + { + if (_aumid != null) + { + _history.Remove(tag, group, _aumid); + } + else + { + _history.Remove(tag, group); + } + } + + /// + /// Removes a group of toast notifications, identified by the specified group label, from action center. + /// + /// The group label of the toast notifications to be removed. + public void RemoveGroup(string group) + { + if (_aumid != null) + { + _history.RemoveGroup(group, _aumid); + } + else + { + _history.RemoveGroup(group); + } + } + } + + /// + /// Apps must implement this activator to handle notification activation. + /// + public abstract class NotificationActivator : NotificationActivator.INotificationActivationCallback + { + public void Activate(string appUserModelId, string invokedArgs, NOTIFICATION_USER_INPUT_DATA[] data, uint dataCount) + { + OnActivated(invokedArgs, new NotificationUserInput(data), appUserModelId); + } + + /// + /// This method will be called when the user clicks on a foreground or background activation on a toast. Parent app must implement this method. + /// + /// The arguments from the original notification. This is either the launch argument if the user clicked the body of your toast, or the arguments from a button on your toast. + /// Text and selection values that the user entered in your toast. + /// Your AUMID. + public abstract void OnActivated(string arguments, NotificationUserInput userInput, string appUserModelId); + + // These are the new APIs for Windows 10 + #region NewAPIs + [StructLayout(LayoutKind.Sequential), Serializable] + public struct NOTIFICATION_USER_INPUT_DATA + { + [MarshalAs(UnmanagedType.LPWStr)] + public string Key; + + [MarshalAs(UnmanagedType.LPWStr)] + public string Value; + } + + [ComImport, + Guid("53E31837-6600-4A81-9395-75CFFE746F94"), ComVisible(true), + InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] + public interface INotificationActivationCallback + { + void Activate( + [In, MarshalAs(UnmanagedType.LPWStr)] + string appUserModelId, + [In, MarshalAs(UnmanagedType.LPWStr)] + string invokedArgs, + [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)] + NOTIFICATION_USER_INPUT_DATA[] data, + [In, MarshalAs(UnmanagedType.U4)] + uint dataCount); + } + #endregion + } + + /// + /// Text and selection values that the user entered on your notification. The Key is the ID of the input, and the Value is what the user entered. + /// + public class NotificationUserInput : IReadOnlyDictionary + { + private NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] _data; + + internal NotificationUserInput(NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] data) + { + _data = data; + } + + public string this[string key] => _data.First(i => i.Key == key).Value; + + public IEnumerable Keys => _data.Select(i => i.Key); + + public IEnumerable Values => _data.Select(i => i.Value); + + public int Count => _data.Length; + + public bool ContainsKey(string key) + { + return _data.Any(i => i.Key == key); + } + + public IEnumerator> GetEnumerator() + { + return _data.Select(i => new KeyValuePair(i.Key, i.Value)).GetEnumerator(); + } + + public bool TryGetValue(string key, out string value) + { + foreach (var item in _data) + { + if (item.Key == key) + { + value = item.Value; + return true; + } + } + + value = null; + return false; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/Tomighty.Windows/Notifications/MyNotificationActivator.cs b/Tomighty.Windows/Notifications/MyNotificationActivator.cs new file mode 100644 index 0000000..636c063 --- /dev/null +++ b/Tomighty.Windows/Notifications/MyNotificationActivator.cs @@ -0,0 +1,23 @@ +using System.Runtime.InteropServices; +using DesktopNotifications; + +// The GUID CLSID must be unique to your app. Create a new GUID if copying this code. +namespace Tomighty.Windows.Notifications +{ + [ClassInterface(ClassInterfaceType.None)] + [ComSourceInterfaces(typeof(INotificationActivationCallback))] + [Guid("f1c09466-e472-45be-8248-c88309824a46"), ComVisible(true)] + public class MyNotificationActivator : NotificationActivator + { + public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId) + { + // dummy activator + if (Toasts.TimerAction.WithArgs.ContainsKey(invokedArgs)) + { + var timerAction = Toasts.TimerAction.WithArgs[invokedArgs]; + + PomodoroEngine.pomodoroEngine.StartTimer(timerAction.IntervalType); + } + } + } +} diff --git a/Tomighty.Windows/Notifications/NotificationsPresenter.cs b/Tomighty.Windows/Notifications/NotificationsPresenter.cs index 98fa461..c636f5d 100644 --- a/Tomighty.Windows/Notifications/NotificationsPresenter.cs +++ b/Tomighty.Windows/Notifications/NotificationsPresenter.cs @@ -5,8 +5,10 @@ // http://www.apache.org/licenses/LICENSE-2.0.txt // +using DesktopNotifications; using Tomighty.Events; using Tomighty.Windows.Events; +using Tomighty.Windows.Timer; using Windows.UI.Notifications; namespace Tomighty.Windows.Notifications @@ -15,7 +17,7 @@ internal class NotificationsPresenter { private readonly IPomodoroEngine pomodoroEngine; private readonly IUserPreferences userPreferences; - private readonly ToastNotifier toastNotifier = ToastNotificationManager.CreateToastNotifier("Tomighty"); + private readonly ToastNotifier toastNotifier = DesktopNotificationManagerCompat.CreateToastNotifier(); public NotificationsPresenter(IPomodoroEngine pomodoroEngine, IUserPreferences userPreferences, IEventHub eventHub) { @@ -42,23 +44,10 @@ private void OnTimerStopped(TimerStopped @event) if (@event.IsIntervalCompleted && userPreferences.ShowToastNotifications) { var toast = Toasts.IntervalCompleted(@event.IntervalType, pomodoroEngine.SuggestedBreakType); - toast.Activated += OnToastActivated; - toastNotifier.Show(toast); - } - } - - private void OnToastActivated(ToastNotification sender, object args) - { - if (args is ToastActivatedEventArgs) - { - var activation = args as ToastActivatedEventArgs; - - if (Toasts.TimerAction.WithArgs.ContainsKey(activation.Arguments)) - { - var timerAction = Toasts.TimerAction.WithArgs[activation.Arguments]; - pomodoroEngine.StartTimer(timerAction.IntervalType); - } + TimerWindow.DispatcherUi.BeginInvoke(new System.Action + (() => toastNotifier.Show(toast)) + ); } } } diff --git a/Tomighty.Windows/Notifications/Toasts.cs b/Tomighty.Windows/Notifications/Toasts.cs index 1007fab..220e70a 100644 --- a/Tomighty.Windows/Notifications/Toasts.cs +++ b/Tomighty.Windows/Notifications/Toasts.cs @@ -15,9 +15,9 @@ namespace Tomighty.Windows.Notifications { internal static class Toasts { - private static readonly string RedTomatoImage = new Uri(Path.GetFullPath(@"Resources\Toasts\image_toast_tomato_red.png")).AbsoluteUri; - private static readonly string GreenTomatoImage = new Uri(Path.GetFullPath(@"Resources\Toasts\image_toast_tomato_green.png")).AbsoluteUri; - private static readonly string BlueTomatoImage = new Uri(Path.GetFullPath(@"Resources\Toasts\image_toast_tomato_blue.png")).AbsoluteUri; + private static readonly string RedTomatoImage = new Uri(Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"\Resources\Toasts\image_toast_tomato_red.png")).AbsoluteUri; + private static readonly string GreenTomatoImage = new Uri(Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"\Resources\Toasts\image_toast_tomato_green.png")).AbsoluteUri; + private static readonly string BlueTomatoImage = new Uri(Path.GetFullPath(AppDomain.CurrentDomain.BaseDirectory + @"\Resources\Toasts\image_toast_tomato_blue.png")).AbsoluteUri; private static readonly XmlDocument PomodoroCompletedTakeShortBreakTemplate = FillIntervalCompletedTemplate("Pomodoro", RedTomatoImage, TimerAction.StartShortBreak); private static readonly XmlDocument PomodoroCompletedTakeLongBreakTemplate = FillIntervalCompletedTemplate("Pomodoro", RedTomatoImage, TimerAction.StartLongBreak); private static readonly XmlDocument ShortBreakCompletedTemplate = FillIntervalCompletedTemplate("Short break", GreenTomatoImage, TimerAction.StartPomodoro); diff --git a/Tomighty.Windows/Program.cs b/Tomighty.Windows/Program.cs index 355ab6e..56ad797 100644 --- a/Tomighty.Windows/Program.cs +++ b/Tomighty.Windows/Program.cs @@ -1,13 +1,15 @@ -// -// Tomighty - http://www.tomighty.org -// -// This software is licensed under the Apache License Version 2.0: -// http://www.apache.org/licenses/LICENSE-2.0.txt +// +// Tomighty - http://www.tomighty.org +// +// This software is licensed under the Apache License Version 2.0: +// http://www.apache.org/licenses/LICENSE-2.0.txt // using System; -using System.Windows.Forms; - +using System.Windows.Forms; +using DesktopNotifications; +using Tomighty.Windows.Notifications; + namespace Tomighty.Windows { static class Program @@ -15,6 +17,11 @@ static class Program [STAThread] static void Main() { + // Register AUMID and COM server (for Desktop Bridge apps, this no-ops) + DesktopNotificationManagerCompat.RegisterAumidAndComServer("Tomighty"); + // Register COM server and activator type + DesktopNotificationManagerCompat.RegisterActivator(); + Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); AppDomain.CurrentDomain.UnhandledException += (sender, args) => HandleUnhandledException(args.ExceptionObject as Exception); diff --git a/Tomighty.Windows/Properties/Resources.Designer.cs b/Tomighty.Windows/Properties/Resources.Designer.cs index eb8487c..585be4f 100644 --- a/Tomighty.Windows/Properties/Resources.Designer.cs +++ b/Tomighty.Windows/Properties/Resources.Designer.cs @@ -19,7 +19,7 @@ namespace Tomighty.Windows.Properties { // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class Resources { @@ -251,16 +251,17 @@ internal static string String_ShortBreak { ///<toast launch="tomighty"> /// <visual> /// <binding template="ToastGeneric"> - /// <text>First time using Tomighty?</text> - /// <text>There should be a tomato-shaped icon somewhere in your task bar.</text> - /// <text>Start by clicking on it. The left button shows the menu.</text> + /// <text>Tomighty Updated</text> + /// <text>A new version of Tomighty has been automatically installed</text> /// <image src='{image_src}' placement='appLogoOverride'/> /// </binding> /// </visual> /// <actions> - /// <action content="Got It" arguments="dismiss" /> + /// <action content="OK" arguments="dismiss" /> /// </actions> - /// <audio src="ms-winsound [rest of string was truncated]";. + /// <audio src="ms-winsoundevent:Notification.Default"/> + ///</toast> + ///. /// internal static string toast_template_app_updated { get { diff --git a/Tomighty.Windows/Properties/Settings.Designer.cs b/Tomighty.Windows/Properties/Settings.Designer.cs index bc64850..49eed05 100644 --- a/Tomighty.Windows/Properties/Settings.Designer.cs +++ b/Tomighty.Windows/Properties/Settings.Designer.cs @@ -8,21 +8,17 @@ // //------------------------------------------------------------------------------ -namespace Tomighty.Windows.Properties -{ - - +namespace Tomighty.Windows.Properties { + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.4.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { + + public static Settings Default { + get { return defaultInstance; } } diff --git a/Tomighty.Windows/Timer/TimerWindow.cs b/Tomighty.Windows/Timer/TimerWindow.cs index 516dd57..e446400 100644 --- a/Tomighty.Windows/Timer/TimerWindow.cs +++ b/Tomighty.Windows/Timer/TimerWindow.cs @@ -9,6 +9,7 @@ using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms; +using System.Windows.Threading; namespace Tomighty.Windows.Timer { @@ -26,8 +27,11 @@ public partial class TimerWindow : Form private readonly Action UpdateTitleDelegate; private readonly Action UpdateTimerButtonTextDelegate; + public static Dispatcher DispatcherUi; + public TimerWindow() { + DispatcherUi = Dispatcher.CurrentDispatcher; InitializeComponent(); currentColorScheme = CreateColorScheme(DarkGray); diff --git a/Tomighty.Windows/Tomighty.Windows.csproj b/Tomighty.Windows/Tomighty.Windows.csproj index cc9d776..d0a191a 100644 --- a/Tomighty.Windows/Tomighty.Windows.csproj +++ b/Tomighty.Windows/Tomighty.Windows.csproj @@ -9,7 +9,7 @@ Properties Tomighty.Windows Tomighty.Windows - v4.5.2 + v4.7.2 512 true publish\ @@ -30,6 +30,7 @@ 8.0 + AnyCPU @@ -57,23 +58,27 @@ Resources\icon_tomato_red.ico - - ..\packages\Microsoft.Toolkit.Uwp.Notifications.1.0.0\lib\dotnet\Microsoft.Toolkit.Uwp.Notifications.dll - True + + ..\packages\Microsoft.Toolkit.Uwp.Notifications.6.0.0\lib\net461\Microsoft.Toolkit.Uwp.Notifications.dll - + + + + ..\packages\System.ValueTuple.4.4.0\lib\net47\System.ValueTuple.dll + + @@ -98,6 +103,9 @@ + + + @@ -122,7 +130,6 @@ - diff --git a/Tomighty.Windows/packages.config b/Tomighty.Windows/packages.config index 31c58d6..29cb2ac 100644 --- a/Tomighty.Windows/packages.config +++ b/Tomighty.Windows/packages.config @@ -1,5 +1,6 @@  - + + \ No newline at end of file diff --git a/dist.bat b/dist.bat index 9c79de8..235ae2d 100644 --- a/dist.bat +++ b/dist.bat @@ -42,6 +42,7 @@ xcopy /f NOTICE.txt %dest% xcopy /f %src%\Tomighty.Windows.exe %dest% xcopy /f %src%\Tomighty.Core.dll %dest% xcopy /f %src%\Microsoft.Toolkit.Uwp.Notifications.dll %dest% +xcopy /f %src%\System.ValueTuple.dll %dest% xcopy /f /s %src%\Resources %dest%\Resources xcopy /f Tomighty.Update.Swap\bin\Release\Tomighty.Update.Swap.exe %dest% diff --git a/setup.nsi b/setup.nsi index cb256f3..e294755 100644 --- a/setup.nsi +++ b/setup.nsi @@ -1,6 +1,10 @@ +ShowInstDetails show + ;====================================================== ; Includes + !include LogicLib.nsh + !include shortcut-properties.nsh !include MUI.nsh !include Sections.nsh !include FileFunc.nsh @@ -69,22 +73,47 @@ Section "Tomighty" File "${BUILD_DIR}\Tomighty.Update.Swap.exe" File "${BUILD_DIR}\Tomighty.Core.dll" File "${BUILD_DIR}\Microsoft.Toolkit.Uwp.Notifications.dll" + File "${BUILD_DIR}\System.ValueTuple.dll" File "${BUILD_DIR}\NOTICE.txt" File "${BUILD_DIR}\LICENSE.txt" File /r "${BUILD_DIR}\Resources" WriteUninstaller $INSTDIR\uninstall.exe + + ;SetRegView 64 ;If the Toast-Application is 64 Bit + WriteRegStr HKLM "SOFTWARE\Classes\CLSID\{f1c09466-e472-45be-8248-c88309824a46}\LocalServer32" "" "C:\ProgramFiles\Snoretoast.exe" ; Add the needed Registry Key (https://docs.microsoft.com/en-us/windows/win32/com/localserver32) + SectionEnd Section "Start menu shortcuts" SetShellVarContext all CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Tomighty.lnk" "$INSTDIR\Tomighty.Windows.exe" "" "$INSTDIR\Tomighty.Windows.exe" 0 + + !insertmacro ShortcutSetToastProperties "$SMPROGRAMS\${PRODUCT_NAME}\Tomighty.lnk" "{f1c09466-e472-45be-8248-c88309824a46}" "Tomighty" + pop $0 + ${If} $0 <> 0 + MessageBox MB_ICONEXCLAMATION "Shortcut-Attributes to enable Toast Messages couldn't be set" + SetErrors + Abort + ${EndIf} + DetailPrint Returncode=$0 + CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0 SectionEnd Section "Desktop shortcut" SetShellVarContext all CreateShortCut "$DESKTOP\Tomighty.lnk" "$INSTDIR\Tomighty.Windows.exe" "" "$INSTDIR\Tomighty.Windows.exe" 0 + + !insertmacro ShortcutSetToastProperties "$DESKTOP\Tomighty.lnk" "{f1c09466-e472-45be-8248-c88309824a46}" "Tomighty" + pop $0 + ${If} $0 <> 0 + MessageBox MB_ICONEXCLAMATION "Shortcut-Attributes to enable Toast Messages couldn't be set" + SetErrors + Abort + ${EndIf} + DetailPrint Returncode=$0 + SectionEnd ; Installer functions @@ -110,6 +139,7 @@ Section "uninstall" RMDir /r "$SMPROGRAMS\${PRODUCT_NAME}" RMDir /r "$INSTDIR" DeleteRegKey HKLM ${REG_UNINSTALL} + DeleteRegKey HKLM "SOFTWARE\Classes\CLSID\{f1c09466-e472-45be-8248-c88309824a46}\LocalServer32" SectionEnd Function .onInit diff --git a/shortcut-properties.nsh b/shortcut-properties.nsh new file mode 100644 index 0000000..1b6376f --- /dev/null +++ b/shortcut-properties.nsh @@ -0,0 +1,130 @@ +; Copyright Safing ICS Technologies GmbH. Use of this source code is governed by the Apache 2 license that can be found in the LICENSE file. + +!include LogicLib.nsh +!include Win\Propkey.nsh +!include Win\COM.nsh + +!define /ifndef PKEY_AppUserModel_ToastActivatorCLSID '"{9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3}",26' ; The GUID of the property key, not the set CLSID DO NOT CHANGE! +!define /ifndef VT_CLSID 72 +!define /ifndef STGM_READWRITE 2 + +; Errorhandler +; if $0 != 0, break and output Returncode and description of the current point in code; r0 is always used for the returncodes of the System::Call +; TODO: safely clean Memory on Error +!define Shortcut_returnOnError `!insertmacro Shortcut_returnOnError $0` +!macro Shortcut_returnOnError _retVal _Msg +${If} ${_retVal} <> 0 ; (<>) == (!=) + DetailPrint "Error-Point: ${_Msg}" + DetailPrint "Error-Code: ${_retVal}" + Goto Shortcut_onError +${EndIf} +!macroend + +;;; +; Sets specified AppID and CLSID as File-Property in the way needed for Toast Notifications +; Pushes return-value (Windows HRESULT) to stack (0: Success) +; Example: !insertmacro ShortcutSetToastProperties "$SMPROGRAMS\test.lnk" "{32860D72-BA7F-64CC-AF50-72B6FB1ECE26}" "com.example.nsisdemo" ; DON'T USE THIS GUID!!! +; WARNING: Because of the limited lifetime of the installer and the minimal RAM usage, it wasn't taken care of freeing all allocated stuff, especially in the case, something goes wrong +;;; +!macro ShortcutSetToastProperties ShortcutPath CLSID AppID + +DetailPrint "Add Toast-Properties to Shortcut" + +System::Store S ; Seems to save the variables $0-$9 somewhere so we can use this variables without danger + + +; internal Variable-Usage: +; $0: Return-Value of operations (was successful?) +; $1: COM-Object-Pointer +; $2: IPersistFile-Pointer +; $3: IPropertyStore-Pointer +; $4: Propertystore-Key-Struct +; $5: Propertystore-Val-Struct +; $6: temporary Property-Val extra storage (if val is pointer) +; $7: temporary length OR strlen of a string + +; Set $0-$9 to known invalid values as additional prevention of unwanted behaviour +IntOp $0 0 - 1 +IntOp $1 0 + 0 +IntOp $2 0 + 0 +IntOp $3 0 + 0 +IntOp $4 0 + 0 +IntOp $5 0 + 0 +IntOp $6 0 + 0 +IntOp $7 0 - 1 +IntOp $8 0 + 0 +IntOp $9 0 + 0 + +!insertmacro ComHlpr_CreateInProcInstance ${CLSID_ShellLink} ${IID_IShellLink} r1 ".r0" ; creates ComHlpr_CreateInProcInstance into $1 +${Shortcut_returnOnError} "CoCreateInstance" ; check for Errors + +${IUnknown::QueryInterface} $1 '("${IID_IPersistFile}",.r2)i.r0' ; creates IPersistFile into $2 +${Shortcut_returnOnError} "Create IPersistFile QueryInterface" + +${IPersistFile::Load} $2 "('${ShortcutPath}', ${STGM_READWRITE})i.r0" ; Loads Shortcut +${Shortcut_returnOnError} "Load Shortcut" + +${IUnknown::QueryInterface} $1 '("${IID_IPropertyStore}",.r3)i.r0' ; creates IPropertyStore into $3 +${Shortcut_returnOnError} "Create IID_IPropertyStore QueryInterface" + + +;;; +; CLSID +;;; +System::Call '*${SYSSTRUCT_PROPERTYKEY}(${PKEY_AppUserModel_ToastActivatorCLSID})p.r4' ; Sets up Struct for PropertyKey (CLSID) +System::Call '*(&g16 "${CLSID}")p.r6' ; Store CLSID into struct just to have the Value stored somewhere we can point to (I don't know, how to define plain variables, so an Struct which has no overhead is used) +System::Call '*${SYSSTRUCT_PROPVARIANT}(${VT_CLSID},,p r6)p.r5' ; Sets up Struct for PropertyValue (actually, the value is a pointer to the actual value) + +${IPropertyStore::SetValue} $3 '($4,$5)i.r0' ; Actually sets Key-Value-Pair in IPropertyStore-Object ($3) +${Shortcut_returnOnError} "Set CLSID" + +System::Free $6 ; Free temporary value-struct +; The structs $4 and $5 are reused for AppID + + +;;; +; AppID +;;; +; $4 and $5 are reused, $6 is reallocated + +System::Call '*$4${SYSSTRUCT_PROPERTYKEY}(${PKEY_AppUserModel_ID})' ; Loads PropertyKey into already existing struct $4 + +; Calculate the Bytes needed to allocate for copying the AppID as wstring, write number into $7 +StrLen $7 "${AppID}" +IntOp $7 $7 + 1 ; incl. \0 +IntOp $7 $7 * 2 ; wchars are 2 Byte wide in Windows +System::Call "ole32::CoTaskMemAlloc(i $7)p.r6" ; allocate $7 Bytes into $6 for temporarily storing the AppID as WSTRING (a pointer to the String is required, so we need manual allocation) + +${If} $6 = 0 ; Is the pointer to the allocated Memory NULL? (= No space left for allocation, then return a Error) + DetailPrint "No RAM could be allocated for AppID" + Goto Shortcut_onError +${EndIf} + +StrLen $7 ${AppID} ; Now store the stringlength of the AppID into $7, not the Bytecount! +IntOp $7 $7 + 1 ; Add \0 +System::Call '*$6(&w$7 "${AppID}")' ; memcpy (wstring)AppID into $6 the temporary storage of the PropertyValue +System::Call '*$5${SYSSTRUCT_PROPVARIANT}(${VT_LPWSTR},,p r6)' ; Load Property-Val into $5 + +${IPropertyStore::SetValue} $3 '($4,$5)i.r0' ; Actually sets Key-Value-Pair in IPropertyStore-Object ($3) +${Shortcut_returnOnError} "Save AppID" + +${IPropertyStore::Commit} $3 "i.r0" ; Commit changes in IPropertyStore-Object ($3) +${Shortcut_returnOnError} "Commit changes to PropertyStore" + +${IPersistFile::Save} $2 '("${ShortcutPath}",1)r.r0' ; Save changes into Shortcut at ${ShortcutPath} (IPersistFile at $2) +${Shortcut_returnOnError} "Save Shortcut" + +; Freeing&Releasing stuff (TODO: do in case of Error?) +System::Call "ole32::CoTaskMemFree(p r6)" ; Free the temporary Storage; It was allocated using ole32::CoTaskMemAlloc(), therefore it needs to be freeed using ole32::CoTaskMemFree() +System::Free $4 ; Freeing PropertyKey-Struct +System::Free $5 ; Freeing PropertyValue-Struct +${IUnknown::Release} $3 "" ; Releasing IPropertyStore-Object at $3 +${IUnknown::Release} $2 "" ; Releasing IPersistFile at $2 +${IUnknown::Release} $1 "" ; Releasing COM-Object at $1 + +DetailPrint "Successfully added Toast-Properties to Shortcut" + +Shortcut_onError: ; Label to which is jumped on previous Error +push $0 ; pushes return Value to Stack +System::Store L ; Seems to load the previous values of $0-$9 ... +!macroend