Working on improving the toast notifications [skip ci]

This commit is contained in:
Robin 2020-03-11 15:22:17 +01:00
parent 7e96c99b3d
commit a29f6faa27
5 changed files with 496 additions and 38 deletions

View file

@ -1,44 +1,48 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" xmlns:winset="http://schemas.microsoft.com/SMI/2005/WindowsSettings" manifestVersion="1.0"> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<!-- Make sure windows Vista and above treat Greenshot as "DPI Aware" <!-- Make sure windows Vista and above treat Greenshot as "DPI Aware" See: http://msdn.microsoft.com/en-us/library/ms633543.aspx -->
See: http://msdn.microsoft.com/en-us/library/ms633543.aspx --> <asmv3:application>
<asmv3:application> <asmv3:windowsSettings xmlns:ws2005="http://schemas.microsoft.com/SMI/2005/WindowsSettings" xmlns:ws2011="http://schemas.microsoft.com/SMI/2011/WindowsSettings" xmlns:ws2016="http://schemas.microsoft.com/SMI/2016/WindowsSettings" xmlns:ws2017="http://schemas.microsoft.com/SMI/2017/WindowsSettings">
<asmv3:windowsSettings> <ws2005:dpiAware>True/PM</ws2005:dpiAware>
<winset:dpiAware>True/PM</winset:dpiAware> <!-- Important for Greenshot, so it can enumerate immersive windows (UWP etc) from the desktop -->
</asmv3:windowsSettings> <ws2011:disableWindowFiltering>true</ws2011:disableWindowFiltering>
</asmv3:application> <ws2011:printerDriverIsolation>true</ws2011:printerDriverIsolation>
<ws2016:dpiAwareness>PerMonitorV2,PerMonitor</ws2016:dpiAwareness>
<ws2016:longPathAware>true</ws2016:longPathAware>
<ws2017:gdiScaling>true</ws2017:gdiScaling>
</asmv3:windowsSettings>
</asmv3:application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>
<!-- Windows 10 --> <!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
<maxversiontested Id="10.0.18362.0"/>
<!-- Windows 8.1 --> <!-- Windows 8.1 -->
<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/> <supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />
<!--Windows 8 --> <!--Windows 8 -->
<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/> <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />
<!-- Windows 7 --> <!-- Windows 7 -->
<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/> <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />
<!-- Windows Vista -->
<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
</application> </application>
</compatibility> </compatibility>
<!-- Set UAC level to "asInvoker" and disable registry virtualization --> <!-- Set UAC level to "asInvoker" and disable registry virtualization -->
<asmv2:trustInfo> <asmv2:trustInfo>
<asmv2:security> <asmv2:security>
<asmv3:requestedPrivileges> <asmv3:requestedPrivileges>
<!-- <!--
The presence of the "requestedExecutionLevel" node will disable The presence of the "requestedExecutionLevel" node will disable
file and registry virtualization on Vista. See: file and registry virtualization on Vista. See:
http://msdn.microsoft.com/en-us/library/aa965884%28v=vs.85%29.aspx http://msdn.microsoft.com/en-us/library/aa965884%28v=vs.85%29.aspx
Use the "level" attribute to specify the User Account Control level: Use the "level" attribute to specify the User Account Control level:
asInvoker = Never prompt for elevation asInvoker = Never prompt for elevation
requireAdministrator = Always prompt for elevation requireAdministrator = Always prompt for elevation
highestAvailable = Prompt for elevation when started by administrator, highestAvailable = Prompt for elevation when started by administrator,
but do not prompt for administrator password when started by but do not prompt for administrator password when started by
standard user. standard user.
--> -->
<asmv3:requestedExecutionLevel level="asInvoker"/> <asmv3:requestedExecutionLevel level="asInvoker" />
</asmv3:requestedPrivileges> </asmv3:requestedPrivileges>
</asmv2:security> </asmv2:security>
</asmv2:trustInfo> </asmv2:trustInfo>
</assembly> </assembly>

View file

