gpodder/share/gpodder/extensions/notification-win32.py

157 lines
6.5 KiB
Python

# -*- coding: utf-8 -*-
#
# gPodder - A media aggregator and podcast client
# Copyright (c) 2005-2018 The gPodder Team
#
# gPodder 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 3 of the License, or
# (at your option) any later version.
#
# gPodder 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/>.
#
# Notification implementation for Windows
# Sean Munkel; 2012-12-29
"""
Current state (2018/07/29 ELL):
- I can't get pywin32 to work in msys2 (the platform used for this python3/gtk3 installer)
so existing code using COM doesn't work.
- Gio.Notification is not implemented on windows yet.
see https://bugzilla.gnome.org/show_bug.cgi?id=776583
- Gtk.StatusIcon with a context works but is deprecated. Showing a balloon using set_tooltip_markup
doesn't work.
See https://github.com/afiskon/py-gtk-example
- hexchat have implemented a solid c++ solution.
See https://github.com/hexchat/hexchat/tree/master/src/fe-gtk/notifications
I've chosen to implement notifications by calling a PowerShell script invoking
Windows Toast Notification API or Balloon Notification as fallback.
It's tested on Win7 32bit and Win10 64bit VMs from modern.ie
So we have a working solution until Gio.Notification is implemented on Windows.
"""
import logging
import os
import os.path
import subprocess
import sys
import tempfile
import gpodder
logger = logging.getLogger(__name__)
_ = gpodder.gettext
__title__ = _('Notification Bubbles for Windows')
__description__ = _('Display notification bubbles for different events.')
__authors__ = 'Sean Munkel <SeanMunkel@gmail.com>'
__category__ = 'desktop-integration'
__mandatory_in__ = 'win32'
__only_for__ = 'win32'
class gPodderExtension(object):
def __init__(self, *args):
gpodder_script = sys.argv[0]
gpodder_script = os.path.realpath(gpodder_script)
self._icon = os.path.join(os.path.dirname(gpodder_script), "gpodder.ico")
def on_notification_show(self, title, message):
script = """
try {{
if ([Environment]::OSVersion.Version -ge (new-object 'Version' 10,0,10240)) {{
# use Windows 10 Toast notification
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.UI.Notifications.ToastNotification, Windows.UI.Notifications, ContentType = WindowsRuntime] | Out-Null
[Windows.Data.Xml.Dom.XmlDocument, Windows.Data.Xml.Dom.XmlDocument, ContentType = WindowsRuntime] | Out-Null
# Need a real AppID (see https://stackoverflow.com/q/46814858)
# use gPodder app id if it's the installed, otherwise use PowerShell's AppID
try {{
$gpo_appid = Get-StartApps -Name "gpodder"
}} catch {{
write-host "Get-StartApps not available"
$gpo_appid = $null
}}
if ($gpo_appid -ne $null) {{
$APP_ID = $gpo_appid[0].AppID
}} else {{
$APP_ID = '{{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}}\\WindowsPowerShell\\v1.0\\powershell.exe'
}}
$template = @"
<toast activationType="protocol" launch="" duration="long">
<visual>
<binding template="ToastGeneric">
<image placement="appLogoOverride" src="{icon}" />
<text><![CDATA[{title}]]></text>
<text><![CDATA[{message}]]></text>
</binding>
</visual>
<audio silent="true" />
</toast>
"@
$xml = New-Object Windows.Data.Xml.Dom.XmlDocument
$xml.LoadXml($template)
$toast = New-Object Windows.UI.Notifications.ToastNotification $xml
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier($APP_ID).Show($toast)
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force # Delete this script temp file.
}} else {{
# use older Balloon notification when not on Windows 10
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$o = New-Object System.Windows.Forms.NotifyIcon
$o.Icon = "{icon}"
$o.BalloonTipIcon = "None"
$o.BalloonTipText = @"
{message}
"@
$o.BalloonTipTitle = @"
{title}
"@
$o.Visible = $True
$Delay = 10 # Delay value in seconds.
$o.ShowBalloonTip($Delay*1000)
Start-Sleep -s $Delay
$o.Dispose()
Remove-Item -LiteralPath $MyInvocation.MyCommand.Path -Force # Delete this script temp file.
}}
}} catch {{
write-host "Caught an exception:"
write-host "Exception Type: $($_.Exception.GetType().FullName)"
write-host "Exception Message: $($_.Exception.Message)"
exit 1
}}
""".format(icon=self._icon, message=message, title=title)
fh, path = tempfile.mkstemp(suffix=".ps1")
with open(fh, "w", encoding="utf_8_sig") as f:
f.write(script)
try:
# hide powershell command window using startupinfo
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags = subprocess.CREATE_NEW_CONSOLE | subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = subprocess.SW_HIDE
# to run 64bit powershell on Win10 64bit when running from 32bit gPodder
# (we need 64bit powershell on Win10 otherwise Get-StartApps is not available)
powershell = r"{}\sysnative\WindowsPowerShell\v1.0\powershell.exe".format(os.environ["SystemRoot"])
if not os.path.exists(powershell):
powershell = "powershell.exe"
subprocess.Popen([powershell,
"-ExecutionPolicy", "Bypass", "-File", path],
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
startupinfo=startupinfo)
except subprocess.CalledProcessError as e:
logger.error("Error in on_notification_show(title=%r, message=%r):\n"
"\t%r exit code %i\n\tstdout=%s\n\tstderr=%s",
title, message, e.cmd, e.returncode, e.stdout, e.stderr)
except FileNotFoundError:
logger.error("Error in on_notification_show(title=%r, message=%r): %s not found",
title, message, powershell)
def on_unload(self):
pass