@ -15,6 +15,7 @@ namespace GreenshotPlugin.Core
/// </summary> /// </summary>
public static Version WinVersion { get; } = Environment.OSVersion.Version; public static Version WinVersion { get; } = Environment.OSVersion.Version;
public static double WinVersionTotal = WinVersion.Major + (double)WinVersion.Minor / 10;
/// <summary> /// <summary>
/// Test if the current OS is Windows 10 /// Test if the current OS is Windows 10
/// </summary> /// </summary>
@ -39,6 +40,8 @@ namespace GreenshotPlugin.Core
/// <returns>true if we are running on Windows 7 or later</returns> /// <returns>true if we are running on Windows 7 or later</returns>
public static bool IsWindows7OrLater { get; } = WinVersion.Major == 6 && WinVersion.Minor >= 1 || WinVersion.Major > 6; public static bool IsWindows7OrLater { get; } = WinVersion.Major == 6 && WinVersion.Minor >= 1 || WinVersion.Major > 6;
public static bool IsWindows7OrLower { get; } = WinVersionTotal <= 6.1;
/// <summary> /// <summary>
/// Test if the current OS is Windows 8.0 /// Test if the current OS is Windows 8.0
/// </summary> /// </summary>

View file

@ -0,0 +1,384 @@
// ******************************************************************
// 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;
using GreenshotPlugin.Core;
namespace GreenshotWin10Plugin.Native
{
public class DesktopNotificationManagerCompat
{
public const string TOAST_ACTIVATED_LAUNCH_ARG = "-ToastActivated";
private static bool _registeredAumidAndComServer;
private static string _aumid;
private static bool _registeredActivator;
/// <summary>
/// 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.
/// </summary>
/// <param name="aumid">An AUMID that uniquely identifies your application.</param>
public static void RegisterAumidAndComServer<T>(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<T>(exePath);
_registeredAumidAndComServer = true;
}
private static void RegisterComServer<T>(string exePath)
where T : NotificationActivator
{
// We register the EXE to start up when the notification is activated
string regString = $"SOFTWARE\\Classes\\CLSID\\{{{typeof(T).GUID}}}\\LocalServer32";
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);
}
/// <summary>
/// Registers the activator type as a COM server client so that Windows can launch your activator.
/// </summary>
/// <typeparam name="T">Your implementation of NotificationActivator. Must have GUID and ComVisible attributes on class.</typeparam>
public static void RegisterActivator<T>() where T : NotificationActivator
{
// Register type
var regService = new RegistrationServices();
regService.RegisterTypeForComClients(
typeof(T),
RegistrationClassContext.LocalServer,
RegistrationConnectionType.MultipleUse);
_registeredActivator = true;
}
/// <summary>
/// Creates a toast notifier. You must have called <see cref="RegisterActivator{T}"/> first (and also <see cref="RegisterAumidAndComServer(string)"/> if you're a classic Win32 app), or this will throw an exception.
/// </summary>
/// <returns></returns>
public static ToastNotifier CreateToastNotifier()
{
EnsureRegistered();
if (_aumid != null)
{
// Non-Desktop Bridge
return ToastNotificationManager.CreateToastNotifier(_aumid);
}
// Desktop Bridge
return ToastNotificationManager.CreateToastNotifier();
}
/// <summary>
/// Gets the <see cref="DesktopNotificationHistoryCompat"/> object. You must have called <see cref="RegisterActivator{T}"/> first (and also <see cref="RegisterAumidAndComServer(string)"/> if you're a classic Win32 app), or this will throw an exception.
/// </summary>
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.");
}
}
/// <summary>
/// Gets a boolean representing whether http images can be used within toasts. This is true if running under Desktop Bridge.
/// </summary>
public static bool CanUseHttpImages { get { return DesktopBridgeHelpers.IsRunningAsUwp(); } }
/// <summary>
/// Code from https://github.com/qmatteoq/DesktopBridgeHelpers/edit/master/DesktopBridge.Helpers/Helpers.cs
/// </summary>
private static 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) return _isRunningAsUwp.Value;
if (WindowsVersion.IsWindows7OrLower)
{
_isRunningAsUwp = false;
}
else
{
int length = 0;
StringBuilder sb = new StringBuilder(0);
GetCurrentPackageFullName(ref length, sb);
sb = new StringBuilder(length);
int result = GetCurrentPackageFullName(ref length, sb);
_isRunningAsUwp = result != APPMODEL_ERROR_NO_PACKAGE;
}
return _isRunningAsUwp.Value;
}
}
}
/// <summary>
/// Manages the toast notifications for an app including the ability the clear all toast history and removing individual toasts.
/// </summary>
public sealed class DesktopNotificationHistoryCompat
{
private string _aumid;
private ToastNotificationHistory _history;
/// <summary>
/// Do not call this. Instead, call <see cref="DesktopNotificationManagerCompat.History"/> to obtain an instance.
/// </summary>
/// <param name="aumid"></param>
internal DesktopNotificationHistoryCompat(string aumid)
{
_aumid = aumid;
_history = ToastNotificationManager.History;
}
/// <summary>
/// Removes all notifications sent by this app from action center.
/// </summary>
public void Clear()
{
if (_aumid != null)
{
_history.Clear(_aumid);
}
else
{
_history.Clear();
}
}
/// <summary>
/// Gets all notifications sent by this app that are currently still in Action Center.
/// </summary>
/// <returns>A collection of toasts.</returns>
public IReadOnlyList<ToastNotification> GetHistory()
{
return _aumid != null ? _history.GetHistory(_aumid) : _history.GetHistory();
}
/// <summary>
/// Removes an individual toast, with the specified tag label, from action center.
/// </summary>
/// <param name="tag">The tag label of the toast notification to be removed.</param>
public void Remove(string tag)
{
if (_aumid != null)
{
_history.Remove(tag, string.Empty, _aumid);
}
else
{
_history.Remove(tag);
}
}
/// <summary>
/// Removes a toast notification from the action using the notification's tag and group labels.
/// </summary>
/// <param name="tag">The tag label of the toast notification to be removed.</param>
/// <param name="group">The group label of the toast notification to be removed.</param>
public void Remove(string tag, string group)
{
if (_aumid != null)
{
_history.Remove(tag, group, _aumid);
}
else
{
_history.Remove(tag, group);
}
}
/// <summary>
/// Removes a group of toast notifications, identified by the specified group label, from action center.
/// </summary>
/// <param name="group">The group label of the toast notifications to be removed.</param>
public void RemoveGroup(string group)
{
if (_aumid != null)
{
_history.RemoveGroup(group, _aumid);
}
else
{
_history.RemoveGroup(group);
}
}
}
/// <summary>
/// Apps must implement this activator to handle notification activation.
/// </summary>
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);
}
/// <summary>
/// This method will be called when the user clicks on a foreground or background activation on a toast. Parent app must implement this method.
/// </summary>
/// <param name="arguments">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.</param>
/// <param name="userInput">Text and selection values that the user entered in your toast.</param>
/// <param name="appUserModelId">Your AUMID.</param>
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
}
/// <summary>
/// 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.
/// </summary>
public class NotificationUserInput : IReadOnlyDictionary<string, string>
{
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<string> Keys => _data.Select(i => i.Key);
public IEnumerable<string> 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<KeyValuePair<string, string>> GetEnumerator()
{
return _data.Select(i => new KeyValuePair<string, string>(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();
}
}
}

View file

@ -0,0 +1,41 @@
/*
* Greenshot - a free and open source screenshot tool
* Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom
*
* For more information see: http://getgreenshot.org/
* The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
using System.Runtime.InteropServices;
using log4net;
namespace GreenshotWin10Plugin.Native
{
// The GUID CLSID must be unique to your app. Create a new GUID if copying this code.
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(INotificationActivationCallback))]
[Guid("F48E86D3-E34C-4DB7-8F8F-9A0EA55F0D08"), ComVisible(true)]
public class GreenshotNotificationActivator : NotificationActivator
{
private static readonly ILog Log = LogManager.GetLogger(typeof(GreenshotNotificationActivator));
public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId)
{
// TODO: Handle activation
Log.Info("Activated");
}
}
}

View file

@ -27,6 +27,7 @@ using Windows.UI.Notifications;
using GreenshotPlugin.Core; using GreenshotPlugin.Core;
using GreenshotPlugin.IniFile; using GreenshotPlugin.IniFile;
using GreenshotPlugin.Interfaces; using GreenshotPlugin.Interfaces;
using GreenshotWin10Plugin.Native;
using log4net; using log4net;
namespace GreenshotWin10Plugin namespace GreenshotWin10Plugin
@ -42,6 +43,11 @@ namespace GreenshotWin10Plugin
private readonly string _imageFilePath; private readonly string _imageFilePath;
public ToastNotificationService() public ToastNotificationService()
{ {
// Register AUMID and COM server (for Desktop Bridge apps, this no-ops)
DesktopNotificationManagerCompat.RegisterAumidAndComServer<GreenshotNotificationActivator>("Greenshot.Greenshot");
// Register COM server and activator type
DesktopNotificationManagerCompat.RegisterActivator<GreenshotNotificationActivator>();
var localAppData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Greenshot"); var localAppData = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Greenshot");
if (!Directory.Exists(localAppData)) if (!Directory.Exists(localAppData))
{ {
@ -58,6 +64,12 @@ namespace GreenshotWin10Plugin
greenshotImage.Save(_imageFilePath, ImageFormat.Png); greenshotImage.Save(_imageFilePath, ImageFormat.Png);
} }
/// <summary>
/// This creates the actual toast
/// </summary>
/// <param name="message">string</param>
/// <param name="onClickAction">Action called when clicked</param>
/// <param name="onClosedAction">Action called when the toast is closed</param>
private void ShowMessage(string message, Action onClickAction, Action onClosedAction) private void ShowMessage(string message, Action onClickAction, Action onClosedAction)
{ {
// Do not inform the user if this is disabled // Do not inform the user if this is disabled
@ -65,6 +77,14 @@ namespace GreenshotWin10Plugin
{ {
return; return;
} }
// Prepare the toast notifier. Be sure to specify the AppUserModelId on your application's shortcut!
var toastNotifier = DesktopNotificationManagerCompat.CreateToastNotifier();
if (toastNotifier.Setting != NotificationSetting.Enabled)
{
Log.DebugFormat("Ignored toast due to {0}", toastNotifier.Setting);
return;
}
// Get a toast XML template // Get a toast XML template
var toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText01); var toastXml = ToastNotificationManager.GetTemplateContent(ToastTemplateType.ToastImageAndText01);
@ -83,7 +103,6 @@ namespace GreenshotWin10Plugin
} }
} }
// Create the toast and attach event listeners // Create the toast and attach event listeners
var toast = new ToastNotification(toastXml); var toast = new ToastNotification(toastXml);
@ -121,10 +140,17 @@ namespace GreenshotWin10Plugin
toast.Dismissed -= ToastDismissedHandler; toast.Dismissed -= ToastDismissedHandler;
// Remove the other handler too // Remove the other handler too
toast.Activated -= ToastActivatedHandler; toast.Activated -= ToastActivatedHandler;
toast.Failed -= ToastOnFailed;
} }
toast.Dismissed += ToastDismissedHandler; toast.Dismissed += ToastDismissedHandler;
// Show the toast. Be sure to specify the AppUserModelId on your application's shortcut! toast.Failed += ToastOnFailed;
ToastNotificationManager.CreateToastNotifier(@"Greenshot").Show(toast); toastNotifier.Show(toast);
}
private void ToastOnFailed(ToastNotification sender, ToastFailedEventArgs args)
{
Log.WarnFormat("Failed to display a toast due to {0}", args.ErrorCode);
Log.Debug(sender.Content.GetXml());
} }
public void ShowWarningMessage(string message, int timeout, Action onClickAction = null, Action onClosedAction = null) public void ShowWarningMessage(string message, int timeout, Action onClickAction = null, Action onClosedAction = null)